[
  {
    "path": ".editorconfig",
    "content": "# Remove the line below if you want to inherit .editorconfig settings from higher directories\nroot = true\n\n# Default settings for all files\n[*]\nend_of_line = lf\n\n# C# files\n[*.cs]\n\n#### Core EditorConfig Options ####\n\n# Indentation and spacing\nindent_size = 4\nindent_style = space\ntab_width = 4\n\n# New line preferences\nend_of_line = lf\ninsert_final_newline = false\n\n#### .NET Code Actions ####\n\n# Type members\ndotnet_hide_advanced_members = false\ndotnet_member_insertion_location = with_other_members_of_the_same_kind\ndotnet_property_generation_behavior = prefer_throwing_properties\n\n# Symbol search\ndotnet_search_reference_assemblies = true\n\n#### .NET Coding Conventions ####\n\n# Organize usings\ndotnet_separate_import_directive_groups = false\ndotnet_sort_system_directives_first = false\nfile_header_template = unset\n\n# this. and Me. preferences\ndotnet_style_qualification_for_event = false\ndotnet_style_qualification_for_field = false\ndotnet_style_qualification_for_method = false\ndotnet_style_qualification_for_property = false\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true\ndotnet_style_predefined_type_for_member_access = true\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members\n\n# Expression-level preferences\ndotnet_prefer_system_hash_code = true\ndotnet_style_coalesce_expression = true\ndotnet_style_collection_initializer = true\ndotnet_style_explicit_tuple_names = true\ndotnet_style_namespace_match_folder = true\ndotnet_style_null_propagation = true\ndotnet_style_object_initializer = true\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\ndotnet_style_prefer_auto_properties = true\ndotnet_style_prefer_collection_expression = when_types_loosely_match\ndotnet_style_prefer_compound_assignment = true\ndotnet_style_prefer_conditional_expression_over_assignment = true\ndotnet_style_prefer_conditional_expression_over_return = true\ndotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed\ndotnet_style_prefer_inferred_anonymous_type_member_names = true\ndotnet_style_prefer_inferred_tuple_names = true\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true\ndotnet_style_prefer_simplified_boolean_expressions = true\ndotnet_style_prefer_simplified_interpolation = true\n\n# Field preferences\ndotnet_style_readonly_field = true\n\n# Parameter preferences\ndotnet_code_quality_unused_parameters = all\n\n# Suppression preferences\ndotnet_remove_unnecessary_suppression_exclusions = none\n\n# New line preferences\ndotnet_style_allow_multiple_blank_lines_experimental = true\ndotnet_style_allow_statement_immediately_after_block_experimental = true\n\n#### C# Coding Conventions ####\n\n# var preferences\ncsharp_style_var_elsewhere = false\ncsharp_style_var_for_built_in_types = false\ncsharp_style_var_when_type_is_apparent = false\n\n# Expression-bodied members\ncsharp_style_expression_bodied_accessors = true\ncsharp_style_expression_bodied_constructors = false\ncsharp_style_expression_bodied_indexers = true\ncsharp_style_expression_bodied_lambdas = true\ncsharp_style_expression_bodied_local_functions = false\ncsharp_style_expression_bodied_methods = false\ncsharp_style_expression_bodied_operators = false\ncsharp_style_expression_bodied_properties = true\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_as_with_null_check = true\ncsharp_style_pattern_matching_over_is_with_cast_check = true\ncsharp_style_prefer_extended_property_pattern = true\ncsharp_style_prefer_not_pattern = true\ncsharp_style_prefer_pattern_matching = true\ncsharp_style_prefer_switch_expression = true\n\n# Null-checking preferences\ncsharp_style_conditional_delegate_call = true\n\n# Modifier preferences\ncsharp_prefer_static_anonymous_function = true\ncsharp_prefer_static_local_function = true\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async\ncsharp_style_prefer_readonly_struct = true\ncsharp_style_prefer_readonly_struct_member = true\n\n# Code-block preferences\ncsharp_prefer_braces = true\ncsharp_prefer_simple_using_statement = true\ncsharp_prefer_system_threading_lock = true\ncsharp_style_namespace_declarations = block_scoped\ncsharp_style_prefer_method_group_conversion = true\ncsharp_style_prefer_primary_constructors = true\ncsharp_style_prefer_top_level_statements = true\n\n# Expression-level preferences\ncsharp_prefer_simple_default_expression = true\ncsharp_style_deconstructed_variable_declaration = true\ncsharp_style_implicit_object_creation_when_type_is_apparent = true\ncsharp_style_inlined_variable_declaration = true\ncsharp_style_prefer_index_operator = true\ncsharp_style_prefer_local_over_anonymous_function = true\ncsharp_style_prefer_null_check_over_type_check = true\ncsharp_style_prefer_range_operator = true\ncsharp_style_prefer_tuple_swap = true\ncsharp_style_prefer_utf8_string_literals = true\ncsharp_style_throw_expression = true\ncsharp_style_unused_value_assignment_preference = discard_variable\ncsharp_style_unused_value_expression_statement_preference = discard_variable\n\n# 'using' directive preferences\ncsharp_using_directive_placement = outside_namespace\n\n# New line preferences\ncsharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true\ncsharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true\ncsharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true\ncsharp_style_allow_blank_lines_between_consecutive_braces_experimental = true\ncsharp_style_allow_embedded_statements_on_same_line_experimental = true\n\n#### C# Formatting Rules ####\n\n# New line preferences\ncsharp_new_line_before_catch = false\ncsharp_new_line_before_else = false\ncsharp_new_line_before_finally = false\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_open_brace = none\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation preferences\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = true\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = true\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_before_comma = false\ncsharp_space_before_dot = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_between_square_brackets = false\n\n# Wrapping preferences\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\n\n#### Naming styles ####\n\n# Naming rules\n\ndotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion\ndotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface\ndotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i\n\ndotnet_naming_rule.types_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.types_should_be_pascal_case.symbols = types\ndotnet_naming_rule.types_should_be_pascal_case.style = pascal_case\n\ndotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members\ndotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case\n\n# Symbol specifications\n\ndotnet_naming_symbols.interface.applicable_kinds = interface\ndotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.interface.required_modifiers =\n\ndotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum\ndotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.types.required_modifiers =\n\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.non_field_members.required_modifiers =\n\n# Naming styles\n\ndotnet_naming_style.pascal_case.required_prefix =\ndotnet_naming_style.pascal_case.required_suffix =\ndotnet_naming_style.pascal_case.word_separator =\ndotnet_naming_style.pascal_case.capitalization = pascal_case\n\ndotnet_naming_style.begins_with_i.required_prefix = I\ndotnet_naming_style.begins_with_i.required_suffix =\ndotnet_naming_style.begins_with_i.word_separator =\ndotnet_naming_style.begins_with_i.capitalization = pascal_case\n\ndotnet_diagnostic.CA1416.severity = none\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n\n*.png binary\n*.psd binary\n*.jpg binary\n*.jpeg binary\n*.gif binary\n*.bmp binary\n*.ico binary\n*.pdf binary\n*.zip binary\n*.7z binary\n*.dll binary\n*.exe binary\n*.pfx binary\n*.snk binary\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "﻿# These are supported funding model platforms\n\ngithub: PrzemyslawKlys\ncustom: https://paypal.me/PrzemyslawKlys"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file changes\n    # paths:\n    #   - \"src/**/*.ts\"\n    #   - \"src/**/*.tsx\"\n    #   - \"src/**/*.js\"\n    #   - \"src/**/*.jsx\"\n\njobs:\n  claude-review:\n    # Optional: Filter by PR author\n    # if: |\n    #   github.event.pull_request.user.login == 'external-contributor' ||\n    #   github.event.pull_request.user.login == 'new-developer' ||\n    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'\n    \n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n    \n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@beta\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)\n          # model: \"claude-opus-4-1-20250805\"\n\n          # Direct prompt for automated review (no @claude mention needed)\n          direct_prompt: |\n            Please review this pull request and provide feedback on:\n            - Code quality and best practices\n            - Potential bugs or issues\n            - Performance considerations\n            - Security concerns\n            - Test coverage\n            \n            Be constructive and helpful in your feedback.\n\n          # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR\n          # use_sticky_comment: true\n          \n          # Optional: Customize review based on file types\n          # direct_prompt: |\n          #   Review this PR focusing on:\n          #   - For TypeScript files: Type safety and proper interface usage\n          #   - For API endpoints: Security, input validation, and error handling\n          #   - For React components: Performance, accessibility, and best practices\n          #   - For tests: Coverage, edge cases, and test quality\n          \n          # Optional: Different prompts for different authors\n          # direct_prompt: |\n          #   ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && \n          #   'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||\n          #   'Please provide a thorough code review focusing on our coding standards and best practices.' }}\n          \n          # Optional: Add specific tools for running tests or linting\n          # allowed_tools: \"Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)\"\n          \n          # Optional: Skip review for certain conditions\n          # if: |\n          #   !contains(github.event.pull_request.title, '[skip-review]') &&\n          #   !contains(github.event.pull_request.title, '[WIP]')\n\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@beta\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n          \n          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)\n          # model: \"claude-opus-4-1-20250805\"\n          \n          # Optional: Customize the trigger phrase (default: @claude)\n          # trigger_phrase: \"/claude\"\n          \n          # Optional: Trigger when specific user is assigned to an issue\n          # assignee_trigger: \"claude-bot\"\n          \n          # Optional: Allow Claude to run specific commands\n          # allowed_tools: \"Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)\"\n          \n          # Optional: Add custom instructions for Claude to customize its behavior for your project\n          # custom_instructions: |\n          #   Follow our coding standards\n          #   Ensure all new code has tests\n          #   Use TypeScript for new files\n          \n          # Optional: Custom environment variables for Claude\n          # claude_env: |\n          #   NODE_ENV: test\n\n"
  },
  {
    "path": ".github/workflows/dotnet-tests.yml",
    "content": "# .NET Testing Workflow\nname: Test .NET Libraries\n\non:\n  push:\n    branches:\n      - v2-speedygonzales\n    paths-ignore:\n      - 'README.md'\n      - 'CHANGELOG.md'\n      - 'Docs/**'\n  pull_request:\n    branches:\n      - v2-speedygonzales\n  workflow_dispatch:  # Manual trigger only\n\nenv:\n  DOTNET_VERSION: '8.x'\n  BUILD_CONFIGURATION: 'Debug'\n  TEST_VERBOSITY: minimal # set to 'detailed' to restore full test output\n  SUMMARIZE_FAILURES: true # set to 'false' to disable summarizing failing tests\n\njobs:\n  test-windows:\n    name: 'Windows'\n    runs-on: windows-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Restore dependencies\n        run: dotnet restore Sources/Mailozaurr.sln\n\n      - name: Build solution\n        run: dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run tests\n        run: dotnet test Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --verbosity ${{ env.TEST_VERBOSITY }} --logger \"console;verbosity=${{ env.TEST_VERBOSITY }}\" --logger trx --collect:\"XPlat Code Coverage\"\n\n      - name: Summarize failing tests\n        if: failure() && env.SUMMARIZE_FAILURES == 'true'\n        shell: pwsh\n        run: |\n          $trxFiles = Get-ChildItem -Recurse -Filter *.trx\n          if ($trxFiles.Count -eq 0) {\n            Write-Host \"No TRX files found for failure analysis\"\n            exit 0\n          }\n\n          Write-Host \"=== Failed Tests Summary ===\"\n          $failureCount = 0\n\n          $trxFiles | ForEach-Object {\n            try {\n              Select-Xml -Path $_.FullName -XPath \"//UnitTestResult[@outcome='Failed']\" |\n                ForEach-Object {\n                  $name = $_.Node.testName\n                  $msg = $_.Node.Output.ErrorInfo.Message ?? \"No error message available\"\n                  Write-Host \"❌ $name\"\n                  Write-Host \"   $msg\"\n                  $failureCount++\n                }\n            } catch {\n              Write-Host \"Warning: Could not parse TRX file $($_.Name): $($_.Exception.Message)\"\n            }\n          }\n\n          if ($failureCount -eq 0) {\n            Write-Host \"No failed tests found in TRX files\"\n          } else {\n            Write-Host \"=== Total failed tests: $failureCount ===\"\n          }\n\n      - name: Upload test results\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: test-results-windows\n          path: '**/*.trx'\n\n      - name: Upload coverage reports\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: coverage-reports-windows\n          path: '**/coverage.cobertura.xml'\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v4\n        with:\n          files: '**/coverage.cobertura.xml'\n          fail_ci_if_error: false\n\n  test-ubuntu:\n    name: 'Ubuntu'\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install Mono for .NET Framework tests\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y mono-complete\n\n      - name: Restore dependencies\n        run: dotnet restore Sources/Mailozaurr.sln\n\n      - name: Build solution\n        run: dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run tests\n        run: dotnet test Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --verbosity ${{ env.TEST_VERBOSITY }} --logger \"console;verbosity=${{ env.TEST_VERBOSITY }}\" --logger trx --collect:\"XPlat Code Coverage\"\n\n      - name: Summarize failing tests\n        if: failure() && env.SUMMARIZE_FAILURES == 'true'\n        shell: pwsh\n        run: |\n          $trxFiles = Get-ChildItem -Recurse -Filter *.trx\n          if ($trxFiles.Count -eq 0) {\n            Write-Host \"No TRX files found for failure analysis\"\n            exit 0\n          }\n\n          Write-Host \"=== Failed Tests Summary ===\"\n          $failureCount = 0\n\n          $trxFiles | ForEach-Object {\n            try {\n              Select-Xml -Path $_.FullName -XPath \"//UnitTestResult[@outcome='Failed']\" |\n                ForEach-Object {\n                  $name = $_.Node.testName\n                  $msg = $_.Node.Output.ErrorInfo.Message ?? \"No error message available\"\n                  Write-Host \"❌ $name\"\n                  Write-Host \"   $msg\"\n                  $failureCount++\n                }\n            } catch {\n              Write-Host \"Warning: Could not parse TRX file $($_.Name): $($_.Exception.Message)\"\n            }\n          }\n\n          if ($failureCount -eq 0) {\n            Write-Host \"No failed tests found in TRX files\"\n          } else {\n            Write-Host \"=== Total failed tests: $failureCount ===\"\n          }\n\n      - name: Upload test results\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: test-results-ubuntu\n          path: '**/*.trx'\n      - name: Upload coverage reports\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: coverage-reports-ubuntu\n          path: '**/coverage.cobertura.xml'\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v4\n        with:\n          files: '**/coverage.cobertura.xml'\n          fail_ci_if_error: false\n\n  test-macos:\n    name: 'macOS'\n    runs-on: macos-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install Mono for .NET Framework tests\n        run: brew install mono\n\n      - name: Restore dependencies\n        run: dotnet restore Sources/Mailozaurr.sln\n\n      - name: Build solution\n        run: dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run tests\n        run: dotnet test Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-build --verbosity ${{ env.TEST_VERBOSITY }} --logger \"console;verbosity=${{ env.TEST_VERBOSITY }}\" --logger trx --collect:\"XPlat Code Coverage\"\n\n      - name: Summarize failing tests\n        if: failure() && env.SUMMARIZE_FAILURES == 'true'\n        shell: pwsh\n        run: |\n          $trxFiles = Get-ChildItem -Recurse -Filter *.trx\n          if ($trxFiles.Count -eq 0) {\n            Write-Host \"No TRX files found for failure analysis\"\n            exit 0\n          }\n\n          Write-Host \"=== Failed Tests Summary ===\"\n          $failureCount = 0\n\n          $trxFiles | ForEach-Object {\n            try {\n              Select-Xml -Path $_.FullName -XPath \"//UnitTestResult[@outcome='Failed']\" |\n                ForEach-Object {\n                  $name = $_.Node.testName\n                  $msg = $_.Node.Output.ErrorInfo.Message ?? \"No error message available\"\n                  Write-Host \"❌ $name\"\n                  Write-Host \"   $msg\"\n                  $failureCount++\n                }\n            } catch {\n              Write-Host \"Warning: Could not parse TRX file $($_.Name): $($_.Exception.Message)\"\n            }\n          }\n\n          if ($failureCount -eq 0) {\n            Write-Host \"No failed tests found in TRX files\"\n          } else {\n            Write-Host \"=== Total failed tests: $failureCount ===\"\n          }\n\n      - name: Upload test results\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: test-results-macos\n          path: '**/*.trx'\n      - name: Upload coverage reports\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: coverage-reports-macos\n          path: '**/coverage.cobertura.xml'\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v4\n        with:\n          files: '**/coverage.cobertura.xml'\n          fail_ci_if_error: false\n\n"
  },
  {
    "path": ".github/workflows/powershell-tests-all.yml",
    "content": "name: Test PowerShell Module (All)\n\n# This workflow is disabled but ready for use\non:\n  workflow_dispatch:  # Manual trigger only\n# To enable, replace above with push/pull_request triggers\n\nenv:\n  DOTNET_VERSION: '8.x'\n  BUILD_CONFIGURATION: 'Debug'\n\njobs:\n  refresh-psd1:\n    name: 'Refresh PSD1'\n    runs-on: windows-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PowerShell modules\n        run: |\n          Install-Module PSPublishModule -Force -Scope CurrentUser -AllowClobber\n        shell: pwsh\n\n      - name: Refresh module manifest\n        env:\n          RefreshPSD1Only: 'true'\n        run: ./Build/Manage-Mailozaurr.ps1\n        shell: pwsh\n\n      - name: Commit refreshed PSD1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          HEAD_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git add Mailozaurr.psd1\n          git commit -m \"chore: regenerate psd1\" || echo \"No changes to commit\"\n          git push origin HEAD:$Env:HEAD_BRANCH\n        shell: pwsh\n\n      - name: Upload refreshed manifest\n        uses: actions/upload-artifact@v4\n        with:\n          name: psd1\n          path: Mailozaurr.psd1\n\n  test-windows-ps5:\n    needs: refresh-psd1\n    name: 'Windows PowerShell 5.1'\n    runs-on: windows-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download manifest\n        uses: actions/download-artifact@v4\n        with:\n          name: psd1\n          path: .\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install PowerShell modules\n        shell: powershell\n        run: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - name: Build .NET solution\n        run: |\n          dotnet restore Sources/Mailozaurr.sln\n          dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run PowerShell tests\n        shell: powershell\n        run: .\\Mailozaurr.Tests.ps1\n\n  test-windows-ps7:\n    needs: refresh-psd1\n    name: 'Windows PowerShell 7'\n    runs-on: windows-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download manifest\n        uses: actions/download-artifact@v4\n        with:\n          name: psd1\n          path: .\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install PowerShell modules\n        shell: pwsh\n        run: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - name: Build .NET solution\n        run: |\n          dotnet restore Sources/Mailozaurr.sln\n          dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run PowerShell tests\n        shell: pwsh\n        run: .\\Mailozaurr.Tests.ps1\n\n  test-ubuntu:\n    needs: refresh-psd1\n    name: 'Ubuntu PowerShell 7'\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download manifest\n        uses: actions/download-artifact@v4\n        with:\n          name: psd1\n          path: .\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install PowerShell\n        run: |\n          curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -\n          curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft.list\n          sudo apt-get update\n          sudo apt-get install -y powershell\n\n      - name: Install Mono for .NET Framework tests\n        run: sudo apt-get install -y mono-complete\n\n      - name: Install PowerShell modules\n        shell: pwsh\n        run: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - name: Build .NET solution\n        run: |\n          dotnet restore Sources/Mailozaurr.sln\n          dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run PowerShell tests\n        shell: pwsh\n        run: ./Mailozaurr.Tests.ps1\n\n  test-macos:\n    needs: refresh-psd1\n    name: 'macOS PowerShell 7'\n    runs-on: macos-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download manifest\n        uses: actions/download-artifact@v4\n        with:\n          name: psd1\n          path: .\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Install PowerShell\n        run: brew install --cask powershell\n\n      - name: Install Mono for .NET Framework tests\n        run: brew install mono\n\n      - name: Install PowerShell modules\n        shell: pwsh\n        run: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - name: Build .NET solution\n        run: |\n          dotnet restore Sources/Mailozaurr.sln\n          dotnet build Sources/Mailozaurr.sln --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore\n\n      - name: Run PowerShell tests\n        shell: pwsh\n        run: ./Mailozaurr.Tests.ps1\n\n"
  },
  {
    "path": ".gitignore",
    "content": "Ignore/*\n.vs/*\n.vscode/*\nExamples/Output/*\nExamples/Input/*\nExamples/Documents/*\nReleases/*\nArtefacts/*\nReleasedUnpacked/*\nSources/.vs\nSources/*/.vs\nSources/*/obj\nSources/*/bin\nSources/*/*/obj\nSources/*/*/bin\nSources/packages/*\nLib/Default/*\nLib/Standard/*\nLib/Core/*\ndotnet-install.sh\npackages-microsoft-prod.deb\n\nSources/*/TestResults/\n.nuget"
  },
  {
    "path": "Build/Artefacts/ProjectBuild/project.build.plan.json",
    "content": "{\n  \"Success\": true,\n  \"ResolvedVersion\": \"2.0.5\",\n  \"ResolvedVersionsByProject\": {\n    \"Mailozaurr.Msg\": \"2.0.5\",\n    \"Mailozaurr\": \"2.0.5\"\n  },\n  \"Projects\": [\n    {\n      \"ProjectName\": \"Mailozaurr.Msg\",\n      \"CsprojPath\": \"C:\\\\Support\\\\GitHub\\\\Mailozaurr\\\\Sources\\\\Mailozaurr.Msg\\\\Mailozaurr.Msg.csproj\",\n      \"IsPackable\": true,\n      \"OldVersion\": \"2.0.4\",\n      \"NewVersion\": \"2.0.5\",\n      \"Packages\": [\n        \"C:\\\\Support\\\\GitHub\\\\Mailozaurr\\\\Artefacts\\\\ProjectBuild\\\\packages\\\\Mailozaurr.Msg.2.0.5.nupkg\"\n      ]\n    },\n    {\n      \"ProjectName\": \"Mailozaurr\",\n      \"CsprojPath\": \"C:\\\\Support\\\\GitHub\\\\Mailozaurr\\\\Sources\\\\Mailozaurr\\\\Mailozaurr.csproj\",\n      \"IsPackable\": true,\n      \"OldVersion\": \"2.0.4\",\n      \"NewVersion\": \"2.0.5\",\n      \"Packages\": [\n        \"C:\\\\Support\\\\GitHub\\\\Mailozaurr\\\\Artefacts\\\\ProjectBuild\\\\packages\\\\Mailozaurr.2.0.5.nupkg\"\n      ]\n    }\n  ],\n  \"PublishedPackages\": []\n}\n"
  },
  {
    "path": "Build/Build-Module.ps1",
    "content": "﻿# Install-Module PSPublishModule -Force\nImport-Module PSPublishModule -Force\n\nBuild-Module -ModuleName 'Mailozaurr' {\n    # Usual defaults as per standard module\n    $Manifest = [ordered] @{\n        ModuleVersion        = '2.0.1'\n        # Supported PSEditions\n        CompatiblePSEditions = @('Desktop', 'Core')\n        # ID used to uniquely identify this module\n        GUID                 = '2b0ea9f1-3ff1-4300-b939-106d5da608fa'\n        # Author of this module\n        Author               = 'Przemyslaw Klys'\n        # Company or vendor of this module\n        CompanyName          = 'Evotec'\n        # Copyright statement for this module\n        Copyright            = \"(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved.\"\n        # Description of the functionality provided by this module\n        Description          = 'Mailozaurr is a PowerShell module that aims to provide SMTP, POP3, IMAP and few other ways to interact with Email. Underneath it uses MimeKit and MailKit and EmailValidation libraries written by Jeffrey Stedfast.            '\n        # Minimum version of the Windows PowerShell engine required by this module\n        PowerShellVersion    = '5.1'\n        # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.\n        Tags                 = @('Windows', 'MacOS', 'Linux', 'Mail', 'Email', 'MX', 'SPF', 'DMARC', 'DKIM', 'GraphApi', 'SendGrid', 'Graph', 'IMAP', 'POP3')\n\n        IconUri              = 'https://evotec.xyz/wp-content/uploads/2020/07/MailoZaurr.png'\n\n        ProjectUri           = 'https://github.com/EvotecIT/MailoZaurr'\n\n        PreReleaseTag        = 'Preview4'\n    }\n    New-ConfigurationManifest @Manifest\n\n    $ConfigurationFormat = [ordered] @{\n        RemoveComments                              = $false\n\n        PlaceOpenBraceEnable                        = $true\n        PlaceOpenBraceOnSameLine                    = $true\n        PlaceOpenBraceNewLineAfter                  = $true\n        PlaceOpenBraceIgnoreOneLineBlock            = $false\n\n        PlaceCloseBraceEnable                       = $true\n        PlaceCloseBraceNewLineAfter                 = $false\n        PlaceCloseBraceIgnoreOneLineBlock           = $false\n        PlaceCloseBraceNoEmptyLineBefore            = $true\n\n        UseConsistentIndentationEnable              = $true\n        UseConsistentIndentationKind                = 'space'\n        UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline'\n        UseConsistentIndentationIndentationSize     = 4\n\n        UseConsistentWhitespaceEnable               = $true\n        UseConsistentWhitespaceCheckInnerBrace      = $true\n        UseConsistentWhitespaceCheckOpenBrace       = $true\n        UseConsistentWhitespaceCheckOpenParen       = $true\n        UseConsistentWhitespaceCheckOperator        = $true\n        UseConsistentWhitespaceCheckPipe            = $true\n        UseConsistentWhitespaceCheckSeparator       = $true\n\n        AlignAssignmentStatementEnable              = $true\n        AlignAssignmentStatementCheckHashtable      = $true\n\n        UseCorrectCasingEnable                      = $true\n    }\n    # format PSD1 and PSM1 files when merging into a single file\n    # enable formatting is not required as Configuration is provided\n    New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat\n    # format PSD1 and PSM1 files within the module\n    # enable formatting is required to make sure that formatting is applied (with default settings)\n    New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None\n    # when creating PSD1 use special style without comments and with only required parameters\n    New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal'\n\n    # configuration for documentation, at the same time it enables documentation processing\n    New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\\Readme.md' -Path 'Docs'\n\n    New-ConfigurationImportModule -ImportSelf #-ImportRequiredModules\n\n\n    $newConfigurationBuildSplat = @{\n        Enable                            = $true\n        SignModule                        = $true\n        MergeModuleOnBuild                = $true\n        MergeFunctionsFromApprovedModules = $true\n        CertificateThumbprint             = '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703'\n        ResolveBinaryConflicts            = $true\n        ResolveBinaryConflictsName        = 'Mailozaurr.PowerShell'\n        NETProjectPath                    = \"$PSScriptRoot\\..\\Sources\\Mailozaurr.PowerShell\"\n        NETProjectName                    = 'Mailozaurr.PowerShell'\n        NETConfiguration                  = 'Release'\n        NETFramework                      = 'net8.0', 'net472'\n        NETHandleAssemblyWithSameName     = $true\n        #NETMergeLibraryDebugging          = $true\n        DotSourceLibraries                = $true\n        DotSourceClasses                  = $true\n        DeleteTargetModuleBeforeBuild     = $true\n        NETBinaryModuleDocumenation       = $true\n        RefreshPSD1Only                   = $true\n    }\n\n    New-ConfigurationBuild @newConfigurationBuildSplat #-DotSourceLibraries -DotSourceClasses -MergeModuleOnBuild -Enable -SignModule -DeleteTargetModuleBeforeBuild -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' -MergeFunctionsFromApprovedModules\n\n    New-ConfigurationArtefact -Type Unpacked -Enable -Path \"$PSScriptRoot\\..\\Artefacts\" -RequiredModulesPath \"$PSScriptRoot\\..\\Artefacts\\Modules\"\n    New-ConfigurationArtefact -Type Packed -Enable -Path \"$PSScriptRoot\\..\\Releases\" -IncludeTagName\n\n    #New-ConfigurationTest -TestsPath \"$PSScriptRoot\\..\\Tests\" -Enable\n\n    # global options for publishing to github/psgallery\n    #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\\Support\\Important\\PowerShellGalleryAPI.txt' -Enabled:$true\n    #New-ConfigurationPublish -Type GitHub -FilePath 'C:\\Support\\Important\\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -GenerateReleaseNotes -OverwriteTagName '{ModuleName}-v{ModuleVersionWithPreRelease}'\n} -ExitCode\n"
  },
  {
    "path": "Build/Build-Project.ps1",
    "content": "param(\n    [string] $ConfigPath = \"$PSScriptRoot\\project.build.json\",\n    [Nullable[bool]] $UpdateVersions,\n    [Nullable[bool]] $Build,\n    [Nullable[bool]] $PublishNuget = $false,\n    [Nullable[bool]] $PublishGitHub = $false,\n    [Nullable[bool]] $Plan,\n    [string] $PlanPath\n)\n\nImport-Module PSPublishModule -Force -ErrorAction Stop\n\n$invokeParams = @{\n    ConfigPath = $ConfigPath\n}\nif ($null -ne $UpdateVersions) { $invokeParams.UpdateVersions = $UpdateVersions }\nif ($null -ne $Build) { $invokeParams.Build = $Build }\nif ($null -ne $PublishNuget) { $invokeParams.PublishNuget = $PublishNuget }\nif ($null -ne $PublishGitHub) { $invokeParams.PublishGitHub = $PublishGitHub }\nif ($null -ne $Plan) { $invokeParams.Plan = $Plan }\nif ($PlanPath) { $invokeParams.PlanPath = $PlanPath }\n\nInvoke-ProjectBuild @invokeParams\n"
  },
  {
    "path": "Build/Refresh-DisposableDomains.ps1",
    "content": "[CmdletBinding()]\nparam(\n    [string]$OutputPath = (Join-Path $PSScriptRoot '..\\Sources\\Mailozaurr\\Resources')\n)\n\n$lists = @{\n    'disposable_email_blocklist.conf' = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/refs/heads/main/disposable_email_blocklist.conf'\n    'allowlist.conf'                 = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/refs/heads/main/allowlist.conf'\n}\n\nforeach ($list in $lists.GetEnumerator()) {\n    $target = Join-Path $OutputPath $list.Key\n    Write-Host \"Checking $($list.Key)...\"\n    $remoteContent = (Invoke-WebRequest -Uri $list.Value -UseBasicParsing).Content -replace \"`r`n\", \"`n\" -split \"`n\" | Where-Object { $_ -and -not $_.StartsWith('#') }\n    if (Test-Path $target) {\n        $localContent = Get-Content $target\n        $diff = Compare-Object -ReferenceObject $localContent -DifferenceObject $remoteContent\n        if ($diff) {\n            Write-Host \"Changes for $($list.Key):\"\n            foreach ($d in $diff) {\n                if ($d.SideIndicator -eq '=>') { Write-Host \"Added: $($d.InputObject)\" }\n                elseif ($d.SideIndicator -eq '<=') { Write-Host \"Removed: $($d.InputObject)\" }\n            }\n        } else {\n            Write-Host \"No changes for $($list.Key).\"\n        }\n    } else {\n        Write-Host \"Local file for $($list.Key) not found. Creating new.\"\n        foreach ($line in $remoteContent) { Write-Host \"Added: $line\" }\n    }\n    $remoteContent | Set-Content $target -Encoding UTF8\n}\n"
  },
  {
    "path": "Build/project.build.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/EvotecIT/PSPublishModule/main/Schemas/project.build.schema.json\",\n  \"RootPath\": \"..\",\n  \"ExpectedVersion\": null,\n  \"ExpectedVersionMap\": {\n    \"Mailozaurr\": \"2.0.X\",\n    \"Mailozaurr.Msg\": \"2.0.X\"\n  },\n  \"ExpectedVersionMapAsInclude\": true,\n  \"ExpectedVersionMapUseWildcards\": false,\n  \"NugetSource\": [],\n  \"IncludePrerelease\": false,\n  \"Configuration\": \"Release\",\n  \"OutputPath\": null,\n  \"StagingPath\": \"Artefacts/ProjectBuild\",\n  \"CleanStaging\": true,\n  \"PlanOutputPath\": null,\n  \"UpdateVersions\": true,\n  \"Build\": true,\n  \"PublishNuget\": false,\n  \"PublishGitHub\": false,\n  \"CreateReleaseZip\": true,\n  \"CertificateThumbprint\": \"483292C9E317AA13B07BB7A96AE9D1A5ED9E7703\",\n  \"CertificateStore\": \"CurrentUser\",\n  \"TimeStampServer\": \"http://timestamp.digicert.com\",\n  \"PublishSource\": \"https://api.nuget.org/v3/index.json\",\n  \"PublishApiKeyFilePath\": \"C:\\\\Support\\\\Important\\\\NugetOrgEvotec.txt\",\n  \"SkipDuplicate\": true,\n  \"PublishFailFast\": true,\n  \"GitHubAccessTokenFilePath\": \"C:\\\\Support\\\\Important\\\\GithubAPI.txt\",\n  \"GitHubUsername\": \"EvotecIT\",\n  \"GitHubRepositoryName\": \"Mailozaurr\",\n  \"GitHubIsPreRelease\": false,\n  \"GitHubIncludeProjectNameInTag\": true,\n  \"GitHubGenerateReleaseNotes\": true,\n  \"GitHubReleaseMode\": \"Single\",\n  \"GitHubPrimaryProject\": \"Mailozaurr\",\n  \"GitHubTagTemplate\": \"{Repo}-v{UtcTimestamp}\",\n  \"GitHubReleaseName\": \"{Repo} {UtcDateTime}\"\n}\n\n"
  },
  {
    "path": "CHANGELOG.MD",
    "content": "﻿#### 2.0.0 - Preview 6\n##### What's Changed\n* docs: fix permissions typo in README by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/66\n* fix: correct verbose message for Get-IMAPFolder by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/67\n* Update POP message retrieval by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/69\n* test: define sample body content for Send-EmailMessage by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/68\n* docs: fix spelling mistakes by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/72\n* Add tests for ConvertFromGraphCredential by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/71\n* Add GetEmailAddress unit tests by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/70\n* Futher improvements by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/73\n* Fix SMTP auth optional by @PrzemyslawKlys in https://github.com/EvotecIT/Mailozaurr/pull/76\n* feat: add `RetryCount` parameter to `Send-EmailMessage` allowing optional retries\n* feat: support configurable delay and exponential backoff via `RetryDelayMilliseconds` and `RetryDelayBackoff`\n\n**Full Changelog**: https://github.com/EvotecIT/Mailozaurr/compare/v2.0.0-Preview5...v2.0.0-Preview6\n\n#### 2.0.0 - Preview 5\n- Removed `Resolve-DnsQuery` => moved to DnsClientX, which uses it's own library\n- Removed `Resolve-DnsRequestRest` => moved to DnsClientX, which uses it's own library\n- Removed all functions related to DNS (will be moved to DomainDetective module)\n  - `Find-MXRecord`\n  - `Find-SPFRecord`\n  - `Find-DKIMRecord`\n  - `Find-DMARCRecord`\n  - `Find-DNSBL`\n  - `Find-DNSSECRecord`\n  - `Find-CAARecord`\n  - `Find-BIMIRecord`\n  - `Find-MTASTSRecord`\n  - `Find-O365OpenIDRecord`\n  - `Find-SecurityTxtRecord`\n  - `Find-TLSRPTRecord`\n  - `Find-IPGeolocation`\n  - `Find-DANERecord`\n  - `Find-CAARecord`\n  - `Find-DNSSECRecord`\n- Replaced `Test-EmailAddress` function with `Test-EmailAddress` binary cmdlet\n- Improvements to  `Send-EmailMessage`\n  - Removed `Email`/`EmailParameters` parameters as they are not needed, and legacy\n  - Added `DeliveryNotificationOption` as an enum instead of strings\n  - Internal improvements\n    - Dispose() of SmtpClient in the end\n    - Replaced PowerShell classes with C# version\n    - Moved some PowerShell code to C# for better performance and simpler logic\n\n#### 2.0.0 - Preview 4 - 2024.03.04\n- Update to DLLs to latest versions\n- Removes some unnecessary DLLs\n- Different way to build module and load dependencies\n- Removal of DLLs from source codes\n- Small updates to DLLs\n- Add new DLLs\n- Added `ConvertFrom-EmlToMsg` to convert EML files to MSG files\n- Added `Import-MailFile` to import MSG/EML files to PowerShell Object\n- Added `Get-MailMessage`\n- Added `Get-MailMessageAttachment`\n- Added `Find-BIMIRecord`\n- Added `Find-CAARecord`\n- Added `Find-DANERecord`\n- Added `Find-MTASTSRecord`\n- Added `Find-O365OpenIDRecord`\n- Added `Find-SecurityTxtRecord`\n- Added `Find-TLSRPTRecord`\n- Added `Find-DNSSECRecord`\n- Improve `Find-DMARCRecord` to return more data\n- Added `Get-DMARCData` to read DMARC data from XML files\n- Added `Find-IPGeolocation` to get IP Geolocation data\n- Small improvements to error handling\n\n#### 1.0.2 - 2023.08.05\n- Rename `SkipCertificateValidatation` to `SkipCertificateValidation` in `Send-EmailMessage` (typo fix)\n- Added `SkipCertificateValidation` to `Connect-IMAP`\n- Added `SkipCertificateValidation` to `Connect-POP3`\n- Added `SkipCertificateRevocation` to `Connect-IMAP`\n- Added `SkipCertificateRevocation` to `Connect-POP3`\n- Use simpler certificate validation callback by @jborean93 in https://github.com/EvotecIT/Mailozaurr/pull/42\n\n#### 1.0.1 - 2023.07.19\n- Updated `Microsoft.Identity.Client.dll` to 4.44 to match `ExchangeOnlineManagement` module\n\n#### 1.0.0 - 2022.12.20\n- Add support for sending emails with graph using Delegated permissions. It uses Microsoft.Graph.Authentication module and `MgGraphRequest` switch on `Send-EmailMessage`.\n\n```powershell\nImport-Module Mailozaurr\nImport-Module Microsoft.Graph.Authentication -Force\n\n# this shows how to send email using combination of Mailozaurr and Microsoft.Graph to use Connect-MgGraph to authorize\n$Body = EmailBody {\n    New-HTMLText -Text \"This is test of Connect-MGGraph functionality\"\n}\n\n# authorize via Connect-MgGraph with delegated rights or any other supported method\nConnect-MgGraph -Scopes Mail.Send\n\n# sending email\n$sendEmailMessageSplat = @{\n    From           = 'przemyslaw.klys@test.pl'\n    To             = 'przemyslaw.klys@test.pl'\n    HTML           = $Body\n    Subject        = 'This tests email as delegated'\n    MgGraphRequest = $true\n    Verbose        = $true\n}\nSend-EmailMessage @sendEmailMessageSplat\n```\n\n#### 0.9.0 - 2022.10.02\n- Fix `Suppress` for sending emails via SendGrid\n- Fix `Suppress` for sending emails via Microsoft Graph\n- Add support to `Send-EmailMessage` for attachments **4MB** to **150MB** in size for Microsoft Graph. Before it would only send attachments up to **4MB** in size. Detects size automatically and uses the appropriate API endpoint.\n- Add `RequestReadReceipt` support to `Send-EmailMessage` for Microsoft Graph to request a read receipt from the recipient.\n- Add `RequestDeliveryReceipt` switch to `Send-EmailMessage` for Microsoft Graph to request a delivery receipt. SMTP uses `DeliveryNotificationOption` and `DeliveryStatusNotificationType`.\n- Require `Send-EmailMessage` to have From field [#33](https://github.com/EvotecIT/Mailozaurr/issues/33)\n- Warn if attachment in `Send-EmailMessage` doesn't exists, no error is thrown [#34](https://github.com/EvotecIT/Mailozaurr/issues/34)\n- Fixes special chars issue in file names [#26](https://github.com/EvotecIT/Mailozaurr/issues/26)\n-\n\n#### 0.0.25 - 2022.06.07\n- Updated `MailKit`\n- Updated `MimeKit`\n- Added missing libraries so that the project can work on more systems without installing higher version of .NET Framework\n- Added tests for SMTP with Auth and Graph API\n\n#### 0.0.24 - 2021.01.24\n  - Improved logging logic for `Send-EmailMessage`\n#### 0.0.23 - 2021.01.22\n  - Added option to `Send-EmailMessage` to specify the `LocalDomain` to be able to troubleshoot issue [#1314](https://github.com/jstedfast/MailKit/issues/1314)\n  - Added option to `Send-EmailMessage` - `LogConsole` and `LogObject` which joins `LogPath` in saving whole conversation to console, or to final object as Message property\n\n#### 0.0.22 - 2021.01.21\n  - Upgraded `MailKit\\MimeKit` to 3.1.0\n  - Added support to save MimeMessage in `Send-EmailMessage` using `MimeMessagePath`\n  - Fixed `Cloudflare DNS calls` for `Find-DMARCRecord`,`Find-DKIMRecord`, `Find-MXRecord`, `Find-SPFRecord`, `Find-DNSBNL`\n  - Updated docs\n  - More output fields on `Send-EmailMessage`\n\n#### 0.0.21 - 2022.01.07\n  - Added support for logging in the `Send-EmailMessage` for SMTP to allow for debugging.\n    - By adding `LogPath $PSScriptRoot\\Output\\Log1.txt` log file will be created with full information\n    - Parameters such as `LogSecrets`, `LogTimeStamps`, `LogTimeStampsFormat` and `LogClientPrefix`,`LogServerPrefix` are available\n#### 0.0.20 - 2022.01.02\n  - Added support for using `Get-MsalToken` instead of built-in token creation for GraphApi\n\n```powershell\n# creating body for HTML using PSWriteHTML\n$Body = EmailBody {\n    New-HTMLText -Text \"this is my test\"\n    New-HTMLText -Text \"Łączy nas piłka\"\n}\n\n# Generating token using Get-MsalToken\n$getMsalTokenSplat = @{\n    ClientId     = '0fb383f1'\n    ClientSecret = 'VKDM_' | ConvertTo-SecureString -AsPlainText\n    Authority    = 'https://login.microsoftonline.com/ceb371f6'\n}\n$MsalToken = Get-MsalToken @getMsalTokenSplat\n\n# Converting it to Credentials object (so that Send-EmailMessage can use it)\n$Credential = ConvertTo-GraphCredential -MsalToken $MsalToken.AccessToken\n\n# sending email\nSend-EmailMessage -From 'przemyslaw.klys@something.pl' -To 'przemyslaw.klys@something.else' -Credential $Credential -HTML $Body -Subject 'This is another test email 2' -Graph -Verbose -Priority Low -DoNotSaveToSentItems\n```\n\n#### 0.0.19 - 2021.12.25\n  - Fixes encoding for Send-EmailMessage wen using GraphApi (forces UTF8)\n#### 0.0.18 - 2021.09.23\n  - Improved error handling of `Send-EmailMessage` when sending with GraphApi\n#### 0.0.17 - 2021.09.19\n  - Upgraded `MailKit/Mimekit` `2.15.0`\n  - Upgraded `DNSClient` to `1.5.0`\n  - Fixes `oAuth2` [#17](https://github.com/EvotecIT/Mailozaurr/issues/17)\n  - Removed wrong parameterset from `Send-EmailMessage`\n  - Improved `ConvertTo-GraphCredential` to support encrypted graph client secret.\n#### 0.0.16 - 2021.07.23\n  - Moved Class.MySmtpClient to be loaded from separate file via dot sourcing - hopefully fixes [#16](https://github.com/EvotecIT/Mailozaurr/issues/16)\n#### 0.0.15 - 2021.07.18\n  - Moved Class.MySmtpClient to be loaded as last in PSM1. This should make it load when loaded by other modules - hopefully!\n#### 0.0.14 - 2021.06.28\n  - Added missing library `System.Buffers`\n#### 0.0.13 - 2021.06.27\n  - Added `SkipCertificateValidatation` to `Send-EmailMessage`\n  - Downgraded `MailKit/MimeKit 2.12.0`\n#### 0.0.12 - 2021.06.20\n  - Added `AsSecureString` to `Send-EmailMessage` which allows to provide `-AsSecureString -Username 'przemyslaw.klys@domain.pl' -Password $secStringPassword`\n#### 0.0.11 - 2021.06.18\n  - Added `SkipCertificateRevocation` to `Send-EmailMessage` [#13](https://github.com/EvotecIT/Mailozaurr/issues/13)\n  - 🐛 Fixed PTR records in `Find-MXRecord` when using HTTPS\n  - Small improvement to `Send-EmailMessage` returning object when using `WhatIf`\n#### 0.0.10 - 2020.10.25\n  - `Send-EmailMessage` - fix for Graph where attachments where not attached and nobody reported\n  - `Send-EmailMessage` - updated error messages with tips what could be wrong\n  - `Send-EmailMessage` - updated ErrorAction Stop in few places for those that prefer errors\n  - `Send-EmailMessage` - added basic support for SendGrid with parameter `-SendGrid`\n  - `Find-MXRecord` - updated with `DNSProvider` (`Cloudflare`/`Google`) for calls over HTTPS\n  - `Find-SPFRecord` - updated with `DNSProvider` (`Cloudflare`/`Google`) for calls over HTTPS\n  - `Find-DKIMRecord` - updated with `DNSProvider` (`Cloudflare`/`Google`) for calls over HTTPS\n  - `Find-DMARCRecord` - updated with `DNSProvider` (`Cloudflare`/`Google`) for calls over HTTPS\n  - `Find-DNSBL` - added, same options as above\n  - `Resolve-DNSRequestRest` - Added just in case\n#### 0.0.9 - 2020.08.11\n  - DNS Records\n    - Fixed `DNSServer` parameter usage [#5](https://github.com/EvotecIT/Mailozaurr/issues/5)\n#### 0.0.8 - 2020.08.06\n  - MS Graph API\n    - Added `Get-MailFolder` - work in progress\n    - Added `Get-MailMessage` - work in progress\n    - Added `Save-MailMessage` - work in progress\n    - Added check for attachments/message body less than 10 characters. - by andrew0wells [#3](https://github.com/EvotecIT/Mailozaurr/issues/3)\n  - SMTP\n    - Updated `Send-EmailMessage` error handling\n#### 0.0.7 - 2020.08.04\n  - More updates making it better toolkit\n#### 0.0.6 - 2020.08.03\n  - Public release\n  - Added GraphAPI support for Send-EmailMessage\n  - Added oAuth2 support for Send-EmailMessage\n  - Added oAuth2 support for POP3\n  - Added oAuth2 support for IMAP4\n  - Fixed lots of stuff\n#### 0.0.3 - 2020.07.26\n  - Added `Test-EmailAddress`\n  - Added `Connect-OAuthGoogle`\n  - Added `Connect-OAuthO365`\n  - Added `Resolve-DNSQuery`\n#### 0.0.2 - 2020.07.25\n  - Added `Find-MXRecord`\n  - Added `Find-DMARCRecord`\n  - Added `Find-DKIMRecord`\n  - Added `Find-SPFRecord`\n#### 0.0.1 - 2020.06.13\n  - Initial release\n"
  },
  {
    "path": "Docs/Configuration-and-Usage.md",
    "content": "# Mailozaurr Configuration and Usage\n\nThis document collects the practical configuration model for Mailozaurr across its reusable application layer, the `mailozaurr` executable, and MCP usage.\n\nIt is meant to answer:\n\n- how Mailozaurr models accounts and providers\n- what settings and secrets belong to a profile\n- which providers support which kinds of operations\n- how to configure CLI storage and run common flows\n- where PowerShell still fits today\n\n## Core idea: profiles\n\nMailozaurr uses reusable `MailProfile` definitions for mailbox and send configuration.\n\nA profile contains:\n\n- `Id`: stable identifier such as `work-imap` or `alerts-smtp`\n- `DisplayName`: human-friendly name\n- `Kind`: provider/technology such as `imap`, `graph`, `gmail`, or `smtp`\n- `DefaultSender`: default sender for send-capable profiles\n- `DefaultMailbox`: default mailbox or principal for read-capable profiles\n- `Settings`: non-secret provider settings\n- secrets stored separately from the profile document\n\nThe shared model is implemented in:\n\n- [MailProfile.cs](../Sources/Mailozaurr.Application/MailProfile.cs)\n- [MailProfileKind.cs](../Sources/Mailozaurr.Application/MailProfileKind.cs)\n- [MailProfileSettingsKeys.cs](../Sources/Mailozaurr.Application/MailProfileSettingsKeys.cs)\n- [MailSecretNames.cs](../Sources/Mailozaurr.Application/MailSecretNames.cs)\n\n## Supported profile kinds\n\nCurrent shared profile kinds are:\n\n- `imap`\n- `pop3`\n- `graph`\n- `gmail`\n- `smtp`\n- `sendgrid`\n- `mailgun`\n- `ses`\n\n## Capability model\n\nMailozaurr does not pretend every provider supports the same operations.\n\nDefault capabilities are defined in [MailCapabilityCatalog.cs](../Sources/Mailozaurr.Application/MailCapabilityCatalog.cs).\n\nIn practice:\n\n| Kind | Read/Search | Folders | Move/Mark/Delete | Send | Notes |\n|---|---|---|---|---|---|\n| `imap` | Yes | Yes | Yes | No | strong mailbox model |\n| `pop3` | Yes | No real folder model | Delete only style workflows | No | limited mailbox model |\n| `graph` | Yes | Yes | Yes | Yes | also supports rules/events/permissions |\n| `gmail` | Yes | Yes | Yes | Yes | Gmail-specific threads/labels |\n| `smtp` | No | No | No | Yes | send only |\n| `sendgrid` | No | No | No | Yes | send only |\n| `mailgun` | No | No | No | Yes | send only |\n| `ses` | No | No | No | Yes | send only |\n\n## Common settings and secrets\n\nCommon non-secret setting keys include:\n\n- `server`\n- `port`\n- `userName`\n- `folder`\n- `mailbox`\n- `clientId`\n- `tenantId`\n- `certificatePath`\n- `redirectUri`\n- `authFlow`\n- `loginHint`\n- `tokenExpiresOn`\n- `authMode`\n- `secureSocketOptions`\n- `useSsl`\n\nCommon secret names include:\n\n- `password`\n- `clientSecret`\n- `accessToken`\n- `refreshToken`\n- `certificatePassword`\n\n## Minimum provider guidance\n\nThe shared validator lives in [MailProfileValidator.cs](../Sources/Mailozaurr.Application/MailProfileValidator.cs).\n\nThe practical minimum shape by provider is:\n\n### IMAP / POP3 / SMTP\n\nUsually define:\n\n- `kind`\n- `server`\n- optional `port`\n- optional `userName`\n- `password` secret when using username/password auth\n\nRecommended:\n\n- `defaultMailbox` for read profiles\n- `defaultSender` for send profiles\n\n### Graph\n\nUsually define:\n\n- `kind=graph`\n- mailbox through `defaultMailbox` or `mailbox`\n\nThen choose one auth story:\n\n- interactive login and saved tokens\n- access token\n- `clientId` + `tenantId` + `clientSecret`\n- `clientId` + `tenantId` + `certificatePath`\n\n### Gmail\n\nUsually define:\n\n- `kind=gmail`\n- mailbox through `defaultMailbox` or `mailbox`\n\nThen choose one auth story:\n\n- interactive login and saved tokens\n- access token\n- `clientId` + `clientSecret` + `refreshToken`\n\n### SendGrid / Mailgun / SES\n\nThese are send-only profiles. The exact provider-specific settings are still best treated as provider-specific recipes, but they fit the same profile model and shared send surface.\n\n## Storage locations\n\nBy default, the application layer stores reusable state under a `Mailozaurr` directory inside local application data.\n\nThe path resolver is implemented in [MailApplicationPaths.cs](../Sources/Mailozaurr.Application/MailApplicationPaths.cs).\n\nDefault subdirectories are:\n\n- `Profiles`\n- `Secrets`\n- `Drafts`\n- `ActionPlanBatches`\n\nEnvironment variable overrides:\n\n- `MAILOZAURR_PROFILE_DIRECTORY`\n- `MAILOZAURR_SECRET_DIRECTORY`\n- `MAILOZAURR_DRAFT_DIRECTORY`\n- `MAILOZAURR_ACTION_PLAN_DIRECTORY`\n\nThe CLI also supports per-run overrides:\n\n- `--profiles-dir`\n- `--secrets-dir`\n- `--drafts-dir`\n- `--plan-batches-dir`\n\n## CLI usage overview\n\nThe executable is built from [Mailozaurr.Cli](../Sources/Mailozaurr.Cli).\n\nTo inspect current commands:\n\n```powershell\ndotnet run --project Sources/Mailozaurr.Cli -- --help\n```\n\nMain command groups:\n\n- `profile ...`\n- `draft ...`\n- `send ...`\n- `queue ...`\n- `mail ...`\n- `mcp serve`\n\nMost commands support `--json`, which is the preferred mode for automation.\n\n## Common CLI recipes\n\n### Generic IMAP profile\n\n```powershell\nmailozaurr profile create --profile work-imap --kind imap --name \"Work IMAP\" `\n  --default-mailbox user@example.com `\n  --setting server=imap.example.com `\n  --setting port=993 `\n  --setting userName=user@example.com `\n  --json\n\nmailozaurr profile set-secret --profile work-imap --name password --value \"secret\" --json\nmailozaurr profile test --profile work-imap --scope mailbox --json\n```\n\n### Generic SMTP profile\n\n```powershell\nmailozaurr profile create --profile alerts-smtp --kind smtp --name \"Alerts SMTP\" `\n  --default-sender alerts@example.com `\n  --setting server=smtp.example.com `\n  --setting port=587 `\n  --setting userName=alerts@example.com `\n  --setting useSsl=true `\n  --json\n\nmailozaurr profile set-secret --profile alerts-smtp --name password --value \"secret\" --json\nmailozaurr profile test --profile alerts-smtp --scope send --json\n```\n\n### Microsoft Graph profile\n\n```powershell\nmailozaurr profile graph-bootstrap --profile work-graph --name \"Work Graph\" `\n  --mailbox user@example.com `\n  --client-id <app-id> `\n  --tenant-id <tenant-id> `\n  --client-secret <secret> `\n  --json\n\nmailozaurr profile auth-status --profile work-graph --json\nmailozaurr profile test --profile work-graph --scope mailbox --json\n```\n\n### Gmail profile\n\n```powershell\nmailozaurr profile gmail-bootstrap --profile personal-gmail --name \"Personal Gmail\" `\n  --mailbox user@gmail.com `\n  --client-id <client-id> `\n  --client-secret <secret> `\n  --refresh-token <token> `\n  --json\n\nmailozaurr profile auth-status --profile personal-gmail --json\nmailozaurr profile test --profile personal-gmail --scope mailbox --json\n```\n\n### Search and retrieve mail\n\n```powershell\nmailozaurr mail folders --profile work-imap --compact --json\nmailozaurr mail search --profile work-imap --folder Inbox --query Invoice --compact --json\nmailozaurr mail get --profile work-imap --folder Inbox --message-id 123 --compact --json\nmailozaurr mail attachments --profile work-imap --folder Inbox --message-id 123 --json\n```\n\n### Save drafts and queue sends\n\n```powershell\nmailozaurr draft save --draft weekly-update --name \"Weekly update\" --profile alerts-smtp `\n  --to team@example.com --subject \"Weekly update\" --text \"Status attached.\" --json\n\nmailozaurr send --draft weekly-update --json\nmailozaurr queue list --compact --json\nmailozaurr queue process --json\n```\n\n### Preview before destructive actions\n\n```powershell\nmailozaurr mail preview-delete --profile work-imap --folder Inbox --message-id 123 --json\nmailozaurr mail delete --profile work-imap --folder Inbox --message-id 123 --confirm-token <token> --json\n```\n\n## MCP usage overview\n\nThe MCP server is hosted by the same executable:\n\n```powershell\nmailozaurr mcp serve\n```\n\nThe MCP tool surface is implemented in [MailMcpTools.cs](../Sources/Mailozaurr.Cli/Mcp/MailMcpTools.cs).\n\nCurrent tool areas include:\n\n- profile listing, save, delete, bootstrap, login, auth status, and live tests\n- folder listing and folder-alias discovery\n- search, get, batch get, and attachment save\n- drafts, send, and queue operations\n- preview and execution for read/flag/move/archive/trash/delete actions\n- action-plan import/export, execution, and stored batch management\n\nA minimal MCP client entry looks like:\n\n```json\n{\n  \"command\": \"mailozaurr\",\n  \"args\": [\"mcp\", \"serve\"]\n}\n```\n\n## PowerShell usage today\n\nPowerShell remains a first-class surface, but it does not use the same profile-oriented headless workflow yet in the same way as the CLI and MCP layers.\n\nToday the best PowerShell references are:\n\n- [README.MD](../README.MD) for module usage and examples\n- [OAuthFlows.md](./OAuthFlows.md) for OAuth flows\n- [PGP.md](./PGP.md) for PGP support\n- the scripts under [Examples](../Examples)\n\n## Recipes and provider-specific guidance\n\nBecause Mailozaurr supports many providers and auth stories, examples matter.\n\nWhen adding a new provider recipe:\n\n- document the minimum profile shape\n- show both settings and secrets\n- include a live verification command such as `profile test`\n- keep reusable logic in shared layers and keep wrapper-specific steps in wrapper docs\n\nIf a feature is reusable across CLI, MCP, GUI, or PowerShell, it should follow the placement rules in [Platform-Architecture.md](./Platform-Architecture.md).\n"
  },
  {
    "path": "Docs/OAuthFlows.md",
    "content": "# OAuth device code and on-behalf-of flows\n\nMailozaurr can acquire delegated Microsoft Graph tokens using additional OAuth flows.\n\n## Device code authentication\n\nUse `Connect-EmailGraph` with the `-DeviceCode` switch to sign in with a device code. Provide the application ID, tenant ID and scopes.\n\n```powershell\n$scopes = @('Mail.ReadWrite','Mail.Send')\n$graph = Connect-EmailGraph -ClientId '<app id>' -DirectoryId '<tenant id>' -DeviceCode -Scopes $scopes\n```\n\nFollow the instructions printed on the console to complete authentication.\n\n## On-behalf-of token exchange\n\nIf you already have a user access token, exchange it for Microsoft Graph scopes using `-OnBehalfOfToken`.\n\n```powershell\n$token = Get-Content -Raw '.\\UserToken.txt'\n$graph = Connect-EmailGraph -ClientId '<app id>' -ClientSecret '<secret>' -DirectoryId '<tenant id>' `\n    -OnBehalfOfToken $token -Scopes $scopes\n```\n\nBoth methods return a `GraphConnectionInfo` object with the OAuth credential available in the `OAuthCredential` property.\n"
  },
  {
    "path": "Docs/PGP.md",
    "content": "# PGP Support in Mailozaurr\n\nMailozaurr is able to sign and encrypt messages using OpenPGP. The implementation relies on [`MimeKit`](https://github.com/jstedfast/MimeKit) and a temporary key store so that no permanent changes are made to the user's environment.\n\n## Ephemeral key store\n\nThe `EphemeralOpenPgpContext` class creates a temporary directory that is removed when the context is disposed. It inherits from `GnuPGContext` and is used internally by the SMTP helper methods when signing or encrypting with PGP.\n\n```csharp\nusing var ctx = new EphemeralOpenPgpContext();\n// import keys and use ctx for signing or encryption\n```\n\n## Sending a PGP encrypted message\n\nUse `Send-EmailMessage` with the `PgpSignAndEncrypt` option. Provide paths to the public and private key files and the password protecting the private key:\n\n```powershell\n$pub = 'Examples/PGPKeys/mimekit.gpg.pub'\n$sec = 'Examples/PGPKeys/mimekit.gpg.sec'\nSend-EmailMessage -From 'mimekit@example.com' -To 'mimekit@example.com' \\ \n    -Server 'smtp.example.com' -Port 25 -Body 'Test' -Subject 'Test' \\ \n    -SignOrEncrypt PgpSignAndEncrypt -PublicKeyPath $pub -PrivateKeyPath $sec \\ \n    -PrivateKeyPassword 'no.secret'\n```\n\nThe message will be signed and encrypted before being sent.\n"
  },
  {
    "path": "Docs/Platform-Architecture.md",
    "content": "# Mailozaurr Platform Architecture\n\nThis document defines how Mailozaurr should evolve across its library, PowerShell, CLI, MCP, and GUI surfaces.\n\nThe main goal is reuse. If a new capability can be shared by more than one surface, it should not be implemented only in a wrapper. It should live in the reusable layer and be consumed by wrappers.\n\n## Goals\n\n- Keep one canonical implementation of reusable mail behavior.\n- Avoid duplicating business logic across PowerShell, CLI, MCP, GUI, and ad hoc examples.\n- Support provider differences without forcing a fake one-size-fits-all abstraction.\n- Make new work easier to place correctly.\n- Keep public surfaces consistent in naming, behavior, and safety rules.\n\n## Product Surfaces\n\nMailozaurr should be treated as one platform with multiple front doors:\n\n- `Mailozaurr`: reusable .NET library and provider engine\n- `Mailozaurr.PowerShell`: PowerShell adapter\n- `mailozaurr`: cross-platform CLI\n- `mailozaurr mcp serve`: MCP server mode\n- Desktop/GUI apps built on the same core concepts\n\nThese are not separate products with separate logic. They are adapters over shared capabilities.\n\n## Target Layering\n\nThe preferred long-term structure is:\n\n- `Mailozaurr`\n- `Mailozaurr.Application`\n- `Mailozaurr.PowerShell`\n- `Mailozaurr.Cli`\n- `Mailozaurr.Mcp`\n- Desktop/GUI app projects\n\n### Layer Responsibilities\n\n`Mailozaurr`\n\n- Provider and protocol implementations\n- SMTP, IMAP, POP3, Graph, Gmail, SendGrid, Mailgun, SES support\n- Authentication primitives and provider clients\n- Message parsing, conversion, attachments, transport rules\n- Reusable send, search, fetch, save, move, mark, delete logic\n- Reusable queue and pending-message primitives\n- Provider-specific models and helpers\n\n`Mailozaurr.Application`\n\n- Cross-surface workflows and orchestration\n- Profile store and profile validation\n- Secret and token orchestration\n- Session management\n- Capability discovery\n- Normalized DTOs used by CLI, MCP, GUI, and optionally PowerShell\n- Safe send, queue, draft, delete, and move workflows\n- Audit and operation result models\n\n`Mailozaurr.PowerShell`\n\n- Cmdlet binding and parameter mapping\n- PowerShell pipeline and `ShouldProcess` integration\n- Formatting and help metadata\n- PowerShell-only compatibility behaviors\n\n`Mailozaurr.Cli`\n\n- Command parsing\n- Human-readable output\n- JSON output\n- Exit codes\n\n`Mailozaurr.Mcp`\n\n- MCP transport and tool registration\n- Tool schema definitions\n- Mapping tool calls to application services\n\nGUI projects\n\n- View models, UI state, platform integration\n- Human-first flows for accounts, mailbox browsing, drafts, and queue review\n\n## Reuse Rules\n\nThese rules should be used when deciding where new work belongs.\n\n### Rule 1\n\nIf logic is reusable by more than one surface, it should not be implemented only in PowerShell, CLI, MCP, or GUI code.\n\nIt should be placed in:\n\n- `Mailozaurr` when it is protocol/provider behavior or reusable low-level mail functionality\n- `Mailozaurr.Application` when it is a cross-surface workflow, orchestration rule, normalized DTO, or policy\n\n### Rule 2\n\nWrappers should adapt, not own business logic.\n\nPowerShell, CLI, MCP, and GUI layers should translate user intent into calls to shared services. They should not each reinvent send/search/profile behavior.\n\n### Rule 3\n\nProvider-neutral concepts should be shared. Provider-specific concepts should remain explicit.\n\nDo not force every provider to look identical. Instead:\n\n- share common operations such as search, get message, save attachment, draft, queue, send, mark, move, delete\n- keep provider-specific operations explicit, such as Graph inbox rules, Graph events, Gmail threads, Gmail labels, IMAP IDLE behavior, and POP3 limitations\n\n### Rule 4\n\nA wrapper-only implementation is acceptable only when the behavior is genuinely wrapper-specific.\n\nExamples:\n\n- PowerShell parameter sets and pipeline binding belong in `Mailozaurr.PowerShell`\n- CLI help text and shell-friendly formatting belong in `Mailozaurr.Cli`\n- MCP tool schemas belong in `Mailozaurr.Mcp`\n- GUI dialogs, views, and local UX behavior belong in the GUI project\n\n## Feature Placement Guide\n\nUse this checklist when adding a new feature.\n\nPlace it in `Mailozaurr` if it is:\n\n- a provider client enhancement\n- a reusable transport or mailbox operation\n- message parsing, attachment, MIME, cryptography, or auth logic\n- reusable folder/message/provider helpers\n- reusable pending-message or send pipeline logic\n\nPlace it in `Mailozaurr.Application` if it is:\n\n- a profile concept shared by multiple front ends\n- a cross-provider workflow\n- a capability model\n- a normalized DTO for message summaries/details\n- a queue or draft workflow shared by CLI, MCP, GUI, or PowerShell\n- a safety policy for destructive operations or sending\n- an operation result or audit event that more than one surface should use\n\nPlace it in `Mailozaurr.PowerShell` if it is:\n\n- cmdlet syntax\n- PowerShell-specific validation and parameter shaping\n- `ShouldProcess`, verbose output, pipeline behavior, and formatting\n\nPlace it in `Mailozaurr.Cli` if it is:\n\n- command syntax\n- argument parsing\n- console output formatting\n- shell completion and exit code concerns\n\nPlace it in `Mailozaurr.Mcp` if it is:\n\n- MCP server bootstrapping\n- tool declaration and transport behavior\n- mapping between tool requests and application services\n\nPlace it in the GUI project if it is:\n\n- view logic\n- window/dialog composition\n- UI state management\n- platform-specific desktop integration\n\n## Canonical Shared Concepts\n\nThe following concepts should use consistent naming across all surfaces:\n\n- `Profile`\n- `ProfileCapabilities`\n- `FolderRef`\n- `MailboxRef`\n- `MessageSummary`\n- `MessageDetail`\n- `AttachmentSummary`\n- `DraftMessage`\n- `QueuedMessage`\n- `SendResult`\n- `OperationResult`\n\nThe same concept should not have different names in different surfaces unless there is a compelling reason.\n\n## Provider Capability Model\n\nMailozaurr should be designed around capabilities, not around the assumption that every provider supports the same things.\n\n### Common Capability Areas\n\n- read/search messages\n- list folders or mailbox containers\n- get message details\n- save attachments\n- mark/move/delete messages\n- send messages\n- wait/listen for new messages\n\n### Provider Notes\n\n- IMAP: strong mailbox support, folders, flags, move, search, and wait/idle patterns\n- POP3: limited mailbox model, fetch/search/delete style workflows, no real folder story\n- Graph: rich mailbox and send support, plus rules, events, and permissions\n- Gmail API: mailbox and send support with Gmail-specific thread and label behavior\n- SMTP: send only, not mailbox browsing\n- SendGrid/Mailgun/SES: send only, provider-specific sending features\n\nThe shared layers should expose common operations where they exist and explicit provider extensions where they do not.\n\n## Profiles and Sessions\n\nFuture work should prefer explicit profiles over implicit global sessions.\n\nProfiles should store:\n\n- provider type\n- display name\n- server or tenant settings\n- auth mode\n- sender defaults where relevant\n- mailbox defaults where relevant\n- non-secret metadata in config\n- secrets in secure OS-backed storage where possible\n\nWrappers should not rely on hidden global state when a profile or explicit handle can be used instead.\n\n## Safety Rules\n\nSafety behavior should be shared, not reimplemented separately in each surface.\n\nPreferred defaults:\n\n- drafts before send when practical\n- queue-first sending for automation and agents\n- explicit confirmation for destructive actions\n- provider-limit validation before send\n- dry-run support where possible\n- reusable audit and logging models\n\n## CLI and MCP Design Direction\n\nThe CLI and MCP server should be thin adapters over shared services.\n\nThe recommended shape is:\n\n- CLI for human and automation use\n- MCP as a structured tool surface over the same workflows\n- one executable host eventually able to run both normal CLI commands and `mcp serve`\n\nThis avoids duplicating logic and gives one headless contract for the ecosystem.\n\n## Skills\n\nSkills should be treated as reusable agent guidance, not as a replacement for shared Mailozaurr capabilities.\n\nThe preferred split is:\n\n- `MCP` exposes executable tools over shared Mailozaurr services\n- `skills` teach agents how to use those tools safely and consistently\n\nExamples of skill responsibilities:\n\n- when to prefer drafts over immediate send\n- when to use queue-first sending\n- how to summarize before delete/move\n- how to select compact versus rich read surfaces\n\nExamples of non-skill responsibilities:\n\n- mailbox search logic\n- message retrieval\n- attachment saving\n- queue processing\n- provider auth/session behavior\n\nThose belong in shared code, not only in prompt guidance.\n\n## HTML Body Projection Roadmap\n\nMailozaurr should eventually support reusable HTML body projection for non-PowerShell headless surfaces, especially CLI and MCP.\n\nThe recommended split is:\n\n- keep raw provider body retrieval in `Mailozaurr`\n- place body projection abstractions and policies in `Mailozaurr.Application`\n- keep PowerShell dependency-light by default\n- document PowerShell recipes for richer HTML handling instead of forcing heavy dependencies into the module\n\n### Recommended integration model\n\n- `CLI` / `MCP`\n  Add optional shared HTML-to-Markdown projection via `OfficeIMO.Markdown.Html`\n- `PowerShell`\n  Keep raw body access in the Mailozaurr module\n  Show examples using `PSWriteOffice` / `OfficeIMO.Markdown.Html` for Markdown projection\n  Show examples using `PSParseHTML` for DOM-style extraction and structured data recovery\n\n### Placement rule\n\nIf HTML-to-Markdown projection becomes reusable across CLI, MCP, GUI, or plain C# consumers, the abstraction belongs in shared Mailozaurr layers.\n\nIf a PowerShell workflow only demonstrates how to pipe a retrieved HTML body into another module, that belongs in documentation/examples, not in the core Mailozaurr PowerShell adapter by default.\n\n## Migration Guidance\n\nNot every existing area must be refactored immediately.\n\nWhen touching an existing feature:\n\n1. Keep behavior working.\n2. Extract reusable logic into `Mailozaurr` or `Mailozaurr.Application` when the change naturally allows it.\n3. Leave only surface-specific adaptation in PowerShell, CLI, MCP, or GUI code.\n\nIncremental improvement is preferred over a disruptive rewrite.\n\n## Decision Rule for New Work\n\nBefore adding code, ask:\n\n1. Is this reusable outside the current wrapper?\n2. Is it protocol/provider logic or workflow/orchestration logic?\n3. Will CLI, MCP, GUI, PowerShell, or plain C# users eventually want the same behavior?\n\nIf the answer to reuse is yes, the default destination should be a shared layer, not the wrapper.\n\n## Working Principle\n\nMailozaurr should have one core behavior model with many adapters, not many implementations with similar names.\n\nThat principle should guide future CLI, MCP, GUI, PowerShell, and library work.\n"
  },
  {
    "path": "Examples/Example-AcquireGoogleTokenInteractive.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$GmailAccount = 'user@gmail.com'\n$ClientID = 'Your-Client-ID'\n$ClientSecret = Read-Host 'Google client secret' -AsSecureString\n$Scopes = @('https://mail.google.com/')\n\n$Credential = Connect-OAuthGoogle -GmailAccount $GmailAccount -ClientID $ClientID -ClientSecretSecureString $ClientSecret -Scope $Scopes\n\n$CredInfo = ConvertFrom-OAuth2Credential -Credential $Credential\n$CredInfo | Format-List\n\n# Send a test email using the token\nSend-EmailMessage -From $GmailAccount -To 'recipient@example.com' `\n    -Server 'smtp.gmail.com' -Subject 'Test OAuth email' -Text 'Hello' `\n    -SecureSocketOptions Auto -Credential $Credential -OAuth2\n\n# Connect to Gmail via IMAP\n$ImapClient = Connect-IMAP -Server 'imap.gmail.com' -Port 993 -Options Auto `\n    -Credential $Credential -OAuth2\nGet-IMAPFolder -Client $ImapClient -Verbose\nDisconnect-IMAP -Client $ImapClient\n\n# And POP3\n$PopClient = Connect-POP3 -Server 'pop.gmail.com' -Port 995 -Options Auto `\n    -Credential $Credential -OAuth2\nDisconnect-POP3 -Client $PopClient\n"
  },
  {
    "path": "Examples/Example-AcquireO365TokenInteractive.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Login = 'user@example.com'\n$ClientID = 'Your-Application-ID'\n$TenantID = 'Your-Tenant-ID'\n$RedirectUri = 'https://login.microsoftonline.com/common/oauth2/nativeclient'\n$Scopes = @(\n    'email',\n    'offline_access',\n    'https://outlook.office.com/IMAP.AccessAsUser.All',\n    'https://outlook.office.com/POP.AccessAsUser.All',\n    'https://outlook.office.com/SMTP.Send'\n)\n\n$Credential = Connect-OAuthO365 -Login $Login -ClientID $ClientID -TenantID $TenantID -RedirectUri $RedirectUri -Scopes $Scopes\n\n$CredInfo = ConvertFrom-OAuth2Credential -Credential $Credential\n$CredInfo | Format-List\n\n# Use the OAuth token to send mail via SMTP\nSend-EmailMessage -From $Login -To 'recipient@example.com' `\n    -Server 'smtp.office365.com' -Subject 'Test OAuth email' -Text 'Hello' `\n    -SecureSocketOptions Auto -Credential $Credential -OAuth2\n\n# Use the same credential with IMAP\n$ImapClient = Connect-IMAP -Server 'outlook.office365.com' -Port 993 -Options Auto `\n    -Credential $Credential -OAuth2\nGet-IMAPFolder -Client $ImapClient -Verbose\nDisconnect-IMAP -Client $ImapClient\n\n# And with POP3\n$PopClient = Connect-POP3 -Server 'outlook.office365.com' -Port 995 -Options Auto `\n    -Credential $Credential -OAuth2\nDisconnect-POP3 -Client $PopClient\n"
  },
  {
    "path": "Examples/Example-ClearGraphJunk.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$graph = Connect-EmailGraph -ClientId 'id' -DirectoryId 'tenant' -ClientSecretSecretName 'graph-client-secret' -ClientSecretVaultName 'MailSecrets'\n$graph | Out-Null\n\n# Example 1: remove everything from junk\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph\n\n# Example 2: preview cleanup using -Preview\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -Preview\n\n# Example 3: simulate cleanup with -WhatIf\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -WhatIf\n\n# Example 4: skip messages from a sender\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipFrom 'boss@example.com'\n\n# Example 5: skip messages sent to a specific recipient\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipTo 'reports@example.com'\n\n# Example 6: skip messages containing a subject keyword\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipSubjectContains 'VIP'\n\n# Example 7: skip specific message IDs\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipId '1','2','3'\n\n# Example 8: combine multiple skip filters\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipFrom 'boss@example.com' -SkipSubjectContains 'Important'\n\n# Example 9: skip messages that have any attachment\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipHasAttachment\n\n# Example 10: skip messages containing PDF attachments\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -SkipAttachmentExtension 'pdf'\n\n# Example 11: preview with additional properties\nClear-GraphJunk -UserPrincipalName 'user@example.com' -Connection $graph -Preview -Property subject,from\n\n# Example 12: clean junk via raw Graph requests\nClear-GraphJunk -UserPrincipalName 'user@example.com' -MgGraphRequest\n\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-ClearImapJunk.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# Example 1: clear default junk folder\nClear-IMAPJunk -Client $imap\n\n# Example 2: preview messages instead of removing them\nClear-IMAPJunk -Client $imap -Preview\n\n# Example 3: simulate cleanup with -WhatIf\nClear-IMAPJunk -Client $imap -WhatIf\n\n# Example 4: skip messages from specific sender\nClear-IMAPJunk -Client $imap -SkipFrom 'boss@example.com'\n\n# Example 5: skip messages sent to a recipient\nClear-IMAPJunk -Client $imap -SkipTo 'reports@example.com'\n\n# Example 6: skip messages containing subject keyword\nClear-IMAPJunk -Client $imap -SkipSubjectContains 'Important'\n\n# Example 7: skip messages with given IDs\nClear-IMAPJunk -Client $imap -SkipMessageId '<id1@example.com>','<id2@example.com>'\n\n# Example 8: skip messages with given UIDs\nClear-IMAPJunk -Client $imap -SkipUid 1000,1001\n\n\n# Example 9: skip messages that have attachments\nClear-IMAPJunk -Client $imap -SkipHasAttachment\n\n# Example 10: skip messages containing ZIP attachments\nClear-IMAPJunk -Client $imap -SkipAttachmentExtension 'zip'\n\n# Example 11: combine multiple skip options\nClear-IMAPJunk -Client $imap -SkipFrom 'boss@example.com' -SkipSubjectContains 'Internal'\n# Example 12: clear a custom junk folder\nClear-IMAPJunk -Client $imap -Folder 'Spam/Junk'\n\nDisconnect-IMAP -Client $imap\n"
  },
  {
    "path": "Examples/Example-ConnecToImap-01.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$UserName = 'email@gmail.com'\n$Password = ''\n\n$Client = Connect-IMAP -Server 'imap.gmail.com' -Password $Password -UserName $UserName -Port 993 -Options Auto\n\nGet-IMAPFolder -Client $Client -Verbose\nGet-IMAPMessage -Client $Client -FromContains 'contoso.com' -HasAttachment\n\nDisconnect-IMAP -Client $Client"
  },
  {
    "path": "Examples/Example-ConnecToImap-02.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credential = Get-Credential\n$Client = Connect-IMAP -Server 'imap.gmail.com' -Credential $Credential -Port 993 -Options Auto\nGet-IMAPFolder -Client $Client -Verbose\nGet-IMAPMessage -Client $Client -Subject 'Alert' -Since (Get-Date).AddDays(-1)\n\nDisconnect-IMAP -Client $Client"
  },
  {
    "path": "Examples/Example-ConnecToImap-03-oAuth.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientID = '9393330741'\n$ClientSecret = 'gk2ztAGU'\n\n$oAuth2 = Connect-OAuthGoogle -ClientID $ClientID -ClientSecret $ClientSecret -GmailAccount 'evotectest@gmail.com' -Scope https://mail.google.com/\n$Client = Connect-IMAP -Server 'imap.gmail.com' -Port 993 -Options Auto -Credential $oAuth2 -OAuth2\n\nGet-IMAPFolder -Client $Client -Verbose\n# Delete messages from a specific sender using OAuth authentication\nGet-IMAPMessage -Client $Client -FromContains 'alerts@example.com' -Since (Get-Date).AddDays(-7) -Delete\n\nDisconnect-IMAP -Client $Client -Verbose\n\n"
  },
  {
    "path": "Examples/Example-ConnecToPop3-01.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$UserName = 'Test@gmail.com'\n$Password = 'TextPassword'\n\n$Client = Connect-POP3 -Server 'pop.gmail.com' -Password $Password -UserName $UserName -Port 995 -Options Auto\nGet-POP3Message -Client $Client -Subject 'Report' -Since (Get-Date).AddDays(-7)\nSave-POP3Message -Client $Client -Index 6 -Path \"$Env:UserProfile\\Desktop\\mail.eml\"\nSave-POP3Message -Client $Client -Index 6 -Path \"$Env:UserProfile\\Desktop\\mail.msg\"\nDisconnect-POP3 -Client $Client\n"
  },
  {
    "path": "Examples/Example-ConnecToPop3-02.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credentials = Get-Credential\n$Client = Connect-POP3 -Server 'pop.gmail.com' -Credential $Credentials -Port 995 -Options Auto\nGet-POP3Message -Client $Client -FromContains 'contoso.com' -Priority High | Format-Table\nSave-POP3Message -Client $Client -Index 6 -Path \"$Env:UserProfile\\Desktop\\mail.eml\"\nSave-POP3Message -Client $Client -Index 6 -Path \"$Env:UserProfile\\Desktop\\mail.msg\"\nDisconnect-POP3 -Client $Client\n"
  },
  {
    "path": "Examples/Example-ConnecToPop3-03-oAuth.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientID = '93933307418'\n$ClientSecret = 'gk2ztAG'\n\n$oAuth2 = Connect-OAuthGoogle -ClientID $ClientID -ClientSecret $ClientSecret -GmailAccount 'email@gmail.com' -Scope https://mail.google.com/\n$Client = Connect-POP3 -Server 'pop.gmail.com' -Credential $oAuth2 -Port 995 -Options Auto -OAuth2\nGet-POP3Message -Client $Client -ToContains 'sales@example.com' -HasAttachment | Format-Table\nSave-POP3Message -Client $Client -Index 7 -Path \"$Env:UserProfile\\Desktop\\mail7.eml\"\nSave-POP3Message -Client $Client -Index 7 -Path \"$Env:UserProfile\\Desktop\\mail7.msg\"\nDisconnect-POP3 -Client $Client -Verbose\n\n"
  },
  {
    "path": "Examples/Example-ConnectEmailGraph-DeviceCode.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'Your-Application-ID'\n$TenantId = 'Your-Tenant-ID'\n$Scopes = @('Mail.ReadWrite','Mail.Send')\n\n# Authenticate using device code flow\n$graph = Connect-EmailGraph -ClientId $ClientId -DirectoryId $TenantId -DeviceCode -Scopes $Scopes -MaxConcurrentRequests 10\n$graph\n"
  },
  {
    "path": "Examples/Example-ConnectEmailGraph-OnBehalfOf.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'Your-Application-ID'\n$TenantId = 'Your-Tenant-ID'\n$ClientSecret = 'Your-Client-Secret'\n$UserToken = Get-Content -Raw -Path './UserToken.txt'\n$Scopes = @('Mail.ReadWrite','Mail.Send')\n\n# Exchange existing token for Graph access\n$graph = Connect-EmailGraph -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId `\n    -OnBehalfOfToken $UserToken -Scopes $Scopes\n$graph\n"
  },
  {
    "path": "Examples/Example-ConvertFromEmlToMsg.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Conversion = ConvertFrom-EmlToMsg -InputPath \"$PSScriptRoot\\Input\\Sample.eml\" -OutputFolder \"$PSScriptRoot\\Output\" -Verbose -Force\n$Conversion | Format-Table\nif ($Conversion.Status) {\n    $Msg = Import-MailFile -FilePath $Conversion.MsgFile\n    $Msg | Format-Table\n    $Msg.Dispose()\n}"
  },
  {
    "path": "Examples/Example-DeleteImapMessages.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credential = Get-Credential\n$Client = Connect-IMAP -Server 'imap.example.com' -Credential $Credential -Port 993 -Options Auto\n\n# Delete messages from the last week containing \"Report\" in the subject\nGet-IMAPMessage -Client $Client -Subject 'Report' -Since (Get-Date).AddDays(-7) -Delete\n\nDisconnect-IMAP -Client $Client\n\n"
  },
  {
    "path": "Examples/Example-DeletePop3Messages.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $credential -Port 995 -Options Auto\n\n# Delete messages older than 30 days that contain \"Report\" in the subject\nGet-POP3Message -Client $client -Subject 'Report' -Before (Get-Date).AddDays(-30) -Delete\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-EmailPendingMessages.ps1",
    "content": "$pending = Join-Path $PSScriptRoot 'pending'\n\n# Existing pending queues created prior to credential protection will still replay,\n# and the entries will be re-encrypted with the new protector the next time they are\n# saved by the module (for example, after another delivery attempt).\n\n# Review queued messages\nGet-EmailPendingMessage -PendingMessagesPath $pending\n\n# Replay messages. Server details and credentials are taken from\n# the pending log so each message is sent using its original\n# configuration, allowing mixed servers in a single file.\nSend-EmailPendingMessage -PendingMessagesPath $pending\n\n# Replay a specific message (and optionally restrict to a provider)\n# $id = 'message-id'\n# Send-EmailPendingMessage -PendingMessagesPath $pending -MessageId $id -Provider None\n\n# Remove a specific message by ID\n# Remove-EmailPendingMessage -PendingMessagesPath $pending -MessageId $id\n"
  },
  {
    "path": "Examples/Example-ExtractLocalImagePaths.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$tmp = New-TemporaryFile\nSet-Content -Path $tmp -Value \"data\"\n$html = \"<img src='$tmp'><p>$tmp</p>\"\n\n$result = [Mailozaurr.HtmlUtils]::ExtractLocalImagePaths($html)\n$result.Html\n$result.Paths\n\nRemove-Item $tmp\n\n"
  },
  {
    "path": "Examples/Example-ForwardMessageAttachmentFiltered.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Forward IMAP messages without attachments\n$imapCred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $imapCred -Port 993 -Options Auto\nGet-IMAPMessage -Client $imap -FromContains 'reports@example.com' -HasAttachment |\n    ForEach-Object {\n        $forward = $_.MimeMessage | Remove-IMAPMessageAttachment\n        Send-EmailMessage -From 'forwarder@example.com' -To 'team@example.com' -Subject $_.MimeMessage.Subject -Message $forward -Server 'smtp.example.com' -Port 25 -WhatIf\n    }\nDisconnect-IMAP -Client $imap\n\n# Forward POP3 messages without attachments\n$popCred = Get-Credential\n$pop = Connect-POP3 -Server 'pop.example.com' -Credential $popCred -Port 995 -Options Auto\nGet-POP3Message -Client $pop -Subject 'Alert' -HasAttachment |\n    ForEach-Object {\n        $forward = $_.MimeMessage | Remove-POP3MessageAttachment\n        Send-EmailMessage -From 'forwarder@example.com' -To 'team@example.com' -Subject $_.MimeMessage.Subject -Message $forward -Server 'smtp.example.com' -Port 25 -WhatIf\n    }\nDisconnect-POP3 -Client $pop\n\n# Forward Microsoft Graph messages without attachments\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n$graphCred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\nConnect-EmailGraph -Credential $graphCred | Out-Null\nGet-EmailGraphMessage -UserPrincipalName 'user@example.com' -Filter \"hasAttachments eq true\" -Limit 5 |\n    ForEach-Object {\n        $forward = Remove-GraphMessageAttachment -Message $_\n        Send-EmailMessage -From 'forwarder@example.com' -To 'team@example.com' `\n            -Subject $forward.Subject -Body $forward.Body.Content -Graph -Credential $graphCred -WhatIf\n    }\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-GetEmailDeliveryMatch.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Correlate non-delivery reports with sent messages\n$repo = [Mailozaurr.FileSentMessageRepository]::new(\"$env:TEMP\\sendlog.json\")\n$resolver = [Mailozaurr.SendLogResolver]::new($repo)\nConnect-IMAP -Server 'imap.example.com' -Credential (Get-Credential) | Out-Null\nGet-EmailDeliveryMatch -Protocol Imap -Resolver $resolver -Recipient 'user@contoso.com'\nDisconnect-IMAP\n"
  },
  {
    "path": "Examples/Example-GetEmailDeliveryStatus.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Search recent non-delivery reports via IMAP\nConnect-IMAP -Server 'imap.example.com' -Credential (Get-Credential) | Out-Null\nGet-EmailDeliveryStatus -Protocol Imap -Recipient 'user@contoso.com' -Since (Get-Date).AddDays(-7) -ParallelDownloadLimit 8\nDisconnect-IMAP\n"
  },
  {
    "path": "Examples/Example-GetGraphDmarcReport.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'Your-Application-ID'\n$TenantId = 'Your-Tenant-ID'\n$Scopes = @('Mail.Read')\n\nConnect-EmailGraph -ClientId $ClientId -DirectoryId $TenantId -DeviceCode -Scopes $Scopes | Out-Null\nGet-DmarcReport -Protocol Graph -UserPrincipalName 'user@example.com' -Since (Get-Date).AddDays(-7) |\n    ForEach-Object {\n        foreach ($att in $_.Attachments) {\n            # Pass the zipped XML stream to Domain Detective for analysis\n            # Invoke-DomainDetective -InputStream $att.Content -Name $att.Name\n        }\n    }\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-GetGraphEvent.ps1",
    "content": "# Retrieve upcoming events for a user\n$cred = Connect-EmailGraph -ClientId 'client' -TenantId 'tenant' -ClientSecret 'secret'\nGet-GraphEvent -UserPrincipalName 'user@example.com' -Connection $cred -Limit 5\n"
  },
  {
    "path": "Examples/Example-GetGraphMailboxStatistics-Console.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$graph = Connect-EmailGraph -ClientId 'id' -DirectoryId 'tenant' -ClientSecretSecretName 'graph-client-secret' -ClientSecretVaultName 'MailSecrets'\n\n$stats = Get-GraphMailboxStatistics -UserPrincipalName 'user@example.com' -Connection $graph\n\n$stats | Format-List\n\nforeach ($folder in $stats.FolderStatistics) {\n    \"{0,-30} {1,5} unread: {2,5}\" -f $folder.DisplayName, $folder.TotalItemCount, $folder.UnreadItemCount\n}\n"
  },
  {
    "path": "Examples/Example-GetGraphMailboxStatistics.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$graph = Connect-EmailGraph -ClientId 'id' -DirectoryId 'tenant' -ClientSecretSecretName 'graph-client-secret' -ClientSecretVaultName 'MailSecrets'\n\nGet-GraphMailboxStatistics -UserPrincipalName 'user@example.com' -Connection $graph |\n    Select-Object UserPrincipalName, MessageCount, MessagesWithAttachments, TotalAttachmentSize, TotalFolders, FolderStatistics |\n    Export-Csv -NoTypeInformation 'mailbox-stats.csv'\n"
  },
  {
    "path": "Examples/Example-GetImapDmarcReport.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nConnect-IMAP -Server 'imap.example.com' -Credential (Get-Credential) | Out-Null\nGet-DmarcReport -Protocol Imap -Folder 'INBOX' -Since (Get-Date).AddDays(-7) |\n    ForEach-Object {\n        foreach ($att in $_.Attachments) {\n            # Pass the zipped XML stream to Domain Detective for analysis\n            # Invoke-DomainDetective -InputStream $att.Content -Name $att.Name\n        }\n    }\nDisconnect-IMAP\n"
  },
  {
    "path": "Examples/Example-GetMailFolder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# using application permissions\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\nConnect-EmailGraph -Credential $cred | Out-Null\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com'\nDisconnect-EmailGraph\n\n# using Connect-MgGraph\nImport-Module Microsoft.Graph.Authentication -Force\nConnect-MgGraph -Scopes Mail.Read -NoWelcome\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' -MgGraphRequest\n"
  },
  {
    "path": "Examples/Example-GetPop3DmarcReport.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nConnect-POP3 -Server 'pop3.example.com' -Credential (Get-Credential) | Out-Null\nGet-DmarcReport -Protocol Pop3 -Since (Get-Date).AddDays(-7) |\n    ForEach-Object {\n        foreach ($att in $_.Attachments) {\n            # Pass the zipped XML stream to Domain Detective for analysis\n            # Invoke-DomainDetective -InputStream $att.Content -Name $att.Name\n        }\n    }\nDisconnect-POP3\n"
  },
  {
    "path": "Examples/Example-GmailApi-01-OAuth.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Connect-OAuthGoogle launches a browser sign-in flow and returns a credential with the acquired token\n$clientSecret = Read-Host 'Google client secret' -AsSecureString\n$oauthCred = Connect-OAuthGoogle -GmailAccount 'user@gmail.com' -ClientID 'id' -ClientSecretSecureString $clientSecret -Scope https://mail.google.com/\n\n# If you already obtained an access token elsewhere, ConvertTo-OAuth2Credential wraps it as a PSCredential\n$accessToken = Read-Host 'OAuth access token' -AsSecureString\n$tokenCred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -TokenSecureString $accessToken\n\n$oauthCred\n$tokenCred\n"
  },
  {
    "path": "Examples/Example-GmailApi-02-SendMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSend-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred `\n    -From 'user@gmail.com' -To 'recipient@example.com' `\n    -Subject 'Test API message' -TextBody 'Hello from Gmail API'\n"
  },
  {
    "path": "Examples/Example-GmailApi-03-SendEmailMessageProvider.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSend-EmailMessage -EmailProvider Gmail -GmailAccount 'user@gmail.com' `\n    -From 'user@gmail.com' -To 'recipient@example.com' -Credential $cred \\\n    -Subject 'Provider example' -Body 'Sent via Send-EmailMessage'\n"
  },
  {
    "path": "Examples/Example-GmailApi-04-ListMessages.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n# List last 5 unread alerts\nGet-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred `\n    -Query 'from:alerts@example.com is:unread' -MaxResults 5\n"
  },
  {
    "path": "Examples/Example-GmailApi-05-GetMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n# Retrieve a single message by id and save it\n$msg = Get-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred -Id 'abcd1234'\n$msg.Raw | Out-File \"$Env:TEMP/message.eml\"\n"
  },
  {
    "path": "Examples/Example-GmailApi-06-SaveAttachments.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSave-GmailMessageAttachment -GmailAccount 'user@gmail.com' -Credential $cred -Id 'abcd1234' -Path $Env:TEMP\n"
  },
  {
    "path": "Examples/Example-GmailApi-07-DeleteMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nRemove-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred -Id 'abcd1234' -Confirm:$false\n"
  },
  {
    "path": "Examples/Example-GmailApi-08-FilterAndDelete.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n$messages = Get-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred `\n    -Query 'subject:\"Report\" older_than:7d'\nforeach ($m in $messages) {\n    Remove-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred -Id $m.Id\n}\n"
  },
  {
    "path": "Examples/Example-GmailApi-09-DownloadAttachments.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n$messages = Get-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred `\n    -Query 'has:attachment newer_than:1d'\nforeach ($m in $messages) {\n    Save-GmailMessageAttachment -GmailAccount 'user@gmail.com' -Credential $cred -Id $m.Id -Path $Env:TEMP\n}\n"
  },
  {
    "path": "Examples/Example-GmailApi-10-SendWithAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSend-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred `\n    -From 'user@gmail.com' -To 'recipient@example.com' `\n    -Subject 'Report' -TextBody 'See attached file' -Attachment 'C:\\Temp\\report.txt'\n"
  },
  {
    "path": "Examples/Example-GmailApi-11-ListThreads.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n# List last 5 threads with alerts\nGet-GmailThread -GmailAccount 'user@gmail.com' -Credential $cred `\n    -Query 'from:alerts@example.com' -MaxResults 5\n"
  },
  {
    "path": "Examples/Example-GmailApi-12-GetThread.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n# Retrieve a thread and output snippets\n$thread = Get-GmailThread -GmailAccount 'user@gmail.com' -Credential $cred -Id 'threadId'\n$thread.Messages | ForEach-Object { $_.Snippet }\n"
  },
  {
    "path": "Examples/Example-GmailApi-13-SendWithHeaders.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSend-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred \\\n    -From 'user@gmail.com' -To 'recipient@example.com' \\\n    -Subject 'Test API message with headers' -TextBody 'Hello from Gmail API' \\\n    -Headers @{ 'X-Custom-Header' = 'Value' }\n"
  },
  {
    "path": "Examples/Example-GmailApi-14-SearchNonDeliveryReports.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\n# Search for non-delivery reports in Gmail\n$reports = Get-EmailDeliveryStatus -Protocol GmailApi -GmailAccount 'user@gmail.com' -Credential $cred -Since (Get-Date).AddDays(-7)\n\n$reports | Format-Table FinalRecipient, OriginalMessageId, Type\n\n"
  },
  {
    "path": "Examples/Example-GmailApi-15-GetDmarcReport.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$clientSecret = Read-Host 'Google client secret' -AsSecureString\n$cred = Connect-OAuthGoogle -GmailAccount 'user@gmail.com' -ClientID 'id' -ClientSecretSecureString $clientSecret -Scope https://mail.google.com/\n\nGet-DmarcReport -Protocol GmailApi -GmailAccount 'user@gmail.com' -Credential $cred -Since (Get-Date).AddDays(-7) |\n    ForEach-Object {\n        foreach ($att in $_.Attachments) {\n            # Pass the zipped XML stream to Domain Detective for analysis\n            # Invoke-DomainDetective -InputStream $att.Content -Name $att.Name\n        }\n    }\n"
  },
  {
    "path": "Examples/Example-GraphMailboxPermissions.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Graph application credentials\n$ClientId     = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId     = 'your-tenant-id'\n\n$cred  = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n# Display existing permissions for the mailbox\n$perms = Get-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com'\n$perms | Format-Table Id, GrantedTo, Roles\n\n# Grant permissions using typed objects and enum roles\n$permissions = @(\n    New-GraphMailboxPermissionObject -Roles Owner -GrantedToUser 'julia@example.com' -UserPrincipalName 'user@example.com',\n    New-GraphMailboxPermissionObject -Roles Read -GrantedToUser 'ken@example.com' -UserPrincipalName 'user@example.com'\n)\nAdd-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -MailboxPermission $permissions\n\n# Alternatively build permissions using a builder object\n$builder = New-GraphMailboxPermissionBuilder -GrantedToUser 'melanie@example.com' -Roles Write\n$permObj = New-GraphMailboxPermissionObject -Builder $builder -UserPrincipalName 'user@example.com'\n\n# Bulk add permissions from CSV (columns should map to Graph permission properties)\nAdd-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -CsvPath '.\\permissions.csv'\n\n# Review permissions after additions\nGet-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com'\n\n# Remove a single permission by id\nRemove-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -PermissionId 'permission-id'\n\n# Remove permissions granted to a specific user\nRemove-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -GrantedToUser 'ken@example.com'\n\n# Remove all owner permissions\nRemove-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -Role Owner\n\n# Advanced: remove using a permission object\n($perms)[0].RemoveAsync($graph.Credential) | Out-Null\n\n# Remove multiple permissions from CSV (file requires PermissionId column)\nRemove-GraphMailboxPermission -Connection $graph -UserPrincipalName 'user@example.com' -CsvPath '.\\permissionsToRemove.csv'\n\n# Using Microsoft.Graph commands directly\nImport-Module Microsoft.Graph.Authentication -Force\nConnect-MgGraph -Scopes 'Mail.ReadWrite' -NoWelcome\nGet-GraphMailboxPermission -UserPrincipalName 'user@example.com' -MgGraphRequest\n\nDisconnect-EmailGraph -Connection $graph\n\n"
  },
  {
    "path": "Examples/Example-GraphManageMessages.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Example: Download attachments and archive the message\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\nConnect-EmailGraph -Credential $cred | Out-Null\n\n$messages = Get-EmailGraphMessage -UserPrincipalName 'user@example.com' -Filter \"hasAttachments eq true\" -Limit 5\nforeach ($m in $messages) {\n    Get-EmailGraphMessageAttachment -UserPrincipalName 'user@example.com' -MessageId $m.Id |\n        Save-GraphMessageAttachment -Path 'C:\\Temp\\Attachments'\n    Set-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $m.Id -Read\n    Move-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $m.Id -DestinationFolderId 'Archive'\n}\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-ImapFilterScenarios.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# 1. Find messages with a specific subject\nGet-IMAPMessage -Client $client -Subject 'Invoice'\n\n# 2. Messages from a sender in the last 7 days\nGet-IMAPMessage -Client $client -FromContains 'alerts@example.com' -Since (Get-Date).AddDays(-7)\n\n# 3. Messages sent to a department mailbox\nGet-IMAPMessage -Client $client -ToContains 'sales@example.com'\n\n# 4. High priority messages only\nGet-IMAPMessage -Client $client -Priority High\n\n# 5. Messages that have attachments\nGet-IMAPMessage -Client $client -HasAttachment\n\n# 6. Delete old reports older than 30 days\nGet-IMAPMessage -Client $client -Subject 'Report' -Before (Get-Date).AddDays(-30) -Delete\n\n# 7. Fetch messages by UID range\nGet-IMAPMessage -Client $client -UidStart 1 -UidEnd 20\n\n# 8. Fetch messages by sequence numbers\nGet-IMAPMessage -Client $client -SequenceStart 1 -SequenceEnd 5\n\n# 9. Messages from the last 24 hours that include attachments\nGet-IMAPMessage -Client $client -Since (Get-Date).AddDays(-1) -HasAttachment\n\n# 10. Delete high priority messages from a specific sender\nGet-IMAPMessage -Client $client -FromContains 'alerts@example.com' -Priority High -Delete\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-ImportMSG.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Msg = Import-MailFile -FilePath \"$PSScriptRoot\\Input\\TestMessage.msg\"\n$Msg | Format-Table\n\n$Eml = Import-MailFile -FilePath \"$PSScriptRoot\\Input\\Sample.eml\"\n$Eml | Format-Table"
  },
  {
    "path": "Examples/Example-ListImapFolderContents.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Authenticate however you like\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# list inbox messages\nGet-IMAPFolder -Client $client\nGet-IMAPMessage -Client $client | Select-Object -First 10\n\n# open a nested folder\nGet-IMAPFolder -Client $client -Path 'Inbox/Reports/2024'\nGet-IMAPMessage -Client $client | Select-Object -First 10\n\n# sent items\nGet-IMAPFolder -Client $client -Path 'Sent'\nGet-IMAPMessage -Client $client | Select-Object -First 10\n\n# deleted items\nGet-IMAPFolder -Client $client -Path 'Deleted Items'\nGet-IMAPMessage -Client $client | Select-Object -First 10\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-ListImapRootFolders.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Acquire credentials using your preferred method\n$credential = Get-Credential\n\n# Connect and list root folders\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $credential -Port 993 -Options Auto\nGet-IMAPFolder -Client $client -Root\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-ListImapRootFoldersOAuth.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Acquire OAuth token first\n$clientSecret = Read-Host 'Google client secret' -AsSecureString\n$token = Connect-OAuthGoogle -ClientId 'id' -ClientSecretSecureString $clientSecret -GmailAccount 'user@example.com' -Scope https://mail.google.com/\n\n# Connect using the OAuth token and list folders\n$client = Connect-IMAP -Server 'imap.gmail.com' -Port 993 -Credential $token -OAuth2\nGet-IMAPFolder -Client $client -Root\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-MoveMailFolder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# ----------------------\n# Move IMAP folder\n# ----------------------\n$cred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# verify folders exist before moving\nGet-IMAPFolder -Client $imap -Root | Select-Object FullName\nGet-IMAPFolder -Client $imap -Path 'Archive'\n\nMove-IMAPFolder -Client $imap -Folder 'OldFolder' -DestinationFolder 'Archive' -WhatIf\n\n# confirm destination folder after move\nGet-IMAPFolder -Client $imap -Path 'Archive'\n\nMove-IMAPFolder -Client $imap -Folder 'Sent/Reports' -DestinationFolder 'Inbox' -WhatIf\nMove-IMAPFolder -Client $imap -Folder 'Drafts/Sub' -Root -WhatIf\nDisconnect-IMAP -Client $imap\n\n# ----------------------\n# Move Microsoft Graph folder\n# ----------------------\n$graphCred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\nConnect-EmailGraph -Credential $graphCred | Out-Null\n\n# verify folders before move\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' | Select-Object displayName,id\n\nMove-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'folder-id' -DestinationFolderId 'archive-id' -WhatIf\n\n# confirm destination after move\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' -Connection $graphCred | Where-Object id -EQ 'archive-id'\n\nMove-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'sent-id' -DestinationFolderId 'inbox-id' -WhatIf\nMove-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'draft-id' -Root -WhatIf\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-NewGraphEvent.ps1",
    "content": "# Create a simple event\n$cred = Connect-EmailGraph -ClientId 'client' -TenantId 'tenant' -ClientSecret 'secret'\n$builder = New-GraphEventBuilder -Subject 'Demo Meeting' -Start (Get-Date).AddHours(1) -End (Get-Date).AddHours(2) -Attendees 'user@example.com'\nNew-GraphEvent -UserPrincipalName 'user@example.com' -EventBuilder $builder -Connection $cred\n"
  },
  {
    "path": "Examples/Example-ParseGraphError.ps1",
    "content": "$sample = @'\nPOST https://graph.microsoft.com/v1.0/users/przemyslaw.klys@company.pl/sendMail\nHTTP/2.0 404 Not Found\nrequest-id: 2ff18766-1395-4fb9-abd1-162774d4b063\nclient-request-id: 6c57f9e6-3cad-48ee-8f7a-d566dc92aca3\nx-ms-ags-diagnostic: {\"ServerInfo\":{\"DataCenter\":\"Poland Central\",\"Slice\":\"E\",\"Ring\":\"2\",\"ScaleUnit\":\"002\",\"RoleInstance\":\"WA3PEPF000004A2\"}}\nDate: Sun, 24 Aug 2025 12:55:18 GMT\nContent-Type: application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8\n\n{\"error\":{\"code\":\"ErrorInvalidUser\",\"message\":\"The requested user 'przemyslaw.klys@company.pl' is invalid.\"}}\n'\n\n$parsed = [Mailozaurr.GraphApiErrorParser]::Parse($sample)\n$parsed.Headers.Diagnostic.ServerInfo\n$parsed.Error\n"
  },
  {
    "path": "Examples/Example-Pop3FilterScenarios.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $cred -Port 995 -Options Auto\n\n# 1. Retrieve messages with subject 'Invoice'\nGet-POP3Message -Client $client -Subject 'Invoice'\n\n# 2. Messages from a sender in the past week\nGet-POP3Message -Client $client -FromContains 'alerts@example.com' -Since (Get-Date).AddDays(-7)\n\n# 3. Messages sent to the sales team\nGet-POP3Message -Client $client -ToContains 'sales@example.com'\n\n# 4. High priority messages\nGet-POP3Message -Client $client -Priority High\n\n# 5. Messages that include attachments\nGet-POP3Message -Client $client -HasAttachment\n\n# 6. Delete old reports older than 30 days\nGet-POP3Message -Client $client -Subject 'Report' -Before (Get-Date).AddDays(-30) -Delete\n\n# 7. Retrieve all messages and remove them\nGet-POP3Message -Client $client -All -Delete\n\n# 8. Fetch messages delivered after a date\nGet-POP3Message -Client $client -Since (Get-Date).AddDays(-14)\n\n# 9. Fetch messages delivered before a date\nGet-POP3Message -Client $client -Before (Get-Date).AddDays(-60)\n\n# 10. Delete messages from a specific sender with attachments\nGet-POP3Message -Client $client -FromContains 'newsletter@example.com' -HasAttachment -Delete\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-ProcessPendingMessages.ps1",
    "content": "$pending = Join-Path $PSScriptRoot 'pending'\n$sent = Join-Path $PSScriptRoot 'sentlog.json'\n$smtp = [Mailozaurr.Smtp]::new()\n$smtp.Server = 'smtp.server'\n$smtp.PendingMessagesPath = $pending\n$smtp.SentMessageRepository = [Mailozaurr.FileSentMessageRepository]::new($sent)\n$smtp.ProcessPendingMessagesAsync().GetAwaiter().GetResult()\n\n"
  },
  {
    "path": "Examples/Example-QueryLanguageSearch.ps1",
    "content": "# Demonstrates searching an IMAP mailbox using query language\nImport-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential (Get-Credential)\n\n# search using query string for messages from boss with subject containing \"report\"\n$result = Search-IMAPMailbox -Client $imap -Query 'from:boss subject:\"report\"'\n\n$result | ForEach-Object {\n    Write-Host \"Found message $($_.Message.Subject)\"\n}\n\nDisconnect-IMAP -Client $imap\n"
  },
  {
    "path": "Examples/Example-ReadGraphEncryption.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = ConvertTo-GraphCredential -ClientId 'clientId' -ClientSecret 'secret' -DirectoryId 'tenantId'\nConnect-EmailGraph -Credential $cred | Out-Null\n\n$msg = Get-EmailGraphMessage -UserPrincipalName 'user@example.com' -First 1\n$mime = $msg | Get-EmailGraphMessageMime\nWrite-Host \"Message $($msg.Id) encryption: $($mime.Encryption)\"\n$decrypted = $mime\nif ($mime.Encryption -eq [Mailozaurr.EmailEncryption]::PgpEncrypted) {\n    $decrypted = $mime | Unprotect-MimeMessage -PrivateKeyPath 'C:\\Keys\\private.asc' -PrivateKeyPassword 'passphrase'\n} elseif ($mime.Encryption -eq [Mailozaurr.EmailEncryption]::SmimeEncrypted) {\n    $cert = Get-PfxCertificate -FilePath 'C:\\Keys\\email.pfx' -Password (ConvertTo-SecureString 'pfx-pass' -AsPlainText -Force)\n    $decrypted = $mime | Unprotect-MimeMessage -Certificate $cert\n}\n$decrypted | Save-MimeMessage -Path \"$Env:TEMP\\graph.eml\"\n$content = $decrypted | Get-MimeMessageContent\n$content.TextBody\n$content.HtmlBody\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-ReadImapEncryption.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# Fetch messages and detect encryption\nGet-IMAPMessage -Client $client -All |\n    ForEach-Object {\n        Write-Host \"Message $($_.Uid.Id) encryption: $($_.Encryption)\"\n        $decrypted = $_\n        if ($_.Encryption -eq [Mailozaurr.EmailEncryption]::PgpEncrypted) {\n            $decrypted = $_ | Unprotect-MimeMessage -PrivateKeyPath 'C:\\Keys\\private.asc' -PrivateKeyPassword 'passphrase'\n        } elseif ($_.Encryption -eq [Mailozaurr.EmailEncryption]::SmimeEncrypted) {\n            $cert = Get-PfxCertificate -FilePath 'C:\\Keys\\email.pfx' -Password (ConvertTo-SecureString 'pfx-pass' -AsPlainText -Force)\n            $decrypted = $_ | Unprotect-MimeMessage -Certificate $cert\n        }\n        $decrypted | Save-MimeMessage -Path \"$Env:TEMP\\$($_.Uid.Id).eml\"\n        $content = $decrypted | Get-MimeMessageContent\n        $content.TextBody\n        $content.HtmlBody\n    }\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-ReadPop3Encryption.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $cred -Port 995 -UseSsl\n\nGet-POP3Message -Client $client -All |\n    ForEach-Object {\n        Write-Host \"Message $($_.Index) encryption: $($_.Encryption)\"\n        $decrypted = $_\n        if ($_.Encryption -eq [Mailozaurr.EmailEncryption]::PgpEncrypted) {\n            $decrypted = $_ | Unprotect-MimeMessage -PrivateKeyPath 'C:\\Keys\\private.asc' -PrivateKeyPassword 'passphrase'\n        } elseif ($_.Encryption -eq [Mailozaurr.EmailEncryption]::SmimeEncrypted) {\n            $cert = Get-PfxCertificate -FilePath 'C:\\Keys\\email.pfx' -Password (ConvertTo-SecureString 'pfx-pass' -AsPlainText -Force)\n            $decrypted = $_ | Unprotect-MimeMessage -Certificate $cert\n        }\n        $decrypted | Save-MimeMessage -Path \"$Env:TEMP\\$($_.Index).eml\"\n        $content = $decrypted | Get-MimeMessageContent\n        $content.TextBody\n        $content.HtmlBody\n    }\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-RemoveGmailMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Acquire OAuth2 credential using Connect-OAuthGoogle\n# $cred = Connect-OAuthGoogle -GmailAccount 'user@gmail.com' -ClientID 'id' -ClientSecret 'secret'\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nRemove-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred -Id 'abcd1234' -WhatIf\n"
  },
  {
    "path": "Examples/Example-RemoveMailFolder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# ----------------------\n# Remove IMAP folder\n# ----------------------\n$cred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# confirm folder presence before deletion\nGet-IMAPFolder -Client $imap -Path 'OldFolder'\n\nRemove-IMAPFolder -Client $imap -Folder 'OldFolder' -WhatIf\n\nRemove-IMAPFolder -Client $imap -Folder 'Archive/Reports' -Recursive -WhatIf\n\n# list archive folder after removal attempt\nGet-IMAPFolder -Client $imap -Path 'Archive'\nDisconnect-IMAP -Client $imap\n\n# ----------------------\n# Remove Microsoft Graph folder\n# ----------------------\n$graphCred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\nConnect-EmailGraph -Credential $graphCred | Out-Null\n\n# check folder before deletion\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' | Select-Object displayName,id\n\nRemove-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'folder-id' -WhatIf\n\n# confirm folder removed\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' -Connection $graphCred | Where-Object id -EQ 'folder-id'\n\nRemove-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'nested-id' -WhatIf\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-RemoveMessageAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Create a MIME message and strip attachments before forwarding\n$builder = [MimeKit.BodyBuilder]::new()\n$builder.TextBody = 'Forwarding without attachments'\n$path = Join-Path $PSScriptRoot 'sample.txt'\n'content' | Set-Content -Path $path\n$builder.Attachments.Add($path) | Out-Null\n$msg = [MimeKit.MimeMessage]::new()\n$msg.From.Add([MimeKit.MailboxAddress]::new('Sender','sender@example.com'))\n$msg.To.Add([MimeKit.MailboxAddress]::new('Recipient','recipient@example.com'))\n$msg.Subject = 'Sample'\n$msg.Body = $builder.ToMessageBody()\n\n$msg = Remove-IMAPMessageAttachment -Message $msg\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject $msg.Subject -Body $builder.TextBody -Server 'smtp.example.com' -Port 25 -Message $msg -WhatIf\n\n# For Graph messages\n$file = Join-Path $PSScriptRoot 'sample2.txt'\n'content2' | Set-Content -Path $file\n$graphMsg = [Mailozaurr.GraphMessage]::new()\n$graphMsg.Subject = 'Graph sample'\n$graphMsg.Body = [Mailozaurr.GraphContent]::new()\n$graphMsg.Attachments = @([Mailozaurr.GraphAttachment]::FromFile($file))\n$graphMsg = Remove-GraphMessageAttachment -Message $graphMsg\n# Forward $graphMsg using your preferred method\n\n# For POP3 messages (after retrieval)\n$popMsg = [MimeKit.MimeMessage]::new()\n$popMsg.Body = $builder.ToMessageBody()\n$popMsg = Remove-POP3MessageAttachment -Message $popMsg\n"
  },
  {
    "path": "Examples/Example-RemoveMessageAttachmentFiltered.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Strip attachments from IMAP messages sent by alerts@example.com in the last week\n$cred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\nGet-IMAPMessage -Client $imap -FromContains 'alerts@example.com' -Since (Get-Date).AddDays(-7) -HasAttachment |\n    ForEach-Object {\n        $updated = $_.MimeMessage | Remove-IMAPMessageAttachment\n        $path = Join-Path $PSScriptRoot \"IMAP-$($_.Uid.Id).eml\"\n        $updated.WriteTo($path)\n    }\nDisconnect-IMAP -Client $imap\n\n# Strip attachments from POP3 messages with subject 'Report'\n$popCred = Get-Credential\n$pop = Connect-POP3 -Server 'pop.example.com' -Credential $popCred -Port 995 -Options Auto\nGet-POP3Message -Client $pop -Subject 'Report' -HasAttachment |\n    ForEach-Object {\n        $updated = $_.MimeMessage | Remove-POP3MessageAttachment\n        $path = Join-Path $PSScriptRoot \"POP3-$($_.Index).eml\"\n        $updated.WriteTo($path)\n    }\nDisconnect-POP3 -Client $pop\n\n# Strip attachments from Graph messages from the same sender\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n$graphCred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\nConnect-EmailGraph -Credential $graphCred | Out-Null\nGet-EmailGraphMessage -UserPrincipalName 'user@example.com' -Filter \"from/emailAddress/address eq 'alerts@example.com' and hasAttachments eq true\" -Limit 5 |\n    ForEach-Object {\n        $updated = Remove-GraphMessageAttachment -Message $_\n        Save-GraphMessage -Message $updated -Path (Join-Path $PSScriptRoot 'Graph')\n    }\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-RemoveMessages-01.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# ----------------------\n# Basic IMAP deletion\n# ----------------------\n$imap = Connect-IMAP -Server 'imap.example.com' -UserName 'user' -Password 'pass'\n# Delete a single message by UID. -WhatIf previews the action.\nRemove-IMAPMessage -Client $imap -Uid 1 -WhatIf\nDisconnect-IMAP -Client $imap\n\n# ----------------------\n# Basic POP3 deletion\n# ----------------------\n$pop = Connect-POP3 -Server 'pop.example.com' -UserName 'user' -Password 'pass'\n# Remove multiple messages by index\nRemove-POP3Message -Client $pop -Index 0,2 -Confirm:$false\nDisconnect-POP3 -Client $pop\n\n# ----------------------\n# Basic Microsoft Graph deletion\n# ----------------------\n$cred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\nConnect-EmailGraph -Credential $cred | Out-Null\nRemove-GraphMessage -UserPrincipalName 'user@example.com' -MessageId 'id' -WhatIf\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-RemoveMessages-02.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# ------------------------------------\n# Advanced IMAP and POP3 clean-up\n# ------------------------------------\n$imap = Connect-IMAP -Server 'imap.example.com' -UserName 'user' -Password 'pass'\n$uids = Get-IMAPMessage -Client $imap -Subject 'Spam' -Limit 10 | Select-Object -ExpandProperty Uid\nRemove-IMAPMessage -Client $imap -Uid $uids -Confirm:$false\nDisconnect-IMAP -Client $imap\n\n$pop = Connect-POP3 -Server 'pop.example.com' -UserName 'user' -Password 'pass'\n$idx = Get-POP3Message -Client $pop -Before (Get-Date).AddDays(-7) | Select-Object -ExpandProperty Index\nRemove-POP3Message -Client $pop -Index $idx -Confirm:$false\nDisconnect-POP3 -Client $pop\n\n# ------------------------------------\n# Advanced Microsoft Graph deletion\n# ------------------------------------\n$cred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\nConnect-EmailGraph -Credential $cred | Out-Null\n$m = Get-EmailGraphMessage -UserPrincipalName 'user@example.com' -Limit 1\nRemove-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $m.Id -MgGraphRequest -Confirm:$false\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-RenameMailFolder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# ----------------------\n# Rename IMAP folder\n# ----------------------\n$cred = Get-Credential\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# verify current folder names before renaming\nGet-IMAPFolder -Client $imap -Path 'OldFolder'\n\nRename-IMAPFolder -Client $imap -Folder 'OldFolder' -NewName 'NewFolder' -WhatIf\n\n# confirm new folder name\nGet-IMAPFolder -Client $imap -Path 'NewFolder'\n\nRename-IMAPFolder -Client $imap -Folder 'Inbox/Report' -NewName 'Report-2024' -WhatIf\nDisconnect-IMAP -Client $imap\n\n# ----------------------\n# Rename Microsoft Graph folder\n# ----------------------\n$graphCred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\nConnect-EmailGraph -Credential $graphCred | Out-Null\n\n# list folders before renaming\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' | Select-Object displayName,id\n\nRename-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'folder-id' -NewName 'New Folder' -WhatIf\n\n# verify new name\nGet-EmailGraphFolder -UserPrincipalName 'user@example.com' -Connection $graphCred | Where-Object id -EQ 'folder-id'\n\nRename-GraphFolder -UserPrincipalName 'user@example.com' -FolderId 'report-id' -NewName 'Report-2024' -WhatIf\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/Example-RetrieveAndCorrelateNdr.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Retrieve recent non-delivery reports and correlate with sent messages\n$repo = [Mailozaurr.FileSentMessageRepository]::new(\"$env:TEMP\\sendlog.json\")\n$resolver = [Mailozaurr.SendLogResolver]::new($repo)\nConnect-IMAP -Server 'imap.example.com' -Credential (Get-Credential) | Out-Null\n$ndrs = Get-EmailDeliveryStatus -Protocol Imap -Since (Get-Date).AddDays(-7) -ParallelDownloadLimit 8\nGet-EmailDeliveryMatch -Protocol Imap -Resolver $resolver -Since (Get-Date).AddDays(-7) | ForEach-Object {\n    Write-Host \"NDR for $($_.Report.FinalRecipient) matches message '$($_.SentMessage.Subject)'\"\n}\nDisconnect-IMAP\n"
  },
  {
    "path": "Examples/Example-RetryAlways.Tests.ps1",
    "content": "Describe 'RetryAlways example' {\n    It 'Uses RetryAlways switch' {\n        $params = @{ \n            From = 'test@contoso.com'\n            To = 'recipient@contoso.com'\n            Server = 'smtp.example.com'\n            Subject = 'Test'\n            Text = 'Hello'\n            RetryAlways = $true\n            RetryCount = 2\n            WhatIf = $true\n        }\n        $result = Send-EmailMessage @params -ErrorAction Stop\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n}\n"
  },
  {
    "path": "Examples/Example-RetryAlways.cs",
    "content": "using Mailozaurr;\nusing System.Net;\n\n// Example demonstrating RetryAlways\nvar smtp = new Smtp();\nsmtp.From = \"sender@example.com\";\nsmtp.To = new object[] { \"recipient@example.com\" };\nsmtp.Subject = \"Retry demo\";\nsmtp.TextBody = \"Hello\";\nsmtp.RetryAlways = true;\nsmtp.RetryCount = 5;\nsmtp.RetryDelayMilliseconds = 500;\nsmtp.RetryDelayBackoff = 1.5;\n// Replace with real credentials and server\nsmtp.Connect(\"smtp.example.com\", 587);\nvar result = smtp.Send();\nSystem.Console.WriteLine($\"Status: {result.Status}, Error: {result.Error}\");\n"
  },
  {
    "path": "Examples/Example-SaveGmailMessageAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Acquire OAuth2 credential using Connect-OAuthGoogle\n# $cred = Connect-OAuthGoogle -GmailAccount 'user@gmail.com' -ClientID 'id' -ClientSecret 'secret'\n\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSave-GmailMessageAttachment -GmailAccount 'user@gmail.com' -Credential $cred -Id 'abcd1234' -Path $Env:TEMP -WhatIf\n"
  },
  {
    "path": "Examples/Example-SaveImapMessageAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $credential -Port 993 -Options Auto\n\n# Download attachments from the newest message\nGet-IMAPMessage -Client $client -SequenceStart 0 -SequenceEnd 0 -HasAttachment |\n    ForEach-Object { Save-IMAPMessageAttachment -Client $client -Uid $_.Uid.Id -Path \"$Env:UserProfile\\Downloads\\MailAttachments\" }\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-SaveImapMessageAttachmentFiltered.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $credential -Port 993 -Options Auto\n\n# Fetch recent messages with attachments matching a subject filter\nGet-IMAPMessage -Client $client -Subject 'Report' -Since (Get-Date).AddDays(-7) -HasAttachment |\n    ForEach-Object { Save-IMAPMessageAttachment -Client $client -Uid $_.Uid.Id -Path \"$Env:UserProfile\\Downloads\\MailAttachments\" }\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-SaveMimeMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n$msg = Get-IMAPMessage -Client $client -SequenceStart 0 -SequenceEnd 0\n$mime = $msg\nif ($msg.Encryption -eq [Mailozaurr.EmailEncryption]::PgpEncrypted) {\n    $mime = $msg | Unprotect-MimeMessage -PrivateKeyPath 'C:\\Keys\\private.asc' -PrivateKeyPassword 'passphrase'\n} elseif ($msg.Encryption -eq [Mailozaurr.EmailEncryption]::SmimeEncrypted) {\n    $cert = Get-PfxCertificate -FilePath 'C:\\Keys\\email.pfx' -Password (ConvertTo-SecureString 'pfx-pass' -AsPlainText -Force)\n    $mime = $msg | Unprotect-MimeMessage -Certificate $cert\n}\n\n# Save to disk in EML format\n$mime | Save-MimeMessage -Path \"$Env:TEMP\\latest.eml\"\n\n# Extract plain text and HTML bodies\n$content = $mime | Get-MimeMessageContent\n$content.TextBody\n$content.HtmlBody\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-SavePop3MessageAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $credential -Port 995 -Options Auto\n\n# Download attachments from the first message\nSave-POP3MessageAttachment -Client $client -Index 0 -Path \"$Env:UserProfile\\Downloads\\MailAttachments\"\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-SavePop3MessageAttachmentFiltered.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $credential -Port 995 -Options Auto\n\n# Search for recent messages with attachments matching subject\nGet-POP3Message -Client $client -Subject 'Report' -Since (Get-Date).AddDays(-7) -HasAttachment |\n    ForEach-Object { Save-POP3MessageAttachment -Client $client -Index $_.Index -Path \"$Env:UserProfile\\Downloads\\MailAttachments\" }\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-SearchAndFind.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Search an IMAP mailbox\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential (Get-Credential)\n$imapMsgs = Search-IMAPMailbox -Client $imap -Subject 'Invoice'\n$imapMsgs | ForEach-Object { Write-Host \"Found IMAP message $($_.Message.Subject)\" }\n# Limit results directly with -Count to stop searching after the first match\n$firstImap = Search-IMAPMailbox -Client $imap -Subject 'Invoice' -Count 1\n# Alternatively search all messages then pick the first\n$firstImapAlt = ($imapMsgs | Select-Object -First 1)\nDisconnect-IMAP -Client $imap\n\n# Search a POP3 mailbox\n$pop = Connect-POP3 -Server 'pop.example.com' -Credential (Get-Credential)\n$popMsgs = Search-POP3Mailbox -Client $pop -FromContains 'contoso.com' -Count 1\n$popMsgs | ForEach-Object { Write-Host \"Found POP3 message $($_.Message.Subject)\" }\nDisconnect-POP3 -Client $pop\n\n# Search a Graph mailbox for a message\n$cred = ConvertTo-GraphCredential -ClientId 'id' -ClientSecret 'secret' -DirectoryId 'tenant'\n$graph = Connect-EmailGraph -Credential $cred\n$gm = Search-GraphMailbox -Connection $graph -UserPrincipalName 'user@example.com' -Query 'subject:Report' -Count 1\n"
  },
  {
    "path": "Examples/Example-SearchMessageBody.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Search IMAP mailbox body\n$imap = Connect-IMAP -Server 'imap.example.com' -Credential (Get-Credential)\n$imapMsgs = Search-IMAPMailbox -Client $imap -BodyContains 'invoice'\n$imapMsgs | ForEach-Object { Write-Host \"IMAP found $($_.Message.Subject)\" }\nDisconnect-IMAP -Client $imap\n\n# Search POP3 mailbox body\n$pop = Connect-POP3 -Server 'pop.example.com' -Credential (Get-Credential)\n$popMsgs = Search-POP3Mailbox -Client $pop -BodyContains 'invoice'\n$popMsgs | ForEach-Object { Write-Host \"POP3 found $($_.Message.Subject)\" }\nDisconnect-POP3 -Client $pop\n"
  },
  {
    "path": "Examples/Example-SendEmail-01-Text.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nif (-not $MailCredentials) {\n    $MailCredentials = Get-Credential\n}\n\n$Text = Get-Content -Path \"$PSScriptRoot\\Input\\Test.txt\" -Raw\n\n# this is simple replacement (drag & drop to Send-MailMessage)\nSend-EmailMessage -To 'przemyslaw.klys@test.pl' -Subject 'Test' -Text $Text -SmtpServer 'smtp.office365.com' -From 'przemyslaw.klys@test.pl' -Priority High -Credential $MailCredentials -UseSsl -Port 587 -Verbose"
  },
  {
    "path": "Examples/Example-SendEmail-01.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nif (-not $MailCredentials) {\n    $MailCredentials = Get-Credential\n}\n# this is simple replacement (drag & drop to Send-MailMessage)\nSend-EmailMessage -To 'przemyslaw.klys@test.pl' -Subject 'Test' -Body 'test me' -SmtpServer 'smtp.office365.com' -From 'przemyslaw.klys@test.pl' `\n    -Attachments \"$PSScriptRoot\\..\\README.MD\", \"$PSScriptRoot\\..\\Mailozaurr.psm1\" -Encoding UTF8 -Cc 'przemyslaw.klys@test.pl' -Priority High -Credential $MailCredentials `\n    -UseSsl -Port 587 -Verbose\n\n$Body = EmailBody {\n    EmailText -Text 'This is my text'\n    EmailTable -DataTable (Get-Process | Select-Object -First 5 -Property Name, Id, PriorityClass, CPU, Product)\n}\n$Text = 'This is my text'\n\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'przemyslaw.klys@test.pl' } -To 'przemyslaw.klys@test.pl' `\n    -Server 'smtp.office365.com' -Credential $MailCredentials -HTML $Body -Text $Text -DeliveryNotificationOption OnSuccess -Priority High `\n    -Subject 'This is another test email' -SecureSocketOptions Auto"
  },
  {
    "path": "Examples/Example-SendEmail-02.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$UserNotify = 'Przemyslaw'\n\n$Body = EmailBody -FontFamily 'Calibri' -Size 15 {\n    EmailText -Text 'Hello ', $UserNotify, ',' -Color None, Blue, None -Verbose -LineBreak\n    EmailText -Text 'Your password is due to expire in ', $PasswordExpiryDays, 'days.' -Color None, Green, None\n    EmailText -LineBreak\n    EmailText -Text 'To change your password: '\n    EmailText -Text '- press ', 'CTRL+ALT+DEL', ' -> ', 'Change a password...' -Color None, BlueViolet, None, Red\n    EmailText -LineBreak\n    EmailTextBox {\n        'If you have forgotten your password and need to reset it, you can do this by clicking here. '\n        'In case of problems please contact the HelpDesk by visiting [Evotec Website](https://evotec.xyz) or by sending an email to Help Desk.'\n    }\n    EmailText -LineBreak\n    EmailText -Text 'Alternatively you can always call ', 'Help Desk', ' at ', '+48 22 00 00 00' `\n        -Color None, LightSkyBlue, None, LightSkyBlue -TextDecoration none, underline, none, underline -FontWeight normal, bold, normal, bold\n    EmailText -LineBreak\n    EmailTextBox {\n        'Kind regards,'\n        'Evotec IT'\n    }\n}\n\nif (-not $MailCredentials) {\n    $MailCredentials = Get-Credential\n}\n\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'przemyslaw.klys@test.pl' } -To 'przemyslaw.klys@test.pl' `\n    -Server 'smtp.office365.com' -SecureSocketOptions Auto -Credential $MailCredentials -HTML $Body -DeliveryNotificationOption OnSuccess -Priority High `\n    -Subject 'This is another test email'\n\nSend-MailMessage -To 'przemyslaw.klys@test.pl' -Subject 'Test' -Body 'test me' -SmtpServer 'smtp.office365.com' -From 'przemyslaw.klys@test.pl' `\n    -Attachments \"$PSScriptRoot\\..\\Mailozaurr.psd1\" -Cc 'przemyslaw.klys@test.pl' -DeliveryNotificationOption OnSuccess -Priority High -Credential $MailCredentials -UseSsl -Port 587 -Verbose # -Encoding UTF8\n"
  },
  {
    "path": "Examples/Example-SendEmail-Attachments.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Custom attachment built from memory\n$bytes = [System.Text.Encoding]::UTF8.GetBytes('Hello from memory')\n$memoryStream = [System.IO.MemoryStream]::new($bytes)\n$mimePart = [MimeKit.MimePart]::new('text/plain')\n$mimePart.Content = [MimeKit.MimeContent]::new($memoryStream)\n$mimePart.FileName = 'Memory.txt'\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' \\\n    -Server 'smtp.example.com' -Port 25 -Subject 'Attachment Demo' -Body 'Check attachments' \\\n    -Attachment 'C:\\\\Temp\\\\report.pdf', $mimePart \\\n    -InlineAttachment 'C:\\\\Temp\\\\logo.png' -WhatIf -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-ConnectionPool-Advanced.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credential = Get-Credential\n\n$common = @{\n    From = 'sender@example.com'\n    Server = 'smtp.example.com'\n    Credential = $Credential\n    UseConnectionPool = $true\n    ConnectionPoolSize = 2\n    WhatIf = $true\n}\n\n# Send a plain text message\nSend-EmailMessage @common -To 'user1@example.com' -Subject 'First' -Body 'First message'\n\n# Send a high priority message with an attachment\nSend-EmailMessage @common -To 'user2@example.com' -Subject 'Second' -Body 'Second message' -Priority High -Attachment \"$PSScriptRoot\\..\\README.MD\"\n\n# Send HTML content\nSend-EmailMessage @common -To 'user3@example.com' -Subject 'Third' -HTML '<b>Third message</b>'\n\n# When done with the pool\nClear-SmtpConnectionPool\n"
  },
  {
    "path": "Examples/Example-SendEmail-ConnectionPool.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Obtain credentials for SMTP authentication\n$Credential = Get-Credential\n\n$common = @{From='sender@example.com'; To='recipient@example.com'; Subject='Test'; Server='smtp.example.com'; Credential=$Credential; WhatIf=$true}\n\n# First send creates and pools the connection\nSend-EmailMessage @common -UseConnectionPool -ConnectionPoolSize 3\n\n# Second send reuses the connection from the pool\nSend-EmailMessage @common -UseConnectionPool\n"
  },
  {
    "path": "Examples/Example-SendEmail-CramMd5.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n\nSend-EmailMessage -From $cred.UserName -To 'recipient@example.com' -Server 'smtp.example.com' \\\n    -Port 587 -Username $cred.UserName -Password $cred.GetNetworkCredential().Password \\\n    -AuthenticationMechanism CramMd5 -Verbose -WhatIf\n"
  },
  {
    "path": "Examples/Example-SendEmail-DeduplicateAttachments.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$path = Join-Path $PSScriptRoot 'duplicate.txt'\n'data' | Set-Content -Path $path\n\nSend-EmailMessage -From 'example@example.com' -To 'recipient@example.com' -Subject 'Dedup attachments' -Body 'Demo' -Server 'smtp.example.com' -Port 25 -Attachment $path,$path -WhatIf\n\nRemove-Item $path\n"
  },
  {
    "path": "Examples/Example-SendEmail-GmailApiProvider.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Acquire OAuth2 credential using Connect-OAuthGoogle\n# $cred = Connect-OAuthGoogle -GmailAccount 'user@gmail.com' -ClientID 'id' -ClientSecret 'secret'\n\n# Or create a credential from an existing token\n$cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'access_token'\n\nSend-EmailMessage -EmailProvider Gmail -GmailAccount 'user@gmail.com' -From 'user@gmail.com' -To 'recipient@example.com' -Credential $cred -Subject 'Gmail API Test' -Body 'Hello from Gmail API' -WhatIf\n"
  },
  {
    "path": "Examples/Example-SendEmail-Graph-Headers.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credential = ConvertTo-GraphCredential -ClientID 'client-id' -ClientSecret 'secret' -DirectoryID 'tenant-id'\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject 'Graph Header Test' -Body 'Hello from Graph' -Graph -Credential $Credential -Headers @{ 'X-Tracking-ID' = 'abc123'; 'X-Source' = 'Mailozaurr' }\n"
  },
  {
    "path": "Examples/Example-SendEmail-Graph-Policy.ps1",
    "content": "<#\nDemonstrates throttling-safe Graph sending with retry/backoff knobs from PowerShell.\n\nSecurity note: Do not hardcode client secrets in scripts. Use secure storage\nsuch as SecretManagement or environment variables and retrieve them at runtime.\n#>\n\n$ClientId = 'your-app-id'\n$ClientSecret = 'your-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n\nSend-EmailMessage -Graph -From 'sender@example.com' -To 'recipient@example.com' `\n  -Credential $cred -HTML '<b>Hello</b>' -Subject 'Graph policy demo' `\n  -RetryCount 4 -RetryDelayMilliseconds 1000 -JitterMilliseconds 500 -MaxDelayMilliseconds 30000 `\n  -GraphMaxConcurrency 2 -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-Graph-SmtpFallback.ps1",
    "content": "<#\nConfigures a global SMTP fallback factory and sends via Graph with fallback enabled.\n#>\n\n# WARNING: Do not hardcode credentials in scripts. Use Get-Credential, environment variables\n# or a secure secret store (e.g., SecretManagement) for production scenarios.\n# Configure once per session\n[Mailozaurr.MailozaurrOptions]::SmtpFallbackFactory = {\n  param($graph)\n  $s = [Mailozaurr.Smtp]::new()\n  $s.Connect('smtp.office365.com', 587)\n  # Example only — replace with secure credential retrieval\n  $s.Authenticate([System.Net.NetworkCredential]::new('user@example.com','password'))\n  $s\n}\n\n$ClientId = 'your-app-id'\n$ClientSecret = 'your-secret'\n$TenantId = 'your-tenant-id'\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n\nSend-EmailMessage -Graph -EnableSmtpFallback -From 'sender@example.com' -To 'recipient@example.com' `\n  -Credential $cred -HTML '<b>Hello via fallback</b>' -Subject 'Graph+SMTP fallback' `\n  -RetryCount 4 -RetryDelayMilliseconds 1000 -JitterMilliseconds 500 -MaxDelayMilliseconds 30000\n"
  },
  {
    "path": "Examples/Example-SendEmail-Graph.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Body = EmailBody {\n    EmailText -Text 'This is my text'\n    EmailTable -DataTable (Get-Process | Select-Object -First 5 -Property Name, Id, PriorityClass, CPU, Product)\n} -Online\n\n# Credentials for Graph\n$ClientID = 'f8f134f3-78c7-48f4-a371-5d6eefa447cd'\n$DirectoryID = 'ceb371f6-8745-4876-a040-69f2d10a9d1a'\n$ClientSecret = Read-Host 'Graph client secret' -AsSecureString\n\n$Credential = ConvertTo-GraphCredential -ClientID $ClientID -ClientSecretSecureString $ClientSecret -DirectoryID $DirectoryID\n\n# Sending email\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'przemyslaw.klys@evotec.pl' } -To 'przemyslaw.klys@evotec.pl' `\n    -Credential $Credential -HTML $Body -Subject 'This is another test email 1' -Graph -Verbose -Priority High\n\n# sending email with From as string (it won't matter for Exchange )\nSend-EmailMessage -From 'przemyslaw.klys@evotec.pl' -To 'przemyslaw.klys@evotec.pl' `\n    -Credential $Credential -HTML $Body -Subject 'This is another test email 2' -Graph -Verbose -Priority Low\n"
  },
  {
    "path": "Examples/Example-SendEmail-GraphCertificate.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n$ClientId = 'your-client-id'\n$TenantId = 'your-tenant-id'\n$CertPath = 'path-to-your.pfx'\n$CertPassword = 'your-cert-password'\n\n$Credential = ConvertTo-GraphCertificateCredential -ClientId $ClientId -TenantId $TenantId -CertificatePath $CertPath -CertificatePassword $CertPassword\n\nSend-EmailMessage -From 'sender@yourtenant.onmicrosoft.com' -To 'recipient@example.com' `\n    -Credential $Credential -Subject 'Certificate Graph Test' `\n    -HTML '<p>Hello from Graph using certificate auth!</p>' -Graph -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-GraphDeviceCode.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'Your-Application-ID'\n$TenantId = 'Your-Tenant-ID'\n$Scopes = @('Mail.ReadWrite','Mail.Send')\n\n# Sign in interactively using device code\n$graph = Connect-EmailGraph -ClientId $ClientId -DirectoryId $TenantId -DeviceCode -Scopes $Scopes -MaxConcurrentRequests 10\n\n$Body = EmailBody {\n    EmailText -Text 'Hello from device code authentication!'\n} -Online\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' \\\n    -Subject 'Device Code Graph Test' -HTML $Body -Graph -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-GraphWithMgRequest.ps1",
    "content": "﻿Import-Module .\\Mailozaurr.psd1 -Force\n#Import-Module Microsoft.Graph.Authentication -Force\n\n# this shows how to send email using combination of Mailozaurr and Microsoft.Graph to use Connect-MgGraph to authorize\n$Body = EmailBody {\n    New-HTMLText -Text \"This is test of Connect-MGGraph functionality\"\n}\n\n$Path = \"C:\\Support\\GitHub\\HtmlForgeX.Emails\\HtmlForgeX.Email.Examples\\EmailTableAdvancedStyling.html\"\n$Path = \"C:/Support/GitHub/HtmlForgeX.Emails/HtmlForgeX.Email.Examples/bin/Debug/net8.0/EmailProductLaunchAnnouncement.html\"\n$Body = Get-Content -Path $Path -Raw\n# authorize via Connect-MgGraph with delegated rights or any other supported method\nConnect-MgGraph -Scopes Mail.Send -NoWelcome\n\n# sending email\n$sendEmailMessageSplat = @{\n    From           = 'przemyslaw.klys@evotec.pl'\n    To             = 'przemyslaw.klys@evotec.pl'\n    HTML           = $Body\n    Subject        = 'This tests email as delegated'\n    MgGraphRequest = $true\n    Verbose        = $true\n}\nSend-EmailMessage @sendEmailMessageSplat"
  },
  {
    "path": "Examples/Example-SendEmail-Headers.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject 'Header Test' -Body 'Hello' -Server 'smtp.example.com' -Headers @{ 'X-Tracking-ID' = 'abc123'; 'X-Custom' = 'Mailozaurr' }\n"
  },
  {
    "path": "Examples/Example-SendEmail-Mailgun-Headers.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ApiKey = 'mailgun-api-key'\n$Credential = ConvertTo-MailgunCredential -ApiKey $ApiKey\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject 'Mailgun Header Test' -Body 'Hello from Mailgun' -EmailProvider Mailgun -Credential $Credential -Headers @{ 'X-Tracking-ID' = 'abc123'; 'X-Source' = 'Mailozaurr' }\n"
  },
  {
    "path": "Examples/Example-SendEmail-Mailgun.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Use Mailgun API\n$Key = Read-Host 'Mailgun API key' -AsSecureString\n$Credential = ConvertTo-MailgunCredential -ApiKeySecureString $Key\n\n$sendEmailMessageSplat = @{\n    From          = 'postmaster@sandbox814085ede3524b939d4b7f518ef9877a.mailgun.org'\n    To            = 'przemyslaw.klys+mailgun@xxx.pl'\n    Subject       = 'Mailgun Test'\n    HTML          = 'Hello from Mailgun'\n    Credential    = $Credential\n    Verbose       = $true\n    EmailProvider = 'Mailgun'\n    WhatIf        = $false\n}\n\nSend-EmailMessage @sendEmailMessageSplat\n"
  },
  {
    "path": "Examples/Example-SendEmail-OAuthGmail.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientID = '939333074185'\n$ClientSecret = 'gk2ztAGU'\n\n$CredentialOAuth2 = Connect-OAuthGoogle -ClientID $ClientID -ClientSecret $ClientSecret -GmailAccount 'evot@gmail.com'\n\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'evot@gmail.com' } -To 'test@evotec.pl' `\n    -Server 'smtp.gmail.com' -HTML $Body -Text $Text -DeliveryNotificationOption OnSuccess -Priority High `\n    -Subject 'This is another test email' -SecureSocketOptions Auto -Credential $CredentialOAuth2 -OAuth2\n\n"
  },
  {
    "path": "Examples/Example-SendEmail-OAuthO365.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientID = '4c1197dd-53'\n$TenantID = 'ceb371f6-87'\n\n$CredentialOAuth2 = Connect-OAuthO365 -ClientID $ClientID -TenantID $TenantID\n\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'test@evotec.pl' } -To 'test@evotec.pl' `\n    -Server 'smtp.office365.com' -HTML $Body -Text $Text -DeliveryNotificationOption OnSuccess -Priority High `\n    -Subject 'This is another test email' -SecureSocketOptions Auto -Credential $CredentialOAuth2 -OAuth2\n\n"
  },
  {
    "path": "Examples/Example-SendEmail-Pgp.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$pub = Join-Path $PSScriptRoot 'PGPKeys/mimekit.gpg.pub'\n$sec = Join-Path $PSScriptRoot 'PGPKeys/mimekit.gpg.sec'\n\nSend-EmailMessage -From 'mimekit@example.com' -To 'mimekit@example.com' \\\n    -Server 'smtp.example.com' -Port 25 -Subject 'PGP Test' -Body 'Hello' \\\n    -SignOrEncrypt PgpSignAndEncrypt -PublicKeyPath $pub -PrivateKeyPath $sec \\\n    -PrivateKeyPassword 'no.secret' -WhatIf -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-SendGrid-Headers.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ApiKey = 'sendgrid-api-key'\n$Credential = ConvertTo-SendGridCredential -ApiKey $ApiKey\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject 'SendGrid Header Test' -Body 'Hello from SendGrid' -SendGrid -Credential $Credential -Headers @{ 'X-Tracking-ID' = 'abc123'; 'X-Source' = 'Mailozaurr' }\n"
  },
  {
    "path": "Examples/Example-SendEmail-SendGrid01.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Key = Get-Content -Raw -Path \"C:\\Support\\Important\\SendGrid.txt\"\n\n# Use SendGrid via Standard SMTP\n# username needs to be named exactly apikey\nSend-EmailMessage -From 'przemyslaw.klys@evo.cool' -To 'przemyslaw.klys@evotec.pl', 'evotectest@gmail.com' `\n    -Username 'apikey' `\n    -Server 'smtp.sendgrid.net' `\n    -Password $Key `\n    -Body 'test me 🤣😍😒💖✨🎁 Przemysław Kłys' -DeliveryNotificationOption OnSuccess `\n    -Priority High -Subject '😒💖 This is another test email 我' -UseSsl -Port 587 -Verbose #-WhatIf"
  },
  {
    "path": "Examples/Example-SendEmail-SendGrid02.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Use SendGrid Api\n$Key = Read-Host 'SendGrid API key' -AsSecureString\n\n$Credential = ConvertTo-SendGridCredential -ApiKeySecureString $Key\n\nSend-EmailMessage -From 'przemyslaw.klys@evo.cool' `\n    -To 'przemyslaw.klys@evotec.pl', 'evotectest@gmail.com' `\n    -Body 'test me 🤣😍😒💖✨🎁 Przemysław Kłys' `\n    -Priority High `\n    -Subject '😒💖 This is another test email 我' `\n    -SendGrid `\n    -Credential $Credential `\n    -Verbose\n\nSend-EmailMessage -From @{ Name = 'Przemysław Kłys'; Email = 'przemyslaw.klys@evo.cool' } -To 'przemyslaw.klys@evotec.pl' -Credential $Credential -Text 'MyTest' -Priority High `\n    -Subject 'Second test email' -SendGrid -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-SentLog.ps1",
    "content": "$path = Join-Path $PSScriptRoot 'sentlog.json'\n# The file collects all sent message records and is appended to on each send.\nSend-EmailMessage -From 'from@example.com' -To 'to@example.com' -Server 'smtp.server' -Subject 'demo' -Text 'body' -SentLogPath $path -WhatIf\n# $ndr = Get-NonDeliveryReport -Path 'ndr.eml'\n# $repo = [Mailozaurr.FileSentMessageRepository]::new($path)\n# $resolver = [Mailozaurr.SendLogResolver]::new($repo)\n# $match = $resolver.ResolveAsync($ndr).GetAwaiter().GetResult()\n# $match | Format-List\n"
  },
  {
    "path": "Examples/Example-SendEmail-Ses-Headers.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$Credential = Get-Credential -UserName 'AKIA...' -Message 'Enter AWS Secret Key'\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Subject 'SES Header Test' -Body 'Hello from SES' -Ses -Region 'us-east-1' -Credential $Credential -Headers @{ 'X-Tracking-ID' = 'abc123'; 'X-Source' = 'Mailozaurr' }\n"
  },
  {
    "path": "Examples/Example-SendEmail-SignEncrypt.ps1",
    "content": "﻿Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$CertPath = 'C:\\Certificates\\email.pfx'\n$CertPassword = 'pfx-password'\n\nif (-not $SmtpCredential) {\n    $SmtpCredential = Get-Credential\n}\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' \\\n    -Server 'smtp.example.com' -Credential $SmtpCredential -Port 587 -UseSsl \\\n    -Subject 'Signed and encrypted' -Body 'This message is signed and encrypted.' \\\n    -CertificatePath $CertPath -CertificatePassword $CertPassword \\\n    -SignOrEncrypt SMIMESignAndEncrypt -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-TemporaryMailCrypto.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Generate temporary PGP keys and keep them on disk\n$out = Join-Path $env:TEMP 'pgp-keys'\n$keys = New-TemporaryMailCrypto -Pgp -OutputPath $out -NoDispose\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' `\n    -Server 'smtp.example.com' -Port 25 -Subject 'Temporary PGP test' `\n    -Body 'Hello from temporary PGP keys' -PublicKeyPath $keys.PublicKeyPath `\n    -PrivateKeyPath $keys.PrivateKeyPath -PrivateKeyPassword $keys.PassPhrase `\n    -SignOrEncrypt PgpSignAndEncrypt -WhatIf -Verbose\n\n# Optionally verify the encrypted message\n$smtp = [Mailozaurr.Smtp]::new()\n$smtp.From = 'sender@example.com'\n$smtp.To   = @('sender@example.com')\n$smtp.Subject = 'Test message'\n$smtp.TextBody = 'Secret text'\n$smtp.CreateMessage()\n$null = $smtp.PgpEncrypt($keys.PublicKeyPath)\n$path = Join-Path $env:TEMP 'test.eml'\n$smtp.Message.WriteTo($path)\n$decrypted = $keys.DecryptToString($path)\nWrite-Verbose \"Decrypted body: $decrypted\"\n\n# Dispose without deleting\n$keys.Dispose()\n\n# Generate temporary S/MIME certificate\n$cert = New-TemporaryMailCrypto -Smime\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' `\n    -Server 'smtp.example.com' -Port 25 -Subject 'Temporary cert test' `\n    -Body 'Hello from temporary certificate' -Certificate $cert `\n    -SignOrEncrypt SmimeSignAndEncrypt -WhatIf -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-TemporaryPgp.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Create a throwaway PGP key pair using the unified cmdlet\n$keys = New-TemporaryMailCrypto -Pgp\n\n# Send an encrypted and signed message (simulation)\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' `\n    -Server 'smtp.example.com' -Port 25 -Subject 'Temporary PGP test' `\n    -Body 'Hello from temporary PGP keys' -PublicKeyPath $keys.PublicKeyPath `\n    -PrivateKeyPath $keys.PrivateKeyPath -PrivateKeyPassword $keys.PassPhrase `\n    -SignOrEncrypt PgpSignAndEncrypt -WhatIf -Verbose\n\n# Demonstrate decrypting the message using the same keys\n$smtp = [Mailozaurr.Smtp]::new()\n$smtp.From = 'sender@example.com'\n$smtp.To   = @('sender@example.com')\n$smtp.Subject = 'Test message'\n$smtp.TextBody = 'Secret text'\n$smtp.CreateMessage()\n$null = $smtp.PgpEncrypt($keys.PublicKeyPath)\n$path = Join-Path $env:TEMP 'test.eml'\n$smtp.Message.WriteTo($path)\n$plaintext = $keys.DecryptToString($path)\nWrite-Host \"Decrypted body: $plaintext\"\n"
  },
  {
    "path": "Examples/Example-SendEmail-TemporarySmime.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Generate a temporary certificate and export it\n$pfx = Join-Path $env:TEMP 'tempcert.pfx'\n$cert = New-TemporaryMailCrypto -Smime -OutputPath $pfx\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' `\n    -Server 'smtp.example.com' -Port 25 -Subject 'Temporary cert test' `\n    -Body 'Hello from temporary certificate' -Certificate $cert `\n    -SignOrEncrypt SmimeSignAndEncrypt -WhatIf -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmail-VerifyAttachment.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$missing = \"$PSScriptRoot\\missing-file.txt\"\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' \\\n    -Server 'smtp.example.com' -Port 25 -Subject 'Verify attachments demo' \\\n    -Body 'demo body' -Attachment $missing -WhatIf -Verbose\n"
  },
  {
    "path": "Examples/Example-SendEmailPendingMessage-ScheduleAndNotify.ps1",
    "content": "$pending = Join-Path $PSScriptRoot 'pending'\n$adminRecipient = 'admin@example.com'\n$notificationSender = 'mailer@example.com'\n$smtpServer = 'smtp.office365.com'\n$notificationSubject = 'Mailozaurr pending message queue failure'\n\nif ($NotificationCredential -and $NotificationCredential -isnot [pscredential]) {\n    throw 'NotificationCredential must be a PSCredential instance.'\n}\n\nif (-not $NotificationCredential) {\n    $NotificationCredential = Get-Credential -Message 'Provide SMTP credentials used for failure notifications'\n    if (-not $NotificationCredential) {\n        throw 'SMTP credentials are required for failure notifications'\n    }\n}\n\n$notificationParameters = @{\n    From       = $notificationSender\n    To         = $adminRecipient\n    Subject    = $notificationSubject\n    SmtpServer = $smtpServer\n    Credential = $NotificationCredential\n    UseSsl     = $true\n    Port       = 587\n}\n\ntry {\n    Send-EmailPendingMessage -PendingMessagesPath $pending -ProcessAll\n} catch {\n    $timestamp = Get-Date -Format o\n    $rawMessage = if ($_.Exception) { $_.Exception.ToString() } else { $_ | Out-String }\n    $sanitizedMessage = $rawMessage -replace '(?i)(password|token|key|secret)=\\S+', '$1=***'\n    $body = @\"\nProcessing of the pending message queue failed at $timestamp.\n\n$sanitizedMessage\n\"@\n    Send-EmailMessage @notificationParameters -Body $body\n    throw\n}\n\n# To automatically process the queue, register a scheduled job that reuses the\n# same notification block. The example below runs every 15 minutes and alerts the\n# administrator when a failure occurs.\n#\n# $trigger = New-JobTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) -RepetitionDuration ([TimeSpan]::MaxValue)\n# Register-ScheduledJob -Name 'Mailozaurr-PendingMessages' -ScriptBlock {\n#     param(\n#         [string]$PendingMessagesPath,\n#         [hashtable]$NotificationParameters\n#     )\n#\n#     try {\n#         Send-EmailPendingMessage -PendingMessagesPath $PendingMessagesPath -ProcessAll\n#     } catch {\n#         $errorTimestamp = Get-Date -Format o\n#         $errorBody = @\"\n# Processing of the pending message queue failed at $errorTimestamp.\n#\n# $($_.Exception.Message)\n# \"@\n#         Send-EmailMessage @NotificationParameters -Body $errorBody\n#         throw\n#     }\n# } -ArgumentList $pending, $notificationParameters -Trigger $trigger -ScheduledJobOption (New-ScheduledJobOption -RunElevated)\n"
  },
  {
    "path": "Examples/Example-SendEmailPendingMessage-Scheduled.ps1",
    "content": "$pending = Join-Path $PSScriptRoot 'pending'\n\n# Run once to flush all messages that are due right now.\nSend-EmailPendingMessage -PendingMessagesPath $pending\n\n# To process the queue from a scheduled task, register a job that calls the cmdlet.\n# The example below runs every 15 minutes and forces processing of all messages\n# (even those scheduled for the future) so that long-lived queues are cleared.\n#\n# $trigger = New-JobTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) -RepetitionDuration ([TimeSpan]::MaxValue)\n# Register-ScheduledJob -Name 'Mailozaurr-PendingMessages' -ScriptBlock {\n#     Send-EmailPendingMessage -PendingMessagesPath '$pending' -ProcessAll\n# } -Trigger $trigger -ScheduledJobOption (New-ScheduledJobOption -RunElevated)\n\n"
  },
  {
    "path": "Examples/Example-SetGraphMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n# 1. Show the first 10 messages with their IDs\nGet-EmailGraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -Limit 10 |\n    Select-Object Id, From, Subject\n\n# 2. Display details for the newest message\n$m = Get-EmailGraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -Limit 1\n$m.Raw | Format-List\n\n# 3. Mark the message as read\nSet-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $m.Id -Read -Connection $graph\n\n# 4. Mark it as unread again\nSet-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $m.Id -Read:$false -Connection $graph\n\n# 5. Find messages from a sender with a given subject\n$reports = Get-EmailGraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -FromContains 'alerts@example.com' -Subject 'Report'\n\n# 6. Mark all matching messages as read\nforeach ($msg in $reports) { Set-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $msg.Id -Read -Connection $graph }\n\n# 7. Move the processed messages to Archive\nforeach ($msg in $reports) { Move-GraphMessage -UserPrincipalName 'user@example.com' -MessageId $msg.Id -DestinationFolderId 'Archive' -Connection $graph }\n\n# 8. Wait for a new message\n$listener = Wait-GraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -TimeoutSeconds 60\n\n# 9. Output the subject of the arrived message\n$listener.Message | Select-Object -ExpandProperty subject\n\n# 10. Disconnect\nDisconnect-EmailGraph -Connection $graph\n"
  },
  {
    "path": "Examples/Example-SetImapFolder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# 1. Display the current folder\n$client.Folder.FullName\n\n# 2. Change the working folder to 'Archive'\nSet-IMAPFolder -Client $client -Path 'Archive'\n\n# 3. List first 10 messages from 'Archive'\nGet-IMAPMessage -Client $client -All | Select-Object -First 10 Uid, Subject\n\n# 4. Switch to a nested folder with write access\nSet-IMAPFolder -Client $client -Path 'Projects/2025' -FolderAccess ReadWrite\n\n# 5. Show message count in the new folder\n$client.Count\n\n# 6. Return to Inbox\nSet-IMAPFolder -Client $client -Path 'INBOX'\n\n# 7. Switch to Sent Items\nSet-IMAPFolder -Client $client -Path '[Gmail]/Sent Mail'\n\n# 8. Mark a message in Sent Items as read\nSet-IMAPMessage -Client $client -Uid 123 -Read\n\n# 9. Change back to 'Archive'\nSet-IMAPFolder -Client $client -Path 'Archive'\n\n# 10. Disconnect\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-SetImapMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred -Port 993 -Options Auto\n\n# 1. Show the first 10 messages with their UIDs\nGet-IMAPMessage -Client $client -All | Select-Object -First 10 Uid, From, Subject\n\n# 2. Display which email has UID 10\nGet-IMAPMessage -Client $client -Uid 10 | ForEach-Object { $_.Raw.Message }\n\n# 3. Mark message with UID 10 as read\nSet-IMAPMessage -Client $client -Uid 10 -Read\n\n# 4. Mark message UID 5 from 'Archive' as unread\nSet-IMAPMessage -Client $client -Uid 5 -Unread -Folder 'Archive'\n\n# 5. Find messages from a sender with a matching subject\n$reports = Get-IMAPMessage -Client $client -FromContains 'alerts@example.com' -Subject 'Report'\n\n# 6. Mark all matching messages as read\nforeach ($msg in $reports) { Set-IMAPMessage -Client $client -Uid $msg.Uid.Id -Read }\n\n# 7. Change folder to 'Archive'\nSet-IMAPFolder -Client $client -Path 'Archive'\n\n# 8. List the top 5 messages from the new folder\nGet-IMAPMessage -Client $client -All | Select-Object -First 5 Uid, Subject\n\n# 9. Mark every message in the folder as read\nGet-IMAPMessage -Client $client -All | ForEach-Object { Set-IMAPMessage -Client $client -Uid $_.Uid.Id -Read }\n\n# 10. Disconnect\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-SetPop3Message.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $cred -Port 995 -Options Auto\n\n# 1. List first 10 messages with their indexes\nGet-POP3Message -Client $client -All | Select-Object -First 10 Index, From, Subject\n\n# 2. Display the message stored at index 1\nGet-POP3Message -Client $client -Index 1 | ForEach-Object { $_.Raw.Message }\n\n# 3. Mark the first message as read\nSet-POP3Message -Client $client -Index 0 -Read\n\n# 4. Mark the second message as unread\nSet-POP3Message -Client $client -Index 1 -Unread\n\n# 5. Find messages from a sender with a subject filter\n$matches = Get-POP3Message -Client $client -FromContains 'alerts@example.com' -Subject 'Report'\n\n# 6. Mark all matching messages as read\nforeach ($msg in $matches) { Set-POP3Message -Client $client -Index $msg.Index -Read }\n\n# 7. Mark every message as unread\nfor ($i = 0; $i -lt $client.Count; $i++) { Set-POP3Message -Client $client -Index $i -Unread }\n\n# 8. List the top 5 messages again\nGet-POP3Message -Client $client -All | Select-Object -First 5 Index, Subject\n\n# 9. Fetch a specific message by index\n$firstMsg = Get-POP3Message -Client $client -Index 0\n\n# 10. Disconnect\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/Example-SmtpConnectionPoolMetrics.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Display pool size changes using a helper cmdlet\n$watcher = Watch-SmtpConnectionPool -Action { param($s) Write-Host \"Pool size: $($s.CurrentPoolSize)\" }\n\n# Reset any existing pooled connections\nClear-SmtpConnectionPool\n\n$common = @{\n    From = 'sender@example.com'\n    To = 'recipient@example.com'\n    Subject = 'Test'\n    Server = 'smtp.example.com'\n    UseConnectionPool = $true\n    ConnectionPoolSize = 3\n    WhatIf = $true\n}\n\n# Send twice to show pool size events\nSend-EmailMessage @common\nSend-EmailMessage @common\n\n# Clear the pool and display final size\nClear-SmtpConnectionPool\n$snapshot = Get-SmtpConnectionPool\nWrite-Host \"Current pool size: $($snapshot.CurrentPoolSize)\"\n\n[Mailozaurr.SmtpConnectionPool]::remove_PoolSizeChanged($watcher)\n"
  },
  {
    "path": "Examples/Example-TestSmtpConnection.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n# Examine server capabilities and check if connections stay open\n$info = Test-SmtpConnection -Server 'smtp.example.com' -Port 587\n\nif ($info.Persistent) {\n    Write-Host \"Server keeps the connection open. Enabling pooling.\"\n    $poolSettings = @{ UseConnectionPool = $true; ConnectionPoolSize = 2 }\n} else {\n    Write-Warning \"Server closes the connection after each command. Pooling disabled.\"\n    $poolSettings = @{}\n}\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Server 'smtp.example.com' @poolSettings -WhatIf\n"
  },
  {
    "path": "Examples/Example-ValidateEmail.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\nWrite-Color -Text \"Testing via parameter\" -Color Green\n\nTest-EmailAddress -EmailAddress 'evotec@test', 'evotec@test.pl', 'evotec.@test.pl', 'evotec.p@test.pl.', 'olly@somewhere...com', 'olly@somewhere.', 'olly@somewhere', 'user@☎.com', '.@domain.tld' | Format-Table\n\nWrite-Color -Text \"Testing via pipeline\" -Color Green\n\n'evotec@test', 'evotec@test.pl', 'evotec.@test.pl', 'evotec.p@test.pl.', 'olly@somewhere...com', 'olly@somewhere.', 'olly@somewhere', 'user@☎.com', '.@domain.tld', \"testme@zumpul.com\" | Test-EmailAddress -Verbose | Format-Table"
  },
  {
    "path": "Examples/Example-WaitGraphMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$graph = Connect-EmailGraph -Credential $cred\n\nWrite-Host 'Waiting for new Graph messages from alice@example.com or until timeout.'\nWait-GraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -Until {\n    param($m)\n    $m.from.emailAddress.address -eq 'alice@example.com'\n} -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($msg)\n    Write-Host \"New message from Alice: $($msg.subject)\"\n}\n\nDisconnect-EmailGraph -Connection $graph\n"
  },
  {
    "path": "Examples/Example-WaitImapMessage.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $credential\n\nWrite-Host 'Waiting for new IMAP messages from alice@example.com.'\nWait-IMAPMessage -Client $client -Until {\n    param($m)\n    $m.Message.From.Mailboxes.Address -contains 'alice@example.com'\n} -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($msg)\n    Write-Host \"New IMAP message from Alice: $($msg.Message.Subject)\"\n}\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-WaitImapMessageSearchQuery.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$credential = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $credential\n\nWrite-Host 'Waiting for new IMAP messages from alice@example.com.'\nWait-IMAPMessage -Client $client -SearchQuery ([MailKit.Search.SearchQuery]::FromContains('alice@example.com')) -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($msg)\n    Write-Host \"New IMAP message from Alice: $($msg.Message.Subject)\"\n}\n\nDisconnect-IMAP -Client $client\n"
  },
  {
    "path": "Examples/Example-WaitPop3Message.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $cred\n\nWrite-Host 'Waiting for new POP3 messages from alice@example.com.'\nWait-POP3Message -Client $client -Until {\n    param($m)\n    $m.Message.From.Mailboxes.Address -contains 'alice@example.com'\n} -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($msg)\n    Write-Host \"New POP3 message from Alice: $($msg.Message.Subject)\"\n}\n\nDisconnect-POP3 -Client $client\n"
  },
  {
    "path": "Examples/GraphRules/Example-GetInboxRules.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n# Retrieve all rules\nGet-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph | Format-Table DisplayName, Id\n\n# Retrieve rule by display name without using Where-Object\nGet-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -Filter \"displayName eq 'Move Boss Mail'\"\n\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/GraphRules/Example-NewInboxRule.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n# Create rule using typed parameters directly\nNew-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph `\n    -DisplayName 'Move Boss Mail' -Sequence 1 -SenderContains 'boss@example.com' `\n    -MoveToFolder 'Archive' -Enabled\n\n# Create another rule to delete spam by subject\nNew-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph `\n    -DisplayName 'Delete Spam Subject' -Sequence 2 -SubjectContains 'spam' -Delete\n\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/GraphRules/Example-RemoveInboxRule.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n$rule = Get-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -Filter \"displayName eq 'Move Boss Mail'\" | Select-Object -First 1\nif ($rule) {\n    Remove-GraphInboxRule -UserPrincipalName 'user@example.com' -RuleId $rule.Id -Connection $graph -WhatIf\n}\n\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/GraphRules/Example-RuleBuilder.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\..\\Mailozaurr.psd1 -Force\n\n$ClientId = 'your-client-id'\n$ClientSecret = 'your-client-secret'\n$TenantId = 'your-tenant-id'\n\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -ClientSecret $ClientSecret -DirectoryId $TenantId\n$graph = Connect-EmailGraph -Credential $cred\n\n# Build a rule that moves messages from the boss to Archive and stops processing\n$builder = New-GraphInboxRuleBuilder -DisplayName 'Boss Archive' -Sequence 1 -SenderContains 'boss@example.com' -MoveToFolder 'Archive' -StopProcessing\nNew-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -RuleBuilder $builder\n\n# Build and create a rule that forwards urgent messages\n$forward = New-GraphInboxRuleBuilder -DisplayName 'Forward Urgent' -Sequence 2 -SubjectContains 'urgent' -ForwardTo 'assistant@example.com' -Enabled\nNew-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -RuleBuilder $forward\n\n# Build and create a rule that deletes spam\n$spam = New-GraphInboxRuleBuilder -DisplayName 'Delete Spam' -Sequence 3 -SenderContains 'spam@example.com' -Delete\nNew-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -RuleBuilder $spam\n"
  },
  {
    "path": "Examples/GraphRules/Example-SetInboxRule.ps1",
    "content": "Import-Module $PSScriptRoot\\..\\..\\Mailozaurr.psd1 -Force\n\n$cred = Get-Credential\n$graph = Connect-EmailGraph -Credential (ConvertTo-GraphCredential -ClientId $cred.UserName -ClientSecret $cred.GetNetworkCredential().Password -DirectoryId 'tenant')\n\n$rule = Get-GraphInboxRule -UserPrincipalName 'user@example.com' -Connection $graph -Filter \"displayName eq 'Move Boss Mail'\" | Select-Object -First 1\nif ($rule) {\n    $rule.Actions.MoveToFolder = 'Inbox'\n    Set-GraphInboxRule -UserPrincipalName 'user@example.com' -RuleId $rule.Id -Connection $graph -RuleObject $rule\n}\n\nDisconnect-EmailGraph\n"
  },
  {
    "path": "Examples/PGPKeys/mimekit.gpg.pub",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1.4.14 (Darwin)\n\nmQENBFJ2lrsBCADGGY9BKBt+8AuCpso1XJBxihBPyQ03WdzAO3u3jRsqyqp+e56D\nOWvW/hKIlCQWnfWEtebGcqSfe/O1bHe7G+kpKbBs2imiSIZN6T2wcGtQGBmQAnqM\npBfTwi6WZyzb+yKOSfSRi1bVn1GjvgTvdzeg4d1dOdUjLsngXopjxYLE3QEb/S2+\nbUE9sgp5YvQmlkgqd1GG06ggtx+T0a5iRvddvGGufKHsyncrQwfCy0FtmJ1a7zgW\nDnbYMk40Il8u9jZ0UOj7qbiBr44/3lKbAp2jr++8oSLGJdqzDoUsjnH0dTzdY671\npnPuh13zrDHIjmK3X19aMWzUay2xnkWNYin9ABEBAAG0J01pbWVLaXQgVW5pdFRl\nc3RzIDxtaW1la2l0QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCUnaWuwIbAwYLCQgH\nAwIGFQgCCQoLBBYCAwECHgECF4AACgkQUNzRB6sIIaL9UwgAma04xyO7cTp8Q52M\nN5aEYr7xr9aXZmnHlMW8UIv23prRcjSxabFJaxknylH7+ekRTfcVxDXA/D9sexEm\n8NYcts2t2sQVdjgAra6e8NBFc3wvLTliT/OQ60lfb+RFcYTDBCsLTecItYWbk34d\n5kg0h7TScmcwTTV5pyKVyi/FkWWejVB93WlqgC35aoebaJBkFh3ePmuGeVE5jfGR\nX5f32OOjy23yy8unQDsAnvcxoQWcPbgFRmt8Mlx+jBoaCLsTIhqrWSNDRmWo7vb8\nYCkQMjaHwZ6zwablJveeOoTJyPlSlmLg9idewcUwwtFPcARolnXT+VdcIRUy63JY\nw+buirkBDQRSdpa7AQgAwrmMqEaAFlDejEAxMiBuYhDSo5mWwUb/RfkwyxhCdsk3\nBcjyNRuge4Z3KEafrzlIi76WQQqDEHJmaM8Cl62oaxv71I8KNuoh/lYHOSYxBMIS\n0ZnMJo42SjQWCmS3hlEpkPDA6ffaK81VM3fIwGKvpEEnvtPMl12DMk2aaaebeWM7\nMRjjtWLNuUutdjTVE3TjXoBkfOv1IfAZ15whkrvKdV1rIl3gEuQ6CAqNaQsY/nlY\nefhpsQ548yueXzqUyQm4AduGGMOkSxQ0pi34C55kFiuwFnFUJzlLTc94TzWpaqv/\nLjdj2d5zM512YkVznrh1y6ZLNlsNdqjKz20DWdMmAQARAQABiQEfBBgBAgAJBQJS\ndpa7AhsMAAoJEFDc0QerCCGiuvoH/AmNNLfaI3qJb2dWKS8/OGkaCOE/yVdqmIg9\n6/iNhIGDiGcKCN332w6qy9BhQJmvlwLQZYn5M6c71t6U5hcsmJBHAOqrZB0WZgdy\nS4/Hg6tWcBnPmfQAoojxahDeSv6C6LuyF08Bv7fiGWQcppBIIzG6rxz69R6/k5hn\nl3rCHrjKoCr9JUPfPzuSJCLrGlmp73iA038R1+X3nGugJA+lFSSEBVMyE9/RUHCb\n6Odjdz1ge7VcFxaVqMYfpFMgKrKUPTB98mv348TGjcUGYaR2ze509KL04Hvg7Za2\n5zF93LblTVWBEeb+laC2uI8Gl0WivgMmXQa9x6BNcLA5f8uN8Xs=\n=ujkw\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "Examples/PGPKeys/mimekit.gpg.sec",
    "content": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: GnuPG v1.4.14 (Darwin)\n\nlQO+BFJ2lrsBCADGGY9BKBt+8AuCpso1XJBxihBPyQ03WdzAO3u3jRsqyqp+e56D\nOWvW/hKIlCQWnfWEtebGcqSfe/O1bHe7G+kpKbBs2imiSIZN6T2wcGtQGBmQAnqM\npBfTwi6WZyzb+yKOSfSRi1bVn1GjvgTvdzeg4d1dOdUjLsngXopjxYLE3QEb/S2+\nbUE9sgp5YvQmlkgqd1GG06ggtx+T0a5iRvddvGGufKHsyncrQwfCy0FtmJ1a7zgW\nDnbYMk40Il8u9jZ0UOj7qbiBr44/3lKbAp2jr++8oSLGJdqzDoUsjnH0dTzdY671\npnPuh13zrDHIjmK3X19aMWzUay2xnkWNYin9ABEBAAH+AwMC5K94HuixOdZggwd0\nKkaW5GBV8rBnM7o3fJ+dHgvn1J4ReqJMwRJj/PMN2nUmXhoZqxOCo5nL4oxz36js\nhrJZB36AepZJfAmAQtxZAkeP5MHWAmGofLCDQnDsrHVhNfFmFO0G4XHOBznqg/N6\nE0e4Mcp+ouS8woBzDYOYe9gxK5iKFYLbQFZjG5ZA1fJdM82h9aQCGXr+ZVIzWr5U\nqHjEq8tcsryhCysoA87fdhoS91YdgDEmgHSyjrl5s+7BOA13lk/fOjUfqA5qjrba\ncJhRGEAI19UTE6lnBrCvDg4sFoQWqzuZdXTRnv+LJrWEjVpv/BqC/8K4ZgIbp0FN\nkdIRqOETl18futWbSuEHeVTsqzOHQWmrxCzd2SU+orIpvXHYCb1yvdZo3YsP4FNv\naTPZu4VCORhBAVO6rhHInARrUmSoKFIuO0j8v2nIAbLjogkDMftpDQ3DYhimbVf0\nMy03+4lnHk0/llLUYqBnGf7J2H5yfXOt6grhC3jYPBlFfpn3r5gU30M8cho3KcXa\nQxehu6u3u38OfHNGbKAG14Zbf5hOtztX4XpxK6oWbXFztGtpWhjsA5WovTaNw/Av\nv5/3iNSTfQJYlgp6B0YpMJ9qsnLF4cMl30pxuZVNEJ1KLXTNkK74juENkVplxpgN\nRy+iYYsGXVMuWDNX9BJ54YbnzF9PFcQ9b95oLhpsH5KcPTuMkQeNSqBkXCPIbpr2\n/DFcdUchJYlzJX0hxCXrTzNr1fQ48JO7jNvC/X1kOrBDCGxFzvinuAzHBuPB2prO\n5xYgjXSqhuGEZL1crB9Sk7ECn7bSxywLj31rBehZ7YurhkIOEffxRUV4uGZhTMqk\nrW92/fzXhZXk4sLBMnqPYWLTtzkrCdIVj5+Gw8LhLj1W6DO1LqpM8kuDFGmJB7C+\n2LQnTWltZUtpdCBVbml0VGVzdHMgPG1pbWVraXRAZXhhbXBsZS5jb20+iQE4BBMB\nAgAiBQJSdpa7AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBQ3NEHqwgh\nov1TCACZrTjHI7txOnxDnYw3loRivvGv1pdmaceUxbxQi/bemtFyNLFpsUlrGSfK\nUfv56RFN9xXENcD8P2x7ESbw1hy2za3axBV2OACtrp7w0EVzfC8tOWJP85DrSV9v\n5EVxhMMEKwtN5wi1hZuTfh3mSDSHtNJyZzBNNXmnIpXKL8WRZZ6NUH3daWqALflq\nh5tokGQWHd4+a4Z5UTmN8ZFfl/fY46PLbfLLy6dAOwCe9zGhBZw9uAVGa3wyXH6M\nGhoIuxMiGqtZI0NGZaju9vxgKRAyNofBnrPBpuUm9546hMnI+VKWYuD2J17BxTDC\n0U9wBGiWddP5V1whFTLrcljD5u6KnQO+BFJ2lrsBCADCuYyoRoAWUN6MQDEyIG5i\nENKjmZbBRv9F+TDLGEJ2yTcFyPI1G6B7hncoRp+vOUiLvpZBCoMQcmZozwKXrahr\nG/vUjwo26iH+Vgc5JjEEwhLRmcwmjjZKNBYKZLeGUSmQ8MDp99orzVUzd8jAYq+k\nQSe+08yXXYMyTZppp5t5YzsxGOO1Ys25S612NNUTdONegGR86/Uh8BnXnCGSu8p1\nXWsiXeAS5DoICo1pCxj+eVh5+GmxDnjzK55fOpTJCbgB24YYw6RLFDSmLfgLnmQW\nK7AWcVQnOUtNz3hPNalqq/8uN2PZ3nMznXZiRXOeuHXLpks2Ww12qMrPbQNZ0yYB\nABEBAAH+AwMC5K94HuixOdZgK/OJU6DvtgeKSAKZuy/UhjUKKc6/+ObFQv8CXEud\nJKfWIQ5Nc15GTcFYcOsYD4WJPicRFx+CWknjwC9FP2szsELGZLOAt9xFFzgCGp2I\nj63UrquLQe/6rgh3hG36yLClmo6qTvMMU+UOmc3IRlPHkm9hnjgI+UWa13oAIKoT\nfNfqR1Oydzob9B6kTyg7KD5UJz3smIef48y2zc3H7xl24kb6nbI8Qp60M38FwwGE\nn076cOR3wjlhEaX48rDwfoOTZVePYNROZQp+V2osE+Kxe5eDzg5CMx4wQtLgNP7N\n7MorIm6kLMyAbs5A8RBe+T4hrYo7DrcYKxPUXOGWugvGcbyn+ClZ5wqDa3sFhSwu\nzyyiM2PdJOoticV0F6exmu6wKquP6BM2yVKzg6U1tR046ezq6xQVwFVZb/6uGRKx\nUoBnYmadOoRnUp3xyWzTp3Yr0SxUH3ZBKYklIY458YN9VPzxeSn2TnxSdPr5Sayg\nz77Ua0/udV06GIrngOBJDyWJVi3iuqMerA9LiZtYvoTtO7eLoUUVF7d+TCDcNKtK\nZ4cDjHhuamSUjAOBZC+GSM2EX8V9hVmGQSIDmE1+aW/Ny6gQD4iDXZO0XIIG1ocJ\nNOS0jEIFZyPkJb9qghwlRIwWaequmTS4OOfC+nbGeshkhn9MYdXlXntKinV2Ao66\n+7mb0YQEF6gGTGG8sWCnY8oOjAtI5OVZDf1YAUrybr1tOrhv0MsqYEeO+5zJx4Nk\n1zIcJqQ/KaBM0/Y1Bj6CXlFV2zwer7O3EbxzRKjOeu6/TFZ1Nfw6EjH3/kXXthh3\nBoW0ZJNBIOeIsVXVMytW4d+/K6GCMxp1obO9NvfaoKTfZZJDsFYJIso+DYRZTm/D\nlOJPc60t9wlidAqlZKz/JVWj3Tfb3YkBHwQYAQIACQUCUnaWuwIbDAAKCRBQ3NEH\nqwghorr6B/wJjTS32iN6iW9nVikvPzhpGgjhP8lXapiIPev4jYSBg4hnCgjd99sO\nqsvQYUCZr5cC0GWJ+TOnO9belOYXLJiQRwDqq2QdFmYHckuPx4OrVnAZz5n0AKKI\n8WoQ3kr+gui7shdPAb+34hlkHKaQSCMxuq8c+vUev5OYZ5d6wh64yqAq/SVD3z87\nkiQi6xpZqe94gNN/Edfl95xroCQPpRUkhAVTMhPf0VBwm+jnY3c9YHu1XBcWlajG\nH6RTICqylD0wffJr9+PExo3FBmGkds3udPSi9OB74O2Wtucxfdy25U1VgRHm/pWg\ntriPBpdFor4DJl0GvcegTXCwOX/LjfF7\n=P/Su\n-----END PGP PRIVATE KEY BLOCK-----\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Przemysław Kłys @ Evotec\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "Mailozaurr.AzurePipelines.yml",
    "content": "trigger: none\npr: none\n\nvariables:\n  buildConfiguration: 'Debug'\n  dotNetVersion: '8.x'\n\nstages:\n# .NET Testing Stage - Core functionality testing\n- stage: DotNetTests\n  displayName: '.NET Build & Test'\n  jobs:\n  - job: DotNet_Windows\n    displayName: '.NET Tests - Windows'\n    pool:\n      vmImage: 'windows-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Restore .NET Dependencies'\n        inputs:\n          command: 'restore'\n          projects: 'Sources/Mailozaurr.sln'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-restore'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Run .NET Tests'\n        inputs:\n          command: 'test'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-build --logger trx --collect:\"XPlat Code Coverage\"'\n\n      - task: PublishTestResults@2\n        displayName: 'Publish .NET Test Results'\n        inputs:\n          testResultsFormat: 'VSTest'\n          testResultsFiles: '**/*.trx'\n          mergeTestResults: true\n        condition: succeededOrFailed()\n\n  - job: DotNet_Ubuntu\n    displayName: '.NET Tests - Ubuntu'\n    pool:\n      vmImage: 'ubuntu-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - script: |\n          sudo apt-get update\n          sudo apt-get install -y mono-complete\n        displayName: 'Install Mono for .NET Framework tests'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Restore .NET Dependencies'\n        inputs:\n          command: 'restore'\n          projects: 'Sources/Mailozaurr.sln'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-restore'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Run .NET Tests'\n        inputs:\n          command: 'test'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-build --logger trx'\n\n  - job: DotNet_MacOS\n    displayName: '.NET Tests - macOS'\n    pool:\n      vmImage: 'macos-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - script: |\n          brew update\n          brew install mono\n        displayName: 'Install Mono for .NET Framework tests'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Restore .NET Dependencies'\n        inputs:\n          command: 'restore'\n          projects: 'Sources/Mailozaurr.sln'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-restore'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Run .NET Tests'\n        inputs:\n          command: 'test'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration) --no-build --logger trx'\n\n# PowerShell Testing Stage - Module integration testing (independent of .NET stage)\n- stage: PowerShellTests\n  displayName: 'PowerShell Module Tests'\n  condition: always()\n  jobs:\n  - job: PowerShell_Windows_PS5\n    displayName: 'PowerShell 5.1 - Windows'\n    pool:\n      vmImage: 'windows-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration)'\n\n      - powershell: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n        displayName: \"Install Required PowerShell Modules\"\n\n      - powershell: |\n          .\\Mailozaurr.Tests.ps1\n        displayName: \"Run PowerShell Tests (PS 5.1)\"\n\n  - job: PowerShell_Windows_PS7\n    displayName: 'PowerShell 7 - Windows'\n    pool:\n      vmImage: 'windows-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration)'\n\n      - pwsh: |\n          Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n          Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n          Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n        displayName: \"Install Required PowerShell Modules\"\n\n      - pwsh: |\n          .\\Mailozaurr.Tests.ps1\n        displayName: \"Run PowerShell Tests (PS 7)\"\n\n  - job: PowerShell_Ubuntu\n    displayName: 'PowerShell 7 - Ubuntu'\n    pool:\n      vmImage: 'ubuntu-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - script: |\n          curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -\n          curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft.list\n          sudo apt-get update\n          sudo apt-get install -y powershell\n        displayName: \"Install PowerShell Core\"\n\n      - script: sudo apt-get install -y mono-complete\n        displayName: 'Install Mono for .NET Framework tests'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration)'\n\n      - task: PowerShell@2\n        displayName: 'Install Required PowerShell Modules'\n        inputs:\n          targetType: 'inline'\n          script: |\n            Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n            Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n            Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - task: PowerShell@2\n        displayName: 'Run PowerShell Tests'\n        inputs:\n          targetType: 'filePath'\n          filePath: './Mailozaurr.Tests.ps1'\n\n  - job: PowerShell_MacOS\n    displayName: 'PowerShell 7 - macOS'\n    pool:\n      vmImage: 'macos-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - bash: |\n          brew install --cask powershell\n        displayName: \"Install PowerShell 7\"\n\n      - bash: brew install mono\n        displayName: 'Install Mono for .NET Framework tests'\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build .NET Solution'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration)'\n\n      - task: PowerShell@2\n        displayName: 'Install Required PowerShell Modules'\n        inputs:\n          targetType: 'inline'\n          script: |\n            Write-Host \"PowerShell Version: $($PSVersionTable.PSVersion)\"\n            Install-Module -Name Pester -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n            Install-Module -Name PSWriteColor -Repository PSGallery -Force -SkipPublisherCheck -AllowClobber\n\n      - task: PowerShell@2\n        displayName: 'Run PowerShell Tests'\n        inputs:\n          targetType: 'filePath'\n          filePath: './Mailozaurr.Tests.ps1'\n\n# Packaging Stage - Only runs if .NET tests pass (PowerShell tests can fail)\n- stage: Package\n  displayName: 'Package Libraries'\n  dependsOn: DotNetTests\n  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/v2-speedygonzales'))\n  jobs:\n  - job: Package\n    displayName: 'Create Library Package'\n    pool:\n      vmImage: 'windows-latest'\n    steps:\n      - task: UseDotNet@2\n        displayName: 'Install .NET SDK'\n        inputs:\n          packageType: 'sdk'\n          version: $(dotNetVersion)\n\n      - task: DotNetCoreCLI@2\n        displayName: 'Build Release'\n        inputs:\n          command: 'build'\n          projects: 'Sources/Mailozaurr.sln'\n          arguments: '--configuration $(buildConfiguration)'\n\n      - powershell: |\n          if (-not (Test-Path \"Libraries\")) {\n              New-Item -Path \"Libraries\" -ItemType Directory -Force\n          }\n          $SourcePath = \"Sources\\Mailozaurr\\bin\\Release\"\n          if (Test-Path $SourcePath) {\n              Copy-Item -Path \"$SourcePath\\*\" -Destination \"Libraries\" -Recurse -Force\n              Write-Host \"Copied .NET assemblies to Libraries directory\"\n          }\n          Write-Host \"PowerShell module packaging should be done via Build/Manage-Mailozaurr.ps1\"\n        displayName: \"Copy .NET Libraries\"\n\n      - task: PublishBuildArtifacts@1\n        displayName: 'Publish .NET Libraries'\n        inputs:\n          PathtoPublish: 'Libraries'\n          ArtifactName: 'Mailozaurr-Libraries'\n          publishLocation: 'Container'\n\n"
  },
  {
    "path": "Mailozaurr.Tests.ps1",
    "content": "﻿$ModuleName = (Get-ChildItem $PSScriptRoot\\*.psd1).BaseName\n$PrimaryModule = Get-ChildItem -Path $PSScriptRoot -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1\nif (-not $PrimaryModule) {\n    throw \"Path $PSScriptRoot doesn't contain PSD1 files. Failing tests.\"\n}\nif ($PrimaryModule.Count -ne 1) {\n    throw 'More than one PSD1 files detected. Failing tests.'\n}\n$developmentBuildPath = Join-Path $PSScriptRoot 'Sources\\Mailozaurr.PowerShell\\bin\\Debug'\nif (-not $env:MAILOZAURR_DEVELOPMENT -and (Test-Path $developmentBuildPath)) {\n    $env:MAILOZAURR_DEVELOPMENT = '1'\n}\n$PSDInformation = Import-PowerShellDataFile -Path $PrimaryModule.FullName\n$RequiredModules = @(\n    'Pester'\n    'PSWriteColor'\n    if ($PSDInformation.RequiredModules) {\n        $PSDInformation.RequiredModules\n    }\n)\nforeach ($Module in $RequiredModules) {\n    if ($Module -is [System.Collections.IDictionary]) {\n        $Exists = Get-Module -ListAvailable -Name $Module.ModuleName\n        if (-not $Exists) {\n            Write-Warning \"$ModuleName - Downloading $($Module.ModuleName) from PSGallery\"\n            Install-Module -Name $Module.ModuleName -Force -SkipPublisherCheck\n        }\n    } else {\n        $Exists = Get-Module -ListAvailable $Module -ErrorAction SilentlyContinue\n        if (-not $Exists) {\n            Install-Module -Name $Module -Force -SkipPublisherCheck\n        }\n    }\n}\n\nWrite-Color 'ModuleName: ', $ModuleName, ' Version: ', $PSDInformation.ModuleVersion -Color Yellow, Green, Yellow, Green -LinesBefore 2\nWrite-Color 'PowerShell Version: ', $PSVersionTable.PSVersion -Color Yellow, Green\nWrite-Color 'PowerShell Edition: ', $PSVersionTable.PSEdition -Color Yellow, Green\nWrite-Color 'Required modules: ' -Color Yellow\nforeach ($Module in $PSDInformation.RequiredModules) {\n    if ($Module -is [System.Collections.IDictionary]) {\n        Write-Color '   [>] ', $Module.ModuleName, ' Version: ', $Module.ModuleVersion -Color Yellow, Green, Yellow, Green\n    } else {\n        Write-Color '   [>] ', $Module -Color Yellow, Green\n    }\n}\nWrite-Color\n$ImportedModule = Import-Module $PSScriptRoot\\*.psd1 -Force -PassThru -ErrorAction Stop\nif (-not $ImportedModule) {\n    throw \"Failed to import module from $PSScriptRoot.\"\n}\nforeach ($CommandName in 'Get-SmtpConnectionPool', 'Send-EmailMessage') {\n    $Command = Get-Command -Name $CommandName -ErrorAction Stop\n    if ($Command.ModuleName -ne $ImportedModule.Name) {\n        throw \"Expected command '$CommandName' to be exported by module '$($ImportedModule.Name)', but got '$($Command.ModuleName)'.\"\n    }\n}\n$result = Invoke-Pester -Script $PSScriptRoot\\Tests -Verbose -PassThru\n\nif ($result.FailedCount -gt 0) {\n    throw \"$($result.FailedCount) tests failed.\"\n}\n"
  },
  {
    "path": "Mailozaurr.psd1",
    "content": "﻿@{\n    AliasesToExport      = @()\n    Author               = 'Przemyslaw Klys'\n    CmdletsToExport      = @('Add-GraphMailboxPermission', 'Clear-GraphJunk', 'Clear-IMAPJunk', 'Clear-SmtpConnectionPool', 'Connect-EmailGraph', 'Connect-IMAP', 'Connect-OAuthGoogle', 'Connect-OAuthO365', 'Connect-POP3', 'ConvertFrom-EmlToMsg', 'ConvertFrom-MsgToEml', 'ConvertFrom-OAuth2Credential', 'ConvertTo-GraphCertificateCredential', 'ConvertTo-GraphCredential', 'ConvertTo-MailgunCredential', 'ConvertTo-OAuth2Credential', 'ConvertTo-SendGridCredential', 'Disconnect-EmailGraph', 'Disconnect-IMAP', 'Disconnect-POP3', 'Get-DmarcReport', 'Get-EmailDeliveryMatch', 'Get-EmailDeliveryStatus', 'Get-EmailGraphFolder', 'Get-EmailGraphMessage', 'Get-EmailGraphMessageAttachment', 'Get-EmailGraphMessageMime', 'Get-EmailPendingMessage', 'Get-GmailMessage', 'Get-GmailThread', 'Get-GraphEvent', 'Get-GraphInboxRule', 'Get-GraphMailboxPermission', 'Get-GraphMailboxStatistics', 'Get-IMAPFolder', 'Get-IMAPMessage', 'Get-MimeMessageContent', 'Get-POP3Message', 'Get-SmtpConnectionPool', 'Import-MailFile', 'Move-GraphFolder', 'Move-GraphMessage', 'Move-IMAPFolder', 'Move-IMAPMessage', 'New-GraphEvent', 'New-GraphEventBuilder', 'New-GraphInboxRule', 'New-GraphInboxRuleBuilder', 'New-GraphInboxRuleObject', 'New-GraphMailboxPermissionBuilder', 'New-GraphMailboxPermissionObject', 'New-TemporaryMailCrypto', 'Remove-EmailPendingMessage', 'Remove-GmailMessage', 'Remove-GraphEvent', 'Remove-GraphFolder', 'Remove-GraphInboxRule', 'Remove-GraphMailboxPermission', 'Remove-GraphMessage', 'Remove-GraphMessageAttachment', 'Remove-IMAPFolder', 'Remove-IMAPMessage', 'Remove-IMAPMessageAttachment', 'Remove-POP3Message', 'Remove-POP3MessageAttachment', 'Rename-GraphFolder', 'Rename-IMAPFolder', 'Save-GmailMessageAttachment', 'Save-GraphMessage', 'Save-GraphMessageAttachment', 'Save-IMAPMessage', 'Save-IMAPMessageAttachment', 'Save-MimeMessage', 'Save-POP3Message', 'Save-POP3MessageAttachment', 'Search-GraphMailbox', 'Search-IMAPMailbox', 'Search-POP3Mailbox', 'Send-EmailMessage', 'Send-EmailPendingMessage', 'Send-GmailMessage', 'Set-GraphEvent', 'Set-GraphInboxRule', 'Set-GraphMessage', 'Set-IMAPFolder', 'Set-IMAPMessage', 'Set-POP3Message', 'Test-EmailAddress', 'Test-MimeMessageSignature', 'Test-SmtpConnection', 'Unprotect-MimeMessage', 'Wait-GraphMessage', 'Wait-IMAPMessage', 'Wait-POP3Message', 'Watch-SmtpConnectionPool')\n    CompanyName          = 'Evotec'\n    CompatiblePSEditions = @('Desktop', 'Core')\n    Copyright            = '(c) 2011 - 2026 Przemyslaw Klys @ Evotec. All rights reserved.'\n    Description          = 'Mailozaurr is a PowerShell module that aims to provide SMTP, POP3, IMAP and few other ways to interact with Email. Underneath it uses MimeKit and MailKit and EmailValidation libraries written by Jeffrey Stedfast.'\n    FunctionsToExport    = @()\n    GUID                 = '2b0ea9f1-3ff1-4300-b939-106d5da608fa'\n    ModuleVersion        = '2.0.1'\n    PowerShellVersion    = '5.1'\n    PrivateData          = @{\n        PSData = @{\n            IconUri                    = 'https://evotec.xyz/wp-content/uploads/2020/07/MailoZaurr.png'\n            Prerelease                 = 'Preview4'\n            ProjectUri                 = 'https://github.com/EvotecIT/MailoZaurr'\n            RequireLicenseAcceptance   = $false\n            Tags                       = @('Windows', 'MacOS', 'Linux', 'Mail', 'Email', 'MX', 'SPF', 'DMARC', 'DKIM', 'GraphApi', 'SendGrid', 'Graph', 'IMAP', 'POP3')\n            ExternalModuleDependencies = @()\n        }\n    }\n    RootModule           = 'Mailozaurr.psm1'\n    RequiredModules      = @()\n    ScriptsToProcess     = @()\n}"
  },
  {
    "path": "Mailozaurr.psm1",
    "content": "# to speed up development you can opt into direct build output instead of the Lib folder\n$Development = @('1', 'true', 'yes') -contains \"$env:MAILOZAURR_DEVELOPMENT\".ToLowerInvariant()\n$DevelopmentPath = \"$PSScriptRoot\\Sources\\Mailozaurr.PowerShell\\bin\\Debug\"\n$DevelopmentFolderCore = 'net8.0'\n$DevelopmentFolderDefault = 'net472'\n$BinaryModules = @(\n    'Mailozaurr.PowerShell.dll'\n)\n$SkipAssemblyNames = @(\n    'System.Management.Automation.dll'\n    'System.Management.dll'\n)\n\nif ($PSEdition -eq 'Core') {\n    $SkipAssemblyNames += @(\n        'System.Formats.Asn1.dll'\n        'System.Security.Cryptography.Pkcs.dll'\n        'System.Security.Cryptography.ProtectedData.dll'\n    )\n}\n\n# Get public and private function definition files.\n$Public = @(Get-ChildItem -Path $PSScriptRoot\\Public\\*.ps1 -ErrorAction SilentlyContinue -Recurse -File)\n$Private = @(Get-ChildItem -Path $PSScriptRoot\\Private\\*.ps1 -ErrorAction SilentlyContinue -Recurse -File)\n$Classes = @(Get-ChildItem -Path $PSScriptRoot\\Classes\\*.ps1 -ErrorAction SilentlyContinue -Recurse -File)\n$Enums = @(Get-ChildItem -Path $PSScriptRoot\\Enums\\*.ps1 -ErrorAction SilentlyContinue -Recurse -File)\n\n# Get all packaged assembly folders.\n$AssemblyFolders = @(Get-ChildItem -Path $PSScriptRoot\\Lib -Directory -ErrorAction SilentlyContinue)\n\nif ($Development -and -not (Test-Path $DevelopmentPath)) {\n    Write-Warning \"Development mode requested, but debug binaries were not found in $DevelopmentPath. Falling back to packaged libraries.\"\n    $Development = $false\n}\n\n# Lets find which libraries we need to load\nif ($Development) {\n    $Framework = 'Core'\n    $FrameworkNet = 'Default'\n} else {\n    $Default = $false\n    $Core = $false\n    $Standard = $false\n    foreach ($A in $AssemblyFolders.Name) {\n        if ($A -eq 'Default') {\n            $Default = $true\n        } elseif ($A -eq 'Core') {\n            $Core = $true\n        } elseif ($A -eq 'Standard') {\n            $Standard = $true\n        }\n    }\n    if ($Standard -and $Core -and $Default) {\n        $FrameworkNet = 'Default'\n        $Framework = 'Standard'\n    } elseif ($Standard -and $Core) {\n        $Framework = 'Standard'\n        $FrameworkNet = 'Standard'\n    } elseif ($Core -and $Default) {\n        $Framework = 'Core'\n        $FrameworkNet = 'Default'\n    } elseif ($Standard -and $Default) {\n        $Framework = 'Standard'\n        $FrameworkNet = 'Default'\n    } elseif ($Standard) {\n        $Framework = 'Standard'\n        $FrameworkNet = 'Standard'\n    } elseif ($Core) {\n        $Framework = 'Core'\n        $FrameworkNet = ''\n    } elseif ($Default) {\n        $Framework = ''\n        $FrameworkNet = 'Default'\n    }\n}\n\n$BinaryDev = @(\n    if ($Development) {\n        foreach ($BinaryModule in $BinaryModules) {\n            if ($PSEdition -eq 'Core') {\n                $Variable = Resolve-Path \"$DevelopmentPath\\$DevelopmentFolderCore\\$BinaryModule\"\n                $DevelopmentAssemblyFolder = Resolve-Path \"$DevelopmentPath\\$DevelopmentFolderCore\"\n            } else {\n                $Variable = Resolve-Path \"$DevelopmentPath\\$DevelopmentFolderDefault\\$BinaryModule\"\n                $DevelopmentAssemblyFolder = Resolve-Path \"$DevelopmentPath\\$DevelopmentFolderDefault\"\n            }\n            $Variable\n            Write-Warning \"Development mode: Using binaries from $Variable\"\n        }\n    }\n)\n\nif ($Development) {\n    $Assembly = @(Get-ChildItem -Path \"$($DevelopmentAssemblyFolder.Path)\\*.dll\" -ErrorAction SilentlyContinue -File | Where-Object {\n            $_.Name -notin $BinaryModules -and $_.Name -notin $SkipAssemblyNames\n        })\n} else {\n    $Assembly = @(\n        if ($Framework -and $PSEdition -eq 'Core') {\n            Get-ChildItem -Path $PSScriptRoot\\Lib\\$Framework\\*.dll -ErrorAction SilentlyContinue | Where-Object {\n                $_.Name -notin $BinaryModules -and $_.Name -notin $SkipAssemblyNames\n            }\n        }\n        if ($FrameworkNet -and $PSEdition -ne 'Core') {\n            Get-ChildItem -Path $PSScriptRoot\\Lib\\$FrameworkNet\\*.dll -ErrorAction SilentlyContinue | Where-Object {\n                $_.Name -notin $BinaryModules -and $_.Name -notin $SkipAssemblyNames\n            }\n        }\n    )\n}\n\n$FoundErrors = @(\n    if ($Development) {\n        foreach ($BinaryModule in $BinaryDev) {\n            try {\n                Import-Module -Name $BinaryModule -Force -ErrorAction Stop\n            } catch {\n                Write-Warning \"Failed to import module $($BinaryModule): $($_.Exception.Message)\"\n                $true\n            }\n        }\n    } else {\n        foreach ($BinaryModule in $BinaryModules) {\n            try {\n                if ($Framework -and $PSEdition -eq 'Core') {\n                    Import-Module -Name \"$PSScriptRoot\\Lib\\$Framework\\$BinaryModule\" -Force -ErrorAction Stop\n                }\n                if ($FrameworkNet -and $PSEdition -ne 'Core') {\n                    Import-Module -Name \"$PSScriptRoot\\Lib\\$FrameworkNet\\$BinaryModule\" -Force -ErrorAction Stop\n                }\n            } catch {\n                Write-Warning \"Failed to import module $($BinaryModule): $($_.Exception.Message)\"\n                $true\n            }\n        }\n    }\n    foreach ($Import in @($Assembly)) {\n        try {\n            Add-Type -Path $Import.FullName -ErrorAction Stop\n        } catch [System.Reflection.ReflectionTypeLoadException] {\n            Write-Warning \"Processing $($Import.Name) Exception: $($_.Exception.Message)\"\n            $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique\n            foreach ($E in $LoaderExceptions) {\n                Write-Warning \"Processing $($Import.Name) LoaderExceptions: $($E.Message)\"\n            }\n            $true\n        } catch {\n            Write-Warning \"Processing $($Import.Name) Exception: $($_.Exception.Message)\"\n            $LoaderExceptions = @()\n            if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) {\n                $LoaderExceptions = @($_.Exception.LoaderExceptions) | Sort-Object -Unique\n            }\n            foreach ($E in $LoaderExceptions) {\n                Write-Warning \"Processing $($Import.Name) LoaderExceptions: $($E.Message)\"\n            }\n            $true\n        }\n    }\n    # Dot source the files\n    foreach ($Import in @($Classes + $Enums + $Private + $Public)) {\n        try {\n            . $Import.FullName\n        } catch {\n            Write-Error -Message \"Failed to import functions from $($Import.FullName): $_\"\n            $true\n        }\n    }\n)\n\nif ($FoundErrors.Count -gt 0) {\n    $ModuleName = (Get-ChildItem $PSScriptRoot\\*.psd1).BaseName\n    Write-Warning \"Importing module $ModuleName failed. Fix errors before continuing.\"\n    break\n}\n\nExport-ModuleMember -Function '*' -Alias '*' -Cmdlet '*'\n"
  },
  {
    "path": "README.MD",
    "content": "# Mailozaurr - Modern Email Toolkit for .NET and PowerShell\n\nMailozaurr is available as a NuGet package and as a PowerShell module from PSGallery.\n\nNuGet Package\n\n[![nuget downloads](https://img.shields.io/nuget/dt/Mailozaurr?label=nuget%20downloads)](https://www.nuget.org/packages/Mailozaurr)\n[![nuget version](https://img.shields.io/nuget/v/Mailozaurr)](https://www.nuget.org/packages/Mailozaurr)\n\nPowerShell Module\n\n[![powershell gallery version](https://img.shields.io/powershellgallery/v/Mailozaurr.svg)](https://www.powershellgallery.com/packages/Mailozaurr)\n[![powershell gallery preview](https://img.shields.io/powershellgallery/v/Mailozaurr.svg?label=powershell%20gallery%20preview&colorB=yellow&include_prereleases)](https://www.powershellgallery.com/packages/Mailozaurr)\n[![powershell gallery platforms](https://img.shields.io/powershellgallery/p/Mailozaurr.svg)](https://www.powershellgallery.com/packages/Mailozaurr)\n[![powershell gallery downloads](https://img.shields.io/powershellgallery/dt/Mailozaurr.svg)](https://www.powershellgallery.com/packages/Mailozaurr)\n\nProject Information\n\n[![build status](https://dev.azure.com/evotecpl/Mailozaurr/_apis/build/status/EvotecIT.Mailozaurr)](https://dev.azure.com/evotecpl/Mailozaurr/_build/results?buildId=latest)\n[![codecov](https://codecov.io/gh/EvotecIT/Mailozaurr/branch/v2-speedygonzales/graph/badge.svg)](https://codecov.io/gh/EvotecIT/Mailozaurr)\n[![top language](https://img.shields.io/github/languages/top/evotecit/Mailozaurr.svg)](https://github.com/EvotecIT/Mailozaurr)\n[![license](https://img.shields.io/github/license/EvotecIT/Mailozaurr.svg)](https://github.com/EvotecIT/Mailozaurr)\n[![code size](https://img.shields.io/github/languages/code-size/evotecit/Mailozaurr.svg)](https://github.com/EvotecIT/Mailozaurr)\n\nAuthor and Social\n\n[![Twitter follow](https://img.shields.io/twitter/follow/PrzemyslawKlys.svg?label=Twitter%20%40PrzemyslawKlys&style=social)](https://twitter.com/PrzemyslawKlys)\n[![Blog](https://img.shields.io/badge/Blog-evotec.xyz-2A6496.svg)](https://evotec.xyz/hub)\n[![LinkedIn](https://img.shields.io/badge/LinkedIn-pklys-0077B5.svg?logo=LinkedIn)](https://www.linkedin.com/in/pklys)\n[![Discord](https://img.shields.io/discord/508328927853281280?style=flat-square&label=discord%20chat)](https://evo.yt/discord)\n\n## What it's all about\n\nMailozaurr provides SMTP, POP3, IMAP, Microsoft Graph mail support, and mail-format tooling for PowerShell and .NET. Underneath it uses MimeKit and MailKit libraries written by Jeffrey Stedfast.\n\nThis repository has two branches:\n- `v1-legacy`: old module generation, written in PowerShell and still maintained until V2 is finalized.\n- `v2-speedygonzales`: current rewrite in C# with newer functionality and better performance.\n\nIn V2, some functionality was moved to dedicated projects:\n- DNS features moved to [DnsClientX](https://github.com/EvotecIT/DnsClientX).\n- SPF, DKIM, DMARC, and MX validation (except download-focused functionality) moved to [DomainDetective](https://github.com/EvotecIT/DomainDetective).\n\n## Core dependencies (V2)\n\n- [MailKit](https://github.com/jstedfast/MailKit) - licensed MIT\n- [MimeKit](https://github.com/jstedfast/MimeKit) - licensed MIT\n- [MsgKit](https://github.com/Sicos1977/MsgKit) - licensed MIT\n- [MsgReader](https://github.com/Sicos1977/MSGReader) - licensed MIT\n\nFor OAuth2 support, Mailozaurr also bundles:\n\n- [Microsoft.Identity.Client](https://www.nuget.org/packages/Microsoft.Identity.Client/)\n- [Google.Apis](https://www.nuget.org/packages/Google.Apis)\n- [Google.Apis.Core](https://www.nuget.org/packages/Google.Apis.Core)\n- [Google.Apis.Auth](https://www.nuget.org/packages/Google.Apis.Auth)\n\nAdditional dependency:\n\n- [System.DataSQLite](https://system.data.sqlite.org/)\n\n## Architecture notes\n\n- [Platform Architecture](Docs/Platform-Architecture.md) - layering and reuse rules for future library, PowerShell, CLI, MCP, and GUI work\n- [Configuration and Usage](Docs/Configuration-and-Usage.md) - profile model, provider setup guidance, CLI storage, recipes, and MCP usage\n\n## Additional surfaces\n\nMailozaurr is no longer only a library and PowerShell module. The repository now also contains shared application-layer work and headless surfaces for broader automation scenarios:\n\n- `.NET library` for direct application and service integration\n- `PowerShell module` for administrators and automation users\n- `mailozaurr` executable for cross-platform CLI workflows\n- `mailozaurr mcp serve` for MCP-based integrations\n\nCLI and MCP are intended to sit on top of shared Mailozaurr services instead of duplicating mailbox, draft, queue, send, and message-action behavior independently.\n\nIf you want the placement rules and longer-term cross-surface direction, see [Platform Architecture](Docs/Platform-Architecture.md).\nIf you want practical profile setup and usage guidance for CLI and MCP, see [Configuration and Usage](Docs/Configuration-and-Usage.md).\n\n## CLI and MCP usage\n\nThe repository now includes a cross-platform `mailozaurr` executable and an MCP server hosted by that same executable.\n\nTo see the current command surface from source:\n\n```powershell\ndotnet run --project Sources/Mailozaurr.Cli -- --help\n```\n\nAfter building, the executable is available at:\n\n- `Sources/Mailozaurr.Cli/bin/Debug/net8.0/mailozaurr.exe` on Windows\n- `Sources/Mailozaurr.Cli/bin/Debug/net8.0/mailozaurr` on macOS/Linux\n\n### CLI command groups\n\nThe executable currently supports:\n\n- `profile ...` for profile creation, bootstrap, login, auth refresh, readiness checks, summaries, validation, and secret management\n- `draft ...` for saving, listing, exporting, importing, and deleting reusable drafts\n- `send ...` for sending from a stored draft, a draft file, or direct command-line fields\n- `queue ...` for viewing and processing queued outbound messages\n- `mail ...` for folders, folder aliases, search, message retrieval, attachment export, mailbox actions, previews, and reusable action-plan batches\n- `mcp serve` for exposing the same shared services as MCP tools over stdio\n\nMost list and inspection commands support `--json`, which makes the executable useful in scripts, CI, and other automation surfaces.\n\n### CLI examples\n\nCreate a profile and inspect it:\n\n```powershell\nmailozaurr profile create --profile work-imap --kind imap --name \"Work IMAP\" `\n  --setting server=imap.example.com --setting port=993 --json\n\nmailozaurr profile set-secret --profile work-imap --name password --value \"secret\" --json\nmailozaurr profile summary --profile work-imap --json\nmailozaurr profile test --profile work-imap --scope mailbox --json\n```\n\nSearch and inspect mail:\n\n```powershell\nmailozaurr mail folders --profile work-imap --compact --json\nmailozaurr mail search --profile work-imap --folder Inbox --query Invoice --compact --json\nmailozaurr mail get --profile work-imap --folder Inbox --message-id 123 --compact --json\nmailozaurr mail attachments --profile work-imap --folder Inbox --message-id 123 --json\nmailozaurr mail save-attachments --profile work-imap --folder Inbox --message-id 123 --path C:\\Temp\\Attachments --json\n```\n\nDraft, send, and queue mail:\n\n```powershell\nmailozaurr draft save --draft weekly-update --name \"Weekly update\" --profile work-smtp `\n  --to team@example.com --subject \"Weekly update\" --text \"Status attached.\" --json\n\nmailozaurr send --draft weekly-update --json\nmailozaurr queue list --compact --json\nmailozaurr queue process --json\n```\n\nPreview and execute mailbox actions:\n\n```powershell\nmailozaurr mail preview-delete --profile work-imap --folder Inbox --message-id 123 --json\nmailozaurr mail delete --profile work-imap --folder Inbox --message-id 123 --confirm-token <token> --json\n\nmailozaurr mail preview-move --profile work-imap --folder Inbox --message-id 123 --target-folder Archive --json\nmailozaurr mail move --profile work-imap --folder Inbox --message-id 123 --target-folder Archive --confirm-token <token> --json\n```\n\n### MCP server usage\n\nMailozaurr can also run as an MCP server over stdio:\n\n```powershell\nmailozaurr mcp serve\n```\n\nThe MCP surface is built on top of the same shared services as the CLI. Current tool areas include:\n\n- profile and auth management\n- folder and folder-alias discovery\n- mail search, get, batch get, and attachment save\n- draft, send, and queue operations\n- mailbox action preview and execution\n- action-plan import/export, execution, and stored batch management\n\nA minimal MCP client entry typically points to the executable and passes `mcp serve` as arguments:\n\n```json\n{\n  \"command\": \"mailozaurr\",\n  \"args\": [\"mcp\", \"serve\"]\n}\n```\n\nIf your client prefers an absolute path, point it to the built executable instead.\n\n### Skills and MCP\n\nSkills are a good fit on top of the MCP server, but they are not a replacement for the server itself.\n\n- `mailozaurr mcp serve` provides the actual mailbox and send tools\n- skills provide workflow guidance, safety rules, and task-specific behavior for an MCP client or agent\n\nIn practice, that means Mailozaurr should own the reusable mailbox, queue, send, and action logic, while skills can teach an agent how to use those tools safely and consistently.\n\nThis started with a single goal to replace `Send-MailMessage` which is deprecated/obsolete with something more modern, but since MailKit and MimeKit have lots of options why not build on that?\n\n## Support This Project\n\nIf you find this project helpful, please consider supporting its development.\nYour sponsorship will help the maintainers dedicate more time to maintenance and new feature development for everyone.\n\nIt takes a lot of time and effort to create and maintain this project.\nBy becoming a sponsor, you can help ensure that it stays free and accessible to everyone who needs it.\n\nTo become a sponsor, you can choose from the following options:\n\n - [Become a sponsor via GitHub Sponsors :heart:](https://github.com/sponsors/PrzemyslawKlys)\n - [Become a sponsor via PayPal :heart:](https://paypal.me/PrzemyslawKlys)\n\nYour sponsorship is completely optional and not required for using this project.\nWe want this project to remain open-source and available for anyone to use for free,\nregardless of whether they choose to sponsor it or not.\n\nIf you work for a company that uses our .NET libraries or PowerShell Modules,\nplease consider asking your manager or marketing team if your company would be interested in supporting this project.\nYour company's support can help us continue to maintain and improve this project for the benefit of everyone.\n\nThank you for considering supporting this project!\n\n## Building project for local use\nTo build a project and play with the sources you need to:\n\n```powershell\nInstall-Module PSPublishModule -Force -Verbose\n```\n\nOnce the module is installed - go to Build folder and run: `Manage-Mailozaurr.ps1`.\nThis will make sure the project has required libraries.\n\n## Features\n\n- Send Email (`Send-EmailMessage`) using:\n  - [x] SMTP with standard password\n  - [x] SMTP with oAuth2 Office 365\n  - [x] SMTP with oAuth2 Google Mail\n  - [x] SMTP with SendGrid\n  - [x] SendGrid API\n  - [x] Amazon SES API\n  - [x] Office 365 Graph API\n  - [x] PGP/MIME encryption and signature verification\n  - Optional retry logic for `Send-EmailMessage` via `-RetryCount`, `-RetryDelayMilliseconds`, `-RetryDelayBackoff` and `-RetryAlways`.  Retries are only attempted for transient errors by default, but `-RetryAlways` forces retries on all errors.\n  - Optional SMTP connection pooling via `-UseConnectionPool` and `-ConnectionPoolSize` for faster repeated sends. Use `Clear-SmtpConnectionPool` to reset the pool and `Test-SmtpConnection` to check if your server keeps connections open.\n- POP3\n  - [x] Connect to POP3\n  - [x] Get POP3 Emails\n  - [x] Save POP3 Emails\n  - [x] Delete POP3 Messages with `Get-POP3Message -Delete`\n  - [x] Wait for new POP3 messages with the `Wait-POP3Message` cmdlet supporting `-Until`, `-StopOnMatch` and `-TimeoutSeconds`\n  - [x] Search POP3 mailboxes with `Search-POP3Mailbox` returning messages (use `-Count` to limit results)\n- IMAP\n  - [x] Connect to IMAP\n  - [x] Get IMAP Folder\n  - [x] List IMAP root folders with `Get-IMAPFolder -Root`\n  - [x] Get IMAP Messages\n  - [x] Delete IMAP Messages with `Get-IMAPMessage -Delete`\n  - [x] Listen for new IMAP messages via `ImapIdleListener`\n  - [x] Wait for new IMAP messages with the `Wait-IMAPMessage` cmdlet supporting `-Until`, `-StopOnMatch` and `-TimeoutSeconds`\n  - [x] Search IMAP mailboxes with `Search-IMAPMailbox` returning messages (use `-Count` to limit results)\n- Office 365 Graph API\n  - [x] Get Mail Folders via `Get-EmailGraphFolder` with `-Connection` or `-MgGraphRequest`\n  - [x] Get Mail Messages\n  - [x] Save Mail Messages\n  - [x] Mark messages read or unread with `Set-GraphMessage`\n  - [x] Wait for new Graph messages with the `Wait-GraphMessage` cmdlet\n  - [x] Search Graph mailboxes with `Search-GraphMailbox` returning message info (use `-Count` to limit results)\n  - [x] Message cmdlets output friendly wrappers exposing From, To, Subject, Date and body text\n- Pipeline input is supported for Graph connection objects when retrieving\n  folders or messages:\n\n```powershell\n$graph = Connect-EmailGraph -Credential $cred\n$graph | Get-EmailGraphFolder -UserPrincipalName 'user@example.com'\n$graph | Get-EmailGraphMessage -UserPrincipalName 'user@example.com' -Limit 10 |\n    Save-GraphMessage -Path 'C:\\Archive'\n```\n- DNS and email validation functionality is now provided by [DomainDetective](https://github.com/EvotecIT/DomainDetective)\n- [x] Convert between EML and MSG message formats\n\n### Authenticating to IMAP and POP3\n\nBoth `Connect-IMAP` and `Connect-POP3` support a variety of sign-in methods. You can use plain credentials, pass a `PSCredential` object, or supply an OAuth token.\n\n```powershell\n# Username and password\n$imap = Connect-IMAP -Server 'imap.example.com' -UserName 'user@example.com' -Password 'p@ssword'\n\n# Credential object\n$cred = Get-Credential\n$pop3 = Connect-POP3 -Server 'pop.example.com' -Credential $cred\n\n# OAuth2 token with a SecureString client secret\n$googleClientSecret = Read-Host 'Google client secret' -AsSecureString\n$oauth = Connect-OAuthGoogle -ClientId 'id' -ClientSecretSecureString $googleClientSecret -GmailAccount 'user@example.com' -Scope https://mail.google.com/\n$imapOAuth = Connect-IMAP -Server 'imap.gmail.com' -Credential $oauth -OAuth2\n```\n\n### Enumerating IMAP folders\n\nAfter connecting you can list the top-level folders:\n\n```powershell\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred\nGet-IMAPFolder -Client $client -Root\n```\n\nUsing OAuth tokens works the same:\n\n```powershell\n$token = Connect-OAuthGoogle -ClientId 'id' -ClientSecretSecretName 'gmail-client-secret' -ClientSecretVaultName 'MailSecrets' -GmailAccount 'user@example.com' -Scope https://mail.google.com/\n$client = Connect-IMAP -Server 'imap.gmail.com' -Credential $token -OAuth2\nGet-IMAPFolder -Client $client -Root\n```\n\nYou can also open a specific folder by path:\n\n```powershell\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred\nGet-IMAPFolder -Client $client -Path 'Inbox/Reports'\n```\n\n### Listing folder contents\n\nOnce a folder is opened you can retrieve its messages using `Get-IMAPMessage`.\n\n```powershell\n# Inbox messages\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred\nGet-IMAPFolder -Client $client\nGet-IMAPMessage -Client $client\n\n# Nested folder\nGet-IMAPFolder -Client $client -Path 'Inbox/Reports/2024'\nGet-IMAPMessage -Client $client\n\n# Sent and deleted items\nGet-IMAPFolder -Client $client -Path 'Sent'\nGet-IMAPMessage -Client $client\nGet-IMAPFolder -Client $client -Path 'Deleted Items'\nGet-IMAPMessage -Client $client\n```\n\n### Listening for new IMAP mail\n\nThe `ImapIdleListener` class uses the IMAP IDLE command to raise events whenever\nnew mail arrives. Create an instance with your connected `ImapClient`, subscribe\nto `MessageArrived`, and call `StartAsync`:\n\n```csharp\nusing var client = new ImapClient();\nawait client.ConnectAsync(\"imap.example.com\", 993, SecureSocketOptions.SslOnConnect);\nawait client.AuthenticateAsync(\"user@example.com\", \"password\");\n\nvar listener = new Mailozaurr.ImapIdleListener(client);\nlistener.MessageArrived += (s, msg) =>\n    Console.WriteLine($\"New message: {msg.Message.Subject}\");\nawait listener.StartAsync();\n```\n\nCall `Stop()` to end listening. See\n`Sources/Mailozaurr.Examples/ImapIdleListenerExample.cs` for a full example.\n\nAlternatively, use the PowerShell cmdlet `Wait-IMAPMessage` to output new\nmessages directly to the pipeline:\n\n```powershell\n$cred = Get-Credential\n$client = Connect-IMAP -Server 'imap.example.com' -Credential $cred\nWait-IMAPMessage -Client $client -Until { $_.Message.From.Mailboxes.Address -contains 'alice@example.com' } -StopOnMatch -TimeoutSeconds 600 -Action { param($m) \"New IMAP from Alice: $($m.Message.Subject)\" }\n```\n\nPress <kbd>Ctrl+C</kbd> to stop waiting for messages.\n\nThe same concept applies to POP3, IMAP and Microsoft Graph mailboxes. `Wait-POP3Message`, `Wait-IMAPMessage` and `Wait-GraphMessage` all support `-Until`, `-StopOnMatch` and `-TimeoutSeconds` parameters so you can wait for specific senders or stop after a period of time:\n\n```powershell\n$cred = Get-Credential\n$graph = Connect-EmailGraph -Credential $cred\nWait-GraphMessage -Connection $graph -UserPrincipalName 'user@example.com' -Until { $_.from.emailAddress.address -eq 'alice@example.com' } -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($m)\n    \"Graph from Alice: $($m.subject)\"\n}\n```\n\n### Throttling‑safe sending (Graph) and retry/backoff knobs\n\nMailozaurr v2 includes a built‑in policy for Microsoft Graph sends that handles throttling and transient errors:\n\n- Concurrency limiting to avoid request bursts\n- Exponential backoff with jitter and a max cap\n- Honors Retry-After header on 429\n- Optional SMTP fallback when Graph ultimately fails\n\nPowerShell\n\n```powershell\n# Graph send with conservative retry/backoff and concurrency\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -SecretName 'graph-client-secret' -VaultName 'MailSecrets' -DirectoryId $TenantId\n\nSend-EmailMessage -Graph -From 'sender@example.com' -To 'user@example.com' `\n  -Credential $cred -HTML '<b>Hello</b>' -Subject 'Graph policy demo' `\n  -RetryCount 4 -RetryDelayMilliseconds 1000 `\n  -JitterMilliseconds 500 -MaxDelayMilliseconds 30000 `\n  -GraphMaxConcurrency 2 -Verbose\n\n# Optional: SMTP fallback when Graph keeps failing (configure once)\n[Mailozaurr.MailozaurrOptions]::SmtpFallbackFactory = {\n  param($graph)\n  $s = [Mailozaurr.Smtp]::new()\n  $s.Connect('smtp.office365.com', 587)\n  $s.Authenticate([System.Net.NetworkCredential]::new('user','pass'))\n  $s\n}\nSend-EmailMessage -Graph -EnableSmtpFallback -From 'sender@example.com' -To 'user@example.com' `\n  -Credential $cred -HTML '<b>Hello via fallback</b>' -Subject 'Graph+SMTP fallback'\n\n# Same jitter/max-delay knobs also apply to SMTP/SendGrid/Mailgun/SES\nSend-EmailMessage -Server 'smtp.office365.com' -Port 587 -UseSsl `\n  -From 'sender@example.com' -To 'user@example.com' -Credential (Get-Credential) `\n  -RetryCount 4 -RetryDelayMilliseconds 2000 -JitterMilliseconds 400 -MaxDelayMilliseconds 30000\n```\n\nC# (Graph)\n\n```csharp\nusing Mailozaurr;\nvar policy = new GraphSendPolicy {\n    MaxConcurrency = 2,\n    MaxRetries = 4,\n    BaseDelayMs = 1000,\n    MaxDelayMs = 30000,\n    JitterMs = 500,\n    RetryOnTransient = true,\n    EnableSmtpFallback = true\n};\n\nvar graph = new Graph()\n    .WithSendPolicy(policy)\n    .WithSmtpFallback(() => {\n        var s = new Smtp();\n        s.Connect(\"smtp.office365.com\", 587);\n        s.Authenticate(\"user@example.com\", \"password\");\n        return s;\n    });\n\ngraph.From = \"sender@example.com\";\ngraph.To = new object[] { \"user@example.com\" };\ngraph.Subject = \"Graph policy demo\";\ngraph.HTML = \"<b>Hello</b>\";\ngraph.Authenticate(new System.Net.NetworkCredential(\"clientid@tenant.onmicrosoft.com\", \"client-secret\"));\nawait graph.ConnectO365GraphAsync();\nawait graph.SendMessageAsync();\n```\n\nNotes\n\n- Defaults are conservative. Override per‑invocation in PowerShell or set `MailozaurrOptions.DefaultGraphPolicy` in code.\n- For raw `Invoke‑MgGraphRequest` scenarios, use `-GraphMaxConcurrency` to keep batch operations within Graph tenancy limits.\n\n```powershell\n$cred = Get-Credential\n$client = Connect-POP3 -Server 'pop.example.com' -Credential $cred\nWait-POP3Message -Client $client -Until { $_.Message.From.Mailboxes.Address -contains 'alice@example.com' } -StopOnMatch -TimeoutSeconds 600 -Action {\n    param($m)\n    \"POP3 from Alice: $($m.Message.Subject)\"\n}\n```\n\n### Using OpenPGP\n\nPGP/MIME encryption allows securing messages using public and private keys. To sign and encrypt a message provide paths to the public and secret key files:\n\n```powershell\n$pub = 'Examples/PGPKeys/mimekit.gpg.pub'\n$sec = 'Examples/PGPKeys/mimekit.gpg.sec'\nSend-EmailMessage -From 'mimekit@example.com' -To 'mimekit@example.com' \\ \n    -Server 'smtp.example.com' -Port 25 -Body 'Test' -Subject 'Test' \\ \n    -SignOrEncrypt PgpSignAndEncrypt -PublicKeyPath $pub -PrivateKeyPath $sec \\ \n    -PrivateKeyPassword 'no.secret'\n```\n\n## Documentation\n\nWhile I didn't spent much time creating WIKI, working on Get-Help documentation, I did write blog articles that should help you get started.\n\n- [x] [Mailozaurr - New mail toolkit (SMTP, IMAP, POP3) with support for oAuth 2.0 and GraphApi for PowerShell](https://evotec.xyz/mailozaurr-new-mail-toolkit-smtp-imap-pop3-with-support-for-oauth-2-0-and-graphapi-for-powershell/)\n- [x] [Easy way to send emails using Microsoft Graph API (Office 365) with PowerShell](https://evotec.xyz/easy-way-to-send-emails-using-microsoft-graph-api-office-365-with-powershell/)\n\nYou can also utilize Examples which should help to understand use cases. Of course it would be great having pretty help so if you can help me out feel free to submit PR's.\n- [Example-SendEmail-SignEncrypt.ps1](Examples/Example-SendEmail-SignEncrypt.ps1) - sign and encrypt an email using SMTP.\n- [Example-SendEmail-Pgp.ps1](Examples/Example-SendEmail-Pgp.ps1) - send a PGP encrypted email.\n- [Example-SendEmail-OAuthGmail.ps1](Examples/Example-SendEmail-OAuthGmail.ps1) - send mail through Gmail using OAuth2.\n- [Example-SendEmail-OAuthO365.ps1](Examples/Example-SendEmail-OAuthO365.ps1) - send mail through Office 365 using OAuth2.\n- [Example-GraphManageMessages.ps1](Examples/Example-GraphManageMessages.ps1) - download attachments, set read state and move messages using Microsoft Graph.\n- [Example-SetGraphMessage.ps1](Examples/Example-SetGraphMessage.ps1) - mark Graph messages read or unread and move them.\n- [Example-SavePop3MessageAttachment.ps1](Examples/Example-SavePop3MessageAttachment.ps1) - download attachments from a POP3 mailbox.\n- [Example-SaveImapMessageAttachment.ps1](Examples/Example-SaveImapMessageAttachment.ps1) - download attachments from an IMAP mailbox.\n- [Example-SavePop3MessageAttachmentFiltered.ps1](Examples/Example-SavePop3MessageAttachmentFiltered.ps1) - search POP3 messages and save matching attachments.\n- [Example-SaveImapMessageAttachmentFiltered.ps1](Examples/Example-SaveImapMessageAttachmentFiltered.ps1) - search IMAP messages and save matching attachments.\n- [Example-ListImapRootFolders.ps1](Examples/Example-ListImapRootFolders.ps1) - list root folders using credentials.\n- [Example-ListImapRootFoldersOAuth.ps1](Examples/Example-ListImapRootFoldersOAuth.ps1) - list root folders using OAuth.\n- [Example-ListImapFolderContents.ps1](Examples/Example-ListImapFolderContents.ps1) - open folders by path and list messages.\n- [Example-GetMailFolder.ps1](Examples/Example-GetMailFolder.ps1) - list folders using either `-Connection` or `-MgGraphRequest`.\n- [Example-ConnectEmailGraph-DeviceCode.ps1](Examples/Example-ConnectEmailGraph-DeviceCode.ps1) - authenticate with device code.\n- [Example-ConnectEmailGraph-OnBehalfOf.ps1](Examples/Example-ConnectEmailGraph-OnBehalfOf.ps1) - exchange a user token for Graph access.\n- [Example-SendEmail-GraphDeviceCode.ps1](Examples/Example-SendEmail-GraphDeviceCode.ps1) - interactive device code sign-in and send an email.\n- [Example-SendEmail-GraphWithMgRequest.ps1](Examples/Example-SendEmail-GraphWithMgRequest.ps1) - send mail using Connect-MgGraph and the `-MgGraphRequest` switch.\n- Use `Connect-EmailGraph` and `Disconnect-EmailGraph` to authenticate and release application credentials for Microsoft Graph. The connection cmdlet supports both client secret and certificate authentication modes.\n- `Connect-EmailGraph`, `Connect-IMAP` and `Connect-POP3` store the last successful connection in module variables. Subsequent cmdlets use these defaults when `-Connection` or `-Client` is omitted.\n- [Example-SendEmail-Attachments.ps1](Examples/Example-SendEmail-Attachments.ps1) - demonstrate file and in-memory attachments.\n- [Example-SendEmail-ConnectionPool.ps1](Examples/Example-SendEmail-ConnectionPool.ps1) - basic connection pool usage.\n- [Example-SendEmail-ConnectionPool-Advanced.ps1](Examples/Example-SendEmail-ConnectionPool-Advanced.ps1) - reuse the connection while sending multiple different messages.\n- [Example-TestSmtpConnection.ps1](Examples/Example-TestSmtpConnection.ps1) - verify if the server keeps connections alive before enabling pooling.\n- [Example-WaitImapMessage.ps1](Examples/Example-WaitImapMessage.ps1) - wait for new mail from PowerShell.\n- [Example-DeleteImapMessages.ps1](Examples/Example-DeleteImapMessages.ps1) - delete messages from an IMAP mailbox.\n- [Example-DeletePop3Messages.ps1](Examples/Example-DeletePop3Messages.ps1) - delete messages from a POP3 mailbox.\n- [Example-ImapFilterScenarios.ps1](Examples/Example-ImapFilterScenarios.ps1) - ten filtering techniques for IMAP.\n- [Example-Pop3FilterScenarios.ps1](Examples/Example-Pop3FilterScenarios.ps1) - ten filtering techniques for POP3.\n- [ImapIdleListenerExample.cs](Sources/Mailozaurr.Examples/ImapIdleListenerExample.cs) - C# sample listening for new mail.\n- [PGP documentation](Docs/PGP.md) - overview of OpenPGP support.\n- [OAuth flow documentation](Docs/OAuthFlows.md) - device code and on-behalf-of usage.\n- [Configuration and Usage](Docs/Configuration-and-Usage.md) - profile configuration, provider recipes, CLI usage, and MCP usage.\n\n-Some useful examples:\n- `Examples/Example-AcquireO365TokenInteractive.ps1` – demonstrates `Connect-OAuthO365`, `ConvertFrom-OAuth2Credential`, and using the token with `Send-EmailMessage`, IMAP, and POP3.\n- `Examples/Example-AcquireGoogleTokenInteractive.ps1` – demonstrates `Connect-OAuthGoogle`, `ConvertFrom-OAuth2Credential`, and using the token with `Send-EmailMessage`, IMAP, and POP3.\n- `Sources/Mailozaurr.Examples/AcquireO365TokenInteractive.cs` – C# sample for `OAuthHelpers.AcquireO365TokenInteractiveAsync`.\n- `Sources/Mailozaurr.Examples/AcquireGoogleTokenInteractive.cs` – C# sample for `OAuthHelpers.AcquireGoogleTokenInteractiveAsync`.\n- `Sources/Mailozaurr.Examples/SendEmailAttachments.cs` – C# sample demonstrating attachments and inline resources.\n\n### Choosing between `-Graph` and `-MgGraphRequest`\n\nThe module supports two ways of calling Microsoft Graph. Use `-Graph` when you connect with `Connect-EmailGraph` and provide credentials created with `ConvertTo-GraphCredential` or `ConvertTo-GraphCertificateCredential`. The `-MgGraphRequest` switch is meant for scenarios where you authenticate through the Microsoft Graph PowerShell SDK using `Connect-MgGraph`.\n\n| Parameter | Connect command | When to use |\n|-----------|-----------------|-------------|\n| `-Graph` | `Connect-EmailGraph` | Application or certificate authentication handled by Mailozaurr. |\n| `-MgGraphRequest` | `Connect-MgGraph` | Already authenticated via the Microsoft Graph PowerShell SDK. |\n\n**Example using `-Graph`**\n\n```powershell\n$cred = ConvertTo-GraphCredential -ClientId $ClientId -SecretName 'graph-client-secret' -VaultName 'MailSecrets' -DirectoryId $TenantId\nConnect-EmailGraph -Credential $cred\nSend-EmailMessage -From 'user@example.com' -To 'user@example.com' -Subject 'Graph Test' -Graph\nDisconnect-EmailGraph\n```\n\n**Example using `-MgGraphRequest`**\n\n```powershell\nImport-Module Microsoft.Graph.Authentication\nConnect-MgGraph -Scopes Mail.Send -NoWelcome\nSend-EmailMessage -From 'user@example.com' -To 'user@example.com' -Subject 'Graph Test' -MgGraphRequest\n```\n\n### Filtering Graph messages\n\n`Get-EmailGraphMessage` supports the `-Filter` parameter for raw OData queries. This value is passed directly to Microsoft Graph's `$filter` option.\n\n```powershell\nGet-EmailGraphMessage -UserPrincipalName 'user@example.com' -Filter \"receivedDateTime ge 2024-01-01T00:00:00Z and contains(subject,'Invoice')\"\n```\n\nSee the [Microsoft Graph query parameters documentation](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) for details.\n\nKeep in mind PSSharedGoods is only required for development. When you use this module from PowerShellGallery it's not installed as everything is merged.\n\n### SMTP connection pooling\n\nYou can reuse SMTP connections by enabling the connection pool with the `-UseConnectionPool` switch of `Send-EmailMessage`. The `-ConnectionPoolSize` parameter controls how many connections are kept alive. Use `Test-SmtpConnection` to examine the banner and capabilities and to check whether the connection remains open after a `NOOP` command. If the returned object has `Persistent` set to `True`, you can safely enable pooling. Call `Clear-SmtpConnectionPool` whenever you need to discard existing pooled clients.\n\n### Limiting access to mailboxes (Microsoft Graph API)\n\nMicrosoft Graph API requires `Send.Mail` permission to send emails. In some cases `Mail.ReadWrite` permission is also required when the size of email is above 4MB.\nIf the application is registered in Azure AD (Entra ID) without any additional configuration, it will have access to all mailboxes in the organization.\nTo limit access to specific mailboxes, you need to use Application Access Policy.\nThis is a feature that allows you to limit access to specific mailboxes. You can read more about it [here](https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access).\n\nFollowing permissions (as shown on the screenshot) are required to send emails using Microsoft Graph API.\n\n![Microsoft Graph Permissions](https://raw.githubusercontent.com/EvotecIT/Mailozaurr/refs/heads/v2-speedygonzales/Docs/Images/MicrosoftGraphPermissions.png)\n\nThe rest of the permissions is not required and is there for other features that Microsoft Graph provides.\n\n## To install\n\n```powershell\nInstall-Module -Name Mailozaurr -AllowClobber -Force\n```\n\nForce and AllowClobber aren't necessary, but they do skip errors in case some appear.\n\n## And to update\n\n```powershell\nUpdate-Module -Name Mailozaurr\n```\n\nThat's it. Whenever there's a new version, you run the command, and you can enjoy it. Remember that you may need to close, reopen PowerShell session if you have already used module before updating it.\n\n**The essential thing** is if something works for you on production, keep using it till you test the new version on a test computer. I do changes that may not be big, but big enough that auto-update may break your code. For example, a small rename to a parameter, and your code stops working! Be responsible!\n"
  },
  {
    "path": "Sources/Mailozaurr/AmazonSES/SesClient.cs",
    "content": "using System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Simple client for sending emails using the Amazon SES REST API.\n/// </summary>\n/// <remarks>\n/// Only a subset of the SES API is implemented, focused on\n/// sending MIME messages with minimal configuration.\n/// </remarks>\npublic class SesClient : IDisposable {\n    private readonly HttpClient _client;\n\n    /// <summary>Measures send latency.</summary>\n    public readonly Stopwatch Stopwatch;\n\n    /// <summary>Credentials used to authenticate with Amazon SES.</summary>\n    public ICredentials Credentials { get; set; } = CredentialCache.DefaultNetworkCredentials;\n    /// <summary>Controls how errors are handled.</summary>\n    public ActionPreference? ErrorAction { get; set; }\n\n    /// <summary>Primary recipients.</summary>\n    public List<object> To { get; set; } = new();\n    /// <summary>Carbon copy recipients.</summary>\n    public List<object> Cc { get; set; } = new();\n    /// <summary>Blind carbon copy recipients.</summary>\n    public List<object> Bcc { get; set; } = new();\n    /// <summary>The sender address.</summary>\n    public object From { get; set; } = string.Empty;\n    /// <summary>Reply-to address.</summary>\n    public object? ReplyTo { get; set; }\n    /// <summary>The message subject.</summary>\n    public string? Subject { get; set; }\n    /// <summary>Plain text body.</summary>\n    public string Text { get; set; } = string.Empty;\n    /// <summary>HTML body.</summary>\n    public string Html { get; set; } = string.Empty;\n    /// <summary>Paths to attachments to include.</summary>\n    public string[]? Attachment { get; set; }\n    /// <summary>Paths to inline attachments to include.</summary>\n    public string[]? InlineAttachment { get; set; }\n\n    /// <summary>Name of the SES template to use.</summary>\n    public string? TemplateName { get; set; }\n    /// <summary>Template data variables.</summary>\n    public Dictionary<string, string>? TemplateData { get; set; }\n\n    /// <summary>Custom headers to include with the message.</summary>\n    public Dictionary<string, string>? Headers { get; set; }\n\n    /// <summary>Number of retry attempts on failure.</summary>\n    public int RetryCount { get; set; } = 0;\n    /// <summary>Base delay in milliseconds between retries.</summary>\n    public int RetryDelayMilliseconds { get; set; } = 0;\n    /// <summary>Exponential backoff multiplier for retries.</summary>\n    public double RetryDelayBackoff { get; set; } = 1.0;\n    /// <summary>Retry even on non-transient errors.</summary>\n    public bool RetryAlways { get; set; } = false;\n    /// <summary>Maximum delay in milliseconds between retries. 0 disables capping.</summary>\n    public int MaxDelayMilliseconds { get; set; } = 0;\n    /// <summary>Jitter window in milliseconds added to retry delay. 0 disables jitter.</summary>\n    public int JitterMilliseconds { get; set; } = 0;\n\n    /// <summary>AWS region to use.</summary>\n    public string Region { get; set; } = \"us-east-1\";\n    /// <summary>Webhook invoked after sending.</summary>\n    public string? WebhookUrl { get; set; }\n\n    /// <summary>Collector used to store log entries.</summary>\n    public LogCollector LogCollector { get; set; } = new();\n\n    /// <summary>\n    /// When set, sending is simulated and no SES request is issued.\n    /// </summary>\n    public bool DryRun { get; set; }\n\n    /// <summary>Repository used to persist messages that require retrying.</summary>\n    public IPendingMessageRepository? PendingMessageRepository { get; set; }\n\n    /// <summary>\n    /// Gets the normalized sender email address.\n    /// </summary>\n    public string SentFrom => Helpers.GetEmailAddress(From);\n\n    /// <summary>\n    /// Gets a comma separated list of recipient email addresses.\n    /// </summary>\n    public string SentTo\n    {\n        get\n        {\n            HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);\n            List<string> addresses = new();\n            if (To != null) addresses.AddRange(Helpers.UniqueAddresses(To, seen).Select(Helpers.GetEmailAddress));\n            if (Cc != null) addresses.AddRange(Helpers.UniqueAddresses(Cc, seen).Select(Helpers.GetEmailAddress));\n            if (Bcc != null) addresses.AddRange(Helpers.UniqueAddresses(Bcc, seen).Select(Helpers.GetEmailAddress));\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SesClient\"/> class using a default <see cref=\"HttpClient\"/>.\n    /// </summary>\n    public SesClient() {\n        Stopwatch = Stopwatch.StartNew();\n        _client = new HttpClient();\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SesClient\"/> class using the specified HTTP handler.\n    /// </summary>\n    /// <param name=\"handler\">The HTTP handler to use for requests.</param>\n    public SesClient(HttpMessageHandler handler) {\n        Stopwatch = Stopwatch.StartNew();\n        _client = new HttpClient(handler);\n    }\n\n    private MimeMessage BuildMessage()\n    {\n        Smtp smtp = new();\n        smtp.From = From;\n        smtp.To = To;\n        smtp.Cc = Cc;\n        smtp.Bcc = Bcc;\n        smtp.ReplyTo = ReplyTo;\n        smtp.Subject = Subject ?? string.Empty;\n        smtp.TextBody = Text;\n        smtp.HtmlBody = Html;\n        if (Attachment != null) smtp.Attachments = Attachment.Select(path => (AttachmentDescriptor)new FileAttachmentDescriptor(path)).ToList();\n        if (InlineAttachment != null) smtp.InlineAttachments = InlineAttachment.Select(path => (AttachmentDescriptor)new FileAttachmentDescriptor(path)).ToList();\n        if (Headers != null) smtp.Headers = Headers;\n        smtp.CreateMessage();\n        return smtp.Message;\n    }\n\n    private static byte[] HmacSha256(byte[] key, string data)\n    {\n        using HMACSHA256 hmac = new(key);\n        return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));\n    }\n\n    private static string Sha256Hex(string data)\n    {\n        using SHA256 sha = SHA256.Create();\n        byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(data));\n        return BitConverter.ToString(hash).Replace(\"-\", string.Empty).ToLowerInvariant();\n    }\n\n    private HttpRequestMessage CreateRequest(string content, DateTime utcNow)\n    {\n        NetworkCredential net = Credentials as NetworkCredential ?? throw new InvalidCastException(\"Credentials must be NetworkCredential\");\n        string accessKey = net.UserName;\n        string secretKey = net.Password;\n        string service = \"ses\";\n        string region = Region;\n        string amzDate = utcNow.ToString(\"yyyyMMdd'T'HHmmss'Z'\");\n        string dateStamp = utcNow.ToString(\"yyyyMMdd\");\n        string canonicalHeaders = $\"content-type:application/x-www-form-urlencoded\\nhost:email.{region}.amazonaws.com\\nx-amz-date:{amzDate}\\n\";\n        string signedHeaders = \"content-type;host;x-amz-date\";\n        string payloadHash = Sha256Hex(content);\n        string canonicalRequest = $\"POST\\n/\\n\\n{canonicalHeaders}\\n{signedHeaders}\\n{payloadHash}\";\n        string credentialScope = $\"{dateStamp}/{region}/{service}/aws4_request\";\n        string stringToSign = $\"AWS4-HMAC-SHA256\\n{amzDate}\\n{credentialScope}\\n{Sha256Hex(canonicalRequest)}\";\n        byte[] kDate = HmacSha256(Encoding.UTF8.GetBytes(\"AWS4\" + secretKey), dateStamp);\n        byte[] kRegion = HmacSha256(kDate, region);\n        byte[] kService = HmacSha256(kRegion, service);\n        byte[] kSigning = HmacSha256(kService, \"aws4_request\");\n        byte[] sigBytes = HmacSha256(kSigning, stringToSign);\n        string signature = BitConverter.ToString(sigBytes).Replace(\"-\", string.Empty).ToLowerInvariant();\n        string authorization = $\"AWS4-HMAC-SHA256 Credential={accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}\";\n\n        HttpRequestMessage request = new(HttpMethod.Post, $\"https://email.{region}.amazonaws.com/\");\n        request.Content = new StringContent(content, Encoding.UTF8, \"application/x-www-form-urlencoded\");\n        request.Headers.TryAddWithoutValidation(\"x-amz-date\", amzDate);\n        request.Headers.TryAddWithoutValidation(\"Authorization\", authorization);\n        return request;\n    }\n\n    private async Task QueuePendingMessageAsync(MimeMessage? message, string? mimeMessageBase64, CancellationToken cancellationToken)\n    {\n        if (PendingMessageRepository == null)\n        {\n            return;\n        }\n\n        if (Credentials is not NetworkCredential net)\n        {\n            LogCollector.LogWarning(\"Send-EmailMessage - Unable to queue SES message because credentials are not network credentials.\");\n            return;\n        }\n\n        if (string.IsNullOrEmpty(net.UserName) || string.IsNullOrEmpty(net.Password))\n        {\n            return;\n        }\n\n        var base64 = mimeMessageBase64;\n        string messageId;\n        if (message != null)\n        {\n            if (string.IsNullOrEmpty(message.MessageId))\n            {\n                message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId();\n            }\n\n            if (string.IsNullOrEmpty(base64))\n            {\n                using var stream = new MemoryStream();\n                await message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n                base64 = Convert.ToBase64String(stream.ToArray());\n            }\n\n            messageId = message.MessageId!;\n        }\n        else\n        {\n            if (string.IsNullOrEmpty(base64))\n            {\n                return;\n            }\n\n            messageId = Guid.NewGuid().ToString(\"N\");\n        }\n\n        if (string.IsNullOrEmpty(base64))\n        {\n            return;\n        }\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord\n        {\n            MessageId = messageId,\n            MimeMessage = base64!,\n            Timestamp = now,\n            NextAttemptAt = now,\n            Provider = EmailProvider.SES\n        };\n        var protector = CredentialProtection.Default;\n        record.ProviderData[SesPendingMessageSender.AccessKeyIdProtectedKey] = protector.Protect(net.UserName);\n        record.ProviderData[SesPendingMessageSender.SecretAccessKeyProtectedKey] = protector.Protect(net.Password);\n        record.ProviderData.Remove(SesPendingMessageSender.AccessKeyIdKey);\n        record.ProviderData.Remove(SesPendingMessageSender.AccessKeyIdBase64Key);\n        record.ProviderData.Remove(SesPendingMessageSender.SecretAccessKeyKey);\n        record.ProviderData.Remove(SesPendingMessageSender.SecretAccessKeyBase64Key);\n        record.ProviderData[SesPendingMessageSender.RegionKey] = Region;\n\n        try\n        {\n            await PendingMessageRepository.SaveAsync(record, cancellationToken).ConfigureAwait(false);\n        }\n        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)\n        {\n            throw;\n        }\n        catch (Exception ex)\n        {\n            LogCollector.LogWarning($\"Send-EmailMessage - Failed to persist SES pending message: {ex.Message}\");\n        }\n    }\n\n    private async Task<SmtpResult> SendSesRequestAsync(string body, CancellationToken cancellationToken, MimeMessage? message = null, string? mimeMessageBase64 = null)\n    {\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping SES send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SESApi\", 0, Stopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do\n        {\n            try\n            {\n                using HttpRequestMessage request = CreateRequest(body, DateTime.UtcNow);\n                HttpResponseMessage response = await _client.SendAsync(request, cancellationToken);\n#if NET5_0_OR_GREATER\n                string respContent = await response.Content.ReadAsStringAsync(cancellationToken);\n#else\n                string respContent = await response.Content.ReadAsStringAsync();\n#endif\n                if (response.IsSuccessStatusCode)\n                {\n                    SmtpResult ok = new(true, EmailAction.Send, SentTo, SentFrom, \"SESApi\", 0, Stopwatch.Elapsed, response.StatusCode.ToString());\n                    await Helpers.PostWebhookAsync(WebhookUrl, ok, cancellationToken, _client);\n                    return ok;\n                }\n\n                lastException = new HttpRequestException(respContent);\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using SES: {respContent}\");\n            }\n            catch (HttpRequestException ex)\n            {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using SES: {ex.Message}\");\n            }\n\n            if ((!Helpers.IsTransient(lastException) && !RetryAlways) || attempts >= RetryCount)\n            {\n                await QueuePendingMessageAsync(message, mimeMessageBase64, cancellationToken).ConfigureAwait(false);\n                if (ErrorAction == ActionPreference.Stop && lastException != null)\n                {\n                    throw lastException;\n                }\n                SmtpResult fail = new(false, EmailAction.Send, SentTo, SentFrom, \"SESApi\", 0, Stopwatch.Elapsed, string.Empty, lastException?.Message);\n                await Helpers.PostWebhookAsync(WebhookUrl, fail, cancellationToken, _client);\n                return fail;\n            }\n\n            int delay = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempts));\n            if (MaxDelayMilliseconds > 0 && delay > MaxDelayMilliseconds) {\n                delay = MaxDelayMilliseconds;\n            }\n            if (JitterMilliseconds > 0 && delay > 0) {\n                delay += GraphRetryHelperRandom.NextInt(JitterMilliseconds + 1);\n            }\n            if (delay > 0)\n            {\n                await Task.Delay(TimeSpan.FromMilliseconds(delay), cancellationToken);\n            }\n            attempts++;\n        }\n        while (attempts <= RetryCount);\n\n        await QueuePendingMessageAsync(message, mimeMessageBase64, cancellationToken).ConfigureAwait(false);\n        SmtpResult final = new(false, EmailAction.Send, SentTo, SentFrom, \"SESApi\", 0, Stopwatch.Elapsed, string.Empty, lastException?.Message);\n        await Helpers.PostWebhookAsync(WebhookUrl, final, cancellationToken, _client);\n        return final;\n    }\n\n    /// <summary>\n    /// Sends the email using Amazon SES.\n    /// </summary>\n    public Task<SmtpResult> SendEmailAsync() => SendEmailAsync(CancellationToken.None);\n\n    /// <summary>\n    /// Sends the email using Amazon SES.\n    /// </summary>\n    public async Task<SmtpResult> SendEmailAsync(CancellationToken cancellationToken)\n    {\n        MimeMessage message = BuildMessage();\n        using MemoryStream stream = new();\n        await message.WriteToAsync(stream, cancellationToken);\n        string raw = Convert.ToBase64String(stream.ToArray());\n        string body = $\"Action=SendRawEmail&RawMessage.Data={Uri.EscapeDataString(raw)}&Version=2010-12-01\";\n        return await SendSesRequestAsync(body, cancellationToken, message, raw);\n    }\n\n    /// <summary>\n    /// Sends a templated email using Amazon SES.\n    /// </summary>\n    public Task<SmtpResult> SendTemplatedEmailAsync() => SendTemplatedEmailAsync(CancellationToken.None);\n\n    /// <summary>\n    /// Sends a templated email using Amazon SES.\n    /// </summary>\n    public async Task<SmtpResult> SendTemplatedEmailAsync(CancellationToken cancellationToken)\n    {\n        StringBuilder sb = new(\"Action=SendTemplatedEmail&Version=2010-12-01\");\n        if (!string.IsNullOrEmpty(TemplateName)) sb.Append(\"&Template=\").Append(Uri.EscapeDataString(TemplateName));\n        sb.Append(\"&Source=\").Append(Uri.EscapeDataString(SentFrom));\n        if (To != null)\n        {\n            for (int i = 0; i < To.Count; i++) sb.Append(\"&Destination.ToAddresses.member.\").Append(i + 1).Append(\"=\").Append(Uri.EscapeDataString(Helpers.GetEmailAddress(To[i])));\n        }\n        if (Cc != null)\n        {\n            for (int i = 0; i < Cc.Count; i++) sb.Append(\"&Destination.CcAddresses.member.\").Append(i + 1).Append(\"=\").Append(Uri.EscapeDataString(Helpers.GetEmailAddress(Cc[i])));\n        }\n        if (Bcc != null)\n        {\n            for (int i = 0; i < Bcc.Count; i++) sb.Append(\"&Destination.BccAddresses.member.\").Append(i + 1).Append(\"=\").Append(Uri.EscapeDataString(Helpers.GetEmailAddress(Bcc[i])));\n        }\n        if (ReplyTo != null) sb.Append(\"&ReplyToAddresses.member.1=\").Append(Uri.EscapeDataString(Helpers.GetEmailAddress(ReplyTo)));\n        string json = TemplateData != null\n            ? JsonSerializer.Serialize(TemplateData, MailozaurrJsonContext.Default.DictionaryStringString)\n            : \"{}\";\n        sb.Append(\"&TemplateData=\").Append(Uri.EscapeDataString(json));\n        MimeMessage message = BuildMessage();\n        using MemoryStream stream = new();\n        await message.WriteToAsync(stream, cancellationToken);\n        var raw = Convert.ToBase64String(stream.ToArray());\n        return await SendSesRequestAsync(sb.ToString(), cancellationToken, message, raw);\n    }\n\n    /// <summary>\n    /// Releases resources used by the <see cref=\"SesClient\"/> instance.\n    /// </summary>\n    public void Dispose() {\n        _client.Dispose();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Attachment.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a file attachment from Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// Only basic metadata required for upload is exposed.\n/// </remarks>\npublic class Attachment {\n    /// <summary>\n    /// Gets or sets the file name of the attachment.\n    /// </summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the attachment content encoded as a Base64 string.\n    /// </summary>\n    public string ContentBytes { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the size of the attachment in bytes.\n    /// </summary>\n    public long Size { get; set; }\n    // Add more properties as needed\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/LoginState.cs",
    "content": "namespace Mailozaurr {\n\n    internal enum LoginState {\n\n        Initial,\n\n        Challenge\n\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/OAuthCredentialCacheEntry.cs",
    "content": "using System;\n\nnamespace Mailozaurr;\n\n#pragma warning disable CS1591\npublic sealed class OAuthCredentialCacheEntry {\n    public string UserName { get; set; } = string.Empty;\n\n    public string? AccessTokenProtected { get; set; }\n\n    public string? AccessToken { get; set; }\n\n    public DateTimeOffset ExpiresOn { get; set; }\n\n    public string? RefreshTokenProtected { get; set; }\n\n    public string? RefreshToken { get; set; }\n\n    public string? ClientId { get; set; }\n\n    public string? ClientSecretProtected { get; set; }\n\n    public string? ClientSecret { get; set; }\n\n    public string? ServiceAccountJsonProtected { get; set; }\n\n    public string? ServiceAccountJson { get; set; }\n\n    public string? ServiceAccountSubject { get; set; }\n\n    public static OAuthCredentialCacheEntry FromCredential(OAuthCredential credential, ICredentialProtector protector) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        if (protector == null) {\n            throw new ArgumentNullException(nameof(protector));\n        }\n\n        return new OAuthCredentialCacheEntry {\n            UserName = credential.UserName,\n            AccessTokenProtected = ProtectRequired(protector, credential.AccessToken),\n            ExpiresOn = credential.ExpiresOn,\n            RefreshTokenProtected = ProtectOptional(protector, credential.RefreshToken),\n            ClientId = credential.ClientId,\n            ClientSecretProtected = ProtectOptional(protector, credential.ClientSecret),\n            ServiceAccountJsonProtected = ProtectOptional(protector, credential.ServiceAccountJson),\n            ServiceAccountSubject = credential.ServiceAccountSubject\n        };\n    }\n\n    public OAuthCredential ToCredential(ICredentialProtector protector) {\n        if (protector == null) {\n            throw new ArgumentNullException(nameof(protector));\n        }\n\n        return new OAuthCredential {\n            UserName = UserName,\n            AccessToken = ReadRequiredSecret(protector, AccessTokenProtected, AccessToken),\n            ExpiresOn = ExpiresOn,\n            RefreshToken = ReadOptionalSecret(protector, RefreshTokenProtected, RefreshToken),\n            ClientId = ClientId,\n            ClientSecret = ReadOptionalSecret(protector, ClientSecretProtected, ClientSecret),\n            ServiceAccountJson = ReadOptionalSecret(protector, ServiceAccountJsonProtected, ServiceAccountJson),\n            ServiceAccountSubject = ServiceAccountSubject\n        };\n    }\n\n    private static string ProtectRequired(ICredentialProtector protector, string value) {\n        if (string.IsNullOrEmpty(value)) {\n            return string.Empty;\n        }\n\n        return protector.Protect(value);\n    }\n\n    private static string? ProtectOptional(ICredentialProtector protector, string? value) {\n        if (string.IsNullOrEmpty(value)) {\n            return null;\n        }\n\n        return protector.Protect(value!);\n    }\n\n    private static string ReadRequiredSecret(ICredentialProtector protector, string? protectedValue, string? legacyValue) {\n        var value = ReadOptionalSecret(protector, protectedValue, legacyValue);\n        return value ?? string.Empty;\n    }\n\n    private static string? ReadOptionalSecret(ICredentialProtector protector, string? protectedValue, string? legacyValue) {\n        if (!string.IsNullOrEmpty(protectedValue)) {\n            var unprotected = CredentialProtection.UnprotectWithFallback(protector, protectedValue);\n            if (!string.IsNullOrEmpty(unprotected) || string.IsNullOrEmpty(legacyValue)) {\n                return unprotected;\n            }\n        }\n\n        return legacyValue;\n    }\n}\n#pragma warning restore CS1591\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/OAuthHelpers.cs",
    "content": "using Microsoft.Identity.Client;\nusing Google.Apis.Auth.OAuth2;\nusing Google.Apis.Auth.OAuth2.Flows;\nusing Google.Apis.Util.Store;\nusing Google.Apis.Auth.OAuth2.Responses;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Linq;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper methods for acquiring OAuth tokens for various services.\n/// </summary>\npublic static class OAuthHelpers {\n    private static string[] NormalizeScopes(IEnumerable<string> scopes) =>\n        scopes?\n            .Where(scope => !string.IsNullOrWhiteSpace(scope))\n            .Select(scope => scope.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .OrderBy(scope => scope, StringComparer.OrdinalIgnoreCase)\n            .ToArray()\n        ?? Array.Empty<string>();\n\n    private static string BuildO365LegacyCacheKey(string userName) => $\"o365:{userName}\";\n\n    private static string? BuildO365CacheKey(\n        string? login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        if (string.IsNullOrWhiteSpace(login)) {\n            return null;\n        }\n\n        var normalizedScopes = NormalizeScopes(scopes);\n        var scopeKey = normalizedScopes.Length == 0 ? \"default\" : string.Join(\" \", normalizedScopes);\n        return $\"o365:{login!.Trim()}|{clientId.Trim()}|{tenantId.Trim()}|{redirectUri.Trim()}|{scopeKey}\";\n    }\n\n    private static string BuildGoogleLegacyCacheKey(string gmailAccount) => $\"google:{gmailAccount}\";\n\n    private static string BuildGoogleCacheKey(string gmailAccount, string clientId) => $\"google:{clientId}:{gmailAccount}\";\n\n    private static async Task PersistO365CredentialAsync(\n        OAuthCredential credential,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n        if (string.IsNullOrWhiteSpace(credential.UserName)) {\n            return;\n        }\n\n        credential.ClientId = clientId;\n        await OAuthTokenCache.SetAsync(BuildO365LegacyCacheKey(credential.UserName), credential).ConfigureAwait(false);\n\n        var compositeKey = BuildO365CacheKey(credential.UserName, clientId, tenantId, redirectUri, scopes);\n        if (compositeKey != null) {\n            await OAuthTokenCache.SetAsync(compositeKey, credential).ConfigureAwait(false);\n        }\n    }\n\n    private static async Task PersistGoogleCredentialAsync(\n        OAuthCredential credential,\n        string gmailAccount,\n        string clientId) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n        if (string.IsNullOrWhiteSpace(gmailAccount) && string.IsNullOrWhiteSpace(credential.UserName)) {\n            return;\n        }\n\n        var account = string.IsNullOrWhiteSpace(gmailAccount) ? credential.UserName : gmailAccount.Trim();\n        credential.ClientId = clientId;\n        await OAuthTokenCache.SetAsync(BuildGoogleLegacyCacheKey(account), credential).ConfigureAwait(false);\n        await OAuthTokenCache.SetAsync(BuildGoogleCacheKey(account, clientId), credential).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Acquires an OAuth token for Office 365 using an interactive browser flow.\n    /// </summary>\n    /// <param name=\"login\">Optional login hint for the account.</param>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant identifier.</param>\n    /// <param name=\"redirectUri\">The redirect URI registered for the application.</param>\n    /// <param name=\"scopes\">The scopes to request for the token.</param>\n    /// <returns>A credential containing the access token.</returns>\n    public static async Task<OAuthCredential> AcquireO365TokenInteractiveAsync(\n        string? login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        var options = new PublicClientApplicationOptions {\n            ClientId = clientId,\n            TenantId = tenantId,\n            RedirectUri = redirectUri\n        };\n        var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build();\n        TokenCacheHelper.RegisterCache(app.UserTokenCache);\n        AuthenticationResult result;\n        var accounts = await app.GetAccountsAsync();\n        IAccount? account = null;\n        if (!string.IsNullOrWhiteSpace(login)) {\n            account = accounts.FirstOrDefault(a => string.Equals(a.Username, login, StringComparison.OrdinalIgnoreCase));\n        } else {\n            account = accounts.FirstOrDefault();\n        }\n        try {\n            if (account != null) {\n                result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();\n            } else {\n                throw new MsalUiRequiredException(\"\", \"no_account\");\n            }\n        } catch (MsalUiRequiredException) {\n            var builder = app.AcquireTokenInteractive(scopes);\n            if (!string.IsNullOrWhiteSpace(login)) {\n                builder = builder.WithLoginHint(login);\n            }\n            result = await builder.ExecuteAsync();\n        }\n        var cred = new OAuthCredential {\n            UserName = result.Account.Username,\n            AccessToken = result.AccessToken,\n            ExpiresOn = result.ExpiresOn,\n            ClientId = clientId\n        };\n        await PersistO365CredentialAsync(cred, clientId, tenantId, redirectUri, scopes).ConfigureAwait(false);\n        return cred;\n    }\n\n    /// <summary>\n    /// Attempts to silently acquire a new Office 365 access token using the cached refresh token.\n    /// </summary>\n    private static async Task<OAuthCredential?> AcquireO365TokenSilentAsync(\n        string? login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        var options = new PublicClientApplicationOptions {\n            ClientId = clientId,\n            TenantId = tenantId,\n            RedirectUri = redirectUri\n        };\n        var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build();\n        TokenCacheHelper.RegisterCache(app.UserTokenCache);\n        var accounts = await app.GetAccountsAsync();\n        IAccount? account = null;\n        if (!string.IsNullOrWhiteSpace(login)) {\n            account = accounts.FirstOrDefault(a => string.Equals(a.Username, login, StringComparison.OrdinalIgnoreCase));\n        } else {\n            account = accounts.FirstOrDefault();\n        }\n        if (account == null) {\n            return null;\n        }\n        try {\n            var result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();\n            return new OAuthCredential {\n                UserName = result.Account.Username,\n                AccessToken = result.AccessToken,\n                ExpiresOn = result.ExpiresOn,\n                ClientId = clientId\n            };\n        } catch (MsalUiRequiredException) {\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// Acquires an OAuth token for a Gmail account using an interactive browser flow.\n    /// </summary>\n    /// <param name=\"gmailAccount\">The Gmail account to authenticate.</param>\n    /// <param name=\"clientId\">The OAuth client identifier.</param>\n    /// <param name=\"clientSecret\">The OAuth client secret.</param>\n    /// <param name=\"scopes\">The scopes to request for the token.</param>\n    /// <returns>A credential containing the access token.</returns>\n    public static async Task<OAuthCredential> AcquireGoogleTokenInteractiveAsync(\n        string gmailAccount,\n        string clientId,\n        string clientSecret,\n        IEnumerable<string> scopes) {\n        var clientSecrets = new ClientSecrets {\n            ClientId = clientId,\n            ClientSecret = clientSecret\n        };\n        var initializer = new GoogleAuthorizationCodeFlow.Initializer {\n            ClientSecrets = clientSecrets,\n            Scopes = scopes,\n            DataStore = new FileDataStore(\"CredentialCacheFolder\", false)\n        };\n        var codeFlow = new GoogleAuthorizationCodeFlow(initializer);\n        var codeReceiver = new LocalServerCodeReceiver();\n        var authCode = new AuthorizationCodeInstalledApp(codeFlow, codeReceiver);\n        var credential = await authCode.AuthorizeAsync(gmailAccount, System.Threading.CancellationToken.None);\n        if (credential.Token.IsStale) {\n            await credential.RefreshTokenAsync(System.Threading.CancellationToken.None);\n        }\n        var cred = new OAuthCredential {\n            UserName = credential.UserId,\n            AccessToken = credential.Token.AccessToken,\n            RefreshToken = credential.Token.RefreshToken,\n            ExpiresOn = credential.Token.IssuedUtc + TimeSpan.FromSeconds(credential.Token.ExpiresInSeconds ?? 0),\n            ClientId = clientId,\n            ClientSecret = clientSecret\n        };\n        await PersistGoogleCredentialAsync(cred, gmailAccount, clientId).ConfigureAwait(false);\n        return cred;\n    }\n\n    /// <summary>\n    /// Attempts to retrieve a cached Office 365 token or acquire a new one if necessary.\n    /// </summary>\n    public static async Task<OAuthCredential> AcquireO365TokenCachedAsync(\n        string? login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        var normalizedScopes = NormalizeScopes(scopes);\n        var cacheKey = BuildO365CacheKey(login, clientId, tenantId, redirectUri, normalizedScopes);\n        if (cacheKey != null) {\n            var cached = await OAuthTokenCache.GetAsync(cacheKey);\n            if (cached != null && cached.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5)) {\n                return cached;\n            }\n            if (cached != null) {\n                var refreshed = await AcquireO365TokenSilentAsync(login, clientId, tenantId, redirectUri, normalizedScopes);\n                if (refreshed != null) {\n                    await PersistO365CredentialAsync(refreshed, clientId, tenantId, redirectUri, normalizedScopes).ConfigureAwait(false);\n                    return refreshed;\n                }\n            }\n        }\n\n        var cred = await AcquireO365TokenInteractiveAsync(login, clientId, tenantId, redirectUri, normalizedScopes);\n        return cred;\n    }\n\n    /// <summary>\n    /// Attempts to silently acquire an Office 365 token from the existing token cache without prompting the user.\n    /// </summary>\n    /// <param name=\"login\">Optional login hint for the cached account.</param>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant identifier.</param>\n    /// <param name=\"redirectUri\">The redirect URI registered for the application.</param>\n    /// <param name=\"scopes\">The scopes to request for the token.</param>\n    /// <returns>The refreshed credential when available; otherwise <c>null</c>.</returns>\n    public static async Task<OAuthCredential?> TryAcquireO365TokenSilentAsync(\n        string? login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        IEnumerable<string> scopes) {\n        var normalizedScopes = NormalizeScopes(scopes);\n        var credential = await AcquireO365TokenSilentAsync(login, clientId, tenantId, redirectUri, normalizedScopes).ConfigureAwait(false);\n        if (credential != null) {\n            await PersistO365CredentialAsync(credential, clientId, tenantId, redirectUri, normalizedScopes).ConfigureAwait(false);\n        }\n\n        return credential;\n    }\n\n    /// <summary>\n    /// Attempts to retrieve a cached Google token or acquire a new one if necessary.\n    /// </summary>\n    public static async Task<OAuthCredential> AcquireGoogleTokenCachedAsync(\n        string gmailAccount,\n        string clientId,\n        string clientSecret,\n        IEnumerable<string> scopes) {\n        // Prefer a cache key that includes client id to avoid token confusion across apps.\n        var compositeKey = BuildGoogleCacheKey(gmailAccount, clientId);\n        var legacyKey = BuildGoogleLegacyCacheKey(gmailAccount);\n\n        bool loadedFromLegacy = false;\n        var cached = await OAuthTokenCache.GetAsync(compositeKey).ConfigureAwait(false);\n        if (cached is null) {\n            cached = await OAuthTokenCache.GetAsync(legacyKey).ConfigureAwait(false);\n            loadedFromLegacy = cached != null;\n        }\n\n        if (cached != null) {\n            // Validate that cached token belongs to the same app/client.\n            if (!string.IsNullOrEmpty(cached.ClientId) && !string.Equals(cached.ClientId, clientId, StringComparison.Ordinal)) {\n                LoggingMessages.Logger.WriteWarning(\"OAuth cache entry for {0} was created with a different ClientId. Ignoring cached token.\", gmailAccount);\n                cached = null;\n            } else {\n                // Fill blanks, but do not override mismatched values.\n                cached.ClientId ??= clientId;\n                if (string.IsNullOrEmpty(cached.ClientSecret)) {\n                    cached.ClientSecret = clientSecret;\n                } else if (!string.Equals(cached.ClientSecret, clientSecret, StringComparison.Ordinal)) {\n                    LoggingMessages.Logger.WriteWarning(\"OAuth cache entry for {0} contains a different ClientSecret than provided. Proceeding to refresh with provided secret.\", gmailAccount);\n                }\n            }\n        }\n\n        if (cached != null && cached.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5)) {\n            // Migrate legacy entries to composite key to prevent cross-app confusion.\n            if (loadedFromLegacy) {\n                await PersistGoogleCredentialAsync(cached, gmailAccount, clientId).ConfigureAwait(false);\n            }\n            return cached;\n        }\n\n        if (cached != null && !string.IsNullOrWhiteSpace(cached.RefreshToken)) {\n            var clientSecrets = new ClientSecrets { ClientId = clientId, ClientSecret = clientSecret };\n            var initializer = new GoogleAuthorizationCodeFlow.Initializer {\n                ClientSecrets = clientSecrets,\n                Scopes = scopes,\n                DataStore = new FileDataStore(\"CredentialCacheFolder\", false)\n            };\n            var flow = new GoogleAuthorizationCodeFlow(initializer);\n            var token = new Google.Apis.Auth.OAuth2.Responses.TokenResponse { RefreshToken = cached.RefreshToken };\n            var userCred = new UserCredential(flow, gmailAccount, token);\n            var refreshed = await userCred.RefreshTokenAsync(System.Threading.CancellationToken.None);\n            if (refreshed) {\n                var newCred = new OAuthCredential {\n                    UserName = gmailAccount,\n                    AccessToken = userCred.Token.AccessToken,\n                    RefreshToken = userCred.Token.RefreshToken,\n                    ExpiresOn = userCred.Token.IssuedUtc + TimeSpan.FromSeconds(userCred.Token.ExpiresInSeconds ?? 0),\n                    ClientId = clientId,\n                    ClientSecret = clientSecret\n                };\n                await PersistGoogleCredentialAsync(newCred, gmailAccount, clientId).ConfigureAwait(false);\n                return newCred;\n            }\n        }\n\n        var cred = await AcquireGoogleTokenInteractiveAsync(gmailAccount, clientId, clientSecret, scopes).ConfigureAwait(false);\n        return cred;\n    }\n\n    /// <summary>\n    /// Acquires an Office 365 access token using the device code flow.\n    /// </summary>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant (directory) identifier.</param>\n    /// <param name=\"scopes\">Scopes to request for the token.</param>\n    /// <param name=\"deviceCodeCallback\">Optional callback to display the device code.</param>\n    /// <returns>The acquired credential.</returns>\n    public static async Task<OAuthCredential> AcquireO365TokenDeviceCodeAsync(\n        string clientId,\n        string tenantId,\n        IEnumerable<string> scopes,\n        Func<DeviceCodeResult, Task>? deviceCodeCallback = null) {\n        var options = new PublicClientApplicationOptions {\n            ClientId = clientId,\n            TenantId = tenantId\n        };\n        var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build();\n        TokenCacheHelper.RegisterCache(app.UserTokenCache);\n        deviceCodeCallback ??= result => {\n            Console.WriteLine(result.Message);\n            return Task.CompletedTask;\n        };\n        var result = await app.AcquireTokenWithDeviceCode(scopes, deviceCodeCallback).ExecuteAsync();\n        var cred = new OAuthCredential {\n            UserName = result.Account.Username,\n            AccessToken = result.AccessToken,\n            ExpiresOn = result.ExpiresOn,\n            ClientId = clientId\n        };\n        await OAuthTokenCache.SetAsync(BuildO365LegacyCacheKey(cred.UserName), cred).ConfigureAwait(false);\n        return cred;\n    }\n\n    /// <summary>\n    /// Acquires an Office 365 access token on behalf of a user using an existing token.\n    /// </summary>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant (directory) identifier.</param>\n    /// <param name=\"clientSecret\">The client secret for the application.</param>\n    /// <param name=\"userAccessToken\">The user access token to exchange.</param>\n    /// <param name=\"scopes\">Scopes to request for the new token.</param>\n    /// <returns>The acquired credential.</returns>\n    public static async Task<OAuthCredential> AcquireO365TokenOnBehalfOfAsync(\n        string clientId,\n        string tenantId,\n        string clientSecret,\n        string userAccessToken,\n        IEnumerable<string> scopes) {\n        var app = ConfidentialClientApplicationBuilder\n            .Create(clientId)\n            .WithTenantId(tenantId)\n            .WithClientSecret(clientSecret)\n            .Build();\n        var assertion = new UserAssertion(userAccessToken);\n        var result = await app.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();\n        return new OAuthCredential {\n            UserName = result.Account.Username,\n            AccessToken = result.AccessToken,\n            ExpiresOn = result.ExpiresOn\n        };\n    }\n\n    /// <summary>\n    /// Acquires an app-only Microsoft Graph token using a certificate.\n    /// </summary>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant identifier.</param>\n    /// <param name=\"certificatePath\">Path to the certificate file (PFX).</param>\n    /// <param name=\"certificatePassword\">Password for the certificate.</param>\n    /// <param name=\"scopes\">Optional scopes to request.</param>\n    /// <returns>The authorization information including access token.</returns>\n    public static async Task<GraphAuthorization> AcquireGraphCertificateTokenAsync(\n        string clientId,\n        string tenantId,\n        string certificatePath,\n        string certificatePassword,\n        IEnumerable<string>? scopes = null) {\n        var certificate = new X509Certificate2(certificatePath, certificatePassword);\n        return await AcquireGraphCertificateTokenInternal(clientId, tenantId, certificate, scopes);\n    }\n\n    /// <summary>\n    /// Acquires an app-only token using a certificate provided as a byte array.\n    /// </summary>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant identifier.</param>\n    /// <param name=\"certificateBytes\">Certificate bytes in PFX format.</param>\n    /// <param name=\"certificatePassword\">Password for the certificate.</param>\n    /// <param name=\"scopes\">Optional scopes to request.</param>\n    /// <returns>The authorization information including access token.</returns>\n    public static async Task<GraphAuthorization> AcquireGraphCertificateTokenAsync(\n        string clientId,\n        string tenantId,\n        byte[] certificateBytes,\n        string certificatePassword,\n        IEnumerable<string>? scopes = null) {\n        var certificate = new X509Certificate2(certificateBytes, certificatePassword);\n        return await AcquireGraphCertificateTokenInternal(clientId, tenantId, certificate, scopes);\n    }\n\n    /// <summary>\n    /// Acquires an app-only token using a PEM encoded certificate file.\n    /// </summary>\n    /// <param name=\"clientId\">The application (client) identifier.</param>\n    /// <param name=\"tenantId\">The tenant identifier.</param>\n    /// <param name=\"pemPath\">Path to the PEM certificate file.</param>\n    /// <param name=\"scopes\">Optional scopes to request.</param>\n    /// <returns>The authorization information including access token.</returns>\n    public static Task<GraphAuthorization> AcquireGraphCertificatePemTokenAsync(\n        string clientId,\n        string tenantId,\n        string pemPath,\n        IEnumerable<string>? scopes = null) {\n#if NET5_0_OR_GREATER\n        var certificate = X509Certificate2.CreateFromPemFile(pemPath);\n        return AcquireGraphCertificateTokenInternal(clientId, tenantId, certificate, scopes);\n#else\n        return Task.FromException<GraphAuthorization>(new NotSupportedException(\"PEM certificates are not supported on this framework.\"));\n#endif\n    }\n\n    private static async Task<GraphAuthorization> AcquireGraphCertificateTokenInternal(\n        string clientId,\n        string tenantId,\n        X509Certificate2 certificate,\n        IEnumerable<string>? scopes) {\n        var app = ConfidentialClientApplicationBuilder\n            .Create(clientId)\n            .WithTenantId(tenantId)\n            .WithCertificate(certificate)\n            .Build();\n        scopes ??= new[] { \"https://graph.microsoft.com/.default\" };\n        var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();\n        return new GraphAuthorization {\n            AccessToken = result.AccessToken,\n            TokenType = \"Bearer\",\n            ExpiresOn = result.ExpiresOn\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/OAuthTokenCache.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides simple persistent caching for OAuth credentials.\n/// </summary>\ninternal static class OAuthTokenCache {\n    private static readonly object LockObj = new();\n    private const int IoRetryCount = 5;\n    private static readonly string CacheFilePath = Path.Combine(\n        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n        \"Mailozaurr\",\n        \"oauth_cache.json\");\n\n    private static Dictionary<string, OAuthCredential>? _cache;\n\n    private static Dictionary<string, OAuthCredential> ConvertCacheEntries(\n        Dictionary<string, OAuthCredentialCacheEntry>? cacheEntries,\n        ICredentialProtector protector) {\n        var cache = new Dictionary<string, OAuthCredential>(StringComparer.Ordinal);\n        if (cacheEntries == null) {\n            return cache;\n        }\n\n        foreach (var pair in cacheEntries) {\n            if (pair.Value == null) {\n                continue;\n            }\n\n            cache[pair.Key] = pair.Value.ToCredential(protector);\n        }\n\n        return cache;\n    }\n\n    private static Dictionary<string, OAuthCredentialCacheEntry> CreateCacheEntries(\n        Dictionary<string, OAuthCredential> cache,\n        ICredentialProtector protector) {\n        var entries = new Dictionary<string, OAuthCredentialCacheEntry>(StringComparer.Ordinal);\n        foreach (var pair in cache) {\n            if (pair.Value == null) {\n                continue;\n            }\n\n            entries[pair.Key] = OAuthCredentialCacheEntry.FromCredential(pair.Value, protector);\n        }\n\n        return entries;\n    }\n\n    private static async Task<Dictionary<string, OAuthCredential>> LoadCacheAsync(CancellationToken cancellationToken = default) {\n        cancellationToken.ThrowIfCancellationRequested();\n        if (_cache != null) {\n            cancellationToken.ThrowIfCancellationRequested();\n            return _cache;\n        }\n\n        Dictionary<string, OAuthCredential> cache;\n        if (File.Exists(CacheFilePath)) {\n            try {\n                var json = await ReadCacheFileAsync(cancellationToken).ConfigureAwait(false);\n                var protector = CredentialProtection.Default;\n                var cacheEntries = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.DictionaryStringOAuthCredentialCacheEntry);\n                cache = ConvertCacheEntries(cacheEntries, protector);\n            } catch (FileNotFoundException) {\n                cache = new Dictionary<string, OAuthCredential>();\n            } catch (DirectoryNotFoundException) {\n                cache = new Dictionary<string, OAuthCredential>();\n            } catch (JsonException) {\n                cache = new Dictionary<string, OAuthCredential>();\n            } catch (IOException) {\n                cache = new Dictionary<string, OAuthCredential>();\n            }\n        } else {\n            cache = new Dictionary<string, OAuthCredential>();\n        }\n\n        lock (LockObj) {\n            _cache ??= cache;\n            return _cache;\n        }\n    }\n\n    /// <summary>\n    /// Retrieves a credential from the cache.\n    /// </summary>\n    /// <param name=\"key\">Unique cache key.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>The cached credential or <c>null</c> if not found.</returns>\n    public static async Task<OAuthCredential?> GetAsync(string key, CancellationToken cancellationToken = default) {\n        var cache = await LoadCacheAsync(cancellationToken).ConfigureAwait(false);\n        cancellationToken.ThrowIfCancellationRequested();\n        lock (LockObj) {\n            cache.TryGetValue(key, out var cred);\n            return cred;\n        }\n    }\n\n    /// <summary>\n    /// Saves a credential to the cache on disk.\n    /// </summary>\n    /// <param name=\"key\">Unique cache key.</param>\n    /// <param name=\"credential\">Credential to cache.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public static async Task SetAsync(string key, OAuthCredential credential, CancellationToken cancellationToken = default) {\n        var cache = await LoadCacheAsync(cancellationToken).ConfigureAwait(false);\n        string? dir;\n        string json;\n        lock (LockObj) {\n            cancellationToken.ThrowIfCancellationRequested();\n            cache[key] = credential;\n            dir = Path.GetDirectoryName(CacheFilePath);\n            if (!Directory.Exists(dir)) {\n                Directory.CreateDirectory(dir!);\n            }\n            var cacheEntries = CreateCacheEntries(cache, CredentialProtection.Default);\n            json = JsonSerializer.Serialize(cacheEntries, MailozaurrJsonContext.Default.DictionaryStringOAuthCredentialCacheEntry);\n        }\n        cancellationToken.ThrowIfCancellationRequested();\n        await WriteCacheFileAsync(json, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<string> ReadCacheFileAsync(CancellationToken cancellationToken) {\n        for (var attempt = 0; ; attempt++) {\n            cancellationToken.ThrowIfCancellationRequested();\n            try {\n                using (var stream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 4096, true))\n                using (cancellationToken.Register(() => stream.Dispose()))\n                using (var reader = new StreamReader(stream))\n                using (cancellationToken.Register(() => reader.Dispose())) {\n                    try {\n                        var json = await reader.ReadToEndAsync().ConfigureAwait(false);\n                        cancellationToken.ThrowIfCancellationRequested();\n                        return json;\n                    } catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) {\n                        throw new OperationCanceledException(cancellationToken);\n                    }\n                }\n            } catch (IOException) when (attempt < IoRetryCount - 1) {\n                await Task.Delay(GetRetryDelay(attempt), cancellationToken).ConfigureAwait(false);\n            }\n        }\n    }\n\n    private static async Task WriteCacheFileAsync(string json, CancellationToken cancellationToken) {\n        var directory = Path.GetDirectoryName(CacheFilePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            throw new InvalidOperationException(\"OAuth cache path is invalid.\");\n        }\n\n        for (var attempt = 0; ; attempt++) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n            try {\n                using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, true))\n                using (cancellationToken.Register(() => stream.Dispose()))\n                using (var writer = new StreamWriter(stream))\n                using (cancellationToken.Register(() => writer.Dispose())) {\n                    try {\n                        await writer.WriteAsync(json).ConfigureAwait(false);\n                        await writer.FlushAsync().ConfigureAwait(false);\n                        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n                        cancellationToken.ThrowIfCancellationRequested();\n                    } catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) {\n                        throw new OperationCanceledException(cancellationToken);\n                    }\n                }\n\n                ReplaceCacheFile(tempPath);\n                tempPath = string.Empty;\n                return;\n            } catch (IOException) when (attempt < IoRetryCount - 1) {\n                await Task.Delay(GetRetryDelay(attempt), cancellationToken).ConfigureAwait(false);\n            } finally {\n                if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                    try {\n                        File.Delete(tempPath);\n                    } catch (IOException) {\n                    }\n                }\n            }\n        }\n    }\n\n    private static void ReplaceCacheFile(string tempPath) {\n        if (File.Exists(CacheFilePath)) {\n            try {\n                File.Replace(tempPath, CacheFilePath, null, ignoreMetadataErrors: true);\n                return;\n            } catch (FileNotFoundException) {\n            } catch (PlatformNotSupportedException) {\n            }\n        }\n\n        if (File.Exists(CacheFilePath)) {\n            File.Copy(tempPath, CacheFilePath, overwrite: true);\n            File.Delete(tempPath);\n            return;\n        }\n\n        try {\n            File.Move(tempPath, CacheFilePath);\n        } catch (IOException) when (File.Exists(CacheFilePath)) {\n            File.Copy(tempPath, CacheFilePath, overwrite: true);\n            File.Delete(tempPath);\n        }\n    }\n\n    private static TimeSpan GetRetryDelay(int attempt) =>\n        TimeSpan.FromMilliseconds(25 * (attempt + 1));\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/SaslMechanismNtlmIntegrated.cs",
    "content": "﻿using NSspi;\nusing NSspi.Contexts;\nusing NSspi.Credentials;\nusing System;\n\nnamespace Mailozaurr {\n    /// <summary>\n    /// The NTLM Integrated Auth SASL mechanism.\n    /// </summary>\n    /// <remarks>\n    /// A SASL mechanism based on NTLM using the credentials of the current user\n    /// via Windows Integrated Authentication (SSPI). It allows MailKit\n    /// to authenticate without explicit user credentials.\n    /// </remarks>\n    /// <inheritdoc cref=\"SaslMechanism\"/>\n    public class SaslMechanismNtlmIntegrated : SaslMechanism {\n        LoginState state;\n        ClientContext sspiContext = null!;\n\n        /// <summary>\n        /// Initializes a new instance of the <see cref=\"SaslMechanismNtlmIntegrated\"/> class.\n        /// </summary>\n        /// <remarks>\n        /// Creates a new NTLM Integrated Auth SASL context.\n        /// </remarks>\n        public SaslMechanismNtlmIntegrated() : base(string.Empty, string.Empty) {\n        }\n\n        /// <inheritdoc cref=\"SaslMechanism.MechanismName\"/>\n        public override string MechanismName => \"NTLM\";\n\n        /// <inheritdoc cref=\"SaslMechanism.SupportsInitialResponse\"/>\n        public override bool SupportsInitialResponse => true;\n\n        /// <summary>\n        /// The authenticated user name.\n        /// </summary>\n        public virtual string AuthenticatedUserName => sspiContext.ContextUserName;\n\n        /// <inheritdoc cref=\"SaslMechanism.Challenge(byte[], int, int, CancellationToken)\"/>\n        /// <exception cref=\"InvalidOperationException\">\n        /// The SASL mechanism is already authenticated.\n        /// </exception>\n        protected override byte[] Challenge(byte[]? token, int startIndex, int length, CancellationToken cancellationToken) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            if (IsAuthenticated) {\n                throw new InvalidOperationException();\n            }\n\n            InitializeSSPIContext();\n\n            byte[] serverResponse;\n\n            if (state == LoginState.Initial) {\n                sspiContext.Init(null, out serverResponse);\n                state = LoginState.Challenge;\n            } else {\n                sspiContext.Init(token ?? Array.Empty<byte>(), out serverResponse);\n                IsAuthenticated = true;\n            }\n\n            return serverResponse;\n        }\n\n        private void InitializeSSPIContext() {\n            if (sspiContext != null) {\n                return;\n            }\n\n            var credential = new ClientCurrentCredential(PackageNames.Ntlm);\n\n            sspiContext = new ClientContext(\n                credential,\n                string.Empty,\n                ContextAttrib.InitIntegrity\n                | ContextAttrib.ReplayDetect\n                | ContextAttrib.SequenceDetect\n                | ContextAttrib.Confidentiality);\n        }\n\n\n        /// <summary>\n        /// Resets the authentication state allowing the mechanism to be reused.\n        /// </summary>\n        public override void Reset() {\n            state = LoginState.Initial;\n            base.Reset();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Authentication/TokenCacheHelper.cs",
    "content": "using System;\nusing System.IO;\nusing System.Threading;\nusing Microsoft.Identity.Client;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for persisting and retrieving MSAL token caches.\n/// </summary>\ninternal static class TokenCacheHelper {\n    private static readonly object FileLock = new();\n    private const int IoRetryCount = 5;\n    private const string CachePathEnvironmentVariable = \"MAILOZAURR_MSAL_CACHE_PATH\";\n    private static string CacheFilePath => ResolveCacheFilePath();\n\n    /// <summary>\n    /// Registers callbacks to persist the token cache before and after access.\n    /// </summary>\n    /// <param name=\"tokenCache\">Token cache instance from MSAL.</param>\n    public static void RegisterCache(ITokenCache tokenCache) {\n        tokenCache.SetBeforeAccess(BeforeAccessNotification);\n        tokenCache.SetAfterAccess(AfterAccessNotification);\n    }\n\n    private static void BeforeAccessNotification(TokenCacheNotificationArgs args) {\n        lock (FileLock) {\n            var data = ReadCacheData();\n            if (data != null && data.Length > 0) {\n                args.TokenCache.DeserializeMsalV3(data, shouldClearExistingCache: true);\n            }\n        }\n    }\n\n    private static void AfterAccessNotification(TokenCacheNotificationArgs args) {\n        if (args.HasStateChanged) {\n            lock (FileLock) {\n                WriteCacheData(args.TokenCache.SerializeMsalV3());\n            }\n        }\n    }\n\n    private static byte[]? ReadCacheData() {\n        if (!File.Exists(CacheFilePath)) {\n            return null;\n        }\n\n        for (var attempt = 0; ; attempt++) {\n            try {\n                using var stream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);\n                using var buffer = new MemoryStream();\n                stream.CopyTo(buffer);\n                return buffer.ToArray();\n            } catch (FileNotFoundException) {\n                return null;\n            } catch (DirectoryNotFoundException) {\n                return null;\n            } catch (IOException) when (attempt < IoRetryCount - 1) {\n                Thread.Sleep(GetRetryDelay(attempt));\n            } catch (IOException ex) {\n                LoggingMessages.Logger.WriteWarning($\"Failed to read MSAL token cache: {ex.Message}\");\n                return null;\n            }\n        }\n    }\n\n    private static void WriteCacheData(byte[] data) {\n        var directory = Path.GetDirectoryName(CacheFilePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            return;\n        }\n\n        if (!Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory!);\n        }\n\n        for (var attempt = 0; ; attempt++) {\n            var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n            try {\n                using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete)) {\n                    stream.Write(data, 0, data.Length);\n                    stream.Flush(flushToDisk: true);\n                }\n\n                ReplaceCacheFile(tempPath);\n                tempPath = string.Empty;\n                return;\n            } catch (IOException) when (attempt < IoRetryCount - 1) {\n                Thread.Sleep(GetRetryDelay(attempt));\n            } catch (IOException ex) {\n                LoggingMessages.Logger.WriteWarning($\"Failed to persist MSAL token cache: {ex.Message}\");\n                return;\n            } finally {\n                if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                    try {\n                        File.Delete(tempPath);\n                    } catch (IOException) {\n                    }\n                }\n            }\n        }\n    }\n\n    private static void ReplaceCacheFile(string tempPath) {\n        if (File.Exists(CacheFilePath)) {\n            try {\n                File.Replace(tempPath, CacheFilePath, null, ignoreMetadataErrors: true);\n                return;\n            } catch (FileNotFoundException) {\n            } catch (PlatformNotSupportedException) {\n            }\n        }\n\n        if (File.Exists(CacheFilePath)) {\n            File.Copy(tempPath, CacheFilePath, overwrite: true);\n            File.Delete(tempPath);\n            return;\n        }\n\n        try {\n            File.Move(tempPath, CacheFilePath);\n        } catch (IOException) when (File.Exists(CacheFilePath)) {\n            File.Copy(tempPath, CacheFilePath, overwrite: true);\n            File.Delete(tempPath);\n        }\n    }\n\n    private static int GetRetryDelay(int attempt) => 25 * (attempt + 1);\n\n    private static string ResolveCacheFilePath() {\n        var overriddenPath = Environment.GetEnvironmentVariable(CachePathEnvironmentVariable);\n        if (!string.IsNullOrWhiteSpace(overriddenPath)) {\n            return overriddenPath.Trim();\n        }\n\n        return Path.Combine(\n            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n            \"Mailozaurr\",\n            \"msal_cache.bin\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Compatibility/IsExternalInit.cs",
    "content": "// Required to support `init` setters / `record` types when targeting older TFMs.\n// This shim is compiled only for TFMs that don't provide IsExternalInit.\n#if NET472 || NETSTANDARD2_0\nnamespace System.Runtime.CompilerServices {\n    internal static class IsExternalInit { }\n}\n#endif\n\n"
  },
  {
    "path": "Sources/Mailozaurr/Compatibility/StringCompatibilityExtensions.cs",
    "content": "using System;\n\nnamespace Mailozaurr;\n\ninternal static class StringCompatibilityExtensions {\n    internal static bool Contains(this string source, string value, StringComparison comparisonType) =>\n        source.IndexOf(value, comparisonType) >= 0;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/ComposeProfileUtilities.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper methods for normalizing reusable compose profiles.\n/// </summary>\npublic static class ComposeProfileUtilities {\n    /// <summary>\n    /// Normalizes compose profiles and guarantees at most one default entry.\n    /// </summary>\n    /// <param name=\"profiles\">Configured profiles.</param>\n    /// <param name=\"fallbackProfile\">Fallback values applied to profiles missing fields.</param>\n    /// <returns>Normalized profile list.</returns>\n    public static IReadOnlyList<MailComposeProfile> NormalizeProfiles(\n        IEnumerable<MailComposeProfile>? profiles,\n        MailComposeProfile? fallbackProfile = null) {\n        var normalizedFallback = NormalizeSingle(fallbackProfile, \"default\", 0, fallbackProfile);\n        var items = new List<MailComposeProfile>();\n        var index = 0;\n\n        if (profiles is not null) {\n            foreach (var profile in profiles) {\n                var normalized = NormalizeSingle(profile, profile?.Id, index, normalizedFallback);\n                if (normalized is not null) {\n                    items.Add(normalized);\n                }\n                index++;\n            }\n        }\n\n        if (items.Count == 0 && HasMeaningfulContent(normalizedFallback)) {\n            items.Add(new MailComposeProfile {\n                Id = normalizedFallback!.Id,\n                Name = normalizedFallback.Name,\n                From = normalizedFallback.From,\n                ReplyTo = normalizedFallback.ReplyTo,\n                SignatureText = normalizedFallback.SignatureText,\n                IsDefault = true\n            });\n        }\n\n        if (items.Count == 0) {\n            return items;\n        }\n\n        var selectedIndex = items.FindIndex(static profile => profile.IsDefault);\n        if (selectedIndex < 0) {\n            selectedIndex = 0;\n        }\n\n        for (var i = 0; i < items.Count; i++) {\n            items[i].IsDefault = i == selectedIndex;\n        }\n\n        return items;\n    }\n\n    /// <summary>\n    /// Resolves the default profile from a list of profiles.\n    /// </summary>\n    /// <param name=\"profiles\">Profiles to inspect.</param>\n    /// <returns>The default profile, when available.</returns>\n    public static MailComposeProfile? GetDefaultProfile(IEnumerable<MailComposeProfile>? profiles) {\n        if (profiles is null) {\n            return null;\n        }\n\n        MailComposeProfile? first = null;\n        foreach (var profile in profiles) {\n            first ??= profile;\n            if (profile?.IsDefault == true) {\n                return profile;\n            }\n        }\n\n        return first;\n    }\n\n    private static MailComposeProfile? NormalizeSingle(\n        MailComposeProfile? profile,\n        string? rawId,\n        int index,\n        MailComposeProfile? fallbackProfile) {\n        var normalizedId = NormalizeOptional(profile?.Id) ?? NormalizeOptional(rawId);\n        var from = NormalizeOptional(profile?.From) ?? NormalizeOptional(fallbackProfile?.From);\n        var replyTo = NormalizeOptional(profile?.ReplyTo) ?? NormalizeOptional(fallbackProfile?.ReplyTo);\n        var signature = NormalizeOptional(profile?.SignatureText) ?? NormalizeOptional(fallbackProfile?.SignatureText);\n        var name = NormalizeOptional(profile?.Name);\n\n        if (string.IsNullOrWhiteSpace(name) &&\n            string.IsNullOrWhiteSpace(from) &&\n            string.IsNullOrWhiteSpace(replyTo) &&\n            string.IsNullOrWhiteSpace(signature)) {\n            return null;\n        }\n\n        normalizedId = BuildProfileId(normalizedId, name, from, index);\n        name ??= from ?? normalizedId;\n\n        return new MailComposeProfile {\n            Id = normalizedId,\n            Name = name,\n            From = from,\n            ReplyTo = replyTo,\n            SignatureText = signature,\n            IsDefault = profile?.IsDefault ?? fallbackProfile?.IsDefault ?? false\n        };\n    }\n\n    private static bool HasMeaningfulContent(MailComposeProfile? profile) =>\n        !string.IsNullOrWhiteSpace(profile?.Name) ||\n        !string.IsNullOrWhiteSpace(profile?.From) ||\n        !string.IsNullOrWhiteSpace(profile?.ReplyTo) ||\n        !string.IsNullOrWhiteSpace(profile?.SignatureText);\n\n    private static string BuildProfileId(string? rawId, string? name, string? from, int index) {\n        if (rawId is not null) {\n            var trimmedId = rawId.Trim();\n            if (trimmedId.Length > 0) {\n                return trimmedId;\n            }\n        }\n\n        string? source = null;\n        if (name is not null) {\n            var trimmedName = name.Trim();\n            if (trimmedName.Length > 0) {\n                source = trimmedName;\n            }\n        }\n        if (source is null && from is not null) {\n            var trimmedFrom = from.Trim();\n            if (trimmedFrom.Length > 0) {\n                source = trimmedFrom;\n            }\n        }\n\n        if (source is not null) {\n            var normalizedSource = source.Trim();\n            var chars = normalizedSource\n                .Trim()\n                .ToLowerInvariant()\n                .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n                .ToArray();\n            var collapsed = new string(chars).Trim('-');\n            if (!string.IsNullOrWhiteSpace(collapsed)) {\n                return collapsed;\n            }\n        }\n\n        return $\"profile-{index + 1}\";\n    }\n\n    private static string? NormalizeOptional(string? value) {\n        if (value is null) {\n            return null;\n        }\n\n        var trimmed = value.Trim();\n        if (trimmed.Length == 0) {\n            return null;\n        }\n\n        return trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/ConnectionRetrier.cs",
    "content": "using MailKit;\nusing MailKit.Security;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides retry logic for MailKit connections.\n/// </summary>\ninternal static class ConnectionRetrier {\n    /// <summary>\n    /// Connects and authenticates to a mail server with retry support.\n    /// </summary>\n    /// <typeparam name=\"TClient\">Type of mail client.</typeparam>\n    /// <param name=\"clientFactory\">Factory creating client instances.</param>\n    /// <param name=\"protocolName\">Protocol name for logging.</param>\n    /// <param name=\"server\">Server hostname.</param>\n    /// <param name=\"port\">Server port.</param>\n    /// <param name=\"options\">Secure socket options.</param>\n    /// <param name=\"timeout\">Connection timeout.</param>\n    /// <param name=\"skipCertificateRevocation\">Skip certificate revocation check.</param>\n    /// <param name=\"skipCertificateValidation\">Skip certificate validation.</param>\n    /// <param name=\"authenticateAsync\">Delegate performing authentication.</param>\n    /// <param name=\"retryCount\">Number of retry attempts.</param>\n    /// <param name=\"retryDelayMilliseconds\">Initial delay between retries.</param>\n    /// <param name=\"retryDelayBackoff\">Multiplier for delay backoff.</param>\n    /// <param name=\"delayAsync\">Delegate used to introduce delay between retries.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Authenticated client instance.</returns>\n    internal static async Task<TClient> ConnectAsync<TClient>(\n        Func<TClient> clientFactory,\n        string protocolName,\n        string server,\n        int port,\n        SecureSocketOptions options,\n        int timeout,\n        bool skipCertificateRevocation,\n        bool skipCertificateValidation,\n        Func<TClient, CancellationToken, Task> authenticateAsync,\n        int retryCount,\n        int retryDelayMilliseconds,\n        double retryDelayBackoff,\n        Func<int, CancellationToken, Task>? delayAsync,\n        CancellationToken cancellationToken = default)\n        where TClient : MailService {\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            var client = clientFactory();\n            try {\n                // These options affect the TLS handshake and must be configured before ConnectAsync.\n                if (skipCertificateRevocation) {\n                    client.CheckCertificateRevocation = false;\n                }\n                if (skipCertificateValidation) {\n                    client.ServerCertificateValidationCallback = static (_, _, _, _) => true;\n                }\n                if (timeout > 0 && client.Timeout != timeout) {\n                    client.Timeout = timeout;\n                }\n\n                await client.ConnectAsync(server, port, options, cancellationToken).ConfigureAwait(false);\n                await authenticateAsync(client, cancellationToken).ConfigureAwait(false);\n                if (!client.IsAuthenticated) {\n                    throw new InvalidOperationException(\"Authentication failed.\");\n                }\n                return client;\n            } catch (Exception ex) {\n                lastException = ex;\n                LoggingMessages.Logger.WriteWarning($\"Connect-{protocolName} - {ex.Message}\");\n                try {\n                    if (client.IsConnected) {\n                        await client.DisconnectAsync(true, cancellationToken).ConfigureAwait(false);\n                    }\n                } catch (Exception ex2) {\n                    LoggingMessages.Logger.WriteWarning($\"Connect-{protocolName} - {ex2.Message}\");\n                } finally {\n                    client.Dispose();\n                }\n                if ((!Helpers.IsTransient(ex)) || attempts >= retryCount) {\n                    throw;\n                }\n                var delay = (int)Math.Round(retryDelayMilliseconds * Math.Pow(retryDelayBackoff, attempts));\n                if (delay > 0) {\n                    if (delayAsync != null) {\n                        await delayAsync(delay, cancellationToken).ConfigureAwait(false);\n                    } else {\n                        await Task.Delay(delay, cancellationToken).ConfigureAwait(false);\n                    }\n                }\n            }\n            attempts++;\n        } while (attempts <= retryCount);\n        throw lastException ?? new InvalidOperationException(\"Operation failed without exception\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/ImapConnectionRequest.cs",
    "content": "using MailKit.Security;\nusing System;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents connection settings used by <see cref=\"ImapConnector\"/>.\n/// </summary>\npublic sealed class ImapConnectionRequest {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ImapConnectionRequest\"/> class.\n    /// </summary>\n    /// <param name=\"server\">IMAP server hostname.</param>\n    /// <param name=\"port\">IMAP server port.</param>\n    /// <param name=\"options\">Secure socket options.</param>\n    /// <param name=\"timeout\">Connection timeout in milliseconds.</param>\n    /// <param name=\"skipCertificateRevocation\">Whether to skip certificate revocation checks.</param>\n    /// <param name=\"skipCertificateValidation\">Whether to skip certificate validation checks.</param>\n    /// <param name=\"retryCount\">Retry count for transient failures.</param>\n    /// <param name=\"retryDelayMilliseconds\">Initial retry delay in milliseconds.</param>\n    /// <param name=\"retryDelayBackoff\">Retry delay backoff multiplier.</param>\n    /// <exception cref=\"ArgumentException\">Thrown when <paramref name=\"server\"/> is empty.</exception>\n    /// <exception cref=\"ArgumentOutOfRangeException\">Thrown when a numeric argument is outside supported range.</exception>\n    public ImapConnectionRequest(\n        string server,\n        int port,\n        SecureSocketOptions options = SecureSocketOptions.Auto,\n        int timeout = 30000,\n        bool skipCertificateRevocation = false,\n        bool skipCertificateValidation = false,\n        int retryCount = 3,\n        int retryDelayMilliseconds = 500,\n        double retryDelayBackoff = 2.0) {\n        if (string.IsNullOrWhiteSpace(server)) {\n            throw new ArgumentException(\"Server cannot be null or whitespace.\", nameof(server));\n        }\n        if (port <= 0 || port > 65535) {\n            throw new ArgumentOutOfRangeException(nameof(port), \"Port must be between 1 and 65535.\");\n        }\n        if (timeout < 0) {\n            throw new ArgumentOutOfRangeException(nameof(timeout), \"Timeout must be >= 0.\");\n        }\n        if (retryCount < 0) {\n            throw new ArgumentOutOfRangeException(nameof(retryCount), \"Retry count must be >= 0.\");\n        }\n        if (retryDelayMilliseconds < 0) {\n            throw new ArgumentOutOfRangeException(nameof(retryDelayMilliseconds), \"Retry delay must be >= 0.\");\n        }\n        if (retryDelayBackoff <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(retryDelayBackoff), \"Retry backoff must be > 0.\");\n        }\n\n        Server = server;\n        Port = port;\n        Options = options;\n        Timeout = timeout;\n        SkipCertificateRevocation = skipCertificateRevocation;\n        SkipCertificateValidation = skipCertificateValidation;\n        RetryCount = retryCount;\n        RetryDelayMilliseconds = retryDelayMilliseconds;\n        RetryDelayBackoff = retryDelayBackoff;\n    }\n\n    /// <summary>\n    /// Gets the IMAP server hostname.\n    /// </summary>\n    public string Server { get; }\n\n    /// <summary>\n    /// Gets the IMAP server port.\n    /// </summary>\n    public int Port { get; }\n\n    /// <summary>\n    /// Gets secure socket options.\n    /// </summary>\n    public SecureSocketOptions Options { get; }\n\n    /// <summary>\n    /// Gets connection timeout in milliseconds.\n    /// </summary>\n    public int Timeout { get; }\n\n    /// <summary>\n    /// Gets a value indicating whether certificate revocation checks are skipped.\n    /// </summary>\n    public bool SkipCertificateRevocation { get; }\n\n    /// <summary>\n    /// Gets a value indicating whether certificate validation checks are skipped.\n    /// </summary>\n    public bool SkipCertificateValidation { get; }\n\n    /// <summary>\n    /// Gets retry count for transient failures.\n    /// </summary>\n    public int RetryCount { get; }\n\n    /// <summary>\n    /// Gets initial retry delay in milliseconds.\n    /// </summary>\n    public int RetryDelayMilliseconds { get; }\n\n    /// <summary>\n    /// Gets retry delay backoff multiplier.\n    /// </summary>\n    public double RetryDelayBackoff { get; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/ImapConnector.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for connecting to IMAP servers with retry logic.\n/// </summary>\npublic static class ImapConnector {\n    /// <summary>\n    /// Factory used to create <see cref=\"ImapClient\"/> instances.\n    /// </summary>\n    public static Func<ImapClient> ClientFactory { get; set; } = () => new ImapClient();\n\n    /// <summary>\n    /// Delegate used to introduce a delay between connection retries.\n    /// </summary>\n    public static Func<int, CancellationToken, Task>? DelayAsync { get; set; }\n\n    /// <summary>\n    /// Connects and authenticates to an IMAP server with retry support.\n    /// </summary>\n    /// <param name=\"request\">Connection request settings.</param>\n    /// <param name=\"authenticateAsync\">Delegate performing authentication.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Authenticated <see cref=\"ImapClient\"/> instance.</returns>\n    public static Task<ImapClient> ConnectAsync(\n        ImapConnectionRequest request,\n        Func<ImapClient, CancellationToken, Task> authenticateAsync,\n        CancellationToken cancellationToken = default) {\n        if (request is null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n        if (authenticateAsync is null) {\n            throw new ArgumentNullException(nameof(authenticateAsync));\n        }\n\n        return ConnectAsync(\n            request.Server,\n            request.Port,\n            request.Options,\n            request.Timeout,\n            request.SkipCertificateRevocation,\n            request.SkipCertificateValidation,\n            authenticateAsync,\n            request.RetryCount,\n            request.RetryDelayMilliseconds,\n            request.RetryDelayBackoff,\n            cancellationToken);\n    }\n\n    /// <summary>\n    /// Connects and authenticates to an IMAP server with retry support.\n    /// </summary>\n    /// <param name=\"server\">Server hostname.</param>\n    /// <param name=\"port\">Server port.</param>\n    /// <param name=\"options\">Secure socket options.</param>\n    /// <param name=\"timeout\">Connection timeout.</param>\n    /// <param name=\"skipCertificateRevocation\">Skip certificate revocation check.</param>\n    /// <param name=\"skipCertificateValidation\">Skip certificate validation.</param>\n    /// <param name=\"authenticateAsync\">Delegate performing authentication.</param>\n    /// <param name=\"retryCount\">Number of retry attempts.</param>\n    /// <param name=\"retryDelayMilliseconds\">Initial delay between retries.</param>\n    /// <param name=\"retryDelayBackoff\">Multiplier for delay backoff.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Authenticated <see cref=\"ImapClient\"/> instance.</returns>\n    public static Task<ImapClient> ConnectAsync(\n        string server,\n        int port,\n        SecureSocketOptions options,\n        int timeout,\n        bool skipCertificateRevocation,\n        bool skipCertificateValidation,\n        Func<ImapClient, CancellationToken, Task> authenticateAsync,\n        int retryCount,\n        int retryDelayMilliseconds,\n        double retryDelayBackoff,\n        CancellationToken cancellationToken = default) =>\n        ConnectionRetrier.ConnectAsync(\n            ClientFactory,\n            \"IMAP\",\n            server,\n            port,\n            options,\n            timeout,\n            skipCertificateRevocation,\n            skipCertificateValidation,\n            authenticateAsync,\n            retryCount,\n            retryDelayMilliseconds,\n            retryDelayBackoff,\n            DelayAsync,\n            cancellationToken);\n\n    /// <summary>\n    /// Connects and authenticates to an IMAP server using protocol auth settings in one step.\n    /// </summary>\n    /// <param name=\"request\">Connection request settings.</param>\n    /// <param name=\"userName\">IMAP user name.</param>\n    /// <param name=\"secret\">IMAP password or OAuth token.</param>\n    /// <param name=\"mode\">Authentication mode.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Authenticated <see cref=\"ImapClient\"/> instance.</returns>\n    public static Task<ImapClient> ConnectAuthenticatedAsync(\n        ImapConnectionRequest request,\n        string userName,\n        string secret,\n        ProtocolAuthMode mode = ProtocolAuthMode.Basic,\n        CancellationToken cancellationToken = default) {\n        if (request is null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        return ConnectAsync(\n            request,\n            (client, ct) => ProtocolAuth.AuthenticateImapAsync(client, userName, secret, mode, ct),\n            cancellationToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/ImapSessionService.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Describes the parameters required for an authenticated IMAP session.\n/// </summary>\npublic sealed class ImapSessionRequest {\n    /// <summary>\n    /// Gets or sets the underlying connection request.\n    /// </summary>\n    public ImapConnectionRequest Connection { get; init; } = new(\"localhost\", 993);\n\n    /// <summary>\n    /// Gets or sets the auth username.\n    /// </summary>\n    public string UserName { get; init; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the auth secret or token.\n    /// </summary>\n    public string Secret { get; init; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the protocol auth mode.\n    /// </summary>\n    public ProtocolAuthMode AuthMode { get; init; } = ProtocolAuthMode.Basic;\n\n    /// <summary>\n    /// Gets or sets an optional authenticate delegate used for testing/custom flows.\n    /// </summary>\n    public Func<ImapClient, CancellationToken, Task>? AuthenticateAsync { get; init; }\n}\n\n/// <summary>\n/// Helpers for establishing authenticated IMAP sessions.\n/// </summary>\npublic static class ImapSessionService {\n    /// <summary>\n    /// Connects and authenticates an IMAP client from a reusable session request.\n    /// </summary>\n    public static Task<ImapClient> ConnectAsync(ImapSessionRequest request, CancellationToken cancellationToken = default) {\n        if (request is null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        return ImapConnector.ConnectAsync(\n            request.Connection,\n            request.AuthenticateAsync ?? ((client, ct) => ProtocolAuth.AuthenticateImapAsync(\n                client,\n                request.UserName,\n                request.Secret,\n                request.AuthMode,\n                ct)),\n            cancellationToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/Pop3Connector.cs",
    "content": "using MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for connecting to POP3 servers with retry logic.\n/// </summary>\npublic static class Pop3Connector {\n    private static Pop3Client CreateDefaultClient() => new Pop3Client();\n\n    /// <summary>\n    /// Factory used to create <see cref=\"Pop3Client\"/> instances.\n    /// </summary>\n    public static Func<Pop3Client> ClientFactory { get; set; } = CreateDefaultClient;\n\n    /// <summary>\n    /// Restores the default POP3 client factory.\n    /// </summary>\n    public static void ResetClientFactory() => ClientFactory = CreateDefaultClient;\n\n    /// <summary>\n    /// Delegate used to delay between connection retries.\n    /// </summary>\n    public static Func<int, CancellationToken, Task>? DelayAsync { get; set; }\n    /// <summary>\n    /// Connects and authenticates to a POP3 server with retry support.\n    /// </summary>\n    /// <param name=\"server\">Server hostname.</param>\n    /// <param name=\"port\">Server port.</param>\n    /// <param name=\"options\">Secure socket options.</param>\n    /// <param name=\"timeout\">Connection timeout.</param>\n    /// <param name=\"skipCertificateRevocation\">Skip certificate revocation check.</param>\n    /// <param name=\"skipCertificateValidation\">Skip certificate validation.</param>\n    /// <param name=\"authenticateAsync\">Delegate performing authentication.</param>\n    /// <param name=\"retryCount\">Number of retry attempts.</param>\n    /// <param name=\"retryDelayMilliseconds\">Initial delay between retries.</param>\n    /// <param name=\"retryDelayBackoff\">Multiplier for delay backoff.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Authenticated <see cref=\"Pop3Client\"/> instance.</returns>\n    public static Task<Pop3Client> ConnectAsync(\n        string server,\n        int port,\n        SecureSocketOptions options,\n        int timeout,\n        bool skipCertificateRevocation,\n        bool skipCertificateValidation,\n        Func<Pop3Client, CancellationToken, Task> authenticateAsync,\n        int retryCount,\n        int retryDelayMilliseconds,\n        double retryDelayBackoff,\n        CancellationToken cancellationToken = default) =>\n        ConnectionRetrier.ConnectAsync(\n            ClientFactory,\n            \"POP3\",\n            server,\n            port,\n            options,\n            timeout,\n            skipCertificateRevocation,\n            skipCertificateValidation,\n            authenticateAsync,\n            retryCount,\n            retryDelayMilliseconds,\n            retryDelayBackoff,\n            DelayAsync,\n            cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/ProtocolAuth.cs",
    "content": "using System;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing MailKit.Net.Smtp;\nusing MailKit.Security;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Protocol authentication mode used by IMAP/POP3/SMTP connectors.\n/// </summary>\npublic enum ProtocolAuthMode {\n    /// <summary>Username/password authentication.</summary>\n    Basic = 0,\n\n    /// <summary>OAuth2 (XOAUTH2) token authentication.</summary>\n    OAuth2 = 1\n}\n\n/// <summary>\n/// Shared protocol authentication helpers for IMAP/POP3/SMTP clients.\n/// </summary>\npublic static class ProtocolAuth {\n    /// <summary>\n    /// Parses protocol auth mode text.\n    /// </summary>\n    public static ProtocolAuthMode ParseMode(string? raw, ProtocolAuthMode fallback = ProtocolAuthMode.Basic) {\n        var value = (raw ?? string.Empty).Trim();\n        if (value.Length == 0) {\n            return fallback;\n        }\n        if (value.Equals(\"basic\", StringComparison.OrdinalIgnoreCase)) {\n            return ProtocolAuthMode.Basic;\n        }\n        if (value.Equals(\"oauth2\", StringComparison.OrdinalIgnoreCase) ||\n            value.Equals(\"xoauth2\", StringComparison.OrdinalIgnoreCase) ||\n            value.Equals(\"oauth\", StringComparison.OrdinalIgnoreCase)) {\n            return ProtocolAuthMode.OAuth2;\n        }\n\n        return fallback;\n    }\n\n    /// <summary>\n    /// Authenticates an IMAP client using selected auth mode.\n    /// </summary>\n    public static Task AuthenticateImapAsync(\n        ImapClient client,\n        string userName,\n        string secret,\n        ProtocolAuthMode mode,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(userName)) {\n            throw new InvalidOperationException(\"IMAP username is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(secret)) {\n            throw new InvalidOperationException(\"IMAP secret/token is required.\");\n        }\n\n        var normalizedUser = userName.Trim();\n        var normalizedSecret = secret.Trim();\n        if (mode == ProtocolAuthMode.OAuth2) {\n            return client.AuthenticateAsync(new SaslMechanismOAuth2(normalizedUser, normalizedSecret), cancellationToken);\n        }\n\n        return client.AuthenticateAsync(new NetworkCredential(normalizedUser, normalizedSecret), cancellationToken);\n    }\n\n    /// <summary>\n    /// Authenticates an SMTP client using selected auth mode.\n    /// </summary>\n    public static Task AuthenticateSmtpAsync(\n        SmtpClient client,\n        string userName,\n        string secret,\n        ProtocolAuthMode mode,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(userName)) {\n            throw new InvalidOperationException(\"SMTP username is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(secret)) {\n            throw new InvalidOperationException(\"SMTP secret/token is required.\");\n        }\n\n        var normalizedUser = userName.Trim();\n        if (mode == ProtocolAuthMode.OAuth2) {\n            return client.AuthenticateAsync(new SaslMechanismOAuth2(normalizedUser, secret.Trim()), cancellationToken);\n        }\n\n        return client.AuthenticateAsync(normalizedUser, secret, cancellationToken);\n    }\n\n    /// <summary>\n    /// Authenticates a POP3 client using selected auth mode.\n    /// </summary>\n    public static Task AuthenticatePop3Async(\n        Pop3Client client,\n        string userName,\n        string secret,\n        ProtocolAuthMode mode,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(userName)) {\n            throw new InvalidOperationException(\"POP3 username is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(secret)) {\n            throw new InvalidOperationException(\"POP3 secret/token is required.\");\n        }\n\n        var normalizedUser = userName.Trim();\n        if (mode == ProtocolAuthMode.OAuth2) {\n            return client.AuthenticateAsync(new SaslMechanismOAuth2(normalizedUser, secret.Trim()), cancellationToken);\n        }\n\n        return client.AuthenticateAsync(normalizedUser, secret, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Connections/SmtpSessionService.cs",
    "content": "using System;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Security;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents the result of an SMTP connect/auth flow.\n/// </summary>\npublic sealed class SmtpConnectResult {\n    /// <summary>\n    /// Gets a value indicating whether the connection and authentication succeeded.\n    /// </summary>\n    public bool IsSuccess { get; }\n\n    /// <summary>\n    /// Gets the secure socket options used during the connection.\n    /// </summary>\n    public SecureSocketOptions SecureSocketOptions { get; }\n\n    /// <summary>\n    /// Gets the error code when the flow failed.\n    /// </summary>\n    public string? ErrorCode { get; }\n\n    /// <summary>\n    /// Gets the error message when the flow failed.\n    /// </summary>\n    public string? Error { get; }\n\n    /// <summary>\n    /// Gets a value indicating whether the failure is transient.\n    /// </summary>\n    public bool IsTransient { get; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SmtpConnectResult\"/> class.\n    /// </summary>\n    public SmtpConnectResult(bool isSuccess, SecureSocketOptions secureSocketOptions, string? errorCode, string? error, bool isTransient) {\n        IsSuccess = isSuccess;\n        SecureSocketOptions = secureSocketOptions;\n        ErrorCode = errorCode;\n        Error = error;\n        IsTransient = isTransient;\n    }\n}\n\n/// <summary>\n/// Describes the parameters required for an SMTP session.\n/// </summary>\npublic sealed class SmtpSessionRequest {\n    /// <summary>SMTP server address.</summary>\n    public string Server { get; init; } = string.Empty;\n    /// <summary>SMTP port.</summary>\n    public int Port { get; init; } = 587;\n    /// <summary>Secure socket options.</summary>\n    public SecureSocketOptions SecureSocketOptions { get; init; } = SecureSocketOptions.Auto;\n    /// <summary>Force SSL flag.</summary>\n    public bool UseSsl { get; init; }\n    /// <summary>Connection timeout in milliseconds.</summary>\n    public int TimeoutMs { get; init; } = 30_000;\n    /// <summary>Retries for connection/auth flows.</summary>\n    public int RetryCount { get; init; }\n    /// <summary>Base delay for retries.</summary>\n    public int RetryDelayMilliseconds { get; init; }\n    /// <summary>Backoff multiplier.</summary>\n    public double RetryDelayBackoff { get; init; } = 1.0;\n    /// <summary>Skip certificate validation.</summary>\n    public bool SkipCertificateValidation { get; init; }\n    /// <summary>Skip certificate revocation checks.</summary>\n    public bool SkipCertificateRevocation { get; init; }\n    /// <summary>Implements DryRun (no network).</summary>\n    public bool DryRun { get; init; }\n    /// <summary>Auth username.</summary>\n    public string UserName { get; init; } = string.Empty;\n    /// <summary>Auth password.</summary>\n    public string Password { get; init; } = string.Empty;\n    /// <summary>Protocol auth mode.</summary>\n    public ProtocolAuthMode AuthMode { get; init; } = ProtocolAuthMode.Basic;\n    /// <summary>Optional connect delegate used for testing.</summary>\n    public Func<Smtp, Task<SmtpResult>>? ConnectAsync { get; init; }\n    /// <summary>Optional authenticate delegate used for testing.</summary>\n    public Func<Smtp, Task<SmtpResult>>? AuthenticateAsync { get; init; }\n}\n\n/// <summary>\n/// Helpers for establishing SMTP sessions.\n/// </summary>\npublic static class SmtpSessionService {\n    /// <summary>\n    /// Attempts to connect and authenticate using the provided SMTP client/configuration.\n    /// </summary>\n    public static async Task<SmtpConnectResult> ConnectAndAuthenticateAsync(Smtp smtp, SmtpSessionRequest request, CancellationToken cancellationToken = default) {\n        if (smtp is null) {\n            throw new ArgumentNullException(nameof(smtp));\n        }\n        if (request is null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        smtp.Timeout = request.TimeoutMs;\n        smtp.RetryCount = request.RetryCount;\n        smtp.RetryDelayMilliseconds = request.RetryDelayMilliseconds;\n        smtp.RetryDelayBackoff = request.RetryDelayBackoff;\n        smtp.SkipCertificateValidation = request.SkipCertificateValidation;\n        smtp.CheckCertificateRevocation = !request.SkipCertificateRevocation;\n        smtp.DryRun = request.DryRun;\n\n        var secureOptions = request.SecureSocketOptions;\n        var connectFunc = request.ConnectAsync ?? (_ => smtp.ConnectAsync(request.Server, request.Port, secureOptions, request.UseSsl));\n        var connectResult = await connectFunc(smtp).ConfigureAwait(false);\n        if (!connectResult.Status) {\n            return new SmtpConnectResult(false, secureOptions, \"connect_failed\", connectResult.Error ?? \"Connect failed.\", true);\n        }\n\n        var authenticateFunc = request.AuthenticateAsync ?? (_ => smtp.AuthenticateAsync(\n            new NetworkCredential(request.UserName, request.Password),\n            request.AuthMode == ProtocolAuthMode.OAuth2));\n\n        var authResult = await authenticateFunc(smtp).ConfigureAwait(false);\n        if (!authResult.Status) {\n            return new SmtpConnectResult(false, secureOptions, \"auth_failed\", authResult.Error ?? \"Authentication failed.\", false);\n        }\n\n        return new SmtpConnectResult(true, secureOptions, null, null, false);\n    }\n\n    /// <summary>\n    /// Best-effort SMTP disconnect/dispose helper.\n    /// </summary>\n    public static void DisposeQuietly(Smtp? smtp) {\n        if (smtp is null) {\n            return;\n        }\n\n        try {\n            smtp.Disconnect();\n        } catch {\n            // best-effort\n        }\n\n        try {\n            smtp.Dispose();\n        } catch {\n            // best-effort\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/AesCredentialProtector.cs",
    "content": "using System;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\ninternal sealed class AesCredentialProtector : ICredentialProtector {\n    private const string KeyFileName = \"credential.key\";\n    private const int KeySizeBytes = 32;\n    private const int IvSizeBytes = 16;\n\n    private readonly byte[] key;\n\n    public AesCredentialProtector() {\n        key = LoadOrCreateKey();\n    }\n\n    public string Protect(string plainText) {\n        if (plainText == null) {\n            throw new ArgumentNullException(nameof(plainText));\n        }\n\n        var plaintextBytes = Encoding.UTF8.GetBytes(plainText);\n\n        using var aes = Aes.Create();\n        aes.Key = key;\n        aes.GenerateIV();\n\n        using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);\n        var cipherBytes = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);\n\n        var payload = new byte[aes.IV.Length + cipherBytes.Length];\n        Buffer.BlockCopy(aes.IV, 0, payload, 0, aes.IV.Length);\n        Buffer.BlockCopy(cipherBytes, 0, payload, aes.IV.Length, cipherBytes.Length);\n\n        return Convert.ToBase64String(payload);\n    }\n\n    public string Unprotect(string protectedData) {\n        if (protectedData == null) {\n            throw new ArgumentNullException(nameof(protectedData));\n        }\n\n        var payload = Convert.FromBase64String(protectedData);\n        if (payload.Length < IvSizeBytes) {\n            throw new CryptographicException(\"Protected payload is too short.\");\n        }\n\n        var iv = new byte[IvSizeBytes];\n        Buffer.BlockCopy(payload, 0, iv, 0, IvSizeBytes);\n        var cipherBytes = new byte[payload.Length - IvSizeBytes];\n        Buffer.BlockCopy(payload, IvSizeBytes, cipherBytes, 0, cipherBytes.Length);\n\n        using var aes = Aes.Create();\n        aes.Key = key;\n        aes.IV = iv;\n\n        using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);\n        var plaintextBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);\n\n        return Encoding.UTF8.GetString(plaintextBytes);\n    }\n\n    private static readonly TimeSpan[] RetryDelays = new[] {\n        TimeSpan.Zero,\n        TimeSpan.FromMilliseconds(20),\n        TimeSpan.FromMilliseconds(50),\n        TimeSpan.FromMilliseconds(100),\n        TimeSpan.FromMilliseconds(200),\n        TimeSpan.FromMilliseconds(400)\n    };\n\n    private static byte[] LoadOrCreateKey() {\n        var directory = CredentialProtectionPaths.ResolveKeyDirectory();\n        Directory.CreateDirectory(directory);\n        var keyPath = Path.Combine(directory, KeyFileName);\n\n        var requiresCleanup = false;\n\n        for (var attempt = 0; attempt < RetryDelays.Length; attempt++) {\n            var delay = RetryDelays[attempt];\n            if (delay > TimeSpan.Zero) {\n                Thread.Sleep(delay);\n            }\n\n            if (requiresCleanup) {\n                if (!TryDeleteInvalidKeyFile(keyPath)) {\n                    continue;\n                }\n\n                requiresCleanup = false;\n            }\n\n            if (TryReadExistingKey(keyPath, out var existingKey, out var invalidLength)) {\n                return existingKey;\n            }\n\n            if (invalidLength) {\n                requiresCleanup = true;\n                continue;\n            }\n\n            if (TryCreateKeyFile(keyPath, out var newKey)) {\n                return newKey;\n            }\n        }\n\n        if (TryReadExistingKey(keyPath, out var fallbackKey, out _)) {\n            return fallbackKey;\n        }\n\n        throw new IOException($\"Failed to create or load credential key at '{keyPath}'.\");\n    }\n\n    private static bool TryReadExistingKey(string keyPath, out byte[] key, out bool invalidLength) {\n        key = Array.Empty<byte>();\n        invalidLength = false;\n\n        if (!File.Exists(keyPath)) {\n            return false;\n        }\n\n        try {\n            using var stream = new FileStream(keyPath, FileMode.Open, FileAccess.Read, FileShare.Read);\n            if (stream.Length != KeySizeBytes) {\n                invalidLength = true;\n                return false;\n            }\n\n            var buffer = new byte[KeySizeBytes];\n            var offset = 0;\n            while (offset < buffer.Length) {\n                var bytesRead = stream.Read(buffer, offset, buffer.Length - offset);\n                if (bytesRead == 0) {\n                    invalidLength = true;\n                    return false;\n                }\n\n                offset += bytesRead;\n            }\n\n            key = buffer;\n            return true;\n        } catch (IOException ex) {\n            if (IsSharingViolation(ex)) {\n                return false;\n            }\n\n            throw;\n        } catch (UnauthorizedAccessException ex) {\n            if (IsSharingViolation(ex)) {\n                return false;\n            }\n\n            throw;\n        }\n    }\n\n    private static bool TryCreateKeyFile(string keyPath, out byte[] key) {\n        key = new byte[KeySizeBytes];\n        using (var rng = RandomNumberGenerator.Create()) {\n            rng.GetBytes(key);\n        }\n\n        try {\n            using var stream = new FileStream(keyPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);\n            stream.Write(key, 0, key.Length);\n            stream.Flush(true);\n            return true;\n        } catch (IOException ex) {\n            if (IsSharingViolation(ex) || File.Exists(keyPath)) {\n                return false;\n            }\n\n            throw;\n        } catch (UnauthorizedAccessException ex) {\n            if (IsSharingViolation(ex)) {\n                return false;\n            }\n\n            throw;\n        }\n    }\n\n    private static bool TryDeleteInvalidKeyFile(string keyPath) {\n        try {\n            if (!File.Exists(keyPath)) {\n                return true;\n            }\n\n            File.Delete(keyPath);\n            return true;\n        } catch (IOException ex) {\n            if (IsSharingViolation(ex)) {\n                return false;\n            }\n\n            throw;\n        } catch (UnauthorizedAccessException ex) {\n            if (IsSharingViolation(ex)) {\n                return false;\n            }\n\n            throw;\n        }\n    }\n\n    private static bool IsSharingViolation(Exception ex) {\n        const int ERROR_SHARING_VIOLATION = 32;\n        const int ERROR_LOCK_VIOLATION = 33;\n        const int ERROR_FILE_EXISTS = 80;\n        const int ERROR_ALREADY_EXISTS = 183;\n        const int ERROR_ACCESS_DENIED = 5;\n        const int EACCES = 13;\n        const int EAGAIN = 11;\n\n        var code = (int)((uint)ex.HResult & 0xFFFF);\n\n        if (code == ERROR_SHARING_VIOLATION\n            || code == ERROR_LOCK_VIOLATION\n            || code == ERROR_FILE_EXISTS\n            || code == ERROR_ALREADY_EXISTS\n            || code == ERROR_ACCESS_DENIED\n            || code == EACCES\n            || code == EAGAIN) {\n            return true;\n        }\n\n        if (ex is IOException ioEx) {\n            var message = ioEx.Message;\n            if (!string.IsNullOrEmpty(message)) {\n                if (message.IndexOf(\"sharing violation\", StringComparison.OrdinalIgnoreCase) >= 0\n                    || message.IndexOf(\"used by another process\", StringComparison.OrdinalIgnoreCase) >= 0) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/CredentialProtection.cs",
    "content": "using System;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides access to credential protection services and legacy compatibility helpers.\n/// </summary>\npublic static class CredentialProtection {\n    private static readonly object SyncRoot = new();\n    private static ICredentialProtector? instance;\n\n    /// <summary>\n    /// Gets the default credential protector for the current platform.\n    /// </summary>\n    public static ICredentialProtector Default {\n        get {\n            if (instance != null) {\n                return instance;\n            }\n\n            lock (SyncRoot) {\n                instance ??= CreateDefaultProtector();\n            }\n\n            return instance;\n        }\n    }\n\n    /// <summary>\n    /// Allows tests to replace the default protector.\n    /// </summary>\n    /// <param name=\"protector\">Protector instance used for subsequent operations.</param>\n    internal static void SetDefault(ICredentialProtector protector) {\n        if (protector == null) {\n            throw new ArgumentNullException(nameof(protector));\n        }\n\n        lock (SyncRoot) {\n            instance = protector;\n        }\n    }\n\n    /// <summary>\n    /// Resets the cached protector so the next access recreates it.\n    /// </summary>\n    internal static void Reset() {\n        lock (SyncRoot) {\n            instance = null;\n        }\n    }\n\n    /// <summary>\n    /// Attempts to decrypt <paramref name=\"protectedData\"/> and gracefully falls back to\n    /// plain Base64 decoding for legacy records created prior to introducing\n    /// <see cref=\"ICredentialProtector\"/> on non-Windows platforms.\n    /// </summary>\n    /// <param name=\"protectedData\">Base64 encoded protected payload.</param>\n    /// <returns>The decrypted secret or an empty string when decoding fails.</returns>\n    internal static string UnprotectWithFallback(string? protectedData) => UnprotectWithFallback(Default, protectedData);\n\n    internal static string UnprotectWithFallback(ICredentialProtector protector, string? protectedData) {\n        if (protector == null) {\n            throw new ArgumentNullException(nameof(protector));\n        }\n\n        if (string.IsNullOrEmpty(protectedData)) {\n            return string.Empty;\n        }\n\n        try {\n            return protector.Unprotect(protectedData!);\n        } catch (FormatException) {\n            // Fall back to legacy behaviour below.\n        } catch (CryptographicException) {\n            // Fall back to legacy behaviour below.\n        } catch (ArgumentException) {\n            // Fall back to legacy behaviour below.\n        }\n\n        try {\n            var raw = Convert.FromBase64String(protectedData);\n            return Encoding.UTF8.GetString(raw);\n        } catch {\n            return string.Empty;\n        }\n    }\n\n    private static ICredentialProtector CreateDefaultProtector() {\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {\n            return new WindowsCredentialProtector();\n        }\n\n        return new AesCredentialProtector();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/CredentialProtectionPaths.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace Mailozaurr;\n\ninternal static class CredentialProtectionPaths {\n    private const string RootDirectoryName = \"Mailozaurr\";\n    private const string SubDirectoryName = \"DataProtection\";\n    private const string OverrideDirectoryVariable = \"MAILOZAURR_KEY_DIRECTORY\";\n\n    public static string ResolveKeyDirectory() {\n        var overrideDirectory = Environment.GetEnvironmentVariable(OverrideDirectoryVariable);\n        if (!string.IsNullOrWhiteSpace(overrideDirectory)) {\n            return Path.GetFullPath(overrideDirectory);\n        }\n\n        var baseDirectory = GetBaseDirectory();\n        return Path.Combine(baseDirectory, RootDirectoryName, SubDirectoryName);\n    }\n\n    private static string GetBaseDirectory() {\n        var candidates = new[] {\n            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),\n            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)\n        };\n\n        foreach (var candidate in candidates) {\n            if (!string.IsNullOrWhiteSpace(candidate)) {\n                return candidate;\n            }\n        }\n\n        return AppContext.BaseDirectory;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/EphemeralOpenPgpContext.cs",
    "content": "using System;\nusing System.IO;\nusing Org.BouncyCastle.Bcpg.OpenPgp;\nusing MimeKit.Cryptography;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a <see cref=\"GnuPGContext\"/> that stores keys in a temporary\n/// directory which gets removed when the instance is disposed.\n/// </summary>\n/// <remarks>\n/// This context is useful when you need short‑lived OpenPGP\n/// encryption without leaving key material on disk.\n/// </remarks>\npublic class EphemeralOpenPgpContext : GnuPGContext, IDisposable {\n    private readonly string? _password;\n    private readonly string _tempDirectory;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"EphemeralOpenPgpContext\"/> class.\n    /// </summary>\n    /// <param name=\"password\">Optional passphrase used when unlocking private keys.</param>\n    public EphemeralOpenPgpContext(string? password = null) : base(CreateTempDirectory(out var dir)) {\n        _password = password;\n        _tempDirectory = dir;\n    }\n\n    /// <summary>\n    /// Creates a temporary directory for the OpenPGP keyring.\n    /// </summary>\n    /// <param name=\"path\">The generated directory path.</param>\n    /// <returns>The created path.</returns>\n    private static string CreateTempDirectory(out string path) {\n        var temp = Path.GetTempPath();\n        while (true) {\n            path = Path.Combine(temp, Path.GetRandomFileName());\n            try {\n                using var fs = new FileStream(path, FileMode.CreateNew);\n                fs.Close();\n                File.Delete(path);\n                Directory.CreateDirectory(path);\n                return path;\n            } catch (IOException) {\n                // Collision occurred, retry with a new path\n            }\n        }\n    }\n\n    /// <summary>\n    /// Retrieves the passphrase for the specified secret key.\n    /// </summary>\n    /// <param name=\"key\">The secret key requiring a passphrase.</param>\n    /// <returns>The passphrase to use.</returns>\n    protected override string GetPasswordForKey(PgpSecretKey key) {\n        return _password ?? string.Empty;\n    }\n\n    /// <summary>\n    /// Releases the resources used by the context and deletes the temporary directory.\n    /// </summary>\n    public new void Dispose() {\n        base.Dispose();\n        if (!Directory.Exists(_tempDirectory))\n            return;\n\n        try {\n            Directory.Delete(_tempDirectory, true);\n        } catch (IOException ex) {\n            LoggingMessages.Logger.WriteWarning($\"Failed to delete temporary directory: {ex.Message}\");\n        } catch (UnauthorizedAccessException ex) {\n            LoggingMessages.Logger.WriteWarning($\"Failed to delete temporary directory due to unauthorized access: {ex.Message}\");\n        }\n    }\n    void IDisposable.Dispose() => Dispose();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/ICredentialProtector.cs",
    "content": "using System;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides symmetric protection for sensitive credential material.\n/// </summary>\npublic interface ICredentialProtector {\n    /// <summary>\n    /// Encrypts <paramref name=\"plainText\"/> and returns a Base64 encoded payload.\n    /// </summary>\n    /// <param name=\"plainText\">Plain text secret that should be protected.</param>\n    /// <returns>Base64 encoded protected payload.</returns>\n    string Protect(string plainText);\n\n    /// <summary>\n    /// Decrypts <paramref name=\"protectedData\"/> previously created by <see cref=\"Protect\"/>.\n    /// </summary>\n    /// <param name=\"protectedData\">Base64 encoded protected payload.</param>\n    /// <returns>The decrypted plain text secret.</returns>\n    string Unprotect(string protectedData);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/TemporaryPgpKeyPair.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing Org.BouncyCastle.Bcpg;\nusing Org.BouncyCastle.Bcpg.OpenPgp;\nusing Org.BouncyCastle.Security;\nusing Org.BouncyCastle.Crypto;\nusing Org.BouncyCastle.Crypto.Generators;\nusing Org.BouncyCastle.Crypto.Parameters;\nusing MimeKit;\nusing MimeKit.Cryptography;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for generating temporary PGP key pairs.\n/// </summary>\npublic sealed class TemporaryPgpKeyPair : IDisposable\n{\n    /// <summary>Path to the generated public key file.</summary>\n    public string PublicKeyPath { get; }\n    /// <summary>Path to the generated private key file.</summary>\n    public string PrivateKeyPath { get; }\n    /// <summary>Passphrase used for the private key.</summary>\n    public string PassPhrase { get; }\n\n    private readonly string _tempDirectory;\n    private readonly bool _removeDirectory;\n    private readonly bool _deleteOnDispose;\n\n    private TemporaryPgpKeyPair(string tempDirectory, bool removeDirectory, string passPhrase, bool deleteOnDispose)\n    {\n        _tempDirectory = tempDirectory;\n        _removeDirectory = removeDirectory;\n        _deleteOnDispose = deleteOnDispose;\n        PassPhrase = passPhrase;\n        PublicKeyPath = Path.Combine(tempDirectory, \"temp.pgp.pub\");\n        PrivateKeyPath = Path.Combine(tempDirectory, \"temp.pgp.sec\");\n    }\n\n    /// <summary>\n    /// Generates a temporary PGP key pair stored in a transient directory.\n    /// </summary>\n    /// <param name=\"identity\">Identity for the key pair.</param>\n    /// <param name=\"passPhrase\">Passphrase protecting the private key.</param>\n    /// <param name=\"keySize\">RSA key size.</param>\n    /// <param name=\"outputDirectory\">Optional output directory; if null, a random temp directory is used.</param>\n    /// <param name=\"deleteOnDispose\">When true, deletes the generated files on dispose.</param>\n    /// <returns>Instance representing the created key pair.</returns>\n    public static TemporaryPgpKeyPair Create(string identity = \"Mailozaurr Test\", string passPhrase = \"\", int keySize = 2048, string? outputDirectory = null, bool deleteOnDispose = true)\n    {\n        string directory = outputDirectory ?? Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        bool removeDir = outputDirectory is null;\n        Directory.CreateDirectory(directory);\n        var pair = new TemporaryPgpKeyPair(directory, removeDir, passPhrase, deleteOnDispose);\n\n        var generator = GenerateKeyRingGenerator(identity, passPhrase.ToCharArray(), keySize);\n        using (var pubOut = File.Create(pair.PublicKeyPath))\n        using (var armoredPubOut = new ArmoredOutputStream(pubOut))\n            generator.GeneratePublicKeyRing().Encode(armoredPubOut);\n\n        using (var secOut = File.Create(pair.PrivateKeyPath))\n        using (var armoredSecOut = new ArmoredOutputStream(secOut))\n            generator.GenerateSecretKeyRing().Encode(armoredSecOut);\n\n        return pair;\n    }\n\n    private static PgpKeyRingGenerator GenerateKeyRingGenerator(string identity, char[] passPhrase, int keySize)\n    {\n        var random = new SecureRandom();\n        var keyGen = new RsaKeyPairGenerator();\n        keyGen.Init(new RsaKeyGenerationParameters(Org.BouncyCastle.Math.BigInteger.ValueOf(0x10001), random, keySize, 12));\n        var master = keyGen.GenerateKeyPair();\n        var encryptor = keyGen.GenerateKeyPair();\n\n        var masterPair = new PgpKeyPair(PublicKeyAlgorithmTag.RsaSign, master, DateTime.UtcNow);\n        var encPair = new PgpKeyPair(PublicKeyAlgorithmTag.RsaEncrypt, encryptor, DateTime.UtcNow);\n\n        var signGen = new PgpSignatureSubpacketGenerator();\n        signGen.SetKeyFlags(false, PgpKeyFlags.CanSign | PgpKeyFlags.CanCertify);\n\n        var encGen = new PgpSignatureSubpacketGenerator();\n        encGen.SetKeyFlags(false, PgpKeyFlags.CanEncryptCommunications | PgpKeyFlags.CanEncryptStorage);\n\n        var generator = new PgpKeyRingGenerator(PgpSignature.DefaultCertification, masterPair, identity,\n            SymmetricKeyAlgorithmTag.Aes256, passPhrase, true, signGen.Generate(), null, random);\n        generator.AddSubKey(encPair, encGen.Generate(), null);\n        return generator;\n    }\n\n    /// <inheritdoc />\n    public void Dispose()\n    {\n        if (_deleteOnDispose)\n        {\n            if (File.Exists(PublicKeyPath))\n            {\n                try\n                {\n                    File.Delete(PublicKeyPath);\n                }\n                catch (IOException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete public key: {ex.Message}\");\n                }\n                catch (UnauthorizedAccessException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete public key due to unauthorized access: {ex.Message}\");\n                }\n            }\n\n            if (File.Exists(PrivateKeyPath))\n            {\n                try\n                {\n                    File.Delete(PrivateKeyPath);\n                }\n                catch (IOException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete private key: {ex.Message}\");\n                }\n                catch (UnauthorizedAccessException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete private key due to unauthorized access: {ex.Message}\");\n                }\n            }\n\n            if (_removeDirectory && Directory.Exists(_tempDirectory))\n            {\n                try\n                {\n                    Directory.Delete(_tempDirectory, true);\n                }\n                catch (IOException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete temporary directory: {ex.Message}\");\n                }\n                catch (UnauthorizedAccessException ex)\n                {\n                    LoggingMessages.Logger.WriteWarning($\"Failed to delete temporary directory due to unauthorized access: {ex.Message}\");\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// Decrypts an encrypted MIME message using this key pair.\n    /// </summary>\n    /// <param name=\"messagePath\">Path to the encrypted message file.</param>\n    /// <returns>Decrypted message body text.</returns>\n    public string DecryptToString(string messagePath)\n    {\n        MimeMessage message = MimeMessage.Load(messagePath);\n        using var ctx = new EphemeralOpenPgpContext(PassPhrase);\n        using (var sec = File.OpenRead(PrivateKeyPath))\n            ctx.Import(new PgpSecretKeyRingBundle(new ArmoredInputStream(sec)));\n\n        if (message.Body is MultipartEncrypted encrypted)\n        {\n            var decrypted = encrypted.Decrypt(ctx);\n            if (decrypted is TextPart text)\n            {\n                return text.Text ?? string.Empty;\n            }\n        }\n\n        throw new InvalidOperationException(\"Message is not PGP encrypted\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/TemporarySmimeCertificate.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing Org.BouncyCastle.Asn1.X509;\nusing Org.BouncyCastle.Asn1;\nusing Org.BouncyCastle.Security;\nusing Org.BouncyCastle.X509;\nusing Org.BouncyCastle.X509.Extension;\nusing Org.BouncyCastle.Pkcs;\nusing Org.BouncyCastle.Crypto;\nusing Org.BouncyCastle.Crypto.Operators;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper methods for generating temporary S/MIME certificates.\n/// </summary>\npublic static class TemporarySmimeCertificate {\n    /// <summary>\n    /// Creates a self-signed certificate for testing purposes.\n    /// </summary>\n    /// <param name=\"subjectName\">Subject name of the certificate.</param>\n    /// <param name=\"validDays\">Number of days the certificate is valid.</param>\n    /// <param name=\"outputPath\">Optional path to save the PFX file.</param>\n    /// <returns>A new <see cref=\"X509Certificate2\"/> instance.</returns>\n    public static X509Certificate2 CreateSelfSigned(string subjectName = \"CN=Mailozaurr Test\", int validDays = 1, string? outputPath = null) {\n#if NETSTANDARD2_0\n        throw new NotSupportedException(\"Temporary S/MIME certificates require .NET Framework 4.7.2 or later.\");\n#elif NETFRAMEWORK\n        // On .NET Framework, only use CertificateRequest on non-Windows platforms if available\n        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Type.GetType(\"System.Security.Cryptography.X509Certificates.CertificateRequest\") != null)\n        {\n            return CreateWithCertificateRequest(subjectName, validDays, outputPath);\n        }\n\n        return CreateWithBouncyCastle(subjectName, validDays, outputPath);\n#else\n        // On .NET Core/.NET 5+, CertificateRequest is always available\n        // ALWAYS use CertificateRequest on non-Windows systems to avoid Mono compatibility issues\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||\n            RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ||\n            RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) {\n            return CreateWithCertificateRequest(subjectName, validDays, outputPath);\n        }\n\n        // On Windows, use CertificateRequest by default\n        return CreateWithCertificateRequest(subjectName, validDays, outputPath);\n#endif\n    }\n\n#if !NETSTANDARD2_0\n    private static X509Certificate2 CreateWithCertificateRequest(string subjectName, int validDays, string? outputPath)\n    {\n        using RSA rsa = RSA.Create();\n        rsa.KeySize = 2048;\n        var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));\n        req.CertificateExtensions.Add(\n            new X509KeyUsageExtension(\n                System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.DigitalSignature |\n                System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.KeyEncipherment,\n                true));\n        req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false));\n\n        // Add Enhanced Key Usage extension for S/MIME certificates\n        var ekuOids = new OidCollection();\n        ekuOids.Add(new Oid(\"1.3.6.1.5.5.7.3.2\")); // Client Authentication\n        ekuOids.Add(new Oid(\"1.3.6.1.5.5.7.3.4\")); // Email Protection\n        req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(ekuOids, true));\n                var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);\n        var notAfter = notBefore.AddDays(validDays);\n        X509Certificate2 cert = req.CreateSelfSigned(notBefore, notAfter);\n\n        if (outputPath != null)\n        {\n            File.WriteAllBytes(outputPath, cert.Export(X509ContentType.Pfx));\n        }\n\n        return cert;\n    }\n#endif\n\n    private static X509Certificate2 CreateWithBouncyCastle(string subjectName, int validDays, string? outputPath) {\n        var random = new SecureRandom();\n        var keyGen = new Org.BouncyCastle.Crypto.Generators.RsaKeyPairGenerator();\n        keyGen.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(random, 2048));\n        AsymmetricCipherKeyPair keyPair = keyGen.GenerateKeyPair();\n\n        var certGen = new X509V3CertificateGenerator();\n        var name = new X509Name(subjectName);\n        var serial = Org.BouncyCastle.Math.BigInteger.ProbablePrime(120, random);\n        certGen.SetSerialNumber(serial);\n        certGen.SetIssuerDN(name);\n        certGen.SetNotBefore(DateTime.UtcNow.AddMinutes(-5));\n        certGen.SetNotAfter(DateTime.UtcNow.AddDays(validDays));\n        certGen.SetSubjectDN(name);\n        certGen.SetPublicKey(keyPair.Public);\n        certGen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false));\n        certGen.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));\n        certGen.AddExtension(X509Extensions.SubjectKeyIdentifier, false, X509ExtensionUtilities.CreateSubjectKeyIdentifier(keyPair.Public));\n\n        var signatureFactory = new Asn1SignatureFactory(\"SHA256WithRSA\", keyPair.Private);\n        Org.BouncyCastle.X509.X509Certificate bouncyCert = certGen.Generate(signatureFactory);\n\n        var store = new Pkcs12StoreBuilder().Build();\n        const string pfxPassword = \"pass\";\n        string friendlyName = subjectName;\n        var certEntry = new X509CertificateEntry(bouncyCert);\n        store.SetCertificateEntry(friendlyName, certEntry);\n        store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(keyPair.Private), new[] { certEntry });\n\n        using var ms = new MemoryStream();\n        store.Save(ms, pfxPassword.ToCharArray(), random);\n        var raw = ms.ToArray();\n\n        if (outputPath != null) {\n            File.WriteAllBytes(outputPath, raw);\n        }\n\n        // Try different approaches for better cross-platform compatibility\n        return CreateCertificateWithFallbacks(raw, pfxPassword);\n    }\n\n    /// <summary>\n    /// Creates an X509Certificate2 with comprehensive fallback strategies for cross-platform compatibility.\n    /// </summary>\n    private static X509Certificate2 CreateCertificateWithFallbacks(byte[] pfxData, string password) {\n        var exceptions = new List<Exception>();\n\n        // Strategy 1: Try different key storage flags with password\n        var flagCombinations = new[] {\n            X509KeyStorageFlags.Exportable,\n            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet,\n            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet,\n            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.DefaultKeySet,\n            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet,\n            X509KeyStorageFlags.MachineKeySet,\n            X509KeyStorageFlags.UserKeySet,\n            X509KeyStorageFlags.DefaultKeySet\n        };\n\n        foreach (var flags in flagCombinations) {\n            try {\n                return new X509Certificate2(pfxData, password, flags);\n            } catch (CryptographicException ex) {\n                exceptions.Add(ex);\n            }\n        }\n\n        // Strategy 2: Try without password (some implementations work better this way)\n        if (!string.IsNullOrEmpty(password)) {\n            foreach (var flags in flagCombinations) {\n                try {\n                    return new X509Certificate2(pfxData, string.Empty, flags);\n                } catch (CryptographicException ex) {\n                    exceptions.Add(ex);\n                }\n            }\n        }\n\n        // Strategy 3: Try with null password\n        foreach (var flags in flagCombinations) {\n            try {\n                return new X509Certificate2(pfxData, (string?)null, flags);\n            } catch (CryptographicException ex) {\n                exceptions.Add(ex);\n            }\n        }\n\n        // Strategy 4: Try the simple constructor without flags\n        try {\n            return new X509Certificate2(pfxData, password);\n        } catch (CryptographicException ex) {\n            exceptions.Add(ex);\n        }\n\n        try {\n            return new X509Certificate2(pfxData);\n        } catch (CryptographicException ex) {\n            exceptions.Add(ex);\n        }\n\n        // If all strategies fail, throw the first exception with context\n        throw new CryptographicException(\n            $\"Failed to create X509Certificate2 on {RuntimeInformation.OSDescription}. \" +\n            $\"Tried {exceptions.Count} different approaches. \" +\n            $\"First error: {exceptions[0].Message}\",\n            exceptions[0]);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Cryptography/WindowsCredentialProtector.cs",
    "content": "using System;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace Mailozaurr;\n\ninternal sealed class WindowsCredentialProtector : ICredentialProtector {\n    public string Protect(string plainText) {\n        if (plainText == null) {\n            throw new ArgumentNullException(nameof(plainText));\n        }\n\n        var plaintextBytes = Encoding.UTF8.GetBytes(plainText);\n        var protectedBytes = ProtectedData.Protect(plaintextBytes, null, DataProtectionScope.CurrentUser);\n        return Convert.ToBase64String(protectedBytes);\n    }\n\n    public string Unprotect(string protectedData) {\n        if (protectedData == null) {\n            throw new ArgumentNullException(nameof(protectedData));\n        }\n\n        var protectedBytes = Convert.FromBase64String(protectedData);\n        var plaintextBytes = ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser);\n        return Encoding.UTF8.GetString(plaintextBytes);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Definitions/AttachmentDescriptor.cs",
    "content": "namespace Mailozaurr.Definitions;\n\nusing MimeKit;\nusing MimeKit.Utils;\n\n/// <summary>\n/// Base descriptor describing an attachment that can be added to outbound messages.\n/// </summary>\npublic abstract class AttachmentDescriptor\n{\n    /// <summary>\n    /// Gets or sets the file name used for the attachment.\n    /// </summary>\n    public string? FileName { get; set; }\n\n    /// <summary>\n    /// Gets or sets the MIME type applied to the attachment.\n    /// </summary>\n    public string? ContentType { get; set; }\n\n    /// <summary>\n    /// Gets or sets the content identifier for inline attachments.\n    /// </summary>\n    public string? ContentId { get; set; }\n\n    /// <summary>\n    /// Gets or sets the content description header value.\n    /// </summary>\n    public string? ContentDescription { get; set; }\n\n    /// <summary>\n    /// Gets or sets the content disposition applied to the attachment.\n    /// </summary>\n    public ContentDisposition? ContentDisposition { get; set; }\n\n    /// <summary>\n    /// Gets or sets the transfer encoding applied to the attachment.\n    /// </summary>\n    public ContentEncoding? TransferEncoding { get; set; }\n\n    /// <summary>\n    /// Gets or sets any additional headers that should be added to the attachment.\n    /// </summary>\n    public IDictionary<string, string>? Headers { get; set; }\n\n    /// <summary>\n    /// Gets the source path associated with the attachment if one exists.\n    /// </summary>\n    internal virtual string? SourcePath => null;\n\n    /// <summary>\n    /// Creates a <see cref=\"MimeEntity\"/> representing the attachment.\n    /// </summary>\n    /// <param name=\"inline\">When set to <c>true</c>, the descriptor will be treated as an inline resource.</param>\n    /// <returns>The created <see cref=\"MimeEntity\"/>.</returns>\n    internal virtual MimeEntity CreateMimeEntity(bool inline)\n    {\n        var stream = CreateContentStream();\n        var mediaType = !string.IsNullOrWhiteSpace(ContentType)\n            ? ContentType\n            : !string.IsNullOrWhiteSpace(FileName)\n                ? MimeTypes.GetMimeType(FileName!)\n                : \"application/octet-stream\";\n\n        var part = new MimePart(mediaType ?? \"application/octet-stream\")\n        {\n            Content = new MimeContent(stream),\n            FileName = FileName,\n        };\n\n        var disposition = ContentDisposition ?? new ContentDisposition(\n            inline ? ContentDisposition.Inline : ContentDisposition.Attachment);\n        part.ContentDisposition = disposition;\n\n        var contentId = ContentId;\n        if (inline)\n        {\n            if (string.IsNullOrWhiteSpace(contentId))\n            {\n                contentId = !string.IsNullOrWhiteSpace(FileName)\n                    ? FileName\n                    : MimeUtils.GenerateMessageId();\n            }\n        }\n\n        if (!string.IsNullOrWhiteSpace(contentId))\n        {\n            part.ContentId = contentId;\n        }\n\n        if (!string.IsNullOrWhiteSpace(ContentDescription))\n        {\n            part.ContentDescription = ContentDescription;\n        }\n\n        if (TransferEncoding.HasValue)\n        {\n            part.ContentTransferEncoding = TransferEncoding.Value;\n        }\n\n        if (Headers != null)\n        {\n            foreach (var header in Headers)\n            {\n                if (!string.IsNullOrWhiteSpace(header.Key) && header.Value is not null)\n                {\n                    part.Headers[header.Key] = header.Value;\n                }\n            }\n        }\n\n        return part;\n    }\n\n    /// <summary>\n    /// Returns the attachment content as a stream positioned at the beginning.\n    /// </summary>\n    /// <returns>A readable stream containing the attachment content.</returns>\n    protected abstract Stream CreateContentStream();\n\n    /// <summary>\n    /// Returns the attachment content as a byte array.\n    /// </summary>\n    /// <returns>Attachment content represented as a byte array.</returns>\n    internal virtual byte[] GetContentBytes()\n    {\n        using var stream = CreateContentStream();\n        using var memory = new MemoryStream();\n        stream.CopyTo(memory);\n        return memory.ToArray();\n    }\n}\n\n/// <summary>\n/// Descriptor that sources attachment content from a file on disk.\n/// </summary>\npublic sealed class FileAttachmentDescriptor : AttachmentDescriptor\n{\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"FileAttachmentDescriptor\"/> class.\n    /// </summary>\n    /// <param name=\"filePath\">Path to the file providing the attachment content.</param>\n    public FileAttachmentDescriptor(string filePath)\n    {\n        if (string.IsNullOrWhiteSpace(filePath))\n        {\n            throw new ArgumentException(\"File path must be provided.\", nameof(filePath));\n        }\n\n        FilePath = filePath;\n        FileName ??= Path.GetFileName(filePath);\n    }\n\n    /// <summary>\n    /// Gets the file path supplying the attachment content.\n    /// </summary>\n    public string FilePath { get; }\n\n    internal override string? SourcePath => FilePath;\n\n    /// <inheritdoc />\n    protected override Stream CreateContentStream()\n    {\n        var data = File.ReadAllBytes(FilePath);\n        return new MemoryStream(data, writable: false);\n    }\n}\n\n/// <summary>\n/// Descriptor that sources attachment content from a <see cref=\"Stream\"/>.\n/// </summary>\npublic sealed class StreamAttachmentDescriptor : AttachmentDescriptor\n{\n    private readonly Stream _stream;\n    private readonly bool _leaveStreamOpen;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"StreamAttachmentDescriptor\"/> class.\n    /// </summary>\n    /// <param name=\"stream\">Readable stream that provides the attachment content.</param>\n    /// <param name=\"fileName\">File name to associate with the attachment.</param>\n    /// <param name=\"leaveStreamOpen\">Whether the provided stream should remain open after being read.</param>\n    public StreamAttachmentDescriptor(Stream stream, string fileName, bool leaveStreamOpen = true)\n    {\n        _stream = stream ?? throw new ArgumentNullException(nameof(stream));\n        if (!stream.CanRead)\n        {\n            throw new ArgumentException(\"Stream must be readable.\", nameof(stream));\n        }\n\n        if (string.IsNullOrWhiteSpace(fileName))\n        {\n            throw new ArgumentException(\"File name must be provided.\", nameof(fileName));\n        }\n\n        FileName = fileName;\n        _leaveStreamOpen = leaveStreamOpen;\n    }\n\n    /// <inheritdoc />\n    protected override Stream CreateContentStream()\n    {\n        if (_stream.CanSeek)\n        {\n            _stream.Position = 0;\n        }\n\n        var memory = new MemoryStream();\n        _stream.CopyTo(memory);\n        memory.Position = 0;\n\n        if (_stream.CanSeek)\n        {\n            try\n            {\n                _stream.Position = 0;\n            }\n            catch (ObjectDisposedException)\n            {\n                // Ignore - stream may have been disposed externally.\n            }\n        }\n\n        if (!_leaveStreamOpen)\n        {\n            _stream.Dispose();\n        }\n\n        return memory;\n    }\n}\n\n/// <summary>\n/// Descriptor that sources attachment content from a byte array.\n/// </summary>\npublic sealed class ByteArrayAttachmentDescriptor : AttachmentDescriptor\n{\n    private readonly byte[] _buffer;\n    private readonly bool _cloneBuffer;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ByteArrayAttachmentDescriptor\"/> class.\n    /// </summary>\n    /// <param name=\"buffer\">Byte array containing the attachment data.</param>\n    /// <param name=\"fileName\">File name to associate with the attachment.</param>\n    /// <param name=\"cloneBuffer\">If <c>true</c>, the buffer will be cloned to prevent external mutations.</param>\n    public ByteArrayAttachmentDescriptor(byte[] buffer, string fileName, bool cloneBuffer = true)\n    {\n        buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));\n        if (string.IsNullOrWhiteSpace(fileName))\n        {\n            throw new ArgumentException(\"File name must be provided.\", nameof(fileName));\n        }\n\n        _buffer = buffer.Length == 0 ? Array.Empty<byte>() : buffer;\n        _cloneBuffer = cloneBuffer;\n        FileName = fileName;\n    }\n\n    /// <inheritdoc />\n    protected override Stream CreateContentStream()\n    {\n        var data = _cloneBuffer ? (byte[])_buffer.Clone() : _buffer;\n        return new MemoryStream(data, writable: false);\n    }\n}\n\n/// <summary>\n/// Descriptor that wraps an existing <see cref=\"MimeEntity\"/> instance.\n/// </summary>\npublic sealed class MimeEntityAttachmentDescriptor : AttachmentDescriptor\n{\n    private readonly MimeEntity _entity;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"MimeEntityAttachmentDescriptor\"/> class.\n    /// </summary>\n    /// <param name=\"entity\">The MIME entity to attach.</param>\n    public MimeEntityAttachmentDescriptor(MimeEntity entity)\n    {\n        _entity = entity ?? throw new ArgumentNullException(nameof(entity));\n    }\n\n    internal override MimeEntity CreateMimeEntity(bool inline)\n    {\n        if (_entity is MimePart part)\n        {\n            if (!string.IsNullOrWhiteSpace(FileName))\n            {\n                part.FileName = FileName;\n            }\n\n            if (ContentDisposition != null)\n            {\n                part.ContentDisposition = ContentDisposition;\n            }\n            else if (inline && part.ContentDisposition == null)\n            {\n                part.ContentDisposition = new ContentDisposition(ContentDisposition.Inline);\n            }\n\n            if (!string.IsNullOrWhiteSpace(ContentDescription))\n            {\n                part.ContentDescription = ContentDescription;\n            }\n\n            if (TransferEncoding.HasValue)\n            {\n                part.ContentTransferEncoding = TransferEncoding.Value;\n            }\n\n            if (Headers != null)\n            {\n                foreach (var header in Headers)\n                {\n                    if (!string.IsNullOrWhiteSpace(header.Key) && header.Value is not null)\n                    {\n                        part.Headers[header.Key] = header.Value;\n                    }\n                }\n            }\n\n            if (string.IsNullOrWhiteSpace(part.ContentId))\n            {\n                if (!string.IsNullOrWhiteSpace(ContentId))\n                {\n                    part.ContentId = ContentId;\n                }\n                else if (inline)\n                {\n                    part.ContentId = MimeUtils.GenerateMessageId();\n                }\n            }\n        }\n\n        return _entity;\n    }\n\n    /// <summary>\n    /// Throwing override — a <see cref=\"MimeEntityAttachmentDescriptor\"/> does not expose a raw content stream.\n    /// </summary>\n    protected override Stream CreateContentStream() => throw new NotSupportedException(\"MimeEntity attachments do not expose a content stream.\");\n\n    internal override byte[] GetContentBytes() => throw new NotSupportedException(\"MimeEntity attachments cannot be converted to raw bytes.\");\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Definitions/MailComposeProfile.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a reusable compose identity/profile.\n/// </summary>\npublic sealed class MailComposeProfile {\n    /// <summary>\n    /// Gets or sets the stable identifier for the profile.\n    /// </summary>\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Gets or sets the user-facing profile name.\n    /// </summary>\n    public string? Name { get; set; }\n\n    /// <summary>\n    /// Gets or sets the from address for the profile.\n    /// </summary>\n    public string? From { get; set; }\n\n    /// <summary>\n    /// Gets or sets the reply-to address for the profile.\n    /// </summary>\n    public string? ReplyTo { get; set; }\n\n    /// <summary>\n    /// Gets or sets the signature text for the profile.\n    /// </summary>\n    public string? SignatureText { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether this profile is the default.\n    /// </summary>\n    public bool IsDefault { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Definitions/OAuthCredential.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Represents OAuth credentials.\n/// </summary>\n/// <remarks>\n/// This structure is used by various helper classes to cache\n/// tokens acquired from identity providers.\n/// </remarks>\npublic class OAuthCredential {\n    /// <summary>\n    /// The username associated with the OAuth credential.\n    /// </summary>\n    public string UserName { get; set; } = string.Empty;\n    /// <summary>\n    /// The access token for the OAuth credential.\n    /// </summary>\n    public string AccessToken { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Time when the access token expires.\n    /// </summary>\n    public DateTimeOffset ExpiresOn { get; set; }\n\n    /// <summary>\n    /// The refresh token, if available.\n    /// </summary>\n    public string? RefreshToken { get; set; }\n\n    /// <summary>\n    /// Identifier of the OAuth client used to acquire the token.\n    /// </summary>\n    public string? ClientId { get; set; }\n\n    /// <summary>\n    /// Secret associated with the OAuth client used to acquire the token.\n    /// </summary>\n    public string? ClientSecret { get; set; }\n\n    /// <summary>\n    /// Raw JSON payload containing service account credentials used for delegated access.\n    /// </summary>\n    public string? ServiceAccountJson { get; set; }\n\n    /// <summary>\n    /// Optional subject impersonated when using service account credentials.\n    /// </summary>\n    public string? ServiceAccountSubject { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Definitions/ValidatedEmail.cs",
    "content": "﻿using EmailValidation;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Wraps an email address that has been validated for correct syntax.\n/// </summary>\n/// <remarks>\n/// Validation is performed using the <c>EmailValidation</c> library.\n/// </remarks>\npublic class ValidatedEmail {\n    /// <summary>\n    /// Email address\n    /// </summary>\n    /// <value>\n    /// The email address.\n    /// </value>\n    public string EmailAddress { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Returns true if email address is valid.\n    /// </summary>\n    /// <value>\n    ///   <c>true</c> if this instance is valid; otherwise, <c>false</c>.\n    /// </value>\n    public bool IsValid { get; set; }\n\n    /// <summary>\n    /// Indicates if the email address is disposable\n    /// </summary>\n    /// <value>\n    ///   <c>true</c> if email address is disposable; otherwise, <c>false</c>.\n    /// </value>\n    public bool IsDisposable { get; set; }\n\n    /// <summary>\n    /// Indicates why email address is invalid (if it is)\n    /// </summary>\n    public EmailValidationErrorCode Reason { get; set; }\n\n    /// <summary>\n    /// Indicates the index of the token that caused the email address to be invalid\n    /// </summary>\n    public int? ReasonTokenIndex { get; set; }\n\n    /// <summary>\n    /// Indicates the index of the error that caused the email address to be invalid\n    /// </summary>\n    public int? ReasonErrorIndex { get; set; }\n\n    /// <summary>\n    /// If error during validation happens, this will contain the error message\n    /// </summary>\n    /// <value>\n    /// The error.\n    /// </value>\n    public string Error { get; set; } = string.Empty;\n}"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/DmarcReport.cs",
    "content": "namespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Represents a DMARC aggregate report extracted from a message.\n/// </summary>\npublic sealed class DmarcReport {\n    /// <summary>Sender of the DMARC report.</summary>\n    public string? From { get; set; }\n\n    /// <summary>Subject of the DMARC report message.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Date the report message was received.</summary>\n    public DateTimeOffset Date { get; set; }\n\n    /// <summary>Collection of zipped XML attachments.</summary>\n    public IList<DmarcReportAttachment> Attachments { get; } = new List<DmarcReportAttachment>();\n}"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/DmarcReportAttachment.cs",
    "content": "namespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Represents a zipped XML attachment belonging to a DMARC report.\n/// </summary>\npublic sealed class DmarcReportAttachment : IDisposable {\n    /// <summary>File name of the attachment.</summary>\n    public string Name { get; }\n\n    /// <summary>Stream containing the zipped attachment.</summary>\n    public Stream Content { get; }\n\n    /// <summary>Initializes a new instance of the <see cref=\"DmarcReportAttachment\"/> class.</summary>\n    /// <param name=\"name\">Attachment file name.</param>\n    /// <param name=\"content\">Stream containing zipped report content.</param>\n    public DmarcReportAttachment(string name, Stream content) {\n        Name = name;\n        Content = content;\n    }\n\n    /// <summary>Releases the underlying stream.</summary>\n    public void Dispose() => Content.Dispose();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/DmarcReportServiceBase.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Provides common functionality for DMARC report services.\n/// </summary>\npublic abstract class DmarcReportServiceBase : IDmarcReportService {\n    private readonly SemaphoreSlim gate = new(1, 1);\n\n    /// <summary>\n    /// Searches for DMARC reports using implementation specific logic.\n    /// </summary>\n    public async Task<IList<DmarcReport>> SearchAsync(\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            return await SearchInternalAsync(since, before, domain, maxResults, cancellationToken).ConfigureAwait(false);\n        } finally {\n            gate.Release();\n        }\n    }\n\n    /// <summary>\n    /// Performs the actual search for DMARC reports.\n    /// </summary>\n    protected abstract Task<IList<DmarcReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        int maxResults,\n        CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/GmailDmarcReportService.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Retrieves DMARC aggregate reports using the Gmail API.\n/// </summary>\npublic sealed class GmailDmarcReportService : DmarcReportServiceBase {\n    private readonly GmailApiClient client;\n    private readonly string userId;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailDmarcReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">Gmail API client.</param>\n    /// <param name=\"userId\">Account identifier, typically 'me'.</param>\n    public GmailDmarcReportService(GmailApiClient client, string userId) {\n        this.client = client;\n        this.userId = userId;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<DmarcReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchDmarcReportsAsync(client, userId, since, before, domain, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/GraphDmarcReportService.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Retrieves DMARC aggregate reports using Microsoft Graph.\n/// </summary>\npublic sealed class GraphDmarcReportService : DmarcReportServiceBase {\n    private readonly GraphCredential credential;\n    private readonly string userPrincipalName;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphDmarcReportService\"/> class.\n    /// </summary>\n    /// <param name=\"credential\">Credential used to access Microsoft Graph.</param>\n    /// <param name=\"userPrincipalName\">UPN of the mailbox to query.</param>\n    public GraphDmarcReportService(GraphCredential credential, string userPrincipalName) {\n        this.credential = credential;\n        this.userPrincipalName = userPrincipalName;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<DmarcReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchDmarcReportsAsync(credential, userPrincipalName, since, before, domain, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/ImapDmarcReportService.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Retrieves DMARC aggregate reports from an IMAP mailbox.\n/// </summary>\npublic sealed class ImapDmarcReportService : DmarcReportServiceBase {\n    private readonly ImapClient client;\n    private readonly string? folder;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ImapDmarcReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">IMAP client used to access the mailbox.</param>\n    /// <param name=\"folder\">Optional folder name to search within.</param>\n    public ImapDmarcReportService(ImapClient client, string? folder = null) {\n        this.client = client;\n        this.folder = folder;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<DmarcReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchDmarcReportsAsync(client, folder, since, before, domain, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/DmarcReports/Pop3DmarcReportService.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Pop3;\n\nnamespace Mailozaurr.DmarcReports;\n\n/// <summary>\n/// Retrieves DMARC aggregate reports using the POP3 protocol.\n/// </summary>\npublic sealed class Pop3DmarcReportService : DmarcReportServiceBase {\n    private readonly Pop3Client client;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Pop3DmarcReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">POP3 client used to access the mailbox.</param>\n    public Pop3DmarcReportService(Pop3Client client) => this.client = client;\n\n    /// <inheritdoc />\n    protected override Task<IList<DmarcReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchDmarcReportsAsync(client, since, before, domain, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/EmailGraphMessage.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Simplified representation of an email message returned from Graph.\n/// </summary>\n/// <remarks>\n/// Only a subset of fields are exposed to keep the object light‑weight\n/// when fetching lists of messages.\n/// </remarks>\npublic class EmailGraphMessage {\n    /// <summary>\n    /// Gets or sets the unique identifier of the message.\n    /// </summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the subject line of the message.\n    /// </summary>\n    public string Subject { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the preview text of the message body.\n    /// </summary>\n    public string BodyPreview { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the full body content of the message.\n    /// </summary>\n    public object? Body { get; set; }\n\n    /// <summary>\n    /// Gets or sets the change key used for concurrency checks.\n    /// </summary>\n    public string ChangeKey { get; set; } = string.Empty;\n    // Add more properties as needed\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/ActionPreference.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Specifies how the client should react when an error occurs.\n/// </summary>\n/// <remarks>\n/// This enumeration is used by cmdlets to determine the level of\n/// interaction required when an operation encounters an issue.\n/// </remarks>\npublic enum ActionPreference {\n    /// <summary>\n    /// Stop execution when an error occurs.\n    /// </summary>\n    Stop,\n\n    /// <summary>\n    /// Continue execution regardless of errors.\n    /// </summary>\n    Continue,\n\n    /// <summary>\n    /// Ask the user whether to continue when an error occurs.\n    /// </summary>\n    Inquire,\n\n    /// <summary>\n    /// Ignore errors and continue without notification.\n    /// </summary>\n    SilentlyContinue,\n\n    /// <summary>\n    /// Pause execution for further action.\n    /// </summary>\n    Suspend\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/AuthenticationMechanism.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Supported SASL authentication mechanisms.\n/// </summary>\n/// <remarks>\n/// Values correspond to mechanisms understood by <c>MailKit</c>\n/// and can be used when configuring SMTP connections.\n/// </remarks>\npublic enum AuthenticationMechanism {\n    /// <summary>\n    /// Plain text authentication mechanism.\n    /// </summary>\n    Plain,\n\n    /// <summary>\n    /// LOGIN authentication mechanism.\n    /// </summary>\n    Login,\n\n    /// <summary>\n    /// Challenge-response authentication using CRAM-MD5.\n    /// </summary>\n    CramMd5\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/DeliveryNotification.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Delivery status notification options.\n/// </summary>\n/// <remarks>\n/// These values map directly to SMTP <c>NOTIFY</c> settings when\n/// requesting delivery status notifications from the server.\n/// </remarks>\npublic enum DeliveryNotification {\n    /// <summary>\n    /// No delivery status notifications are requested.\n    /// </summary>\n    None,\n\n    /// <summary>\n    /// Notify when delivery is delayed.\n    /// </summary>\n    Delay,\n\n    /// <summary>\n    /// Suppress all delivery notifications.\n    /// </summary>\n    Never,\n\n    /// <summary>\n    /// Notify on delivery failure.\n    /// </summary>\n    OnFailure,\n\n    /// <summary>\n    /// Notify on successful delivery.\n    /// </summary>\n    OnSuccess,\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/EmailAction.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Represents actions performed when sending or preparing an email.\n/// </summary>\n/// <remarks>\n/// These values are used internally when tracing the steps of\n/// the email sending pipeline.\n/// </remarks>\npublic enum EmailAction {\n    /// <summary>\n    /// Authenticate with the mail server or provider.\n    /// </summary>\n    Authenticate,\n\n    /// <summary>\n    /// Establish a connection to the mail server.\n    /// </summary>\n    Connect,\n\n    /// <summary>\n    /// Sign the message using S/MIME.\n    /// </summary>\n    SMimeSignature,\n\n    /// <summary>\n    /// Sign the message using S/MIME in PKCS#7 format.\n    /// </summary>\n    SMimeSignaturePKCS7,\n\n    /// <summary>\n    /// Encrypt the message using S/MIME.\n    /// </summary>\n    SMimeEncrypt,\n\n    /// <summary>\n    /// Sign and encrypt the message using S/MIME.\n    /// </summary>\n    SMimeSignAndEncrypt,\n\n    /// <summary>\n    /// Sign the message using PGP.\n    /// </summary>\n    PgpSign,\n\n    /// <summary>\n    /// Encrypt the message using PGP.\n    /// </summary>\n    PgpEncrypt,\n\n    /// <summary>\n    /// Sign and encrypt the message using PGP.\n    /// </summary>\n    PgpSignAndEncrypt,\n\n    /// <summary>\n    /// Send the message via the configured provider.\n    /// </summary>\n    Send,\n\n    /// <summary>\n    /// Send the message as a draft.\n    /// </summary>\n    SendDraftMessage,\n\n    /// <summary>\n    /// Send message attachments only.\n    /// </summary>\n    SendAttachment\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/EmailActionEncryption.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Specifies encryption or signing actions for S/MIME operations.\n/// </summary>\n/// <remarks>\n/// The order of the enum values mirrors the processing flow when\n/// applying signatures or encryption to a message.\n/// </remarks>\npublic enum EmailActionEncryption {\n    /// <summary>\n    /// No signing or encryption is applied.\n    /// </summary>\n    None,\n\n    /// <summary>\n    /// Sign the message using an S/MIME certificate.\n    /// </summary>\n    SMIMESign,\n\n    /// <summary>\n    /// Sign the message using an S/MIME certificate in PKCS#7 format.\n    /// </summary>\n    SMIMESignPkcs7,\n\n    /// <summary>\n    /// Encrypt the message using S/MIME.\n    /// </summary>\n    SMIMEEncrypt,\n\n    /// <summary>\n    /// Sign and encrypt the message using S/MIME.\n    /// </summary>\n    SMIMESignAndEncrypt,\n\n    /// <summary>\n    /// Sign the message using a PGP key.\n    /// </summary>\n    PGPSign,\n\n    /// <summary>\n    /// Encrypt the message using a PGP key.\n    /// </summary>\n    PGPEncrypt,\n\n    /// <summary>\n    /// Sign and encrypt the message using a PGP key.\n    /// </summary>\n    PGPSignAndEncrypt,\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/EmailEncryption.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Indicates the encryption or signing format of an email message.\n/// </summary>\n/// <remarks>\n/// Determined using MIME headers and helps identify whether a\n/// message has been secured.\n/// </remarks>\npublic enum EmailEncryption {\n    /// <summary>No encryption or signature detected.</summary>\n    None,\n    /// <summary>The message is encrypted with OpenPGP.</summary>\n    PgpEncrypted,\n    /// <summary>The message is signed using OpenPGP.</summary>\n    PgpSigned,\n    /// <summary>The message is encrypted using S/MIME.</summary>\n    SmimeEncrypted,\n    /// <summary>The message is signed using S/MIME.</summary>\n    SmimeSigned\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/EmailProvider.cs",
    "content": "﻿namespace Mailozaurr;\n/// <summary>\n/// Supported external email providers.\n/// </summary>\n/// <remarks>\n/// These values determine which implementation class is\n/// instantiated when a provider is selected.\n/// </remarks>\npublic enum EmailProvider {\n    /// <summary>\n    /// Use the built-in SMTP client.\n    /// </summary>\n    None,\n\n    /// <summary>\n    /// Use the SendGrid REST API to deliver mail.\n    /// </summary>\n    SendGrid,\n\n    /// <summary>\n    /// Use the Mailgun REST API to deliver mail.\n    /// </summary>\n    Mailgun,\n\n    /// <summary>\n    /// Use the Amazon SES REST API to deliver mail.\n    /// </summary>\n    SES,\n\n    /// <summary>\n    /// Use the Gmail REST API to deliver mail.\n    /// </summary>\n    Gmail,\n\n    /// <summary>\n    /// Use Microsoft Graph REST API to deliver mail.\n    /// </summary>\n    Graph,\n    //MailChimp,\n    //Moosend,\n    //Postmark,\n    //Brevo (Sendinblue),\n    //MessageBird (SparkPost),\n    //MailerSend\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/GraphEndpoint.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Known Microsoft Graph API endpoints.\n/// </summary>\n/// <remarks>\n/// These values are used when constructing Graph URLs for\n/// requests to either the stable or beta API surface.\n/// </remarks>\npublic enum GraphEndpoint {\n    /// <summary>Represents the v1.0 endpoint.</summary>\n    V1,\n    /// <summary>Represents the beta endpoint.</summary>\n    Beta\n}"
  },
  {
    "path": "Sources/Mailozaurr/Enums/GraphImportance.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Specifies the importance level of a Microsoft Graph message.\n/// </summary>\npublic enum GraphImportance {\n    /// <summary>Low importance.</summary>\n    Low,\n\n    /// <summary>Normal importance.</summary>\n    Normal,\n\n    /// <summary>High importance.</summary>\n    High\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/GraphMailboxRole.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Specifies roles available for mailbox permissions.\n/// </summary>\n/// <remarks>\n/// Used when granting or querying delegated access to another\n/// mailbox using Microsoft Graph.\n/// </remarks>\npublic enum GraphMailboxRole {\n    /// <summary>Full owner access.</summary>\n    Owner,\n\n    /// <summary>Read-only access.</summary>\n    Read,\n\n    /// <summary>Write access.</summary>\n    Write,\n\n    /// <summary>Other or custom role not represented by predefined values.</summary>\n    Custom\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Enums/MessagePriority.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Indicates the priority of an email message.\n/// </summary>\n/// <remarks>\n/// Many mail clients surface priority as \"importance\" when\n/// displaying messages to end users.\n/// </remarks>\npublic enum MessagePriority {\n    /// <summary>\n    /// High priority message.\n    /// </summary>\n    High,\n\n    /// <summary>\n    /// Low priority message.\n    /// </summary>\n    Low,\n\n    /// <summary>\n    /// Normal priority message.\n    /// </summary>\n    Normal\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailApiClient.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Lightweight client for sending and retrieving messages using Gmail REST API.\n/// </summary>\npublic sealed class GmailApiClient : IDisposable {\n    private readonly HttpClient _client;\n    private readonly Func<CancellationToken, Task<string>>? _refreshToken;\n    private readonly OAuthCredential? _credential;\n    private readonly bool _disposeClient;\n    private bool _disposed;\n\n    private void ThrowIfDisposed() {\n        if (_disposed) {\n            throw new ObjectDisposedException(nameof(GmailApiClient));\n        }\n    }\n\n    /// <summary>\n    /// Initializes the client using the provided OAuth credential.\n    /// </summary>\n    public GmailApiClient(OAuthCredential credential, Func<CancellationToken, Task<string>>? refreshToken = null) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        _credential = credential;\n        _disposeClient = true;\n        _client = new HttpClient {\n            BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n        };\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", credential.AccessToken);\n        _refreshToken = refreshToken;\n    }\n\n    /// <summary>\n    /// Initializes the client using an externally managed <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <remarks>\n    /// If <paramref name=\"client\"/> does not specify <see cref=\"HttpClient.BaseAddress\"/>, it will be set to the Gmail v1 endpoint\n    /// (or <paramref name=\"baseAddress\"/> if provided).\n    /// </remarks>\n    /// <param name=\"client\">HTTP client to use for requests.</param>\n    /// <param name=\"refreshToken\">Optional delegate used to refresh an access token when a request returns 401/403.</param>\n    /// <param name=\"credential\">Optional OAuth credential holding an access token.</param>\n    /// <param name=\"baseAddress\">Optional Gmail base address used when <paramref name=\"client\"/> has no base address configured.</param>\n    /// <param name=\"ownsHttpClient\">When <c>true</c>, disposing this API client also disposes <paramref name=\"client\"/>.</param>\n    public GmailApiClient(\n        HttpClient client,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        OAuthCredential? credential = null,\n        Uri? baseAddress = null,\n        bool ownsHttpClient = false) {\n        _client = client ?? throw new ArgumentNullException(nameof(client));\n        _refreshToken = refreshToken;\n        _credential = credential;\n        _disposeClient = ownsHttpClient;\n        if (_client.BaseAddress == null) {\n            _client.BaseAddress = baseAddress ?? new Uri(\"https://gmail.googleapis.com/gmail/v1/\");\n        }\n        if (credential != null && !string.IsNullOrEmpty(credential.AccessToken) && _client.DefaultRequestHeaders.Authorization == null) {\n            _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", credential.AccessToken);\n        }\n    }\n\n    /// <summary>\n    /// Repository used to persist messages that require retrying.\n    /// </summary>\n    public IPendingMessageRepository? PendingMessageRepository { get; set; }\n\n    /// <summary>\n    /// When set, sending is simulated and no Gmail request is issued.\n    /// </summary>\n    public bool DryRun { get; set; }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        if (_disposed) {\n            return;\n        }\n\n        if (_disposeClient) {\n            _client.Dispose();\n        }\n\n        _disposed = true;\n        GC.SuppressFinalize(this);\n    }\n\n    private async Task ThrowIfAuthErrorAsync(HttpResponseMessage response, CancellationToken cancellationToken) {\n        if (response.StatusCode == HttpStatusCode.Unauthorized ||\n            response.StatusCode == HttpStatusCode.Forbidden) {\n            if (_refreshToken != null) {\n                string token = await _refreshToken(cancellationToken).ConfigureAwait(false);\n                _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n                if (_credential != null) {\n                    _credential.AccessToken = token;\n                }\n            }\n#if NET5_0_OR_GREATER\n            string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new GmailAuthenticationException(response.StatusCode, content);\n        }\n    }\n\n    private string? ResolveAccessToken() {\n        var token = _credential?.AccessToken;\n        if (!string.IsNullOrEmpty(token)) {\n            return token;\n        }\n\n        var authorization = _client.DefaultRequestHeaders.Authorization;\n        if (authorization != null && authorization.Scheme.Equals(\"Bearer\", StringComparison.OrdinalIgnoreCase)) {\n            return authorization.Parameter;\n        }\n\n        return null;\n    }\n\n    private async Task QueuePendingMessageAsync(string userId, MimeMessage message, CancellationToken cancellationToken) {\n        if (PendingMessageRepository == null) {\n            return;\n        }\n\n        var accessToken = ResolveAccessToken();\n        if (string.IsNullOrEmpty(accessToken)) {\n            return;\n        }\n\n        if (string.IsNullOrEmpty(message.MessageId)) {\n            message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId();\n        }\n\n        using var stream = new MemoryStream();\n        await message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord {\n            MessageId = message.MessageId!,\n            MimeMessage = Convert.ToBase64String(stream.ToArray()),\n            Timestamp = now,\n            NextAttemptAt = now,\n            Provider = EmailProvider.Gmail\n        };\n\n        var credential = _credential;\n        var userName = !string.IsNullOrWhiteSpace(credential?.UserName) ? credential!.UserName : userId;\n        var expiresOn = credential?.ExpiresOn ?? DateTimeOffset.MaxValue;\n        record.ProviderData[GmailPendingMessageSender.UserIdKey] = userId;\n        record.ProviderData[GmailPendingMessageSender.UserNameKey] = userName;\n        record.ProviderData[GmailPendingMessageSender.ExpiresOnKey] = expiresOn.ToString(\"o\", CultureInfo.InvariantCulture);\n\n        var protector = CredentialProtection.Default;\n        record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey] = protector.Protect(accessToken!);\n        record.ProviderData.Remove(GmailPendingMessageSender.AccessTokenKey);\n        record.ProviderData.Remove(GmailPendingMessageSender.AccessTokenBase64Key);\n        var refresh = credential?.RefreshToken;\n        if (!string.IsNullOrEmpty(refresh)) {\n            record.ProviderData[GmailPendingMessageSender.RefreshTokenProtectedKey] = protector.Protect(refresh!);\n        }\n        record.ProviderData.Remove(GmailPendingMessageSender.RefreshTokenKey);\n        record.ProviderData.Remove(GmailPendingMessageSender.RefreshTokenBase64Key);\n\n        var clientId = credential?.ClientId;\n        if (!string.IsNullOrEmpty(clientId)) {\n            record.ProviderData[GmailPendingMessageSender.ClientIdKey] = clientId!;\n        }\n\n        var clientSecret = credential?.ClientSecret;\n        if (!string.IsNullOrEmpty(clientSecret)) {\n            record.ProviderData[GmailPendingMessageSender.ClientSecretProtectedKey] = protector.Protect(clientSecret!);\n        }\n\n        var serviceJson = credential?.ServiceAccountJson;\n        if (!string.IsNullOrEmpty(serviceJson)) {\n            record.ProviderData[GmailPendingMessageSender.ServiceAccountJsonProtectedKey] = protector.Protect(serviceJson!);\n        }\n\n        var serviceSubject = credential?.ServiceAccountSubject;\n        if (!string.IsNullOrEmpty(serviceSubject)) {\n            record.ProviderData[GmailPendingMessageSender.ServiceAccountSubjectKey] = serviceSubject!;\n        }\n\n        try {\n            await PendingMessageRepository.SaveAsync(record, cancellationToken).ConfigureAwait(false);\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (Exception ex) {\n            LoggingMessages.Logger.WriteWarning($\"Failed to persist Gmail pending message: {ex.Message}\");\n        }\n    }\n\n    /// <summary>\n    /// Sends the specified MIME message via Gmail API.\n    /// </summary>\n    public async Task<GmailMessage> SendAsync(string userId, MimeMessage message, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (DryRun) {\n            return new GmailMessage { Id = string.Empty, ThreadId = string.Empty };\n        }\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms, cancellationToken).ConfigureAwait(false);\n        var raw = Convert.ToBase64String(ms.ToArray())\n            .Replace('+', '-')\n            .Replace('/', '_')\n            .Replace(\"=\", string.Empty);\n        var json = JsonSerializer.Serialize(new GmailRawRequest(raw), MailozaurrJsonContext.Default.GmailRawRequest);\n        using var content = new StringContent(json, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/messages/send\", content, cancellationToken).ConfigureAwait(false);\n        var queued = false;\n        try {\n            await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n            if (!response.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n                var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n                var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                await QueuePendingMessageAsync(userId, message, cancellationToken).ConfigureAwait(false);\n                queued = true;\n                throw new HttpRequestException($\"Gmail returned {(int)response.StatusCode} ({response.StatusCode}): {error}\");\n            }\n        } catch (GmailAuthenticationException) {\n            if (!queued) {\n                await QueuePendingMessageAsync(userId, message, cancellationToken).ConfigureAwait(false);\n                queued = true;\n            }\n            throw;\n        } catch (HttpRequestException) {\n            if (!queued) {\n                await QueuePendingMessageAsync(userId, message, cancellationToken).ConfigureAwait(false);\n                queued = true;\n            }\n            throw;\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (TaskCanceledException) {\n            if (!queued) {\n                await QueuePendingMessageAsync(userId, message, cancellationToken).ConfigureAwait(false);\n                queued = true;\n            }\n            throw;\n        }\n#if NET5_0_OR_GREATER\n        var resultJson = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var resultJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? result;\n        try {\n            result = JsonSerializer.Deserialize(resultJson, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API send response.\", resultJson, ex);\n        }\n        if (result is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid send response.\");\n        }\n        return result;\n    }\n\n    /// <summary>\n    /// Lists messages matching the supplied query.\n    /// </summary>\n    public Task<IList<GmailMessage>> ListAsync(string userId, string? query = null, int? maxResults = null, CancellationToken cancellationToken = default) =>\n        ListAdvancedAsync(userId, query, labelIds: null, includeSpamTrash: false, maxResultsTotal: maxResults, fields: null, cancellationToken: cancellationToken);\n\n    private static int? ClampMaxResults(int? maxResults) {\n        if (!maxResults.HasValue) {\n            return null;\n        }\n        var v = maxResults.Value;\n        if (v < 1) {\n            return 1;\n        }\n        if (v > 500) {\n            return 500;\n        }\n        return v;\n    }\n\n    /// <summary>\n    /// Lists a single page of messages.\n    /// </summary>\n    public async Task<GmailListResponse> ListPageAsync(\n        string userId,\n        string? query = null,\n        IReadOnlyList<string>? labelIds = null,\n        bool includeSpamTrash = false,\n        int? maxResults = null,\n        string? pageToken = null,\n        string? fields = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n\n        var url = new StringBuilder($\"users/{userId}/messages\");\n        var qs = new List<string>();\n        if (!string.IsNullOrWhiteSpace(query)) qs.Add($\"q={Uri.EscapeDataString(query)}\");\n        var safeMax = ClampMaxResults(maxResults);\n        if (safeMax.HasValue) qs.Add($\"maxResults={safeMax.Value}\");\n        if (!string.IsNullOrWhiteSpace(pageToken)) qs.Add($\"pageToken={Uri.EscapeDataString(pageToken)}\");\n        if (includeSpamTrash) qs.Add(\"includeSpamTrash=true\");\n        if (labelIds != null) {\n            for (var i = 0; i < labelIds.Count; i++) {\n                var lid = labelIds[i];\n                if (!string.IsNullOrWhiteSpace(lid)) {\n                    qs.Add($\"labelIds={Uri.EscapeDataString(lid.Trim())}\");\n                }\n            }\n        }\n        if (!string.IsNullOrWhiteSpace(fields)) qs.Add($\"fields={Uri.EscapeDataString(fields)}\");\n        if (qs.Count > 0) {\n            url.Append('?').Append(string.Join(\"&\", qs));\n        }\n\n        using var response = await _client.GetAsync(url.ToString(), cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailListResponse? list;\n        try {\n            list = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailListResponse);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API list response.\", json, ex);\n        }\n        if (list is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid list response.\");\n        }\n        return list;\n    }\n\n    /// <summary>\n    /// Lists messages matching the supplied query.\n    /// </summary>\n    public async Task<IList<GmailMessage>> ListAdvancedAsync(\n        string userId,\n        string? query = null,\n        IReadOnlyList<string>? labelIds = null,\n        bool includeSpamTrash = false,\n        int? maxResultsTotal = null,\n        string? fields = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        var messages = new List<GmailMessage>();\n        string? pageToken = null;\n        int? remaining = maxResultsTotal;\n        while (true) {\n            var list = await ListPageAsync(\n                userId,\n                query: query,\n                labelIds: labelIds,\n                includeSpamTrash: includeSpamTrash,\n                maxResults: remaining,\n                pageToken: pageToken,\n                fields: fields,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            if (list.Messages != null) {\n                messages.AddRange(list.Messages);\n            }\n            if (maxResultsTotal.HasValue && messages.Count >= maxResultsTotal.Value) {\n                break;\n            }\n            pageToken = list.NextPageToken;\n            if (string.IsNullOrEmpty(pageToken)) {\n                break;\n            }\n            if (maxResultsTotal.HasValue) {\n                remaining = maxResultsTotal.Value - messages.Count;\n            }\n        }\n\n        if (maxResultsTotal.HasValue && messages.Count > maxResultsTotal.Value) {\n            messages = messages.GetRange(0, maxResultsTotal.Value);\n        }\n        return messages;\n    }\n\n    /// <summary>\n    /// Retrieves a single message by id.\n    /// </summary>\n    public async Task<GmailMessage> GetAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/messages/{id}\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? message;\n        try {\n            message = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API message response.\", json, ex);\n        }\n        if (message is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid message response.\");\n        }\n        return message;\n    }\n\n    /// <summary>\n    /// Retrieves a single message by id, optionally requesting a specific format/fields subset and metadata headers.\n    /// </summary>\n    /// <remarks>\n    /// This is a low-level helper for callers that need Gmail partial responses or <c>format=metadata</c>.\n    /// Prefer <see cref=\"GetFullAsync\"/> and <see cref=\"GetRawAsync\"/> when possible.\n    /// </remarks>\n    public Task<GmailMessage> GetMessageWithOptionsAsync(\n        string userId,\n        string id,\n        string? format = null,\n        IReadOnlyCollection<string>? metadataHeaders = null,\n        string? fields = null,\n        CancellationToken cancellationToken = default)\n        => GetMessageWithOptionsCoreAsync(userId, id, format, metadataHeaders, fields, cancellationToken);\n\n    private async Task<GmailMessage> GetMessageWithOptionsCoreAsync(\n        string userId,\n        string id,\n        string? format,\n        IReadOnlyCollection<string>? metadataHeaders,\n        string? fields,\n        CancellationToken cancellationToken) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(id)) {\n            throw new ArgumentException(\"id is required.\", nameof(id));\n        }\n\n        var safeId = Uri.EscapeDataString(id.Trim());\n        var url = new StringBuilder($\"users/{userId}/messages/{safeId}\");\n        var qs = new List<string>();\n        if (!string.IsNullOrWhiteSpace(format)) qs.Add($\"format={Uri.EscapeDataString(format!.Trim())}\");\n        if (!string.IsNullOrWhiteSpace(fields)) qs.Add($\"fields={Uri.EscapeDataString(fields!.Trim())}\");\n        if (metadataHeaders != null) {\n            foreach (var h in metadataHeaders) {\n                if (!string.IsNullOrWhiteSpace(h)) {\n                    qs.Add($\"metadataHeaders={Uri.EscapeDataString(h.Trim())}\");\n                }\n            }\n        }\n        if (qs.Count > 0) {\n            url.Append('?').Append(string.Join(\"&\", qs));\n        }\n\n        using var response = await _client.GetAsync(url.ToString(), cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? message;\n        try {\n            message = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API message response.\", json, ex);\n        }\n        if (message is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid message response.\");\n        }\n        return message;\n    }\n\n    private Task<GmailMessage> GetMessageWithFormatAsync(\n        string userId,\n        string id,\n        string format,\n        string? fields,\n        CancellationToken cancellationToken)\n        => GetMessageWithOptionsCoreAsync(userId, id, format, metadataHeaders: null, fields: fields, cancellationToken);\n\n    /// <summary>\n    /// Retrieves a MIME message by id.\n    /// </summary>\n    public async Task<MimeMessage> GetMimeMessageAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        var msg = await GetMessageWithFormatAsync(userId, id, format: \"raw\", fields: null, cancellationToken: cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrEmpty(msg.Raw)) {\n            throw new InvalidDataException(\"Gmail API returned an invalid message response.\");\n        }\n        var raw = msg.Raw!;\n        var data = raw.Replace('-', '+').Replace('_', '/');\n        int padding = (4 - data.Length % 4) % 4;\n        if (padding > 0) data = data.PadRight(data.Length + padding, '=');\n        var bytes = Convert.FromBase64String(data);\n        using var ms = new MemoryStream(bytes);\n        return MimeMessage.Load(ms);\n    }\n\n    /// <summary>\n    /// Retrieves a raw message (base64url MIME) by id.\n    /// </summary>\n    public Task<GmailMessage> GetRawAsync(string userId, string id, string? fields = null, CancellationToken cancellationToken = default) =>\n        GetMessageWithFormatAsync(userId, id, format: \"raw\", fields: fields, cancellationToken: cancellationToken);\n\n    /// <summary>\n    /// Retrieves a full message by id.\n    /// </summary>\n    public Task<GmailMessage> GetFullAsync(string userId, string id, string? fields = null, CancellationToken cancellationToken = default) =>\n        GetMessageWithFormatAsync(userId, id, format: \"full\", fields: fields, cancellationToken: cancellationToken);\n\n    /// <summary>\n    /// Imports a message (MIME base64url) into a mailbox.\n    /// </summary>\n    /// <remarks>\n    /// This calls <c>users.messages.import</c>. It is typically used to append a sent copy into Gmail.\n    /// </remarks>\n    public async Task<GmailMessage> ImportAsync(\n        string userId,\n        string raw,\n        IReadOnlyCollection<string>? labelIds = null,\n        string internalDateSource = \"dateHeader\",\n        bool neverMarkSpam = true,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(raw)) {\n            throw new ArgumentException(\"raw is required.\", nameof(raw));\n        }\n        if (DryRun) {\n            return new GmailMessage { Id = string.Empty, ThreadId = string.Empty };\n        }\n\n        var url = new StringBuilder($\"users/{userId}/messages/import\");\n        var qs = new List<string>();\n        if (!string.IsNullOrWhiteSpace(internalDateSource)) qs.Add($\"internalDateSource={Uri.EscapeDataString(internalDateSource.Trim())}\");\n        qs.Add($\"neverMarkSpam={(neverMarkSpam ? \"true\" : \"false\")}\");\n        if (qs.Count > 0) {\n            url.Append('?').Append(string.Join(\"&\", qs));\n        }\n\n        var request = new GmailImportMessageRequest {\n            Raw = raw.Trim(),\n            LabelIds = labelIds is null || labelIds.Count == 0 ? null : new List<string>(labelIds)\n        };\n        var jsonRequest = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailImportMessageRequest);\n        using var content = new StringContent(jsonRequest, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync(url.ToString(), content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? message;\n        try {\n            message = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API import response.\", json, ex);\n        }\n        if (message is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid import response.\");\n        }\n        return message;\n    }\n\n    /// <summary>\n    /// Deletes a message by id.\n    /// </summary>\n    public async Task DeleteAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return;\n        }\n        using var response = await _client.DeleteAsync($\"users/{userId}/messages/{id}\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n    }\n\n    /// <summary>\n    /// Moves a message to trash.\n    /// </summary>\n    public async Task<GmailMessage> TrashMessageAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return new GmailMessage { Id = id, ThreadId = string.Empty };\n        }\n        using var response = await _client.PostAsync($\"users/{userId}/messages/{id}/trash\", content: null, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? message;\n        try {\n            message = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API trash response.\", json, ex);\n        }\n        if (message is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid trash response.\");\n        }\n        return message;\n    }\n\n    /// <summary>\n    /// Modifies labels on a single message.\n    /// </summary>\n    public async Task<GmailMessage> ModifyMessageLabelsAsync(\n        string userId,\n        string id,\n        IReadOnlyCollection<string>? addLabelIds = null,\n        IReadOnlyCollection<string>? removeLabelIds = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return new GmailMessage { Id = id, ThreadId = string.Empty };\n        }\n        var request = new GmailModifyLabelsRequest {\n            AddLabelIds = addLabelIds ?? Array.Empty<string>(),\n            RemoveLabelIds = removeLabelIds ?? Array.Empty<string>()\n        };\n        var jsonRequest = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailModifyLabelsRequest);\n        using var content = new StringContent(jsonRequest, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/messages/{id}/modify\", content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailMessage? message;\n        try {\n            message = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API message modify response.\", json, ex);\n        }\n        if (message is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid message modify response.\");\n        }\n        return message;\n    }\n\n    /// <summary>\n    /// Modifies labels on multiple messages.\n    /// </summary>\n    public async Task BatchModifyMessagesAsync(\n        string userId,\n        IReadOnlyCollection<string> ids,\n        IReadOnlyCollection<string>? addLabelIds = null,\n        IReadOnlyCollection<string>? removeLabelIds = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return;\n        }\n        if (ids == null) {\n            throw new ArgumentNullException(nameof(ids));\n        }\n        if (ids.Count == 0) {\n            return;\n        }\n        var request = new GmailBatchModifyRequest {\n            Ids = ids,\n            AddLabelIds = addLabelIds ?? Array.Empty<string>(),\n            RemoveLabelIds = removeLabelIds ?? Array.Empty<string>()\n        };\n        var jsonRequest = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailBatchModifyRequest);\n        using var content = new StringContent(jsonRequest, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/messages/batchModify\", content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n    }\n\n    /// <summary>\n    /// Deletes multiple messages.\n    /// </summary>\n    public async Task BatchDeleteMessagesAsync(string userId, IReadOnlyCollection<string> ids, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return;\n        }\n        if (ids == null) {\n            throw new ArgumentNullException(nameof(ids));\n        }\n        if (ids.Count == 0) {\n            return;\n        }\n        var request = new GmailBatchDeleteRequest { Ids = ids };\n        var jsonRequest = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailBatchDeleteRequest);\n        using var content = new StringContent(jsonRequest, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/messages/batchDelete\", content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n    }\n\n    /// <summary>\n    /// Lists labels for the specified user.\n    /// </summary>\n    public async Task<IList<GmailLabel>> ListLabelsAsync(string userId, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/labels?fields=labels(id,name,type)\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailLabelListResponse? list;\n        try {\n            list = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailLabelListResponse);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API labels response.\", json, ex);\n        }\n        return (IList<GmailLabel>)(list?.Labels ?? new List<GmailLabel>());\n    }\n\n    /// <summary>\n    /// Modifies labels on a thread.\n    /// </summary>\n    public async Task<GmailThread> ModifyThreadLabelsAsync(\n        string userId,\n        string id,\n        IReadOnlyCollection<string>? addLabelIds = null,\n        IReadOnlyCollection<string>? removeLabelIds = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return new GmailThread { Id = id, Messages = new List<GmailMessage>() };\n        }\n        var request = new GmailModifyLabelsRequest {\n            AddLabelIds = addLabelIds ?? Array.Empty<string>(),\n            RemoveLabelIds = removeLabelIds ?? Array.Empty<string>()\n        };\n        var jsonRequest = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailModifyLabelsRequest);\n        using var content = new StringContent(jsonRequest, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/threads/{id}/modify\", content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailThread? thread;\n        try {\n            thread = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailThread);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API thread modify response.\", json, ex);\n        }\n        if (thread is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid thread modify response.\");\n        }\n        return thread;\n    }\n\n    /// <summary>\n    /// Moves a thread to trash.\n    /// </summary>\n    public async Task<GmailThread> TrashThreadAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return new GmailThread { Id = id, Messages = new List<GmailMessage>() };\n        }\n        using var response = await _client.PostAsync($\"users/{userId}/threads/{id}/trash\", content: null, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailThread? thread;\n        try {\n            thread = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailThread);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API thread trash response.\", json, ex);\n        }\n        if (thread is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid thread trash response.\");\n        }\n        return thread;\n    }\n\n    /// <summary>\n    /// Deletes a thread by id.\n    /// </summary>\n    public async Task DeleteThreadAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return;\n        }\n        using var response = await _client.DeleteAsync($\"users/{userId}/threads/{id}\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n    }\n\n    /// <summary>\n    /// Gets the Gmail profile for the specified user.\n    /// </summary>\n    public async Task<GmailProfile> GetProfileAsync(string userId, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/profile\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailProfile? profile;\n        try {\n            profile = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailProfile);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API profile response.\", json, ex);\n        }\n        if (profile is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid profile response.\");\n        }\n        return profile;\n    }\n\n    /// <summary>\n    /// Starts a Gmail push notification watch for the specified topic.\n    /// </summary>\n    /// <remarks>\n    /// This calls <c>users.watch</c> and returns the watch response (historyId + expiration).\n    /// </remarks>\n    public async Task<GmailWatchResponse> WatchAsync(\n        string userId,\n        string topicName,\n        IReadOnlyList<string>? labelIds = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(topicName)) {\n            throw new ArgumentException(\"topicName is required.\", nameof(topicName));\n        }\n        if (DryRun) {\n            return new GmailWatchResponse { HistoryId = string.Empty, Expiration = 0 };\n        }\n\n        var request = new GmailWatchRequest { TopicName = topicName };\n        if (labelIds != null && labelIds.Count > 0) {\n            request.LabelIds = new List<string>(labelIds);\n            request.LabelFilterAction = \"include\";\n        }\n\n        var body = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GmailWatchRequest);\n        using var content = new StringContent(body, Encoding.UTF8, \"application/json\");\n        using var response = await _client.PostAsync($\"users/{userId}/watch\", content, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailWatchResponse? watch;\n        try {\n            watch = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailWatchResponse);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API watch response.\", json, ex);\n        }\n        if (watch is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid watch response.\");\n        }\n        return watch;\n    }\n\n    /// <summary>\n    /// Stops Gmail push notification watches for the specified user.\n    /// </summary>\n    /// <remarks>\n    /// This calls <c>users.stop</c>.\n    /// </remarks>\n    public async Task StopWatchAsync(string userId, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            return;\n        }\n        using var response = await _client.PostAsync($\"users/{userId}/stop\", new StringContent(\"{}\", Encoding.UTF8, \"application/json\"), cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        if (!response.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n            var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new GmailApiException(\n                response.StatusCode,\n                $\"Gmail users.stop failed ({(int)response.StatusCode}).\",\n                content);\n        }\n    }\n\n    /// <summary>\n    /// Lists Gmail history changes for the specified user.\n    /// </summary>\n    public async Task<GmailHistoryListResponse> ListHistoryAsync(\n        string userId,\n        string startHistoryId,\n        string? labelId = null,\n        IReadOnlyList<string>? historyTypes = null,\n        int? maxResults = null,\n        string? pageToken = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(startHistoryId)) {\n            throw new ArgumentException(\"startHistoryId is required.\", nameof(startHistoryId));\n        }\n\n        var url = new StringBuilder($\"users/{userId}/history\");\n        var qs = new List<string> {\n            $\"startHistoryId={Uri.EscapeDataString(startHistoryId)}\"\n        };\n        if (!string.IsNullOrWhiteSpace(labelId)) qs.Add($\"labelId={Uri.EscapeDataString(labelId)}\");\n        if (maxResults.HasValue) qs.Add($\"maxResults={maxResults.Value}\");\n        if (!string.IsNullOrWhiteSpace(pageToken)) qs.Add($\"pageToken={Uri.EscapeDataString(pageToken)}\");\n        if (historyTypes != null) {\n            for (var i = 0; i < historyTypes.Count; i++) {\n                var t = historyTypes[i];\n                if (!string.IsNullOrWhiteSpace(t)) {\n                    qs.Add($\"historyTypes={Uri.EscapeDataString(t)}\");\n                }\n            }\n        }\n        if (qs.Count > 0) {\n            url.Append('?').Append(string.Join(\"&\", qs));\n        }\n\n        using var response = await _client.GetAsync(url.ToString(), cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailHistoryListResponse? history;\n        try {\n            history = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailHistoryListResponse);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API history response.\", json, ex);\n        }\n        if (history is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid history response.\");\n        }\n        return history;\n    }\n\n    /// <summary>\n    /// Lists threads matching the supplied query.\n    /// </summary>\n    public async Task<IList<GmailThreadInfo>> ListThreadsAsync(string userId, string? query = null, int? maxResults = null, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        var threads = new List<GmailThreadInfo>();\n        string? pageToken = null;\n        do {\n            var url = new StringBuilder($\"users/{userId}/threads\");\n            var qs = new List<string>();\n            if (!string.IsNullOrWhiteSpace(query)) qs.Add($\"q={Uri.EscapeDataString(query)}\");\n            if (maxResults.HasValue) qs.Add($\"maxResults={maxResults.Value}\");\n            if (!string.IsNullOrEmpty(pageToken)) qs.Add($\"pageToken={pageToken}\");\n            if (qs.Count > 0) {\n                url.Append('?').Append(string.Join(\"&\", qs));\n            }\n            using var response = await _client.GetAsync(url.ToString(), cancellationToken).ConfigureAwait(false);\n            await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n            response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n            var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            GmailThreadListResponse? list;\n            try {\n                list = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailThreadListResponse);\n            } catch (JsonException ex) {\n                throw new GmailApiException(\"Failed to parse Gmail API thread list response.\", json, ex);\n            }\n            if (list?.Threads != null) {\n                threads.AddRange(list.Threads);\n            }\n            pageToken = list?.NextPageToken;\n        } while (!string.IsNullOrEmpty(pageToken));\n\n        return threads;\n    }\n\n    /// <summary>\n    /// Retrieves a single thread by id.\n    /// </summary>\n    public async Task<GmailThread> GetThreadAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/threads/{id}\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailThread? thread;\n        try {\n            thread = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailThread);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API thread response.\", json, ex);\n        }\n        if (thread is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid thread response.\");\n        }\n        return thread;\n    }\n\n    /// <summary>\n    /// Retrieves a single thread by id, optionally requesting a partial response.\n    /// </summary>\n    public async Task<GmailThread> GetThreadWithOptionsAsync(\n        string userId,\n        string id,\n        string? format = null,\n        string? fields = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(id)) {\n            throw new ArgumentException(\"id is required.\", nameof(id));\n        }\n\n        var safeId = Uri.EscapeDataString(id.Trim());\n        var url = new StringBuilder($\"users/{userId}/threads/{safeId}\");\n        var qs = new List<string>();\n        if (!string.IsNullOrWhiteSpace(format)) qs.Add($\"format={Uri.EscapeDataString(format!.Trim())}\");\n        if (!string.IsNullOrWhiteSpace(fields)) qs.Add($\"fields={Uri.EscapeDataString(fields!.Trim())}\");\n        if (qs.Count > 0) {\n            url.Append('?').Append(string.Join(\"&\", qs));\n        }\n\n        using var response = await _client.GetAsync(url.ToString(), cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        GmailThread? thread;\n        try {\n            thread = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailThread);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API thread response.\", json, ex);\n        }\n        if (thread is null) {\n            throw new InvalidDataException(\"Gmail API returned an invalid thread response.\");\n        }\n        return thread;\n    }\n\n    /// <summary>\n    /// Lists attachment metadata for a message.\n    /// </summary>\n    public async Task<IList<GmailAttachmentInfo>> ListAttachmentsAsync(string userId, string id, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/messages/{id}?format=full\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);\n#else\n        using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);\n#endif\n        using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);\n        var list = new List<GmailAttachmentInfo>();\n        if (doc.RootElement.TryGetProperty(\"payload\", out var payload)) {\n            ExtractAttachments(payload, list);\n        }\n        return list;\n    }\n\n    /// <summary>\n    /// Downloads a single attachment by id.\n    /// </summary>\n    public async Task<byte[]> DownloadAttachmentAsync(string userId, string messageId, string attachmentId, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var response = await _client.GetAsync($\"users/{userId}/messages/{messageId}/attachments/{attachmentId}\", cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n        response.EnsureSuccessStatusCode();\n#if NET5_0_OR_GREATER\n        var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        AttachmentResponse? result;\n        try {\n            result = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.AttachmentResponse);\n        } catch (JsonException ex) {\n            throw new GmailApiException(\"Failed to parse Gmail API attachment response.\", json, ex);\n        }\n        if (string.IsNullOrEmpty(result?.Data)) {\n            return Array.Empty<byte>();\n        }\n\n        var dataProp = result!.Data!;\n        var data = dataProp.Replace('-', '+').Replace('_', '/');\n\n        if (data.Length % 4 == 1) {\n            throw new InvalidDataException(\"Attachment data is not a valid Base64 string.\");\n        }\n\n        int padding = (4 - data.Length % 4) % 4;\n        if (padding > 0) {\n            data = data.PadRight(data.Length + padding, '=');\n        }\n\n        try {\n            return Convert.FromBase64String(data);\n        } catch (FormatException ex) {\n            throw new InvalidDataException(\"Attachment data is not a valid Base64 string.\", ex);\n        }\n    }\n\n    private static void ExtractAttachments(JsonElement part, List<GmailAttachmentInfo> list) {\n        string? fileName = part.GetProperty(\"filename\").GetString();\n        string? mime = part.GetProperty(\"mimeType\").GetString();\n        if (!string.IsNullOrEmpty(fileName) &&\n            part.TryGetProperty(\"body\", out var body) &&\n            body.TryGetProperty(\"attachmentId\", out var idProp)) {\n            list.Add(new GmailAttachmentInfo { Id = idProp.GetString(), FileName = fileName, MimeType = mime });\n        }\n        if (part.TryGetProperty(\"parts\", out var parts)) {\n            foreach (var p in parts.EnumerateArray()) {\n                ExtractAttachments(p, list);\n            }\n        }\n    }\n\n    /// <summary>Attachment metadata returned by Gmail API.</summary>\n    public sealed class AttachmentResponse {\n        /// <summary>Base64 encoded attachment data.</summary>\n        public string? Data { get; set; }\n    }\n\n    /// <summary>Response envelope for Gmail list messages API.</summary>\n    public sealed class GmailListResponse {\n        /// <summary>Messages returned by the API.</summary>\n        public List<GmailMessage>? Messages { get; set; }\n        /// <summary>Token for the next page of results.</summary>\n        public string? NextPageToken { get; set; }\n        /// <summary>Estimated total number of results.</summary>\n        [JsonPropertyName(\"resultSizeEstimate\")]\n        [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]\n        public long? ResultSizeEstimate { get; set; }\n    }\n\n    /// <summary>Response envelope for Gmail thread listing.</summary>\n    public sealed class GmailThreadListResponse {\n        /// <summary>Threads returned by the API.</summary>\n        public List<GmailThreadInfo>? Threads { get; set; }\n        /// <summary>Token for the next page of results.</summary>\n        public string? NextPageToken { get; set; }\n    }\n\n    /// <summary>Request payload for Gmail watch API.</summary>\n    public sealed class GmailWatchRequest {\n        /// <summary>Pub/Sub topic name to deliver notifications to.</summary>\n        [JsonPropertyName(\"topicName\")]\n        public string TopicName { get; set; } = string.Empty;\n        /// <summary>Optional label filters.</summary>\n        [JsonPropertyName(\"labelIds\")]\n        public List<string>? LabelIds { get; set; }\n        /// <summary>Action to apply to the label filter (usually <c>include</c>).</summary>\n        [JsonPropertyName(\"labelFilterAction\")]\n        public string? LabelFilterAction { get; set; }\n    }\n\n    /// <summary>Response payload for Gmail watch API.</summary>\n    public sealed class GmailWatchResponse {\n        /// <summary>History id at the start of the watch.</summary>\n        [JsonPropertyName(\"historyId\")]\n        public string? HistoryId { get; set; }\n        /// <summary>Watch expiration as milliseconds since epoch.</summary>\n        [JsonPropertyName(\"expiration\")]\n        [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]\n        public long Expiration { get; set; }\n    }\n\n    /// <summary>Gmail profile response.</summary>\n    public sealed class GmailProfile {\n        /// <summary>Email address associated with the mailbox.</summary>\n        [JsonPropertyName(\"emailAddress\")]\n        public string? EmailAddress { get; set; }\n        /// <summary>Total number of messages.</summary>\n        [JsonPropertyName(\"messagesTotal\")]\n        public long MessagesTotal { get; set; }\n        /// <summary>Total number of threads.</summary>\n        [JsonPropertyName(\"threadsTotal\")]\n        public long ThreadsTotal { get; set; }\n        /// <summary>Current history id.</summary>\n        [JsonPropertyName(\"historyId\")]\n        public string? HistoryId { get; set; }\n    }\n\n    /// <summary>Gmail history list response.</summary>\n    public sealed class GmailHistoryListResponse {\n        /// <summary>History records.</summary>\n        [JsonPropertyName(\"history\")]\n        public List<GmailHistoryRecord>? History { get; set; }\n        /// <summary>Token for the next page of results.</summary>\n        [JsonPropertyName(\"nextPageToken\")]\n        public string? NextPageToken { get; set; }\n        /// <summary>Latest history id.</summary>\n        [JsonPropertyName(\"historyId\")]\n        public string? HistoryId { get; set; }\n    }\n\n    /// <summary>History record returned by Gmail history API.</summary>\n    public sealed class GmailHistoryRecord {\n        /// <summary>History record id.</summary>\n        public string? Id { get; set; }\n        /// <summary>Messages added in this history record.</summary>\n        public List<GmailHistoryMessageAdded>? MessagesAdded { get; set; }\n        /// <summary>Messages deleted in this history record.</summary>\n        public List<GmailHistoryMessageDeleted>? MessagesDeleted { get; set; }\n        /// <summary>Labels added in this history record.</summary>\n        public List<GmailHistoryLabelAdded>? LabelsAdded { get; set; }\n        /// <summary>Labels removed in this history record.</summary>\n        public List<GmailHistoryLabelRemoved>? LabelsRemoved { get; set; }\n    }\n\n    /// <summary>History wrapper for a message added event.</summary>\n    public sealed class GmailHistoryMessageAdded {\n        /// <summary>Message reference associated with the event.</summary>\n        public GmailHistoryMessageRef? Message { get; set; }\n    }\n\n    /// <summary>History wrapper for a message deleted event.</summary>\n    public sealed class GmailHistoryMessageDeleted {\n        /// <summary>Message reference associated with the event.</summary>\n        public GmailHistoryMessageRef? Message { get; set; }\n    }\n\n    /// <summary>History wrapper for a label added event.</summary>\n    public sealed class GmailHistoryLabelAdded {\n        /// <summary>Message reference associated with the event.</summary>\n        public GmailHistoryMessageRef? Message { get; set; }\n        /// <summary>Label ids associated with the event.</summary>\n        public List<string>? LabelIds { get; set; }\n    }\n\n    /// <summary>History wrapper for a label removed event.</summary>\n    public sealed class GmailHistoryLabelRemoved {\n        /// <summary>Message reference associated with the event.</summary>\n        public GmailHistoryMessageRef? Message { get; set; }\n        /// <summary>Label ids associated with the event.</summary>\n        public List<string>? LabelIds { get; set; }\n    }\n\n    /// <summary>Reference to a Gmail message returned by the history API.</summary>\n    public sealed class GmailHistoryMessageRef {\n        /// <summary>Message id.</summary>\n        public string? Id { get; set; }\n        /// <summary>Thread id.</summary>\n        public string? ThreadId { get; set; }\n    }\n\n    /// <summary>Response envelope for Gmail list labels API.</summary>\n    public sealed class GmailLabelListResponse {\n        /// <summary>Labels returned by the API.</summary>\n        public List<GmailLabel>? Labels { get; set; }\n    }\n\n    /// <summary>Request payload for Gmail modify label endpoints.</summary>\n    public sealed class GmailModifyLabelsRequest {\n        /// <summary>Label ids to add.</summary>\n        [JsonPropertyName(\"addLabelIds\")]\n        public IReadOnlyCollection<string> AddLabelIds { get; set; } = Array.Empty<string>();\n\n        /// <summary>Label ids to remove.</summary>\n        [JsonPropertyName(\"removeLabelIds\")]\n        public IReadOnlyCollection<string> RemoveLabelIds { get; set; } = Array.Empty<string>();\n    }\n\n    /// <summary>Request payload for Gmail batch modify messages endpoint.</summary>\n    public sealed class GmailBatchModifyRequest {\n        /// <summary>Message ids.</summary>\n        [JsonPropertyName(\"ids\")]\n        public IReadOnlyCollection<string> Ids { get; set; } = Array.Empty<string>();\n\n        /// <summary>Label ids to add.</summary>\n        [JsonPropertyName(\"addLabelIds\")]\n        public IReadOnlyCollection<string> AddLabelIds { get; set; } = Array.Empty<string>();\n\n        /// <summary>Label ids to remove.</summary>\n        [JsonPropertyName(\"removeLabelIds\")]\n        public IReadOnlyCollection<string> RemoveLabelIds { get; set; } = Array.Empty<string>();\n    }\n\n    /// <summary>Request payload for Gmail batch delete messages endpoint.</summary>\n    public sealed class GmailBatchDeleteRequest {\n        /// <summary>Message ids.</summary>\n        [JsonPropertyName(\"ids\")]\n        public IReadOnlyCollection<string> Ids { get; set; } = Array.Empty<string>();\n    }\n\n    /// <summary>Request payload for Gmail import message endpoint.</summary>\n    public sealed class GmailImportMessageRequest {\n        /// <summary>Raw message content as base64url.</summary>\n        [JsonPropertyName(\"raw\")]\n        public string Raw { get; set; } = string.Empty;\n\n        /// <summary>Optional label ids to apply to the imported message.</summary>\n        [JsonPropertyName(\"labelIds\")]\n        public List<string>? LabelIds { get; set; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailApiException.cs",
    "content": "using System;\nusing System.Net;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Exception thrown when Gmail API returns an unexpected response.\n/// </summary>\npublic class GmailApiException : Exception {\n    /// <summary>\n    /// HTTP status code returned by the Gmail API when available.\n    /// </summary>\n    public HttpStatusCode? StatusCode { get; }\n\n    /// <summary>\n    /// Raw response content returned by the Gmail API.\n    /// </summary>\n    public string ResponseContent { get; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailApiException\"/> class.\n    /// </summary>\n    /// <param name=\"message\">The error message.</param>\n    /// <param name=\"responseContent\">Raw response content from the server.</param>\n    /// <param name=\"innerException\">The original exception.</param>\n    public GmailApiException(string message, string responseContent, Exception innerException)\n        : base(message, innerException) {\n        StatusCode = null;\n        ResponseContent = responseContent;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailApiException\"/> class.\n    /// </summary>\n    /// <param name=\"statusCode\">The HTTP status code.</param>\n    /// <param name=\"message\">The error message.</param>\n    /// <param name=\"responseContent\">Raw response content from the server.</param>\n    public GmailApiException(HttpStatusCode statusCode, string message, string responseContent)\n        : base(message) {\n        StatusCode = statusCode;\n        ResponseContent = responseContent;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailAttachmentInfo.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Metadata about a Gmail message attachment.\n/// </summary>\npublic sealed class GmailAttachmentInfo {\n    /// <summary>Attachment identifier.</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Attachment file name.</summary>\n    public string? FileName { get; set; }\n\n    /// <summary>MIME type of the attachment.</summary>\n    public string? MimeType { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailAuthenticationException.cs",
    "content": "using System;\nusing System.Net;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Exception thrown when Gmail API authentication fails.\n/// </summary>\npublic class GmailAuthenticationException : Exception\n{\n    /// <summary>\n    /// HTTP status code returned by the Gmail API.\n    /// </summary>\n    public HttpStatusCode StatusCode { get; }\n\n    /// <summary>\n    /// Raw response content returned by the Gmail API.\n    /// </summary>\n    public string ResponseContent { get; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailAuthenticationException\"/> class.\n    /// </summary>\n    /// <param name=\"statusCode\">The HTTP status code.</param>\n    /// <param name=\"responseContent\">Raw response content from the server.</param>\n    public GmailAuthenticationException(HttpStatusCode statusCode, string responseContent)\n        : base($\"Gmail API authentication failed with status {(int)statusCode} ({statusCode}).\")\n    {\n        StatusCode = statusCode;\n        ResponseContent = responseContent;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailLabel.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>Gmail label returned by the Gmail API.</summary>\npublic sealed class GmailLabel {\n    /// <summary>Label id (for system labels often equals the name).</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Label name.</summary>\n    public string? Name { get; set; }\n\n    /// <summary>Label type (for example \"system\" or \"user\").</summary>\n    public string? Type { get; set; }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailMailboxBrowser.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// High-level delegated-token mailbox browsing helpers built on top of <see cref=\"GmailApiClient\"/>.\n/// </summary>\npublic sealed class GmailMailboxBrowser {\n    private const int BatchMaxIds = 1000;\n    private const string ListFields = \"messages(id,threadId),nextPageToken,resultSizeEstimate\";\n    private const string MessageSummaryFields = \"id,threadId,internalDate,labelIds,payload(headers,name,value,parts,filename,body/attachmentId,body/size,mimeType)\";\n    private const string ThreadFields = \"id,messages(id,threadId,internalDate,labelIds,payload(headers,name,value,parts,filename,body/attachmentId,body/size,mimeType))\";\n    private const string RawFields = \"id,threadId,internalDate,labelIds,raw\";\n    private readonly GmailApiClient _gmail;\n    private readonly string _userId;\n\n    /// <summary>\n    /// Maximum MIME payload size used by <see cref=\"GetMessageContentAsync\"/> when no explicit limit is provided.\n    /// </summary>\n    public const int DefaultMaxMimeBytes = 25 * 1024 * 1024;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailMailboxBrowser\"/> class.\n    /// </summary>\n    /// <param name=\"gmail\">Gmail API client.</param>\n    /// <param name=\"userId\">Mailbox user id (use <c>me</c> for delegated tokens).</param>\n    public GmailMailboxBrowser(GmailApiClient gmail, string userId = \"me\") {\n        _gmail = gmail ?? throw new ArgumentNullException(nameof(gmail));\n        _userId = string.IsNullOrWhiteSpace(userId) ? \"me\" : userId.Trim();\n    }\n\n    /// <summary>\n    /// Resolves a folder/label selector to a Gmail label id.\n    /// </summary>\n    public async Task<string?> ResolveLabelIdAsync(\n        string? folder,\n        CancellationToken cancellationToken = default) {\n        var raw = (folder ?? string.Empty).Trim();\n        if (raw.Length == 0) {\n            return \"INBOX\";\n        }\n\n        if (TryResolveSystemLabel(raw, out var systemLabelId)) {\n            return systemLabelId;\n        }\n\n        var labels = await _gmail.ListLabelsAsync(_userId, cancellationToken).ConfigureAwait(false);\n        foreach (var label in labels) {\n            if (label == null) {\n                continue;\n            }\n\n            var id = NormalizeOptional(label.Id);\n            var name = NormalizeOptional(label.Name);\n            if (id == null || name == null) {\n                continue;\n            }\n            if (id.Equals(raw, StringComparison.OrdinalIgnoreCase) || name.Equals(raw, StringComparison.OrdinalIgnoreCase)) {\n                return id;\n            }\n        }\n\n        return null;\n    }\n\n    /// <summary>\n    /// Resolves a folder selector for Gmail watch requests.\n    /// </summary>\n    public async Task<string?> ResolveWatchLabelIdAsync(\n        string folder,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(folder)) {\n            return null;\n        }\n\n        if (TryResolveWatchSystemLabel(folder, out var watchLabelId)) {\n            return watchLabelId;\n        }\n\n        return await ResolveLabelIdAsync(folder, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Lists available Gmail labels as mailbox folders.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxFolderSummary>> ListFoldersAsync(\n        CancellationToken cancellationToken = default) {\n        var labels = await _gmail.ListLabelsAsync(_userId, cancellationToken).ConfigureAwait(false);\n        if (labels == null || labels.Count == 0) {\n            return Array.Empty<GmailMailboxFolderSummary>();\n        }\n\n        var output = new List<GmailMailboxFolderSummary>(labels.Count);\n        foreach (var label in labels) {\n            if (label == null) {\n                continue;\n            }\n\n            var id = NormalizeOptional(label.Id);\n            var name = NormalizeOptional(label.Name);\n            if (id == null || name == null) {\n                continue;\n            }\n\n            output.Add(new GmailMailboxFolderSummary {\n                Id = id,\n                Name = name,\n                Type = NormalizeOptional(label.Type)\n            });\n        }\n\n        output.Sort(static (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));\n        return output;\n    }\n\n    /// <summary>\n    /// Lists messages in a Gmail label.\n    /// </summary>\n    public async Task<GmailMailboxListResult> ListMessagesAsync(\n        string? folder,\n        int limit,\n        int offset,\n        CancellationToken cancellationToken = default) {\n        var safeLimit = ClampInt(limit, 1, 2000);\n        var skipRemaining = Math.Max(0, offset);\n\n        var resolvedLabelId = NormalizeOptional(await ResolveLabelIdAsync(folder, cancellationToken).ConfigureAwait(false));\n        if (resolvedLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail folder/label.\");\n        }\n\n        string? pageToken = null;\n        long totalEstimate = 0;\n        var seenIds = new HashSet<string>(StringComparer.Ordinal);\n        var selectedIds = new List<string>();\n\n        while (selectedIds.Count < safeLimit) {\n            var page = await _gmail.ListPageAsync(\n                _userId,\n                query: null,\n                labelIds: new[] { resolvedLabelId },\n                includeSpamTrash: false,\n                maxResults: 100,\n                pageToken: pageToken,\n                fields: ListFields,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            if (page.ResultSizeEstimate.HasValue && page.ResultSizeEstimate.Value > 0) {\n                totalEstimate = page.ResultSizeEstimate.Value;\n            }\n\n            pageToken = page.NextPageToken;\n            if (page.Messages == null || page.Messages.Count == 0) {\n                break;\n            }\n\n            foreach (var message in page.Messages) {\n                var id = NormalizeOptional(message?.Id);\n                if (id == null || !seenIds.Add(id)) {\n                    continue;\n                }\n\n                if (skipRemaining > 0) {\n                    skipRemaining--;\n                    continue;\n                }\n\n                selectedIds.Add(id);\n                if (selectedIds.Count >= safeLimit) {\n                    break;\n                }\n            }\n\n            if (string.IsNullOrWhiteSpace(pageToken)) {\n                break;\n            }\n        }\n\n        var summaries = new List<GmailMailboxMessageSummary>(selectedIds.Count);\n        foreach (var id in selectedIds) {\n            var summary = await TryGetMessageSummaryAsync(id, cancellationToken).ConfigureAwait(false);\n            if (summary != null) {\n                summaries.Add(summary);\n            }\n        }\n\n        return new GmailMailboxListResult {\n            ResolvedLabelId = resolvedLabelId,\n            TotalCount = totalEstimate > int.MaxValue ? int.MaxValue : (int)totalEstimate,\n            Messages = summaries\n        };\n    }\n\n    /// <summary>\n    /// Lists messages in a Gmail thread.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxMessageSummary>> ListThreadMessagesAsync(\n        string threadId,\n        int maxItems = 2000,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        var thread = await _gmail.GetThreadWithOptionsAsync(\n            _userId,\n            threadId.Trim(),\n            format: \"full\",\n            fields: ThreadFields,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var summaries = MapSummaries(thread.Messages);\n        summaries.Sort(static (a, b) => b.DateUtc.CompareTo(a.DateUtc));\n        var cap = ClampInt(maxItems, 1, 10000);\n        if (summaries.Count > cap) {\n            summaries = summaries.GetRange(0, cap);\n        }\n\n        return summaries;\n    }\n\n    /// <summary>\n    /// Lists a paged slice of messages in a Gmail thread.\n    /// </summary>\n    public async Task<GmailMailboxThreadListResult> ListThreadMessagesPageAsync(\n        string threadId,\n        int limit,\n        int offset,\n        int maxItems = 2000,\n        CancellationToken cancellationToken = default) {\n        var messages = await ListThreadMessagesAsync(\n            threadId,\n            maxItems: maxItems,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var total = messages.Count;\n        var skip = Math.Max(0, offset);\n        var take = ClampInt(limit, 1, 1000);\n        var page = messages.Skip(skip).Take(take).ToList();\n\n        return new GmailMailboxThreadListResult {\n            ThreadId = threadId.Trim(),\n            TotalCount = total,\n            Messages = page\n        };\n    }\n\n    /// <summary>\n    /// Searches messages in a Gmail label.\n    /// </summary>\n    public async Task<GmailMailboxSearchResult> SearchMessagesAsync(\n        GmailMailboxSearchRequest request,\n        int max,\n        CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var safeMax = ClampInt(max, 1, 2000);\n        var resolvedLabelId = NormalizeOptional(await ResolveLabelIdAsync(request.Folder, cancellationToken).ConfigureAwait(false));\n        if (resolvedLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail folder/label.\");\n        }\n\n        var query = BuildSearchQuery(request);\n        var selected = new List<string>();\n        var seenIds = new HashSet<string>(StringComparer.Ordinal);\n        string? pageToken = null;\n\n        while (selected.Count < safeMax) {\n            var page = await _gmail.ListPageAsync(\n                _userId,\n                query: string.IsNullOrWhiteSpace(query) ? null : query,\n                labelIds: new[] { resolvedLabelId },\n                includeSpamTrash: false,\n                maxResults: 100,\n                pageToken: pageToken,\n                fields: ListFields,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            pageToken = page.NextPageToken;\n            if (page.Messages == null || page.Messages.Count == 0) {\n                break;\n            }\n\n            foreach (var message in page.Messages) {\n                var id = NormalizeOptional(message?.Id);\n                if (id == null || !seenIds.Add(id)) {\n                    continue;\n                }\n\n                selected.Add(id);\n                if (selected.Count >= safeMax) {\n                    break;\n                }\n            }\n\n            if (string.IsNullOrWhiteSpace(pageToken)) {\n                break;\n            }\n        }\n\n        var summaries = new List<GmailMailboxMessageSummary>(selected.Count);\n        foreach (var id in selected) {\n            var summary = await TryGetMessageSummaryAsync(id, cancellationToken).ConfigureAwait(false);\n            if (summary != null) {\n                summaries.Add(summary);\n            }\n        }\n\n        summaries.Sort(static (a, b) => b.DateUtc.CompareTo(a.DateUtc));\n        return new GmailMailboxSearchResult {\n            ResolvedLabelId = resolvedLabelId,\n            Messages = summaries\n        };\n    }\n\n    /// <summary>\n    /// Builds a Gmail query string from mailbox search filters.\n    /// </summary>\n    public static string BuildSearchQuery(GmailMailboxSearchRequest request) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var parts = new List<string>();\n        void Add(string? value) {\n            var normalized = NormalizeOptional(value);\n            if (normalized != null) {\n                parts.Add(normalized);\n            }\n        }\n\n        if (request.UnseenOnly) {\n            parts.Add(\"is:unread\");\n        }\n        if (request.HasAttachment) {\n            parts.Add(\"has:attachment\");\n        }\n\n        if (request.SinceUtc.HasValue) {\n            var sinceUtc = DateTime.SpecifyKind(request.SinceUtc.Value, DateTimeKind.Utc);\n            var after = new DateTimeOffset(sinceUtc).ToUnixTimeSeconds();\n            parts.Add(\"after:\" + after.ToString(CultureInfo.InvariantCulture));\n        }\n        if (request.BeforeUtc.HasValue) {\n            var beforeUtc = DateTime.SpecifyKind(request.BeforeUtc.Value, DateTimeKind.Utc);\n            var before = new DateTimeOffset(beforeUtc).ToUnixTimeSeconds();\n            parts.Add(\"before:\" + before.ToString(CultureInfo.InvariantCulture));\n        }\n\n        var subjectContains = NormalizeOptional(request.SubjectContains);\n        if (!string.IsNullOrWhiteSpace(subjectContains)) {\n            Add(\"subject:(\" + subjectContains + \")\");\n        }\n        var fromContains = NormalizeOptional(request.FromContains);\n        if (!string.IsNullOrWhiteSpace(fromContains)) {\n            Add(\"from:(\" + fromContains + \")\");\n        }\n        var toContains = NormalizeOptional(request.ToContains);\n        if (!string.IsNullOrWhiteSpace(toContains)) {\n            Add(\"to:(\" + toContains + \")\");\n        }\n        if (!string.IsNullOrWhiteSpace(request.BodyContains)) {\n            Add(request.BodyContains);\n        }\n        if (!string.IsNullOrWhiteSpace(request.Query)) {\n            Add(request.Query);\n        }\n\n        return string.Join(\" \", parts);\n    }\n\n    /// <summary>\n    /// Gets one message summary.\n    /// </summary>\n    public async Task<GmailMailboxMessageSummary> GetMessageSummaryAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var message = await _gmail.GetFullAsync(\n            _userId,\n            messageId.Trim(),\n            fields: MessageSummaryFields,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return MapSummary(message);\n    }\n\n    /// <summary>\n    /// Imports a MIME message into Gmail with a target label (typically <c>SENT</c>).\n    /// </summary>\n    /// <param name=\"message\">MIME message to import.</param>\n    /// <param name=\"labelId\">Target label id.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Import result.</returns>\n    public async Task<GmailMailboxImportResult> ImportMessageAsync(\n        MimeMessage message,\n        string labelId = \"SENT\",\n        CancellationToken cancellationToken = default) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (string.IsNullOrWhiteSpace(labelId)) {\n            throw new ArgumentException(\"labelId is required.\", nameof(labelId));\n        }\n\n        string raw;\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms, cancellationToken).ConfigureAwait(false);\n            raw = Base64UrlEncode(ms.ToArray());\n        }\n\n        var imported = await _gmail.ImportAsync(\n            _userId,\n            raw,\n            labelIds: new[] { labelId.Trim() },\n            internalDateSource: \"dateHeader\",\n            neverMarkSpam: true,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new GmailMailboxImportResult {\n            LabelId = labelId.Trim(),\n            NativeId = NormalizeOptional(imported.Id),\n            NativeThreadId = NormalizeOptional(imported.ThreadId)\n        };\n    }\n\n    /// <summary>\n    /// Sends a MIME message through Gmail.\n    /// </summary>\n    /// <param name=\"message\">MIME message to send.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Send result metadata.</returns>\n    public async Task<GmailMailboxSendResult> SendMessageAsync(\n        MimeMessage message,\n        CancellationToken cancellationToken = default) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var sent = await _gmail.SendAsync(_userId, message, cancellationToken).ConfigureAwait(false);\n        return new GmailMailboxSendResult {\n            NativeId = NormalizeOptional(sent.Id),\n            NativeThreadId = NormalizeOptional(sent.ThreadId)\n        };\n    }\n\n    /// <summary>\n    /// Probes a Gmail label for a message with a matching RFC822 <c>Message-Id</c> token.\n    /// </summary>\n    /// <param name=\"messageIdToken\">Message-Id token (with or without angle brackets).</param>\n    /// <param name=\"sentLabelId\">Label id to probe. Defaults to <c>SENT</c>.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Duplicate probe result.</returns>\n    public async Task<GmailMailboxDuplicateProbeResult> FindSentMessageByRfc822MessageIdAsync(\n        string messageIdToken,\n        string sentLabelId = \"SENT\",\n        CancellationToken cancellationToken = default) {\n        var normalizedToken = NormalizeMessageIdValue(messageIdToken);\n        if (normalizedToken == null) {\n            throw new ArgumentException(\"messageIdToken is required.\", nameof(messageIdToken));\n        }\n\n        var query = \"rfc822msgid:\" + normalizedToken;\n        var page = await _gmail.ListPageAsync(\n            _userId,\n            query: query,\n            labelIds: new[] { sentLabelId },\n            includeSpamTrash: false,\n            maxResults: 1,\n            pageToken: null,\n            fields: ListFields,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var first = page.Messages?.FirstOrDefault();\n        if (first == null) {\n            return new GmailMailboxDuplicateProbeResult {\n                IsMatch = false,\n                LabelId = sentLabelId\n            };\n        }\n\n        return new GmailMailboxDuplicateProbeResult {\n            IsMatch = true,\n            LabelId = sentLabelId,\n            NativeId = NormalizeOptional(first.Id),\n            NativeThreadId = NormalizeOptional(first.ThreadId),\n            MessageId = normalizedToken\n        };\n    }\n\n    /// <summary>\n    /// Reads provider threading metadata for a single message.\n    /// </summary>\n    /// <param name=\"messageId\">Gmail message id.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Threading metadata parsed from selected headers.</returns>\n    public async Task<GmailMailboxThreadingMetadataResult> GetThreadingMetadataAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var message = await _gmail.GetMessageWithOptionsAsync(\n            _userId,\n            messageId.Trim(),\n            format: \"metadata\",\n            metadataHeaders: new[] { \"Message-ID\", \"In-Reply-To\", \"References\", \"Reply-To\", \"Cc\" },\n            fields: \"id,payload(headers)\",\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var headers = message.Payload?.Headers;\n        if (headers == null || headers.Count == 0) {\n            return new GmailMailboxThreadingMetadataResult();\n        }\n\n        string? messageIdHeader = null;\n        string? inReplyTo = null;\n        string? referencesRaw = null;\n        string? replyTo = null;\n        string? cc = null;\n\n        foreach (var header in headers) {\n            var name = header?.Name;\n            var value = header?.Value;\n            if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) {\n                continue;\n            }\n\n            var headerName = name!.Trim();\n            var headerValue = value!.Trim();\n            if (headerName.Equals(\"Message-ID\", StringComparison.OrdinalIgnoreCase)) {\n                messageIdHeader = NormalizeMessageIdValue(headerValue);\n            } else if (headerName.Equals(\"In-Reply-To\", StringComparison.OrdinalIgnoreCase)) {\n                inReplyTo = NormalizeMessageIdValue(headerValue);\n            } else if (headerName.Equals(\"References\", StringComparison.OrdinalIgnoreCase)) {\n                referencesRaw = headerValue;\n            } else if (headerName.Equals(\"Reply-To\", StringComparison.OrdinalIgnoreCase)) {\n                replyTo = NormalizeOptional(headerValue);\n            } else if (headerName.Equals(\"Cc\", StringComparison.OrdinalIgnoreCase)) {\n                cc = NormalizeOptional(headerValue);\n            }\n        }\n\n        return new GmailMailboxThreadingMetadataResult {\n            MessageId = messageIdHeader,\n            ReplyTo = replyTo,\n            Cc = cc,\n            InReplyTo = inReplyTo,\n            References = SplitMessageIdTokens(referencesRaw)\n        };\n    }\n\n    /// <summary>\n    /// Gets one message content by downloading MIME payload and parsing it to <see cref=\"MimeMessage\"/>.\n    /// </summary>\n    public async Task<GmailMailboxGetResult> GetMessageContentAsync(\n        string messageId,\n        int maxMimeBytes = DefaultMaxMimeBytes,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (maxMimeBytes <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxMimeBytes), \"maxMimeBytes must be greater than zero.\");\n        }\n\n        var message = await _gmail.GetRawAsync(\n            _userId,\n            messageId.Trim(),\n            fields: RawFields,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        if (string.IsNullOrWhiteSpace(message.Raw)) {\n            throw new InvalidDataException(\"Gmail message response did not contain raw RFC822 payload.\");\n        }\n\n        byte[] mimeBytes;\n        try {\n            mimeBytes = Base64UrlDecode(message.Raw!);\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to decode Gmail raw MIME payload.\", ex);\n        }\n\n        if (mimeBytes.Length > maxMimeBytes) {\n            throw new InvalidOperationException(\"Gmail raw message exceeds \" + maxMimeBytes.ToString(CultureInfo.InvariantCulture) + \" bytes.\");\n        }\n\n        MimeMessage mimeMessage;\n        try {\n            mimeMessage = MimeMessage.Load(new MemoryStream(mimeBytes, writable: false));\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to parse Gmail MIME message.\", ex);\n        }\n\n        return new GmailMailboxGetResult {\n            Message = mimeMessage,\n            Seen = !HasLabel(message.LabelIds, \"UNREAD\"),\n            Flagged = HasLabel(message.LabelIds, \"STARRED\"),\n            NativeThreadId = NormalizeOptional(message.ThreadId)\n        };\n    }\n\n    /// <summary>\n    /// Sets read/unread state on a single message.\n    /// </summary>\n    public async Task SetMessageSeenAsync(\n        string messageId,\n        bool seen,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _gmail.ModifyMessageLabelsAsync(\n            _userId,\n            messageId.Trim(),\n            addLabelIds: seen ? Array.Empty<string>() : new[] { \"UNREAD\" },\n            removeLabelIds: seen ? new[] { \"UNREAD\" } : Array.Empty<string>(),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on a single message.\n    /// </summary>\n    public async Task SetMessageFlaggedAsync(\n        string messageId,\n        bool flagged,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _gmail.ModifyMessageLabelsAsync(\n            _userId,\n            messageId.Trim(),\n            addLabelIds: flagged ? new[] { \"STARRED\" } : Array.Empty<string>(),\n            removeLabelIds: flagged ? Array.Empty<string>() : new[] { \"STARRED\" },\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves a single message to a target folder/label.\n    /// </summary>\n    public async Task MoveMessageAsync(\n        string messageId,\n        string? sourceFolder,\n        string targetFolder,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (string.IsNullOrWhiteSpace(targetFolder)) {\n            throw new ArgumentException(\"targetFolder is required.\", nameof(targetFolder));\n        }\n\n        var targetLabelId = NormalizeOptional(await ResolveLabelIdAsync(targetFolder, cancellationToken).ConfigureAwait(false));\n        if (targetLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail target folder/label.\");\n        }\n\n        if (targetLabelId.Equals(\"TRASH\", StringComparison.OrdinalIgnoreCase)) {\n            _ = await _gmail.TrashMessageAsync(_userId, messageId.Trim(), cancellationToken).ConfigureAwait(false);\n            return;\n        }\n\n        var remove = new List<string>();\n        var sourceLabelId = await ResolveSourceLabelIdAsync(sourceFolder, cancellationToken).ConfigureAwait(false);\n        if (sourceLabelId != null) {\n            remove.Add(sourceLabelId);\n        }\n        remove.Add(\"TRASH\");\n\n        _ = await _gmail.ModifyMessageLabelsAsync(\n            _userId,\n            messageId.Trim(),\n            addLabelIds: new[] { targetLabelId },\n            removeLabelIds: remove,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives a single message (removes INBOX/TRASH labels).\n    /// </summary>\n    public async Task ArchiveMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        _ = await _gmail.ModifyMessageLabelsAsync(\n            _userId,\n            messageId.Trim(),\n            addLabelIds: Array.Empty<string>(),\n            removeLabelIds: new[] { \"INBOX\", \"TRASH\" },\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves a single message to trash.\n    /// </summary>\n    public async Task TrashMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        _ = await _gmail.TrashMessageAsync(_userId, messageId.Trim(), cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes a single message.\n    /// </summary>\n    public async Task DeleteMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _gmail.DeleteAsync(_userId, messageId.Trim(), cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state on many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> SetMessagesSeenAsync(\n        IEnumerable<string> messageIds,\n        bool seen,\n        int batchSize = BatchMaxIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await ExecuteBatchModifyAsync(\n            messageIds,\n            addLabelIds: seen ? Array.Empty<string>() : new[] { \"UNREAD\" },\n            removeLabelIds: seen ? new[] { \"UNREAD\" } : Array.Empty<string>(),\n            batchSize,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> SetMessagesFlaggedAsync(\n        IEnumerable<string> messageIds,\n        bool flagged,\n        int batchSize = BatchMaxIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await ExecuteBatchModifyAsync(\n            messageIds,\n            addLabelIds: flagged ? new[] { \"STARRED\" } : Array.Empty<string>(),\n            removeLabelIds: flagged ? Array.Empty<string>() : new[] { \"STARRED\" },\n            batchSize,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state on a single thread.\n    /// </summary>\n    public async Task SetThreadSeenAsync(\n        string threadId,\n        bool seen,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        _ = await _gmail.ModifyThreadLabelsAsync(\n            _userId,\n            threadId.Trim(),\n            addLabelIds: seen ? Array.Empty<string>() : new[] { \"UNREAD\" },\n            removeLabelIds: seen ? new[] { \"UNREAD\" } : Array.Empty<string>(),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on a single thread.\n    /// </summary>\n    public async Task SetThreadFlaggedAsync(\n        string threadId,\n        bool flagged,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        _ = await _gmail.ModifyThreadLabelsAsync(\n            _userId,\n            threadId.Trim(),\n            addLabelIds: flagged ? new[] { \"STARRED\" } : Array.Empty<string>(),\n            removeLabelIds: flagged ? Array.Empty<string>() : new[] { \"STARRED\" },\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many messages to a target folder/label.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> MoveMessagesAsync(\n        IEnumerable<string> messageIds,\n        string? sourceFolder,\n        string targetFolder,\n        int batchSize = BatchMaxIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n        if (string.IsNullOrWhiteSpace(targetFolder)) {\n            throw new ArgumentException(\"targetFolder is required.\", nameof(targetFolder));\n        }\n\n        var ids = NormalizeIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GmailMailboxBulkOperationResult>();\n        }\n\n        var targetLabelId = NormalizeOptional(await ResolveLabelIdAsync(targetFolder, cancellationToken).ConfigureAwait(false));\n        if (targetLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail target folder/label.\");\n        }\n\n        if (targetLabelId.Equals(\"TRASH\", StringComparison.OrdinalIgnoreCase)) {\n            return await TrashMessagesAsync(ids, cancellationToken).ConfigureAwait(false);\n        }\n\n        var remove = new List<string>();\n        var sourceLabelId = await ResolveSourceLabelIdAsync(sourceFolder, cancellationToken).ConfigureAwait(false);\n        if (sourceLabelId != null) {\n            remove.Add(sourceLabelId);\n        }\n        remove.Add(\"TRASH\");\n\n        return await ExecuteBatchModifyAsync(\n            ids,\n            addLabelIds: new[] { targetLabelId },\n            removeLabelIds: remove,\n            batchSize,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves a single thread to a target folder/label.\n    /// </summary>\n    public async Task MoveThreadAsync(\n        string threadId,\n        string? sourceFolder,\n        string targetFolder,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n        if (string.IsNullOrWhiteSpace(targetFolder)) {\n            throw new ArgumentException(\"targetFolder is required.\", nameof(targetFolder));\n        }\n\n        var targetRaw = targetFolder.Trim();\n        if (targetRaw.Equals(\"Archive\", StringComparison.OrdinalIgnoreCase)) {\n            await ArchiveThreadAsync(threadId, cancellationToken).ConfigureAwait(false);\n            return;\n        }\n\n        var targetLabelId = NormalizeOptional(await ResolveLabelIdAsync(targetRaw, cancellationToken).ConfigureAwait(false));\n        if (targetLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail target folder/label.\");\n        }\n\n        if (targetLabelId.Equals(\"TRASH\", StringComparison.OrdinalIgnoreCase)) {\n            await TrashThreadAsync(threadId, cancellationToken).ConfigureAwait(false);\n            return;\n        }\n\n        var remove = new List<string>();\n        var sourceLabelId = NormalizeOptional(await ResolveLabelIdAsync(sourceFolder, cancellationToken).ConfigureAwait(false));\n        if (sourceLabelId != null) {\n            remove.Add(sourceLabelId);\n        }\n        remove.Add(\"TRASH\");\n\n        _ = await _gmail.ModifyThreadLabelsAsync(\n            _userId,\n            threadId.Trim(),\n            addLabelIds: new[] { targetLabelId },\n            removeLabelIds: remove,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many threads to a target folder/label.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> MoveThreadsAsync(\n        IEnumerable<string> threadIds,\n        string? sourceFolder,\n        string targetFolder,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(\n            threadIds,\n            (threadId, token) => MoveThreadAsync(threadId, sourceFolder, targetFolder, token),\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives many messages (removes INBOX/TRASH labels).\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> ArchiveMessagesAsync(\n        IEnumerable<string> messageIds,\n        int batchSize = BatchMaxIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await ExecuteBatchModifyAsync(\n            messageIds,\n            addLabelIds: Array.Empty<string>(),\n            removeLabelIds: new[] { \"INBOX\", \"TRASH\" },\n            batchSize,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many messages to trash.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> TrashMessagesAsync(\n        IEnumerable<string> messageIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GmailMailboxBulkOperationResult>();\n        }\n\n        var results = new List<GmailMailboxBulkOperationResult>(ids.Count);\n        foreach (var id in ids) {\n            cancellationToken.ThrowIfCancellationRequested();\n            try {\n                _ = await _gmail.TrashMessageAsync(_userId, id, cancellationToken).ConfigureAwait(false);\n                results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = true });\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = false, Error = ex.Message });\n            }\n        }\n\n        return results;\n    }\n\n    /// <summary>\n    /// Deletes many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> DeleteMessagesAsync(\n        IEnumerable<string> messageIds,\n        int batchSize = BatchMaxIds,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GmailMailboxBulkOperationResult>();\n        }\n\n        var results = new List<GmailMailboxBulkOperationResult>(ids.Count);\n        foreach (var chunk in Chunk(ids, ClampInt(batchSize, 1, BatchMaxIds))) {\n            cancellationToken.ThrowIfCancellationRequested();\n            try {\n                await _gmail.BatchDeleteMessagesAsync(_userId, chunk, cancellationToken).ConfigureAwait(false);\n                foreach (var id in chunk) {\n                    results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = true });\n                }\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                foreach (var id in chunk) {\n                    results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = false, Error = ex.Message });\n                }\n            }\n        }\n\n        return results;\n    }\n\n    /// <summary>\n    /// Archives a single thread (removes INBOX/TRASH labels).\n    /// </summary>\n    public async Task ArchiveThreadAsync(\n        string threadId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        _ = await _gmail.ModifyThreadLabelsAsync(\n            _userId,\n            threadId.Trim(),\n            addLabelIds: Array.Empty<string>(),\n            removeLabelIds: new[] { \"INBOX\", \"TRASH\" },\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves a single thread to trash.\n    /// </summary>\n    public async Task TrashThreadAsync(\n        string threadId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        _ = await _gmail.TrashThreadAsync(_userId, threadId.Trim(), cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes a single thread.\n    /// </summary>\n    public async Task DeleteThreadAsync(\n        string threadId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(threadId)) {\n            throw new ArgumentException(\"threadId is required.\", nameof(threadId));\n        }\n\n        await _gmail.DeleteThreadAsync(_userId, threadId.Trim(), cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives many threads.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> ArchiveThreadsAsync(\n        IEnumerable<string> threadIds,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(threadIds, ArchiveThreadAsync, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many threads to trash.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> TrashThreadsAsync(\n        IEnumerable<string> threadIds,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(threadIds, TrashThreadAsync, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes many threads.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> DeleteThreadsAsync(\n        IEnumerable<string> threadIds,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(threadIds, DeleteThreadAsync, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state on many threads.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> SetThreadsSeenAsync(\n        IEnumerable<string> threadIds,\n        bool seen,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(\n            threadIds,\n            (threadId, token) => SetThreadSeenAsync(threadId, seen, token),\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on many threads.\n    /// </summary>\n    public async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> SetThreadsFlaggedAsync(\n        IEnumerable<string> threadIds,\n        bool flagged,\n        CancellationToken cancellationToken = default) {\n        return await ExecuteThreadActionAsync(\n            threadIds,\n            (threadId, token) => SetThreadFlaggedAsync(threadId, flagged, token),\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Gets mailbox profile.\n    /// </summary>\n    public async Task<GmailMailboxProfileResult> GetProfileAsync(CancellationToken cancellationToken = default) {\n        var profile = await _gmail.GetProfileAsync(_userId, cancellationToken).ConfigureAwait(false);\n        return new GmailMailboxProfileResult {\n            EmailAddress = NormalizeOptional(profile.EmailAddress),\n            MessagesTotal = profile.MessagesTotal,\n            ThreadsTotal = profile.ThreadsTotal,\n            HistoryId = NormalizeOptional(profile.HistoryId)\n        };\n    }\n\n    /// <summary>\n    /// Starts Gmail watch subscription for selected folders.\n    /// </summary>\n    public async Task<GmailMailboxWatchResult> WatchAsync(\n        string topicName,\n        IReadOnlyCollection<string>? folders,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(topicName)) {\n            throw new ArgumentException(\"topicName is required.\", nameof(topicName));\n        }\n\n        var folderInput = folders == null || folders.Count == 0\n            ? new[] { \"INBOX\" }\n            : folders;\n\n        var labelIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var raw in folderInput) {\n            if (string.IsNullOrWhiteSpace(raw)) {\n                continue;\n            }\n            var labelId = await ResolveWatchLabelIdAsync(raw, cancellationToken).ConfigureAwait(false);\n            var normalizedLabelId = NormalizeOptional(labelId);\n            if (normalizedLabelId != null) {\n                labelIds.Add(normalizedLabelId);\n            }\n        }\n\n        var watch = await _gmail.WatchAsync(\n            _userId,\n            topicName.Trim(),\n            labelIds.Count == 0 ? null : labelIds.ToArray(),\n            cancellationToken).ConfigureAwait(false);\n\n        DateTime? expirationUtc = null;\n        if (watch.Expiration > 0) {\n            expirationUtc = DateTimeOffset.FromUnixTimeMilliseconds(watch.Expiration).UtcDateTime;\n        }\n\n        return new GmailMailboxWatchResult {\n            HistoryId = NormalizeOptional(watch.HistoryId),\n            ExpirationUtc = expirationUtc,\n            LabelIds = labelIds.ToList()\n        };\n    }\n\n    /// <summary>\n    /// Stops Gmail watch subscription.\n    /// </summary>\n    public Task StopWatchAsync(CancellationToken cancellationToken = default) =>\n        _gmail.StopWatchAsync(_userId, cancellationToken);\n\n    /// <summary>\n    /// Stops Gmail watch subscription with stale-remote handling.\n    /// </summary>\n    public async Task<GmailMailboxStopWatchResult> StopWatchAsync(\n        bool treatMissingAsSuccess,\n        CancellationToken cancellationToken = default) {\n        try {\n            await _gmail.StopWatchAsync(_userId, cancellationToken).ConfigureAwait(false);\n            return new GmailMailboxStopWatchResult {\n                Stopped = true\n            };\n        } catch (GmailApiException ex) when (treatMissingAsSuccess &&\n                                             (ex.StatusCode == HttpStatusCode.NotFound || ex.StatusCode == HttpStatusCode.Gone)) {\n            return new GmailMailboxStopWatchResult {\n                Stopped = true,\n                AlreadyStopped = true\n            };\n        }\n    }\n\n    /// <summary>\n    /// Gets Gmail history changes for a folder.\n    /// </summary>\n    public async Task<GmailMailboxHistoryResult> GetHistoryAsync(\n        string folder,\n        string startHistoryId,\n        int maxChanges,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(startHistoryId)) {\n            throw new ArgumentException(\"startHistoryId is required.\", nameof(startHistoryId));\n        }\n\n        var resolvedLabelId = NormalizeOptional(await ResolveLabelIdAsync(folder, cancellationToken).ConfigureAwait(false));\n        if (resolvedLabelId == null) {\n            throw new InvalidOperationException(\"Unable to resolve Gmail folder/label.\");\n        }\n\n        var max = ClampInt(maxChanges, 1, 2000);\n        var upserts = new HashSet<string>(StringComparer.Ordinal);\n        var deletes = new HashSet<string>(StringComparer.Ordinal);\n        var historyTypes = new[] { \"messageAdded\", \"messageDeleted\", \"labelAdded\", \"labelRemoved\" };\n        string? pageToken = null;\n        string? newHistoryId = null;\n        var pageCount = 0;\n\n        while (pageCount++ < 25 && (upserts.Count + deletes.Count) < max) {\n            var history = await _gmail.ListHistoryAsync(\n                _userId,\n                startHistoryId.Trim(),\n                labelId: resolvedLabelId,\n                historyTypes: historyTypes,\n                maxResults: 500,\n                pageToken: pageToken,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            var historyId = NormalizeOptional(history.HistoryId);\n            if (historyId != null) {\n                newHistoryId = historyId;\n            }\n\n            pageToken = NormalizeOptional(history.NextPageToken);\n            if (history.History == null || history.History.Count == 0) {\n                break;\n            }\n\n            foreach (var entry in history.History) {\n                if (entry == null) {\n                    continue;\n                }\n\n                AddHistoryRefs(entry.MessagesAdded, upserts);\n                AddHistoryRefs(entry.LabelsAdded, upserts);\n                AddHistoryRefs(entry.MessagesDeleted, deletes);\n                AddHistoryRefs(entry.LabelsRemoved, deletes);\n            }\n\n            if (string.IsNullOrWhiteSpace(pageToken)) {\n                break;\n            }\n        }\n\n        foreach (var deletedId in deletes) {\n            _ = upserts.Remove(deletedId);\n        }\n\n        var upsertIds = upserts.ToList();\n        upsertIds.Sort(StringComparer.Ordinal);\n        var deleteIds = deletes.ToList();\n        deleteIds.Sort(StringComparer.Ordinal);\n\n        return new GmailMailboxHistoryResult {\n            ResolvedLabelId = resolvedLabelId,\n            NewHistoryId = newHistoryId,\n            UpsertNativeIds = upsertIds,\n            DeletedNativeIds = deleteIds\n        };\n    }\n\n    private async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> ExecuteBatchModifyAsync(\n        IEnumerable<string> messageIds,\n        IReadOnlyCollection<string> addLabelIds,\n        IReadOnlyCollection<string> removeLabelIds,\n        int batchSize,\n        CancellationToken cancellationToken) {\n        var ids = NormalizeIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GmailMailboxBulkOperationResult>();\n        }\n\n        var results = new List<GmailMailboxBulkOperationResult>(ids.Count);\n        foreach (var chunk in Chunk(ids, ClampInt(batchSize, 1, BatchMaxIds))) {\n            cancellationToken.ThrowIfCancellationRequested();\n            try {\n                await _gmail.BatchModifyMessagesAsync(\n                    _userId,\n                    chunk,\n                    addLabelIds: addLabelIds,\n                    removeLabelIds: removeLabelIds,\n                    cancellationToken: cancellationToken).ConfigureAwait(false);\n                foreach (var id in chunk) {\n                    results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = true });\n                }\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                foreach (var id in chunk) {\n                    results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = false, Error = ex.Message });\n                }\n            }\n        }\n\n        return results;\n    }\n\n    private async Task<IReadOnlyList<GmailMailboxBulkOperationResult>> ExecuteThreadActionAsync(\n        IEnumerable<string> threadIds,\n        Func<string, CancellationToken, Task> actionAsync,\n        CancellationToken cancellationToken) {\n        if (threadIds == null) {\n            throw new ArgumentNullException(nameof(threadIds));\n        }\n        if (actionAsync == null) {\n            throw new ArgumentNullException(nameof(actionAsync));\n        }\n\n        var ids = NormalizeIds(threadIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GmailMailboxBulkOperationResult>();\n        }\n\n        var results = new List<GmailMailboxBulkOperationResult>(ids.Count);\n        foreach (var id in ids) {\n            cancellationToken.ThrowIfCancellationRequested();\n            try {\n                await actionAsync(id, cancellationToken).ConfigureAwait(false);\n                results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = true });\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                results.Add(new GmailMailboxBulkOperationResult { Id = id, Ok = false, Error = ex.Message });\n            }\n        }\n\n        return results;\n    }\n\n    private async Task<string?> ResolveSourceLabelIdAsync(\n        string? sourceFolder,\n        CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(sourceFolder)) {\n            return null;\n        }\n\n        return NormalizeOptional(await ResolveLabelIdAsync(sourceFolder, cancellationToken).ConfigureAwait(false));\n    }\n\n    private static List<string> NormalizeIds(IEnumerable<string> ids) {\n        var output = new List<string>();\n        if (ids == null) {\n            return output;\n        }\n\n        foreach (var raw in ids) {\n            var id = NormalizeOptional(raw);\n            if (id != null) {\n                output.Add(id);\n            }\n        }\n\n        return output;\n    }\n\n    private static IEnumerable<List<string>> Chunk(IReadOnlyList<string> ids, int chunkSize) {\n        var safeChunkSize = chunkSize <= 0 ? 1 : chunkSize;\n        for (var i = 0; i < ids.Count; i += safeChunkSize) {\n            var count = Math.Min(safeChunkSize, ids.Count - i);\n            var chunk = new List<string>(count);\n            for (var j = 0; j < count; j++) {\n                chunk.Add(ids[i + j]);\n            }\n            yield return chunk;\n        }\n    }\n\n    private static void AddHistoryRefs(\n        IReadOnlyCollection<GmailApiClient.GmailHistoryMessageAdded>? refs,\n        HashSet<string> output) {\n        if (refs == null || refs.Count == 0) {\n            return;\n        }\n\n        foreach (var entry in refs) {\n            var id = NormalizeOptional(entry?.Message?.Id);\n            if (id != null) {\n                output.Add(id);\n            }\n        }\n    }\n\n    private static void AddHistoryRefs(\n        IReadOnlyCollection<GmailApiClient.GmailHistoryLabelAdded>? refs,\n        HashSet<string> output) {\n        if (refs == null || refs.Count == 0) {\n            return;\n        }\n\n        foreach (var entry in refs) {\n            var id = NormalizeOptional(entry?.Message?.Id);\n            if (id != null) {\n                output.Add(id);\n            }\n        }\n    }\n\n    private static void AddHistoryRefs(\n        IReadOnlyCollection<GmailApiClient.GmailHistoryMessageDeleted>? refs,\n        HashSet<string> output) {\n        if (refs == null || refs.Count == 0) {\n            return;\n        }\n\n        foreach (var entry in refs) {\n            var id = NormalizeOptional(entry?.Message?.Id);\n            if (id != null) {\n                output.Add(id);\n            }\n        }\n    }\n\n    private static void AddHistoryRefs(\n        IReadOnlyCollection<GmailApiClient.GmailHistoryLabelRemoved>? refs,\n        HashSet<string> output) {\n        if (refs == null || refs.Count == 0) {\n            return;\n        }\n\n        foreach (var entry in refs) {\n            var id = NormalizeOptional(entry?.Message?.Id);\n            if (id != null) {\n                output.Add(id);\n            }\n        }\n    }\n\n    private async Task<GmailMailboxMessageSummary?> TryGetMessageSummaryAsync(string messageId, CancellationToken cancellationToken) {\n        try {\n            return await GetMessageSummaryAsync(messageId, cancellationToken).ConfigureAwait(false);\n        } catch {\n            return null;\n        }\n    }\n\n    private static bool TryResolveSystemLabel(string raw, out string labelId) {\n        var normalized = raw.Trim();\n        if (normalized.Equals(\"INBOX\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"INBOX\";\n            return true;\n        }\n        if (normalized.Equals(\"SENT\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"SENTITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"SENT ITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"SENT MAIL\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"SENT\";\n            return true;\n        }\n        if (normalized.Equals(\"TRASH\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"DELETED\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"DELETED ITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"DELETEDITEMS\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"TRASH\";\n            return true;\n        }\n        if (normalized.Equals(\"DRAFT\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"DRAFTS\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"DRAFT\";\n            return true;\n        }\n        if (normalized.Equals(\"SPAM\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"JUNK\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"JUNK EMAIL\", StringComparison.OrdinalIgnoreCase) ||\n            normalized.Equals(\"JUNKEMAIL\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"SPAM\";\n            return true;\n        }\n        if (normalized.Equals(\"STARRED\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"STARRED\";\n            return true;\n        }\n        if (normalized.Equals(\"IMPORTANT\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"IMPORTANT\";\n            return true;\n        }\n\n        labelId = string.Empty;\n        return false;\n    }\n\n    private static bool TryResolveWatchSystemLabel(string raw, out string labelId) {\n        if (raw.Equals(\"INBOX\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"INBOX\";\n            return true;\n        }\n        if (raw.Equals(\"SENT\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"SENTITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"SENT ITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"SENT MAIL\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"SENT\";\n            return true;\n        }\n        if (raw.Equals(\"TRASH\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"DELETED\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"DELETED ITEMS\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"DELETEDITEMS\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"TRASH\";\n            return true;\n        }\n        if (raw.Equals(\"SPAM\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"JUNK\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"JUNK EMAIL\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"JUNKEMAIL\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"SPAM\";\n            return true;\n        }\n        if (raw.Equals(\"DRAFT\", StringComparison.OrdinalIgnoreCase) ||\n            raw.Equals(\"DRAFTS\", StringComparison.OrdinalIgnoreCase)) {\n            labelId = \"DRAFT\";\n            return true;\n        }\n\n        labelId = string.Empty;\n        return false;\n    }\n\n    private static int ClampInt(int value, int min, int max) {\n        if (value < min) return min;\n        if (value > max) return max;\n        return value;\n    }\n\n    private static string? NormalizeOptional(string? raw) {\n        var trimmed = raw == null ? string.Empty : raw.Trim();\n        if (trimmed.Length == 0) {\n            return null;\n        }\n        return trimmed;\n    }\n\n    private static DateTime ResolveInternalDateUtc(long? internalDateMs) {\n        if (!internalDateMs.HasValue || internalDateMs.Value <= 0) {\n            return DateTime.UtcNow;\n        }\n\n        try {\n            return DateTimeOffset.FromUnixTimeMilliseconds(internalDateMs.Value).UtcDateTime;\n        } catch {\n            return DateTime.UtcNow;\n        }\n    }\n\n    private static Dictionary<string, string> BuildHeaderMap(GmailMessagePayload? payload) {\n        var output = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n        var headers = payload?.Headers;\n        if (headers == null || headers.Count == 0) {\n            return output;\n        }\n\n        foreach (var header in headers) {\n            var name = NormalizeOptional(header?.Name);\n            if (name == null) {\n                continue;\n            }\n\n            output[name] = header?.Value ?? string.Empty;\n        }\n\n        return output;\n    }\n\n    private static bool PayloadHasAttachments(GmailMessagePayload? payload) {\n        if (payload == null) {\n            return false;\n        }\n        if (!string.IsNullOrWhiteSpace(payload.Filename) || !string.IsNullOrWhiteSpace(payload.Body?.AttachmentId)) {\n            return true;\n        }\n\n        var parts = payload.Parts;\n        if (parts == null || parts.Count == 0) {\n            return false;\n        }\n\n        foreach (var part in parts) {\n            if (PayloadHasAttachments(part)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static bool HasLabel(IReadOnlyCollection<string>? labelIds, string labelId) {\n        if (labelIds == null || labelIds.Count == 0 || string.IsNullOrWhiteSpace(labelId)) {\n            return false;\n        }\n\n        foreach (var current in labelIds) {\n            if (current != null && current.Equals(labelId, StringComparison.OrdinalIgnoreCase)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static string? NormalizeMessageIdValue(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var normalized = value == null ? string.Empty : value.Trim();\n        if (normalized.StartsWith(\"<\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(1);\n        }\n        if (normalized.EndsWith(\">\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(0, normalized.Length - 1);\n        }\n\n        normalized = normalized.Trim();\n        return normalized.Length == 0 ? null : normalized;\n    }\n\n    private static GmailMailboxMessageSummary MapSummary(GmailMessage message) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var headers = BuildHeaderMap(message.Payload);\n        _ = headers.TryGetValue(\"From\", out var from);\n        _ = headers.TryGetValue(\"To\", out var to);\n        _ = headers.TryGetValue(\"Subject\", out var subject);\n        _ = headers.TryGetValue(\"Message-Id\", out var messageIdHeader);\n\n        return new GmailMailboxMessageSummary {\n            NativeId = NormalizeOptional(message.Id) ?? string.Empty,\n            NativeThreadId = NormalizeOptional(message.ThreadId),\n            MessageId = NormalizeMessageIdValue(messageIdHeader),\n            From = from ?? string.Empty,\n            To = to ?? string.Empty,\n            Subject = NormalizeOptional(subject),\n            DateUtc = ResolveInternalDateUtc(message.InternalDate),\n            HasAttachments = PayloadHasAttachments(message.Payload),\n            Seen = !HasLabel(message.LabelIds, \"UNREAD\"),\n            Flagged = HasLabel(message.LabelIds, \"STARRED\")\n        };\n    }\n\n    private static List<GmailMailboxMessageSummary> MapSummaries(IList<GmailMessage>? messages) {\n        if (messages == null || messages.Count == 0) {\n            return new List<GmailMailboxMessageSummary>();\n        }\n\n        var output = new List<GmailMailboxMessageSummary>(messages.Count);\n        foreach (var message in messages) {\n            if (message == null) {\n                continue;\n            }\n            output.Add(MapSummary(message));\n        }\n\n        return output;\n    }\n\n    private static byte[] Base64UrlDecode(string value) {\n        var data = value.Replace('-', '+').Replace('_', '/');\n        if (data.Length % 4 == 1) {\n            throw new InvalidDataException(\"Attachment data is not a valid Base64 string.\");\n        }\n\n        var padding = (4 - data.Length % 4) % 4;\n        if (padding > 0) {\n            data = data.PadRight(data.Length + padding, '=');\n        }\n\n        try {\n            return Convert.FromBase64String(data);\n        } catch (FormatException ex) {\n            throw new InvalidDataException(\"Attachment data is not a valid Base64 string.\", ex);\n        }\n    }\n\n    private static string Base64UrlEncode(byte[] bytes) {\n        var base64 = Convert.ToBase64String(bytes);\n        return base64.TrimEnd('=').Replace('+', '-').Replace('/', '_');\n    }\n\n    private static List<string> SplitMessageIdTokens(string? raw) {\n        if (string.IsNullOrWhiteSpace(raw)) {\n            return new List<string>();\n        }\n\n        var output = new List<string>();\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        var tokens = raw!.Split(new[] { ' ', '\\t', '\\r', '\\n' }, StringSplitOptions.RemoveEmptyEntries);\n        foreach (var token in tokens) {\n            var normalized = NormalizeMessageIdValue(token);\n            if (normalized == null || !seen.Add(normalized)) {\n                continue;\n            }\n            output.Add(normalized);\n        }\n        return output;\n    }\n\n    /// <summary>\n    /// Gmail mailbox folder summary.\n    /// </summary>\n    public sealed class GmailMailboxFolderSummary {\n        /// <summary>Gmail label id.</summary>\n        public string Id { get; set; } = string.Empty;\n\n        /// <summary>Gmail label display name.</summary>\n        public string Name { get; set; } = string.Empty;\n\n        /// <summary>Gmail label type (for example, <c>system</c> or <c>user</c>).</summary>\n        public string? Type { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox list result.\n    /// </summary>\n    public sealed class GmailMailboxListResult {\n        /// <summary>Resolved Gmail label id used for listing.</summary>\n        public string ResolvedLabelId { get; set; } = string.Empty;\n\n        /// <summary>Total item count estimate as reported by Gmail list response.</summary>\n        public int TotalCount { get; set; }\n\n        /// <summary>Message summaries.</summary>\n        public List<GmailMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox thread list result.\n    /// </summary>\n    public sealed class GmailMailboxThreadListResult {\n        /// <summary>Gmail thread id used for listing.</summary>\n        public string ThreadId { get; set; } = string.Empty;\n\n        /// <summary>Total number of messages available in the thread slice source.</summary>\n        public int TotalCount { get; set; }\n\n        /// <summary>Paged thread message summaries.</summary>\n        public List<GmailMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox search request.\n    /// </summary>\n    public sealed class GmailMailboxSearchRequest {\n        /// <summary>Folder/label filter.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Free-form Gmail query text.</summary>\n        public string? Query { get; set; }\n\n        /// <summary>Subject contains filter.</summary>\n        public string? SubjectContains { get; set; }\n\n        /// <summary>From contains filter.</summary>\n        public string? FromContains { get; set; }\n\n        /// <summary>To contains filter.</summary>\n        public string? ToContains { get; set; }\n\n        /// <summary>Body contains filter.</summary>\n        public string? BodyContains { get; set; }\n\n        /// <summary>Unseen-only filter.</summary>\n        public bool UnseenOnly { get; set; }\n\n        /// <summary>Has-attachment filter.</summary>\n        public bool HasAttachment { get; set; }\n\n        /// <summary>Lower bound for message date/time (UTC).</summary>\n        public DateTime? SinceUtc { get; set; }\n\n        /// <summary>Upper bound for message date/time (UTC).</summary>\n        public DateTime? BeforeUtc { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox search result.\n    /// </summary>\n    public sealed class GmailMailboxSearchResult {\n        /// <summary>Resolved Gmail label id used for searching.</summary>\n        public string ResolvedLabelId { get; set; } = string.Empty;\n\n        /// <summary>Matched messages.</summary>\n        public List<GmailMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox import result.\n    /// </summary>\n    public sealed class GmailMailboxImportResult {\n        /// <summary>Label id used for import.</summary>\n        public string? LabelId { get; set; }\n\n        /// <summary>Imported Gmail native message id, when available.</summary>\n        public string? NativeId { get; set; }\n\n        /// <summary>Imported Gmail native thread id, when available.</summary>\n        public string? NativeThreadId { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox send result.\n    /// </summary>\n    public sealed class GmailMailboxSendResult {\n        /// <summary>Sent Gmail native message id, when available.</summary>\n        public string? NativeId { get; set; }\n\n        /// <summary>Sent Gmail native thread id, when available.</summary>\n        public string? NativeThreadId { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox duplicate probe result.\n    /// </summary>\n    public sealed class GmailMailboxDuplicateProbeResult {\n        /// <summary>True when a matching message was found.</summary>\n        public bool IsMatch { get; set; }\n\n        /// <summary>Label id used for probing.</summary>\n        public string? LabelId { get; set; }\n\n        /// <summary>Matched Gmail native message id, when available.</summary>\n        public string? NativeId { get; set; }\n\n        /// <summary>Matched Gmail native thread id, when available.</summary>\n        public string? NativeThreadId { get; set; }\n\n        /// <summary>Matched normalized RFC822 Message-Id.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox threading metadata.\n    /// </summary>\n    public sealed class GmailMailboxThreadingMetadataResult {\n        /// <summary>Normalized RFC822 Message-Id.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Reply-To header value.</summary>\n        public string? ReplyTo { get; set; }\n\n        /// <summary>Cc header value.</summary>\n        public string? Cc { get; set; }\n\n        /// <summary>Normalized RFC822 In-Reply-To value.</summary>\n        public string? InReplyTo { get; set; }\n\n        /// <summary>Normalized RFC822 References tokens.</summary>\n        public List<string> References { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox get-message result.\n    /// </summary>\n    public sealed class GmailMailboxGetResult {\n        /// <summary>Parsed MIME message.</summary>\n        public MimeMessage Message { get; set; } = new MimeMessage();\n\n        /// <summary>Read state from Gmail labels.</summary>\n        public bool? Seen { get; set; }\n\n        /// <summary>Flagged state from Gmail labels.</summary>\n        public bool? Flagged { get; set; }\n\n        /// <summary>Gmail thread id.</summary>\n        public string? NativeThreadId { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox profile result.\n    /// </summary>\n    public sealed class GmailMailboxProfileResult {\n        /// <summary>Email address associated with the mailbox.</summary>\n        public string? EmailAddress { get; set; }\n\n        /// <summary>Total number of messages.</summary>\n        public long MessagesTotal { get; set; }\n\n        /// <summary>Total number of threads.</summary>\n        public long ThreadsTotal { get; set; }\n\n        /// <summary>Current mailbox history id.</summary>\n        public string? HistoryId { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox watch result.\n    /// </summary>\n    public sealed class GmailMailboxWatchResult {\n        /// <summary>History id returned by watch.</summary>\n        public string? HistoryId { get; set; }\n\n        /// <summary>Watch expiration in UTC when available.</summary>\n        public DateTime? ExpirationUtc { get; set; }\n\n        /// <summary>Resolved label ids used for the watch request.</summary>\n        public List<string> LabelIds { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox stop-watch result.\n    /// </summary>\n    public sealed class GmailMailboxStopWatchResult {\n        /// <summary>True when stop call succeeded.</summary>\n        public bool Stopped { get; set; }\n\n        /// <summary>True when stop succeeded because watch was already missing.</summary>\n        public bool AlreadyStopped { get; set; }\n    }\n\n    /// <summary>\n    /// Gmail mailbox history result.\n    /// </summary>\n    public sealed class GmailMailboxHistoryResult {\n        /// <summary>Resolved Gmail label id used for history query.</summary>\n        public string ResolvedLabelId { get; set; } = string.Empty;\n\n        /// <summary>Newest history id seen while reading history pages.</summary>\n        public string? NewHistoryId { get; set; }\n\n        /// <summary>Message ids that should be upserted.</summary>\n        public List<string> UpsertNativeIds { get; set; } = new();\n\n        /// <summary>Message ids that should be deleted.</summary>\n        public List<string> DeletedNativeIds { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Gmail mailbox bulk action result.\n    /// </summary>\n    public sealed class GmailMailboxBulkOperationResult : MailboxBulkOperationResult;\n\n    /// <summary>\n    /// Provider-agnostic Gmail mailbox message summary.\n    /// </summary>\n    public sealed class GmailMailboxMessageSummary {\n        /// <summary>Gmail message id.</summary>\n        public string NativeId { get; set; } = string.Empty;\n\n        /// <summary>Gmail thread id.</summary>\n        public string? NativeThreadId { get; set; }\n\n        /// <summary>Normalized message-id header value.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Sender address.</summary>\n        public string From { get; set; } = string.Empty;\n\n        /// <summary>Recipient list.</summary>\n        public string To { get; set; } = string.Empty;\n\n        /// <summary>Subject line.</summary>\n        public string? Subject { get; set; }\n\n        /// <summary>Message date/time (UTC).</summary>\n        public DateTime DateUtc { get; set; }\n\n        /// <summary>True when message has attachments.</summary>\n        public bool HasAttachments { get; set; }\n\n        /// <summary>True when message is seen.</summary>\n        public bool Seen { get; set; }\n\n        /// <summary>True when message is flagged.</summary>\n        public bool Flagged { get; set; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailMessage.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a message returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailMessage {\n    /// <summary>Unique message identifier.</summary>\n    [JsonPropertyName(\"id\")]\n    public string? Id { get; set; }\n\n    /// <summary>Thread identifier.</summary>\n    [JsonPropertyName(\"threadId\")]\n    public string? ThreadId { get; set; }\n\n    /// <summary>Message snippet.</summary>\n    [JsonPropertyName(\"snippet\")]\n    public string? Snippet { get; set; }\n\n    /// <summary>Message internal date as milliseconds since epoch.</summary>\n    [JsonPropertyName(\"internalDate\")]\n    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]\n    public long? InternalDate { get; set; }\n\n    /// <summary>Labels applied to the message.</summary>\n    [JsonPropertyName(\"labelIds\")]\n    public List<string>? LabelIds { get; set; }\n\n    /// <summary>Message payload.</summary>\n    [JsonPropertyName(\"payload\")]\n    public GmailMessagePayload? Payload { get; set; }\n\n    /// <summary>Raw MIME content encoded as base64url.</summary>\n    [JsonPropertyName(\"raw\")]\n    public string? Raw { get; set; }\n}\n\n/// <summary>\n/// Message part/payload returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailMessagePayload {\n    /// <summary>Part identifier.</summary>\n    [JsonPropertyName(\"partId\")]\n    public string? PartId { get; set; }\n\n    /// <summary>MIME type of the part.</summary>\n    [JsonPropertyName(\"mimeType\")]\n    public string? MimeType { get; set; }\n\n    /// <summary>Filename, when present (usually indicates an attachment).</summary>\n    [JsonPropertyName(\"filename\")]\n    public string? Filename { get; set; }\n\n    /// <summary>Part headers.</summary>\n    [JsonPropertyName(\"headers\")]\n    public List<GmailMessageHeader>? Headers { get; set; }\n\n    /// <summary>Part body.</summary>\n    [JsonPropertyName(\"body\")]\n    public GmailMessageBody? Body { get; set; }\n\n    /// <summary>Child parts.</summary>\n    [JsonPropertyName(\"parts\")]\n    public List<GmailMessagePayload>? Parts { get; set; }\n}\n\n/// <summary>\n/// Header entry returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailMessageHeader {\n    /// <summary>Header name.</summary>\n    [JsonPropertyName(\"name\")]\n    public string? Name { get; set; }\n\n    /// <summary>Header value.</summary>\n    [JsonPropertyName(\"value\")]\n    public string? Value { get; set; }\n}\n\n/// <summary>\n/// Message body returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailMessageBody {\n    /// <summary>Attachment identifier, when this body represents an attachment.</summary>\n    [JsonPropertyName(\"attachmentId\")]\n    public string? AttachmentId { get; set; }\n\n    /// <summary>Size of this body in bytes.</summary>\n    [JsonPropertyName(\"size\")]\n    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]\n    public long? Size { get; set; }\n\n    /// <summary>Base64url-encoded data for inline bodies (often empty for attachments).</summary>\n    [JsonPropertyName(\"data\")]\n    public string? Data { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailThread.cs",
    "content": "using System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a thread returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailThread {\n    /// <summary>Unique thread identifier.</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Thread history identifier.</summary>\n    public string? HistoryId { get; set; }\n\n    /// <summary>Thread snippet.</summary>\n    public string? Snippet { get; set; }\n\n    /// <summary>Messages contained in this thread.</summary>\n    public IList<GmailMessage>? Messages { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Gmail/GmailThreadInfo.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a thread item returned by Gmail REST API.\n/// </summary>\npublic sealed class GmailThreadInfo {\n    /// <summary>Unique thread identifier.</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Thread history identifier.</summary>\n    public string? HistoryId { get; set; }\n\n    /// <summary>Thread snippet.</summary>\n    public string? Snippet { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/GraphCredential.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents credentials required for Microsoft Graph authentication.\n/// </summary>\n/// <remarks>\n/// This POCO is typically deserialized from a secure source\n/// and passed to the <c>Graph</c> helper class.\n/// </remarks>\npublic class GraphCredential {\n    /// <summary>\n    /// Gets or sets the application (client) identifier.\n    /// </summary>\n    public string ClientId { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the directory (tenant) identifier.\n    /// </summary>\n    public string DirectoryId { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the client secret if using secret-based auth.\n    /// </summary>\n    public string? ClientSecret { get; set; }\n\n    /// <summary>\n    /// Gets or sets an access token for delegated Microsoft Graph operations.\n    /// </summary>\n    public string? AccessToken { get; set; }\n\n    /// <summary>\n    /// Gets or sets the path to a certificate used for authentication.\n    /// </summary>\n    public string? CertificatePath { get; set; }\n\n    /// <summary>\n    /// Gets or sets the certificate password.\n    /// </summary>\n    public string? CertificatePassword { get; set; }\n\n    /// <summary>\n    /// Gets or sets certificate bytes when provided programmatically.\n    /// </summary>\n    public byte[]? CertificateBytes { get; set; }\n\n    /// <summary>\n    /// Gets or sets the PEM certificate path, if applicable.\n    /// </summary>\n    public string? CertificatePemPath { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/GraphEmailMessage.cs",
    "content": "namespace Mailozaurr;\n\nusing Mailozaurr.NonDeliveryReports;\nusing MimeKit;\n\n/// <summary>\n/// Represents a MIME message retrieved via Microsoft Graph along with its identifier.\n/// </summary>\npublic class GraphEmailMessage {\n    /// <summary>\n    /// Creates a new instance of <see cref=\"GraphEmailMessage\"/>.\n    /// </summary>\n    /// <param name=\"id\">Unique identifier of the message.</param>\n    /// <param name=\"message\">The MIME message.</param>\n    /// <param name=\"nonDeliveryReports\">Optional parsed NDRs associated with the message.</param>\n    public GraphEmailMessage(string id, MimeMessage message, IList<NonDeliveryReport>? nonDeliveryReports = null) {\n        Id = id;\n        Message = message;\n        Encryption = MimeKitUtils.GetEncryption(message);\n        NonDeliveryReports = nonDeliveryReports ?? MimeKitUtils.GetNonDeliveryReports(message);\n    }\n\n    /// <summary>Unique identifier of the message.</summary>\n    public string Id { get; }\n\n    /// <summary>The underlying <see cref=\"MimeMessage\"/>.</summary>\n    public MimeMessage Message { get; }\n\n    /// <summary>Detected encryption or signature type.</summary>\n    public EmailEncryption Encryption { get; }\n\n    /// <summary>Parsed Non-Delivery Report details, if available.</summary>\n    public IList<NonDeliveryReport> NonDeliveryReports { get; }\n\n    /// <inheritdoc />\n    public override string ToString() => Message.Subject ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/CAPI.cs",
    "content": "#if !UNIX\nusing System.Runtime.InteropServices;\n\nnamespace Mailozaurr;\n\ninternal static class CAPI {\n    internal const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1;\n    internal const uint CRYPTPROTECT_LOCAL_MACHINE = 0x4;\n\n    internal const int E_FILENOTFOUND = unchecked((int)0x80070002);\n    internal const int ERROR_FILE_NOT_FOUND = 2;\n\n    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n    internal struct CRYPTOAPI_BLOB {\n        internal uint cbData;\n        internal IntPtr pbData;\n    }\n\n    internal static bool ErrorMayBeCausedByUnloadedProfile(int errorCode) {\n        return errorCode == E_FILENOTFOUND || errorCode == ERROR_FILE_NOT_FOUND;\n    }\n\n    [DllImport(\"CRYPT32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n    [return: MarshalAs(UnmanagedType.Bool)]\n    internal static extern bool CryptProtectData(\n            [In] IntPtr pDataIn,\n            [In] string szDataDescr,\n            [In] IntPtr pOptionalEntropy,\n            [In] IntPtr pvReserved,\n            [In] IntPtr pPromptStruct,\n            [In] uint dwFlags,\n            [In, Out] IntPtr pDataBlob);\n\n    [DllImport(\"CRYPT32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n    [return: MarshalAs(UnmanagedType.Bool)]\n    internal static extern bool CryptUnprotectData(\n            [In] IntPtr pDataIn,\n            [In] IntPtr ppszDataDescr,\n            [In] IntPtr pOptionalEntropy,\n            [In] IntPtr pvReserved,\n            [In] IntPtr pPromptStruct,\n            [In] uint dwFlags,\n            [In, Out] IntPtr pDataBlob);\n\n    [DllImport(\"ntdll.dll\", EntryPoint = \"RtlZeroMemory\", SetLastError = true)]\n    internal static extern void ZeroMemory(IntPtr handle, uint length);\n\n    [DllImport(\"api-ms-win-core-misc-l1-1-0.dll\", SetLastError = true)]\n    internal static extern IntPtr LocalFree(IntPtr handle);\n}\n#endif\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/DictionaryExtensions.cs",
    "content": "#if NETSTANDARD2_0 || NET472\nusing System.Collections.Generic;\n\nnamespace Mailozaurr\n{\n    internal static class DictionaryExtensions\n    {\n        public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)\n        {\n            if (dictionary.ContainsKey(key))\n            {\n                return false;\n            }\n\n            dictionary[key] = value;\n            return true;\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/EncryptionResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Helper class to return encryption results and the IV used to do the encryption.\n/// </summary>\ninternal class EncryptionResult {\n    internal EncryptionResult(string encrypted, string IV) {\n        EncryptedData = encrypted;\n        this.IV = IV;\n    }\n\n    /// <summary>Gets the encrypted data.</summary>\n    internal string EncryptedData { get; }\n\n    /// <summary>Gets the IV used to encrypt the data.</summary>\n    internal string IV { get; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/Helpers.cs",
    "content": "﻿using System.Net;\nusing System;\nusing System.Net.Http;\nusing System.Security;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Linq;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Utility methods used throughout the library.\n/// </summary>\npublic static class Helpers {\n    private static HttpClient s_sharedHttpClient = new HttpClient();\n\n    internal static HttpClient SharedHttpClient {\n        get => Volatile.Read(ref s_sharedHttpClient);\n        set {\n            if (value is null) {\n                throw new ArgumentNullException(nameof(value));\n            }\n\n            var old = Interlocked.Exchange(ref s_sharedHttpClient, value);\n            if (!ReferenceEquals(old, value)) {\n                old.Dispose();\n            }\n        }\n    }\n    /// <summary>Converts a credential into an OAuth token tuple.</summary>\n    /// <param name=\"credential\">The credential containing the token.</param>\n    /// <returns>The username and token.</returns>\n    public static (string UserName, string Token) ConvertFromOAuth2Credential(NetworkCredential credential) {\n        if (credential is null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n        return (credential.UserName, credential.Password);\n    }\n\n    /// <summary>Creates a <see cref=\"NetworkCredential\"/> from plain text.</summary>\n    /// <param name=\"userName\">The user name.</param>\n    /// <param name=\"password\">The password.</param>\n    /// <returns>The resulting credential.</returns>\n    /// <exception cref=\"ArgumentNullException\">\n    /// Thrown when <paramref name=\"userName\"/> or <paramref name=\"password\"/> is <see langword=\"null\"/>.\n    /// </exception>\n    /// <exception cref=\"ArgumentException\">\n    /// Thrown when <paramref name=\"userName\"/> or <paramref name=\"password\"/> is empty.\n    /// </exception>\n    public static NetworkCredential ConvertFromPlainText(string userName, string password) {\n        if (userName is null) {\n            throw new ArgumentNullException(nameof(userName));\n        }\n\n        if (userName.Length == 0) {\n            throw new ArgumentException(\"Value cannot be empty.\", nameof(userName));\n        }\n\n        if (password is null) {\n            throw new ArgumentNullException(nameof(password));\n        }\n\n        if (password.Length == 0) {\n            throw new ArgumentException(\"Value cannot be empty.\", nameof(password));\n        }\n\n        var secStringPassword = new SecureString();\n        foreach (char c in password) {\n            secStringPassword.AppendChar(c);\n        }\n        secStringPassword.MakeReadOnly();\n        return new NetworkCredential(userName, secStringPassword);\n    }\n\n    /// <summary>Extracts the API key from a credential object.</summary>\n    /// <param name=\"credentials\">Credential containing the key.</param>\n    /// <returns>The API key.</returns>\n    /// <exception cref=\"ArgumentException\">\n    /// Thrown when <paramref name=\"credentials\"/> is not a <see cref=\"NetworkCredential\"/>.\n    /// </exception>\n    public static string CredentialToApiKey(ICredentials credentials) {\n        if (credentials is NetworkCredential networkCredential) {\n            return networkCredential.Password;\n        }\n        throw new ArgumentException(\"Credential must be of type NetworkCredential\", nameof(credentials));\n    }\n\n    /// <summary>Retrieves the email address string from various types of objects.</summary>\n    /// <param name=\"from\">String or dictionary representation.</param>\n    /// <returns>The email address.</returns>\n    public static string GetEmailAddress(object from) {\n        if (from is string s) {\n            return s;\n        }\n        if (from is IDictionary<string, object> dict) {\n            if (dict.TryGetValue(\"Email\", out var emailObj)) {\n                return emailObj?.ToString() ?? string.Empty;\n            }\n            return string.Empty;\n        }\n        return from?.ToString() ?? string.Empty;\n    }\n\n    /// <summary>Creates an object representing the sender.</summary>\n    /// <param name=\"email\">Email address.</param>\n    /// <param name=\"name\">Display name.</param>\n    /// <returns>The object to be used as sender.</returns>\n    public static object GetFromObject(string email, string name) {\n        if (!string.IsNullOrWhiteSpace(name)) {\n            return new Dictionary<string, object> { { \"Name\", name }, { \"Email\", email } };\n        }\n        return email;\n    }\n\n    /// <summary>Parses an object into an email and optional name.</summary>\n    /// <param name=\"from\">String or dictionary representation.</param>\n    /// <returns>Tuple containing the email and name.</returns>\n    public static (string? Email, string? Name) GetEmailAndName(object? from) {\n        if (from is string s) {\n            return (s, null);\n        }\n        if (from is IDictionary dict) {\n            var email = dict.Contains(\"Email\") ? dict[\"Email\"]?.ToString() : null;\n            var name = dict.Contains(\"Name\") ? dict[\"Name\"]?.ToString() : null;\n            return (email, name);\n        }\n        return (from?.ToString(), null);\n    }\n\n    /// <summary>\n    /// Enumerates unique address objects based on email value using a hash set to track already yielded addresses.\n    /// </summary>\n    /// <param name=\"addresses\">Collection of address objects.</param>\n    /// <param name=\"seen\">Optional hash set tracking emails that were already yielded. If <see langword=\"null\"/> a new set is created.</param>\n    /// <returns>Unique address objects.</returns>\n    public static IEnumerable<object> UniqueAddresses(IEnumerable<object>? addresses, HashSet<string>? seen) {\n        if (addresses == null) yield break;\n\n        seen ??= new HashSet<string>();\n\n        foreach (var address in addresses) {\n            var email = GetEmailAddress(address);\n            if (string.IsNullOrWhiteSpace(email)) {\n                continue;\n            }\n\n            var normalized = string\n                .Concat(email.Where(c => !char.IsWhiteSpace(c)))\n                .ToLowerInvariant();\n            if (seen.Add(normalized)) {\n                yield return address;\n            }\n        }\n    }\n\n    /// <summary>\n    /// Determines whether the specified exception represents a transient error\n    /// that can be retried safely.\n    /// </summary>\n    /// <param name=\"ex\">The exception to inspect.</param>\n    /// <returns><c>true</c> if the error is transient; otherwise <c>false</c>.</returns>\n    public static bool IsTransient(Exception ex) {\n        switch (ex) {\n            case HttpRequestException httpEx:\n                // HttpRequestException.StatusCode was introduced in .NET 5.0\n#if NET5_0_OR_GREATER\n                if (httpEx.StatusCode.HasValue) {\n                    var code = (int)httpEx.StatusCode.Value;\n                    return code >= 500 || code == 408 || code == 429;\n                }\n#endif\n                return true;\n            case SmtpCommandException smtpEx:\n                var smtpCode = (int)smtpEx.StatusCode;\n                return smtpCode >= 400 && smtpCode < 500;\n            case SmtpProtocolException:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    /// <summary>\n    /// Posts the provided <see cref=\"SmtpResult\"/> to a webhook endpoint.\n    /// </summary>\n    /// <param name=\"url\">Destination webhook URL.</param>\n    /// <param name=\"result\">Result object describing the send operation.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the request.</param>\n    /// <param name=\"client\">Optional HTTP client to reuse.</param>\n    public static async Task PostWebhookAsync(string? url, SmtpResult result, CancellationToken cancellationToken = default, HttpClient? client = null) {\n        if (string.IsNullOrWhiteSpace(url)) {\n            return;\n        }\n\n        client ??= SharedHttpClient;\n\n        try {\n            var json = JsonSerializer.Serialize(result, MailozaurrJsonContext.Default.SmtpResult);\n            using var content = new StringContent(json, Encoding.UTF8, \"application/json\");\n            using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false);\n            if (!response.IsSuccessStatusCode) {\n                LoggingMessages.Logger.WriteWarning(\n                    $\"Failed to post webhook: {(int)response.StatusCode} {response.ReasonPhrase}\");\n            }\n        } catch (HttpRequestException ex) {\n            LoggingMessages.Logger.WriteWarning($\"Failed to post webhook: {ex.Message}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/HtmlUtils.cs",
    "content": "using System.Net.Http;\nusing System.Threading;\nusing System.Text.RegularExpressions;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper utilities for working with HTML content.\n/// </summary>\n/// <remarks>\n/// Methods on this class assist with embedding images and\n/// performing minor HTML transformations.\n/// </remarks>\npublic static class HtmlUtils {\n    internal static HttpClient HttpClient { get; } = new HttpClient();\n\n    private static readonly Regex ImageSrcRegex = new(\"(?<=<img[^>]+src=[\\\"'])([^\\\"']+)(?=[\\\"'])\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n    static HtmlUtils() {\n        AppDomain.CurrentDomain.ProcessExit += (_, _) => HttpClient.Dispose();\n    }\n\n    /// <summary>\n    /// Represents an image downloaded from a remote location for\n    /// embedding into an HTML message.\n    /// </summary>\n    public class RemoteImage {\n        /// <summary>Content identifier used when embedding.</summary>\n        public string ContentId { get; set; } = string.Empty;\n        /// <summary>Binary data of the image.</summary>\n        public byte[] Data { get; set; } = Array.Empty<byte>();\n        /// <summary>MIME type of the image data.</summary>\n        public string MediaType { get; set; } = string.Empty;\n    }\n    /// <summary>\n    /// Replaces local image <c>src</c> references with <c>cid:</c> links and returns the\n    /// updated HTML and a collection of the embedded file paths.\n    /// </summary>\n    /// <param name=\"html\">HTML content that may contain local image paths.</param>\n    /// <returns>The updated HTML and list of file paths that were replaced.</returns>\n    public static (string Html, List<string> Paths) ExtractLocalImagePaths(string html) {\n        var paths = new List<string>();\n        if (string.IsNullOrWhiteSpace(html)) return (html, paths);\n\n        html = ImageSrcRegex.Replace(html, match => {\n            var path = match.Value;\n            if (string.IsNullOrWhiteSpace(path)) return path;\n            if (path.StartsWith(\"http\", StringComparison.OrdinalIgnoreCase) ||\n                path.StartsWith(\"cid:\", StringComparison.OrdinalIgnoreCase) ||\n                path.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase)) {\n                return path;\n            }\n            if (File.Exists(path)) {\n                var fileName = Path.GetFileName(path);\n                paths.Add(path);\n                return $\"cid:{fileName}\";\n            }\n            return path;\n        });\n        return (html, paths);\n    }\n\n    /// <summary>\n    /// Downloads externally referenced images and replaces their sources with cid links.\n    /// </summary>\n    /// <param name=\"html\">HTML content to inspect.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Modified HTML and list of downloaded images.</returns>\n    public static async Task<(string Html, List<RemoteImage> Images)> DownloadRemoteImagesAsync(string html, CancellationToken cancellationToken = default) {\n        var images = new List<RemoteImage>();\n        if (string.IsNullOrWhiteSpace(html)) return (html, images);\n\n        var matches = ImageSrcRegex.Matches(html);\n        var replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n\n        foreach (Match match in matches) {\n            var url = match.Value;\n            if (string.IsNullOrWhiteSpace(url)) continue;\n            if (!url.StartsWith(\"http\", StringComparison.OrdinalIgnoreCase)) continue;\n            if (replacements.ContainsKey(url)) continue;\n            try {\n                using var response = await HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);\n                if (!response.IsSuccessStatusCode) continue;\n#if NETFRAMEWORK || NETSTANDARD2_0\n                var data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);\n#else\n                var data = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);\n#endif\n                var mediaType = response.Content.Headers.ContentType?.MediaType ?? MimeTypes.GetMimeType(Path.GetFileName(url));\n                var fileName = Path.GetFileName(new Uri(url).AbsolutePath);\n                if (string.IsNullOrEmpty(fileName)) fileName = Guid.NewGuid().ToString(\"N\");\n                replacements[url] = $\"cid:{fileName}\";\n                images.Add(new RemoteImage { ContentId = fileName, Data = data, MediaType = mediaType });\n            } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) {\n                throw new OperationCanceledException(ex.Message, ex, cancellationToken);\n            } catch (Exception ex) {\n                LoggingMessages.Logger.WriteWarning($\"Failed to download image '{url}': {ex.Message}\");\n            }\n        }\n\n        html = ImageSrcRegex.Replace(html, m => replacements.TryGetValue(m.Value, out var value) ? value : m.Value);\n\n        return (html, images);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/LimitedStream.cs",
    "content": "namespace Mailozaurr;\n\nusing System;\nusing System.IO;\n\n/// <summary>\n/// Stream wrapper that limits the number of bytes that can be read.\n/// </summary>\ninternal sealed class LimitedStream : Stream {\n    private readonly Stream _inner;\n    private readonly long _maxBytes;\n    private long _totalBytes;\n\n    public LimitedStream(Stream inner, long maxBytes) {\n        _inner = inner;\n        _maxBytes = maxBytes;\n    }\n\n    public override bool CanRead => _inner.CanRead;\n    public override bool CanSeek => false;\n    public override bool CanWrite => false;\n    public override long Length => _inner.Length;\n    public override long Position {\n        get => _inner.Position;\n        set => throw new NotSupportedException();\n    }\n\n    public override void Flush() => _inner.Flush();\n\n    public override int Read(byte[] buffer, int offset, int count) {\n        var remaining = _maxBytes - _totalBytes;\n        if (remaining <= 0) throw new InvalidDataException(\"Uncompressed data exceeds allowed limit.\");\n        if (count > remaining) count = (int)remaining;\n        var read = _inner.Read(buffer, offset, count);\n        _totalBytes += read;\n        if (_totalBytes > _maxBytes) throw new InvalidDataException(\"Uncompressed data exceeds allowed limit.\");\n        return read;\n    }\n\n    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();\n    public override void SetLength(long value) => throw new NotSupportedException();\n    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/ProtectedData.cs",
    "content": "#if WINDOWS\nusing System;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography;\n\nnamespace Mailozaurr;\n\ninternal static class ProtectedData {\n    /// <summary>Protect.</summary>\n    public static byte[] Protect(byte[] userData, byte[]? optionalEntropy, System.Security.Cryptography.DataProtectionScope scope) {\n        if (userData.Length == 0) {\n            throw new ArgumentException(\"Cryptography_DpApi_InvalidDataToProtect\", nameof(userData));\n        }\n\n        GCHandle pbDataIn = new();\n        GCHandle pOptionalEntropy = new();\n        CAPI.CRYPTOAPI_BLOB blob = new();\n\n        try {\n            pbDataIn = GCHandle.Alloc(userData, GCHandleType.Pinned);\n            CAPI.CRYPTOAPI_BLOB dataIn = new() {\n                cbData = (uint)userData.Length,\n                pbData = pbDataIn.AddrOfPinnedObject()\n            };\n            CAPI.CRYPTOAPI_BLOB entropy = new();\n            if (optionalEntropy != null) {\n                pOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);\n                entropy.cbData = (uint)optionalEntropy.Length;\n                entropy.pbData = pOptionalEntropy.AddrOfPinnedObject();\n            }\n\n            uint dwFlags = CAPI.CRYPTPROTECT_UI_FORBIDDEN;\n            if (scope == DataProtectionScope.LocalMachine)\n                dwFlags |= CAPI.CRYPTPROTECT_LOCAL_MACHINE;\n            unsafe {\n                if (!CAPI.CryptProtectData(new IntPtr(&dataIn), string.Empty, new IntPtr(&entropy), IntPtr.Zero, IntPtr.Zero, dwFlags, new IntPtr(&blob))) {\n                    int lastWin32Error = Marshal.GetLastWin32Error();\n                    if (CAPI.ErrorMayBeCausedByUnloadedProfile(lastWin32Error)) {\n                        throw new CryptographicException(\"Cryptography_DpApi_ProfileMayNotBeLoaded\");\n                    } else {\n                        throw new CryptographicException(lastWin32Error);\n                    }\n                }\n            }\n\n            if (blob.pbData == IntPtr.Zero) {\n                throw new OutOfMemoryException();\n            }\n\n            byte[] encryptedData = new byte[(int)blob.cbData];\n            Marshal.Copy(blob.pbData, encryptedData, 0, encryptedData.Length);\n\n            return encryptedData;\n        } finally {\n            if (pbDataIn.IsAllocated) pbDataIn.Free();\n            if (pOptionalEntropy.IsAllocated) pOptionalEntropy.Free();\n            if (blob.pbData != IntPtr.Zero) {\n                CAPI.ZeroMemory(blob.pbData, blob.cbData);\n                CAPI.LocalFree(blob.pbData);\n            }\n        }\n    }\n\n    /// <summary>Unprotect.</summary>\n    public static byte[] Unprotect(byte[] encryptedData, byte[]? optionalEntropy, System.Security.Cryptography.DataProtectionScope scope) {\n        if (encryptedData.Length == 0) {\n            throw new ArgumentException(\"Cryptography_DpApi_InvalidDataToUnprotect\", nameof(encryptedData));\n        }\n\n        GCHandle pbDataIn = new();\n        GCHandle pOptionalEntropy = new();\n        CAPI.CRYPTOAPI_BLOB userData = new();\n\n        try {\n            pbDataIn = GCHandle.Alloc(encryptedData, GCHandleType.Pinned);\n            CAPI.CRYPTOAPI_BLOB dataIn = new() {\n                cbData = (uint)encryptedData.Length,\n                pbData = pbDataIn.AddrOfPinnedObject()\n            };\n            CAPI.CRYPTOAPI_BLOB entropy = new();\n            if (optionalEntropy != null) {\n                pOptionalEntropy = GCHandle.Alloc(optionalEntropy, GCHandleType.Pinned);\n                entropy.cbData = (uint)optionalEntropy.Length;\n                entropy.pbData = pOptionalEntropy.AddrOfPinnedObject();\n            }\n\n            uint dwFlags = CAPI.CRYPTPROTECT_UI_FORBIDDEN;\n            if (scope == DataProtectionScope.LocalMachine) {\n                dwFlags |= CAPI.CRYPTPROTECT_LOCAL_MACHINE;\n            }\n\n            unsafe {\n                if (!CAPI.CryptUnprotectData(new IntPtr(&dataIn), IntPtr.Zero, new IntPtr(&entropy), IntPtr.Zero, IntPtr.Zero, dwFlags, new IntPtr(&userData))) {\n                    throw new CryptographicException(Marshal.GetLastWin32Error());\n                }\n            }\n\n            if (userData.pbData == IntPtr.Zero) {\n                throw new OutOfMemoryException();\n            }\n\n            byte[] data = new byte[(int)userData.cbData];\n            Marshal.Copy(userData.pbData, data, 0, data.Length);\n\n            return data;\n        } finally {\n            if (pbDataIn.IsAllocated) pbDataIn.Free();\n            if (pOptionalEntropy.IsAllocated) pOptionalEntropy.Free();\n            if (userData.pbData != IntPtr.Zero) {\n                CAPI.ZeroMemory(userData.pbData, userData.cbData);\n                CAPI.LocalFree(userData.pbData);\n            }\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Mailozaurr/Helpers/SecureStringHelper.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.Runtime.InteropServices;\nusing System.Security;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr {\n    /// <summary>\n    /// Helper class for secure string related functionality.\n    /// </summary>\n    internal static class SecureStringHelper {\n        // Some random hex characters to identify the beginning of a\n        // V2-exported SecureString.\n        internal static readonly string SecureStringExportHeader = \"76492d1116743f0423413b16050a5345\";\n\n        /// <summary>\n        /// Create a new SecureString based on the specified binary data.\n        ///\n        /// The binary data must be byte[] version of unicode char[],\n        /// otherwise the results are unpredictable.\n        /// </summary>\n        /// <param name=\"data\">Input data.</param>\n        /// <returns>A SecureString .</returns>\n        private static SecureString New(byte[] data) {\n            if ((data.Length % 2) != 0) {\n                // If the data is not an even length, they supplied an invalid key\n                const string InvalidKey = \"The provided key is invalid.\";\n                throw new ArgumentException(InvalidKey);\n            }\n\n            char ch;\n            SecureString ss = new SecureString();\n\n            //\n            // each unicode char is 2 bytes.\n            //\n            int len = data.Length / 2;\n\n            for (int i = 0; i < len; i++) {\n                ch = (char)(data[2 * i + 1] * 256 + data[2 * i]);\n                ss.AppendChar(ch);\n\n                //\n                // zero out the data slots as soon as we use them\n                //\n                data[2 * i] = 0;\n                data[2 * i + 1] = 0;\n            }\n\n            return ss;\n        }\n\n        /// <summary>\n        /// Get the contents of a SecureString as byte[]\n        /// </summary>\n        /// <param name=\"s\">Input string.</param>\n        /// <returns>Contents of s (char[]) converted to byte[].</returns>\n        internal static byte[] GetData(SecureString s) {\n            //\n            // each unicode char is 2 bytes.\n            //\n            byte[] data = new byte[s.Length * 2];\n\n            if (s.Length > 0) {\n                IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(s);\n\n                try {\n                    Marshal.Copy(ptr, data, 0, data.Length);\n                } finally {\n                    Marshal.ZeroFreeCoTaskMemUnicode(ptr);\n                }\n            }\n\n            return data;\n        }\n\n        /// <summary>\n        /// Encode the specified byte[] as a unicode string.\n        ///\n        /// Currently we use simple hex encoding but this\n        /// method can be changed to use a better encoding\n        /// such as base64.\n        /// </summary>\n        /// <param name=\"data\">Binary data to encode.</param>\n        /// <returns>A string representing encoded data.</returns>\n        internal static string ByteArrayToString(byte[] data) {\n            StringBuilder sb = new StringBuilder();\n\n            for (int i = 0; i < data.Length; i++) {\n                sb.Append(data[i].ToString(\"x2\", System.Globalization.CultureInfo.InvariantCulture));\n            }\n\n            return sb.ToString();\n        }\n\n        /// <summary>\n        /// Convert a string obtained using ByteArrayToString()\n        /// back to byte[] format.\n        /// </summary>\n        /// <param name=\"s\">Encoded input string.</param>\n        /// <returns>Bin data as byte[].</returns>\n        internal static byte[] ByteArrayFromString(string s) {\n            //\n            // two hex chars per byte\n            //\n            int dataLen = s.Length / 2;\n            byte[] data = new byte[dataLen];\n\n            if (s.Length > 0) {\n                for (int i = 0; i < dataLen; i++) {\n                    data[i] = byte.Parse(s.AsSpan(2 * i, 2).ToString(),\n                        NumberStyles.AllowHexSpecifier,\n                        System.Globalization.CultureInfo.InvariantCulture);\n                }\n            }\n\n            return data;\n        }\n\n        /// <summary>\n        /// Return contents of the SecureString after encrypting\n        /// using DPAPI and encoding the encrypted blob as a string.\n        /// </summary>\n        /// <param name=\"input\">SecureString to protect.</param>\n        /// <returns>A string (see summary) .</returns>\n        internal static string Protect(SecureString input) {\n            //Utils.CheckSecureStringArg(input, \"input\");\n\n            string output = string.Empty;\n            byte[] data = Array.Empty<byte>();\n            byte[] protectedData = Array.Empty<byte>();\n\n            data = GetData(input);\n#if UNIX\n            // DPAPI doesn't exist on UNIX so we simply use the string as a byte-array\n            protectedData = data;\n#else\n            protectedData = ProtectedData.Protect(data, null,\n                                                  DataProtectionScope.CurrentUser);\n            for (int i = 0; i < data.Length; i++) {\n                data[i] = 0;\n            }\n#endif\n\n            output = ByteArrayToString(protectedData);\n\n            return output;\n        }\n\n        /// <summary>\n        /// Decrypts the specified string using DPAPI and return\n        /// equivalent SecureString.\n        ///\n        /// The string must be obtained earlier by a call to Protect()\n        /// </summary>\n        /// <param name=\"input\">Encrypted string.</param>\n        /// <returns>SecureString .</returns>\n        internal static SecureString Unprotect(string input) {\n            //Utils.CheckArgForNullOrEmpty(input, \"input\");\n            //if ((input.Length % 2) != 0) {\n            //    throw PSTraceSource.NewArgumentException(nameof(input), Serialization.InvalidEncryptedString, input);\n            //}\n\n            byte[] data = Array.Empty<byte>();\n            byte[] protectedData = Array.Empty<byte>();\n            SecureString s;\n\n            protectedData = ByteArrayFromString(input);\n\n#if UNIX\n            // DPAPI isn't supported in UNIX, so we just translate the byte-array back to a string\n            data = protectedData;\n#else\n            data = ProtectedData.Unprotect(protectedData, null,\n                                           DataProtectionScope.CurrentUser);\n\n#endif\n            s = New(data);\n\n            return s;\n        }\n\n        /// <summary>\n        /// Return contents of the SecureString after encrypting\n        /// using the specified key and encoding the encrypted blob as a string.\n        /// </summary>\n        /// <param name=\"input\">Input string to encrypt.</param>\n        /// <param name=\"key\">Encryption key.</param>\n        /// <returns>A string (see summary).</returns>\n        internal static EncryptionResult Encrypt(SecureString input, SecureString key) {\n            //\n            // get clear text key from the SecureString key\n            //\n            byte[] keyBlob = GetData(key);\n\n            //\n            // encrypt the data\n            //\n            try {\n                return Encrypt(input, keyBlob);\n            } finally {\n                Array.Clear(keyBlob, 0, keyBlob.Length);\n            }\n        }\n\n        /// <summary>\n        /// Return contents of the SecureString after encrypting\n        /// using the specified key and encoding the encrypted blob as a string.\n        /// </summary>\n        /// <param name=\"input\">Input string to encrypt.</param>\n        /// <param name=\"key\">Encryption key.</param>\n        /// <returns>A string (see summary).</returns>\n        internal static EncryptionResult Encrypt(SecureString input, byte[] key) {\n            return Encrypt(input, key, null);\n        }\n\n        internal static EncryptionResult Encrypt(SecureString input, byte[] key, byte[]? iv) {\n            //Utils.CheckSecureStringArg(input, \"input\");\n            //Utils.CheckKeyArg(key, \"key\");\n\n            //\n            // prepare the crypto stuff. Initialization Vector is\n            // randomized by default.\n            //\n            using (Aes aes = Aes.Create()) {\n                iv ??= aes.IV;\n\n                //\n                // get clear text data from the input SecureString\n                //\n                byte[] data = GetData(input);\n                try {\n                    using (ICryptoTransform encryptor = aes.CreateEncryptor(key, iv))\n                    using (var sourceStream = new MemoryStream(data))\n                    using (var encryptedStream = new MemoryStream()) {\n                        //\n                        // encrypt it\n                        //\n                        using (var cryptoStream = new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write)) {\n                            sourceStream.CopyTo(cryptoStream);\n                        }\n\n                        //\n                        // return encrypted data\n                        //\n                        byte[] encryptedData = encryptedStream.ToArray();\n                        return new EncryptionResult(ByteArrayToString(encryptedData), Convert.ToBase64String(iv));\n                    }\n                } finally {\n                    Array.Clear(data, 0, data.Length);\n                }\n            }\n        }\n\n        /// <summary>\n        /// Asynchronously encrypts the SecureString using the specified key and\n        /// returns the encrypted blob with the initialization vector.\n        /// </summary>\n        internal static async Task<EncryptionResult> EncryptAsync(\n            SecureString input,\n            byte[] key,\n            byte[]? iv = null,\n            CancellationToken cancellationToken = default) {\n            using Aes aes = Aes.Create();\n            iv ??= aes.IV;\n\n            byte[] data = GetData(input);\n            try {\n                using ICryptoTransform encryptor = aes.CreateEncryptor(key, iv);\n#if NETFRAMEWORK || NETSTANDARD2_0\n                using var sourceStream = new MemoryStream(data);\n                using var encryptedStream = new MemoryStream();\n                using var cryptoStream = new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write);\n                await sourceStream.CopyToAsync(cryptoStream, 81920, cancellationToken).ConfigureAwait(false);\n                cryptoStream.FlushFinalBlock();\n#else\n                await using var sourceStream = new MemoryStream(data);\n                await using var encryptedStream = new MemoryStream();\n                await using var cryptoStream = new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write);\n                await sourceStream.CopyToAsync(cryptoStream, cancellationToken).ConfigureAwait(false);\n                await cryptoStream.FlushAsync(cancellationToken).ConfigureAwait(false);\n                cryptoStream.FlushFinalBlock();\n#endif\n                byte[] encryptedData = encryptedStream.ToArray();\n                return new EncryptionResult(ByteArrayToString(encryptedData), Convert.ToBase64String(iv));\n            } finally {\n                Array.Clear(data, 0, data.Length);\n            }\n        }\n\n        internal static async Task<EncryptionResult> EncryptAsync(\n            SecureString input,\n            SecureString key,\n            CancellationToken cancellationToken = default) {\n            byte[] keyBlob = GetData(key);\n            try {\n                return await EncryptAsync(input, keyBlob, null, cancellationToken).ConfigureAwait(false);\n            } finally {\n                Array.Clear(keyBlob, 0, keyBlob.Length);\n            }\n        }\n\n        /// <summary>\n        /// Decrypts the specified string using the specified key\n        /// and return equivalent SecureString.\n        ///\n        /// The string must be obtained earlier by a call to Encrypt()\n        /// </summary>\n        /// <param name=\"input\">Encrypted string.</param>\n        /// <param name=\"key\">Encryption key.</param>\n        /// <param name=\"IV\">Encryption initialization vector. If this is set to null, the method uses internally computed strong random number as IV.</param>\n        /// <returns>SecureString .</returns>\n        internal static SecureString Decrypt(string input, SecureString key, byte[] IV) {\n            //\n            // get clear text key from the SecureString key\n            //\n            byte[] keyBlob = GetData(key);\n\n            //\n            // decrypt the data\n            //\n            try {\n                return Decrypt(input, keyBlob, IV);\n            } finally {\n                Array.Clear(keyBlob, 0, keyBlob.Length);\n            }\n        }\n\n        /// <summary>\n        /// Decrypts the specified string using the specified key\n        /// and return equivalent SecureString.\n        ///\n        /// The string must be obtained earlier by a call to Encrypt()\n        /// </summary>\n        /// <param name=\"input\">Encrypted string.</param>\n        /// <param name=\"key\">Encryption key.</param>\n        /// <param name=\"IV\">Encryption initialization vector. If this is set to null, the method uses internally computed strong random number as IV.</param>\n        /// <returns>SecureString .</returns>\n        internal static SecureString Decrypt(string input, byte[] key, byte[] IV) {\n            //Utils.CheckArgForNullOrEmpty(input, \"input\");\n            //Utils.CheckKeyArg(key, \"key\");\n\n            //\n            // prepare the crypto stuff\n            //\n            using (var aes = Aes.Create()) {\n                using (ICryptoTransform decryptor = aes.CreateDecryptor(key, IV ?? aes.IV))\n                using (var encryptedStream = new MemoryStream(ByteArrayFromString(input)))\n                using (var targetStream = new MemoryStream()) {\n                    //\n                    // decrypt the data and return as SecureString\n                    //\n                    using (var sourceStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read)) {\n                        sourceStream.CopyTo(targetStream);\n                    }\n\n                    byte[] decryptedData = targetStream.ToArray();\n                    try {\n                        return New(decryptedData);\n                    } finally {\n                        Array.Clear(decryptedData, 0, decryptedData.Length);\n                    }\n                }\n            }\n        }\n\n        /// <summary>\n        /// Asynchronously decrypts the specified string using the provided key.\n        /// </summary>\n        internal static async Task<SecureString> DecryptAsync(\n            string input,\n            byte[] key,\n            byte[] iv,\n            CancellationToken cancellationToken = default) {\n            using var aes = Aes.Create();\n            using ICryptoTransform decryptor = aes.CreateDecryptor(key, iv ?? aes.IV);\n#if NETFRAMEWORK || NETSTANDARD2_0\n            using var encryptedStream = new MemoryStream(ByteArrayFromString(input));\n            using var targetStream = new MemoryStream();\n            using var sourceStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read);\n            await sourceStream.CopyToAsync(targetStream, 81920, cancellationToken).ConfigureAwait(false);\n#else\n            await using var encryptedStream = new MemoryStream(ByteArrayFromString(input));\n            await using var targetStream = new MemoryStream();\n            await using var sourceStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read);\n            await sourceStream.CopyToAsync(targetStream, cancellationToken).ConfigureAwait(false);\n#endif\n            byte[] decryptedData = targetStream.ToArray();\n            try {\n                return New(decryptedData);\n            } finally {\n                Array.Clear(decryptedData, 0, decryptedData.Length);\n            }\n        }\n\n        internal static async Task<SecureString> DecryptAsync(\n            string input,\n            SecureString key,\n            byte[] iv,\n            CancellationToken cancellationToken = default) {\n            byte[] keyBlob = GetData(key);\n            try {\n                return await DecryptAsync(input, keyBlob, iv, cancellationToken).ConfigureAwait(false);\n            } finally {\n                Array.Clear(keyBlob, 0, keyBlob.Length);\n            }\n        }\n\n#nullable enable\n        /// <summary>Creates a new <see cref=\"SecureString\"/> from a <see cref=\"string\"/>.</summary>\n        /// <param name=\"plainTextString\">Plain text string. Must not be null.</param>\n        /// <returns>A new SecureString.</returns>\n        internal static SecureString FromPlainTextString(string? plainTextString) {\n            if (string.IsNullOrEmpty(plainTextString)) {\n                return new SecureString();\n            }\n\n            string inputString = plainTextString!;\n            SecureString secureString = new();\n            foreach (char c in inputString) {\n                secureString.AppendChar(c);\n            }\n\n            secureString.MakeReadOnly();\n            return secureString;\n        }\n#nullable restore\n    }\n\n}"
  },
  {
    "path": "Sources/Mailozaurr/IDmarcReportService.cs",
    "content": "using Mailozaurr.DmarcReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Defines operations for searching DMARC reports.\n/// </summary>\npublic interface IDmarcReportService {\n    /// <summary>Searches for DMARC aggregate reports.</summary>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"domain\">Optional domain filter.</param>\n    /// <param name=\"maxResults\">Maximum number of results to return. Use 0 for unlimited.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    Task<IList<DmarcReport>> SearchAsync(\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/INonDeliveryReportService.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Defines operations for searching Non-Delivery Reports (DSNs).\n/// </summary>\npublic interface INonDeliveryReportService {\n    /// <summary>Searches for Non-Delivery Reports.</summary>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    /// <param name=\"maxResults\">Maximum number of results to return. Use 0 for unlimited.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    Task<IList<NonDeliveryReportResult>> SearchAsync(\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/ImapEmailMessage.cs",
    "content": "using MailKit;\nusing Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents an IMAP email message along with its unique identifier.\n/// </summary>\n/// <remarks>\n/// Provides a small wrapper over <see cref=\"MimeMessage\"/> so that\n/// metadata such as encryption state can be carried with the message.\n/// </remarks>\npublic class ImapEmailMessage {\n    /// <summary>\n    /// Creates a new instance of <see cref=\"ImapEmailMessage\"/>.\n    /// </summary>\n    /// <param name=\"uid\">Unique identifier of the message.</param>\n    /// <param name=\"message\">The actual MIME message.</param>\n    /// <param name=\"nonDeliveryReports\">Optional parsed NDRs associated with the message.</param>\n    public ImapEmailMessage(UniqueId uid, MimeMessage message, IList<NonDeliveryReport>? nonDeliveryReports = null) {\n        Uid = uid;\n        Message = message;\n        Encryption = MimeKitUtils.GetEncryption(message);\n        NonDeliveryReports = nonDeliveryReports ?? MimeKitUtils.GetNonDeliveryReports(message);\n    }\n\n    /// <summary>Unique identifier of the message.</summary>\n    public UniqueId Uid { get; }\n\n    /// <summary>The underlying <see cref=\"MimeMessage\"/>.</summary>\n    public MimeMessage Message { get; }\n\n    /// <summary>Detected encryption or signature type.</summary>\n    public EmailEncryption Encryption { get; }\n\n    /// <summary>Parsed Non-Delivery Report details, if available.</summary>\n    public IList<NonDeliveryReport> NonDeliveryReports { get; }\n\n    /// <inheritdoc />\n    public override string ToString() => Message.Subject ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/ImapMessageInfo.cs",
    "content": "using System;\nusing System.Linq;\nusing MailKit;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a user friendly view over an IMAP email message.\n/// </summary>\n/// <remarks>\n/// This class flattens common message properties so that scripts\n/// can easily inspect or output them without navigating the full\n/// MIME structure.\n/// </remarks>\npublic class ImapMessageInfo {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ImapMessageInfo\"/> class.\n    /// </summary>\n    /// <param name=\"message\">The underlying IMAP email message.</param>\n    public ImapMessageInfo(ImapEmailMessage message) {\n        Raw = message;\n    }\n\n    /// <summary>The wrapped <see cref=\"ImapEmailMessage\"/>.</summary>\n    public ImapEmailMessage Raw { get; }\n\n    /// <summary>Unique identifier of the message.</summary>\n    public UniqueId Uid => Raw.Uid;\n\n    /// <summary>Sender addresses.</summary>\n    public string From => string.Join(\", \", Raw.Message.From.Mailboxes.Select(m => m.ToString()));\n\n    /// <summary>Recipient addresses.</summary>\n    public string To => string.Join(\", \", Raw.Message.To.Mailboxes.Select(m => m.ToString()));\n\n    /// <summary>Subject of the message.</summary>\n    public string? Subject => Raw.Message.Subject;\n\n    /// <summary>Date the message was sent.</summary>\n    public DateTime Date => Raw.Message.Date.DateTime;\n\n    /// <summary>Plain text body.</summary>\n    public string? TextBody => Raw.Message.TextBody;\n\n    /// <summary>HTML body.</summary>\n    public string? HtmlBody => Raw.Message.HtmlBody;\n\n    /// <summary>Message priority.</summary>\n    public MessagePriority Priority => Raw.Message.Priority switch {\n        MimeKit.MessagePriority.Urgent => MessagePriority.High,\n        MimeKit.MessagePriority.NonUrgent => MessagePriority.Low,\n        _ => MessagePriority.Normal,\n    };\n\n    /// <summary>Whether the message has any attachments.</summary>\n    public bool HasAttachments => Raw.Message.Attachments.Any();\n\n    /// <summary>Encryption or signature detected.</summary>\n    public EmailEncryption Encryption => Raw.Encryption;\n\n    /// <inheritdoc />\n    public override string ToString() => Subject ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Logging/InternalLogger.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Internal logger that allows writing diagnostic messages to various streams.\n/// </summary>\n/// <remarks>\n/// This logger provides the base implementation for the public\n/// cmdlet logging interface.\n/// </remarks>\npublic class InternalLogger {\n    private readonly object _lock = new object();\n\n    /// <summary>\n    /// Occurs when a verbose message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnVerboseMessage;\n\n    /// <summary>\n    /// Occurs when a warning message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnWarningMessage;\n\n    /// <summary>\n    /// Occurs when an error message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnErrorMessage;\n\n    /// <summary>\n    /// Occurs when a debug message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnDebugMessage;\n\n    /// <summary>\n    /// Occurs when a progress message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnProgressMessage;\n\n    /// <summary>\n    /// Occurs when an information message is logged.\n    /// </summary>\n    public event EventHandler<LogEventArgs>? OnInformationMessage;\n\n    /// <summary>\n    /// Gets or sets a value indicating whether verbose messages should be logged.\n    /// </summary>\n    public bool IsVerbose { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether error messages should be logged.\n    /// </summary>\n    public bool IsError { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether warning messages should be logged.\n    /// </summary>\n    public bool IsWarning { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether debug messages should be logged.\n    /// </summary>\n    public bool IsDebug { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether information messages should be logged.\n    /// </summary>\n    public bool IsInformation { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether progress messages should be logged.\n    /// </summary>\n    public bool IsProgress { get; set; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"InternalLogger\"/> class.\n    /// </summary>\n    /// <param name=\"isVerbose\">If set to <c>true</c>, verbose messages will be logged.</param>\n    public InternalLogger(bool isVerbose = false) {\n        IsVerbose = isVerbose;\n    }\n\n    /// <summary>\n    /// Writes a progress message to the console and invokes the OnProgressMessage event.\n    /// </summary>\n    /// <param name=\"activity\">The activity being logged.</param>\n    /// <param name=\"currentOperation\">The current operation being logged.</param>\n    /// <param name=\"percentCompleted\">The percentage of the operation that is completed.</param>\n    /// <param name=\"currentSteps\">The current step of the operation (optional).</param>\n    /// <param name=\"totalSteps\">The total steps of the operation (optional).</param>\n    public void WriteProgress(string activity, string currentOperation, int percentCompleted, int? currentSteps = null, int? totalSteps = null) {\n        lock (_lock) {\n            OnProgressMessage?.Invoke(this, new LogEventArgs(activity, currentOperation, currentSteps, totalSteps, percentCompleted));\n            if (IsProgress) {\n                if (currentSteps.HasValue && totalSteps.HasValue) {\n                    Console.WriteLine(\"[progress] activity: {0} / operation: {1} / percent completed: {2}% ({3} out of {4})\", activity, currentOperation, percentCompleted, currentSteps, totalSteps);\n                } else {\n                    Console.WriteLine(\"[progress] activity: {0} / operation: {1} / percent completed: {2}%\", activity, currentOperation, percentCompleted);\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes an error message to the console and invokes the OnErrorMessage event.\n    /// </summary>\n    /// <param name=\"message\">The error message to be logged.</param>\n    public void WriteError(string message) {\n        lock (_lock) {\n            OnErrorMessage?.Invoke(this, new LogEventArgs(message));\n            if (IsError) {\n                Console.WriteLine(\"[error] \" + message);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a formatted error message to the console and invokes the OnErrorMessage event.\n    /// </summary>\n    /// <param name=\"message\">The error message to be logged, with format items.</param>\n    /// <param name=\"args\">An array of objects to write using format.</param>\n    public void WriteError(string message, params object[] args) {\n        lock (_lock) {\n            OnErrorMessage?.Invoke(this, new LogEventArgs(string.Format(message, args)));\n            if (IsError) {\n                Console.WriteLine(\"[error] \" + message, args);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a warning message to the console and invokes the OnWarningMessage event.\n    /// </summary>\n    /// <param name=\"message\">The warning message to be logged.</param>\n    public void WriteWarning(string message) {\n        lock (_lock) {\n            OnWarningMessage?.Invoke(this, new LogEventArgs(message));\n            if (IsWarning) {\n                Console.WriteLine(\"[warning] \" + message);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a formatted warning message to the console and invokes the OnWarningMessage event.\n    /// </summary>\n    /// <param name=\"message\">The warning message to be logged, with format items.</param>\n    /// <param name=\"args\">An array of objects to write using format.</param>\n    public void WriteWarning(string message, params object[] args) {\n        lock (_lock) {\n            OnWarningMessage?.Invoke(this, new LogEventArgs(string.Format(message, args)));\n            if (IsWarning) {\n                Console.WriteLine(\"[warning] \" + message, args);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a verbose message to the console and invokes the OnVerboseMessage event.\n    /// </summary>\n    /// <param name=\"message\">The verbose message to be logged.</param>\n    public void WriteVerbose(string message) {\n        lock (_lock) {\n            OnVerboseMessage?.Invoke(this, new LogEventArgs(message));\n            if (IsVerbose) {\n                Console.WriteLine(message);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a formatted verbose message to the console and invokes the OnVerboseMessage event.\n    /// </summary>\n    /// <param name=\"message\">The verbose message to be logged, with format items.</param>\n    /// <param name=\"args\">An array of objects to write using format.</param>\n    public void WriteVerbose(string message, params object[] args) {\n        lock (_lock) {\n            OnVerboseMessage?.Invoke(this, new LogEventArgs(message, args));\n            if (IsVerbose) {\n                Console.WriteLine(message, args);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes a debug message to the console and invokes the OnDebugMessage event.\n    /// </summary>\n    /// <param name=\"message\">The debug message to be logged.</param>\n    /// <param name=\"args\">An array of objects to write using format.</param>\n    public void WriteDebug(string message, params object[] args) {\n        lock (_lock) {\n            OnDebugMessage?.Invoke(this, new LogEventArgs(message, args));\n            if (IsDebug) {\n                Console.WriteLine(\"[debug] \" + message, args);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Writes an information message to the console and invokes the OnInformationMessage event.\n    /// </summary>\n    /// <param name=\"message\">The information message to be logged.</param>\n    /// <param name=\"args\">An array of objects to write using format.</param>\n    public void WriteInformation(string message, params object[] args) {\n        lock (_lock) {\n            OnInformationMessage?.Invoke(this, new LogEventArgs(message, args));\n            if (IsInformation) {\n                Console.WriteLine(\"[information] \" + message, args);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LogCollector.cs",
    "content": "using System.Collections.Concurrent;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Collects log entries in a thread-safe queue for later emission by PowerShell cmdlets.\n/// Intended for use in client classes (e.g., SendGridClient) to decouple logging from cmdlet pipeline output.\n/// </summary>\n/// <remarks>\n/// Items remain in the queue until consumed by a cmdlet that flushes\n/// the log collector's contents to the appropriate stream.\n/// </remarks>\npublic class LogCollector {\n    /// <summary>\n    /// The thread-safe queue of log entries.\n    /// </summary>\n    public ConcurrentQueue<LogEntry> Logs { get; } = new();\n\n    /// <summary>Adds a warning log entry.</summary>\n    public void LogWarning(string message) => Logs.Enqueue(new LogEntry { Message = message, Type = LogType.Warning });\n    /// <summary>Adds an error log entry.</summary>\n    public void LogError(string message) => Logs.Enqueue(new LogEntry { Message = message, Type = LogType.Error });\n    /// <summary>Adds a verbose log entry.</summary>\n    public void LogVerbose(string message) => Logs.Enqueue(new LogEntry { Message = message, Type = LogType.Verbose });\n    /// <summary>Adds an informational log entry.</summary>\n    public void LogInformation(string message) => Logs.Enqueue(new LogEntry { Message = message, Type = LogType.Information });\n}"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LogEntry.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a single log entry, including its message and type.\n/// Used to collect logs in client classes and emit them in cmdlets.\n/// </summary>\n/// <remarks>\n/// The <see cref=\"LogCollector\"/> class stores collections of these\n/// entries until they are written to output.\n/// </remarks>\npublic class LogEntry {\n    /// <summary>The log message.</summary>\n    public string Message { get; set; } = string.Empty;\n    /// <summary>The type of log (warning, error, etc.).</summary>\n    public LogType Type { get; set; }\n}"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LogEventArgs.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents the arguments for a log event.\n/// </summary>\n/// <remarks>\n/// Provides progress information and message text when writing\n/// verbose, warning or error messages during operations.\n/// </remarks>\npublic class LogEventArgs : EventArgs {\n    /// <summary>Progress percentage.</summary>\n    public int? ProgressPercentage { get; set; }\n\n    /// <summary>Progress total steps.</summary>\n    public int? ProgressTotalSteps { get; set; }\n\n    /// <summary>Progress current steps.</summary>\n    public int? ProgressCurrentSteps { get; set; }\n\n    /// <summary>Progress current operation.</summary>\n    public string ProgressCurrentOperation { get; set; } = string.Empty;\n\n    /// <summary>Progress activity.</summary>\n    public string ProgressActivity { get; set; } = string.Empty;\n\n    /// <summary>Message to be written including arguments substitution.</summary>\n    public string FullMessage { get; set; } = string.Empty;\n\n    /// <summary>Message to be written.</summary>\n    public string Message { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the arguments.</summary>\n    public object[] Args { get; set; } = Array.Empty<object>();\n\n    /// <summary>Initializes a new instance of the <see cref=\"LogEventArgs\"/> class.</summary>\n    public LogEventArgs(string message, object[] args) {\n        Message = message;\n        Args = args;\n        FullMessage = string.Format(message, args);\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"LogEventArgs\"/> class.</summary>\n    public LogEventArgs(string message) {\n        Message = message;\n        FullMessage = message;\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"LogEventArgs\"/> class.</summary>\n    public LogEventArgs(string activity, string currentOperation, int? currentSteps, int? totalSteps, int? percentage) {\n        ProgressActivity = activity;\n        ProgressCurrentOperation = currentOperation;\n        ProgressCurrentSteps = currentSteps;\n        ProgressTotalSteps = totalSteps;\n        ProgressPercentage = percentage;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LogType.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents the type of a log entry for use in cmdlet and client logging.\n/// </summary>\n/// <remarks>\n/// Each value corresponds to a message stream exposed by the\n/// PowerShell module when executing operations.\n/// </remarks>\npublic enum LogType {\n    /// <summary>A warning message.</summary>\n    Warning,\n    /// <summary>An error message.</summary>\n    Error,\n    /// <summary>A verbose message.</summary>\n    Verbose,\n    /// <summary>An informational message.</summary>\n    Information\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LoggingConfigurator.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Configures protocol logging for SMTP and other clients.\n/// </summary>\n/// <remarks>\n/// Enables capturing protocol transcripts either in memory or on\n/// disk so that troubleshooting information can be reviewed.\n/// </remarks>\npublic class LoggingConfigurator : IDisposable {\n    /// <summary>Gets the in-memory log stream when logging to an object.</summary>\n    public MemoryStream? LogStream { get; private set; }\n    /// <summary>True when logging to the console is enabled.</summary>\n    public bool LogConsole { get; private set; }\n    /// <summary>True when log entries should be captured for later emission.</summary>\n    public bool LogObject { get; private set; }\n    /// <summary>True when timestamps should be included.</summary>\n    public bool LogTimestamps { get; private set; }\n    /// <summary>True when secrets should appear in logs.</summary>\n    public bool LogSecrets { get; private set; }\n    /// <summary>Optional format string for timestamps.</summary>\n    public string? LogTimestampsFormat { get; private set; }\n    /// <summary>Optional prefix for server messages.</summary>\n    public string? LogServerPrefix { get; private set; }\n    /// <summary>Optional prefix for client messages.</summary>\n    public string? LogClientPrefix { get; private set; }\n    /// <summary>Indicates whether existing log files should be overwritten.</summary>\n    public bool LogOverwrite { get; private set; }\n    /// <summary>The path of the log file or <see langword=\"null\"/> when not logging to a file.</summary>\n    public string? LogPath { get; private set; }\n    internal ProtocolLogger? ProtocolLogger { get; set; }\n    private bool _disposed;\n\n    /// <summary>\n    /// Configures protocol logging.\n    /// </summary>\n    /// <param name=\"logPath\">Path to the log file or <see langword=\"null\"/> to disable file logging.</param>\n    /// <param name=\"logConsole\">Enable console logging.</param>\n    /// <param name=\"logObject\">Capture log entries in memory for later use.</param>\n    /// <param name=\"logTimestamps\">Include timestamps in the log.</param>\n    /// <param name=\"logSecrets\">Include secret values in the log.</param>\n    /// <param name=\"logTimestampsFormat\">Optional timestamp format.</param>\n    /// <param name=\"logServerPrefix\">Optional prefix for server messages.</param>\n    /// <param name=\"logClientPrefix\">Optional prefix for client messages.</param>\n    /// <param name=\"logOverwrite\">Overwrite existing log file if it exists.</param>\n    public void ConfigureLogging(string? logPath, bool logConsole, bool logObject, bool logTimestamps, bool logSecrets, string? logTimestampsFormat = null, string? logServerPrefix = null, string? logClientPrefix = null, bool logOverwrite = false) {\n        LogTimestamps = logTimestamps;\n        LogSecrets = logSecrets;\n        // Validate formats and prefixes\n        if (!string.IsNullOrWhiteSpace(logTimestampsFormat)) {\n            try {\n                _ = DateTimeOffset.UtcNow.ToString(logTimestampsFormat);\n                LogTimestampsFormat = logTimestampsFormat;\n            } catch (FormatException) {\n                LoggingMessages.Logger.WriteWarning(\"Invalid log timestamp format '{0}'. Falling back to default.\", logTimestampsFormat!);\n                LogTimestampsFormat = null;\n            }\n        } else {\n            LogTimestampsFormat = null;\n        }\n        if (!string.IsNullOrEmpty(logServerPrefix) && (logServerPrefix.Contains('\\n') || logServerPrefix.Contains('\\r'))) {\n            LoggingMessages.Logger.WriteWarning(\"Server log prefix contains new lines. Stripping them for safety.\");\n            logServerPrefix = logServerPrefix!.Replace(\"\\r\", string.Empty).Replace(\"\\n\", string.Empty);\n        }\n        if (!string.IsNullOrEmpty(logClientPrefix) && (logClientPrefix.Contains('\\n') || logClientPrefix.Contains('\\r'))) {\n            LoggingMessages.Logger.WriteWarning(\"Client log prefix contains new lines. Stripping them for safety.\");\n            logClientPrefix = logClientPrefix!.Replace(\"\\r\", string.Empty).Replace(\"\\n\", string.Empty);\n        }\n        LogServerPrefix = logServerPrefix;\n        LogClientPrefix = logClientPrefix;\n        LogOverwrite = logOverwrite;\n        LogObject = logObject;\n        LogConsole = logConsole;\n        LogPath = logPath;\n\n        ProtocolLogger? protocolLogger = null;\n        if (!string.IsNullOrWhiteSpace(logPath) || logConsole || logObject) {\n            if (!string.IsNullOrWhiteSpace(logPath)) {\n                var protocolLogPath = logPath!;\n                try {\n                    var directory = Path.GetDirectoryName(protocolLogPath);\n                    if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {\n                        Directory.CreateDirectory(directory);\n                    }\n                } catch (Exception ex) {\n                    LoggingMessages.Logger.WriteWarning(\"Couldn't create directory for protocol logs at '{0}': {1}. Using console output instead.\", protocolLogPath, ex.Message);\n                    protocolLogPath = string.Empty;\n                }\n                if (protocolLogPath.Length == 0) {\n                    protocolLogger = new ProtocolLogger(Console.OpenStandardOutput());\n                } else {\n                    try {\n                        protocolLogger = new ProtocolLogger(protocolLogPath, logOverwrite);\n                    } catch (IOException ex) {\n                        LoggingMessages.Logger.WriteWarning($\"Couldn't create protocol logger with {protocolLogPath}: {ex.Message}. Using console output instead.\");\n                        protocolLogger = new ProtocolLogger(Console.OpenStandardOutput());\n                    }\n                }\n            } else if (logConsole) {\n                protocolLogger = new ProtocolLogger(Console.OpenStandardOutput());\n            } else {\n                LogStream = new MemoryStream();\n                protocolLogger = new ProtocolLogger(LogStream);\n            }\n\n            protocolLogger.LogTimestamps = logTimestamps;\n            protocolLogger.RedactSecrets = !logSecrets;\n\n            if (!string.IsNullOrWhiteSpace(logTimestampsFormat)) {\n                protocolLogger.TimestampFormat = logTimestampsFormat!;\n            }\n\n            if (!string.IsNullOrWhiteSpace(logServerPrefix)) {\n                protocolLogger.ServerPrefix = logServerPrefix!;\n            }\n\n            if (!string.IsNullOrWhiteSpace(logClientPrefix)) {\n                protocolLogger.ClientPrefix = logClientPrefix!;\n            }\n        }\n        ProtocolLogger = protocolLogger;\n    }\n\n    /// <summary>Finalizer that ensures unmanaged resources are released.</summary>\n    ~LoggingConfigurator() => Dispose(false);\n\n    /// <summary>Releases resources used by the logger.</summary>\n    /// <param name=\"disposing\">When true, disposes managed resources as well.</param>\n    protected virtual void Dispose(bool disposing) {\n        if (_disposed) {\n            return;\n        }\n\n        if (disposing) {\n            ProtocolLogger?.Dispose();\n            ProtocolLogger = null;\n            LogStream?.Dispose();\n            LogStream = null;\n        }\n\n        _disposed = true;\n    }\n\n    /// <summary>Disposes the configurator and suppresses finalization.</summary>\n    public void Dispose() {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Logging/LoggingMessages.cs",
    "content": "using System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Static accessors for the global <see cref=\"InternalLogger\"/> instance used throughout the library.\n/// </summary>\n/// <remarks>\n/// The logger can be enabled or disabled via these properties to\n/// control how much diagnostic information is emitted.\n/// </remarks>\npublic class LoggingMessages {\n    private static readonly AsyncLocal<InternalLogger?> _logger = new AsyncLocal<InternalLogger?>();\n\n    /// <summary>Gets the global logger for the current asynchronous context.</summary>\n    public static InternalLogger Logger {\n        get => _logger.Value ??= new InternalLogger();\n        set => _logger.Value = value;\n    }\n\n    /// <summary>Enable or disable error logging.</summary>\n    public static bool Error {\n        get => Logger.IsError;\n        set => Logger.IsError = value;\n    }\n\n    /// <summary>Enable or disable verbose logging.</summary>\n    public static bool Verbose {\n        get => Logger.IsVerbose;\n        set => Logger.IsVerbose = value;\n    }\n\n    /// <summary>Enable or disable warning logging.</summary>\n    public static bool Warning {\n        get => Logger.IsWarning;\n        set => Logger.IsWarning = value;\n    }\n\n    /// <summary>Enable or disable progress logging.</summary>\n    public static bool Progress {\n        get => Logger.IsProgress;\n        set => Logger.IsProgress = value;\n    }\n\n    /// <summary>Enable or disable debug logging.</summary>\n    public static bool Debug {\n        get => Logger.IsDebug;\n        set => Logger.IsDebug = value;\n    }\n\n    /// <summary>\n    /// Internal lock object used to synchronize logging operations.\n    /// </summary>\n    internal static object _LockObject = new object();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MailboxBulkOperationResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Provider-agnostic result item for mailbox bulk operations.\n/// </summary>\npublic class MailboxBulkOperationResult {\n    /// <summary>Original message/thread/conversation identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>True when the operation completed successfully for this identifier.</summary>\n    public bool Ok { get; set; }\n\n    /// <summary>Error text when <see cref=\"Ok\"/> is false.</summary>\n    public string? Error { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Mailgun/Mailgun.cs",
    "content": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Diagnostics;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.IO;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Simple client for sending emails using the Mailgun API.\n/// </summary>\npublic class MailgunClient : IDisposable {\n    private readonly HttpClient _client;\n    private int _disposed;\n    /// <summary>Measures total time spent sending.</summary>\n    public readonly Stopwatch Stopwatch;\n\n    private string ApiKey {\n        get {\n            try {\n                return Helpers.CredentialToApiKey(Credentials);\n            } catch (ArgumentException ex) {\n                throw new InvalidOperationException(\"Credentials must be a NetworkCredential\", ex);\n            }\n        }\n    }\n    private string EmailDomain {\n        get {\n            var address = Helpers.GetEmailAddress(From);\n            if (!address.Contains('@')) {\n                throw new ArgumentException($\"Invalid email address: {address}\", nameof(From));\n            }\n            return address.Split('@')[1];\n        }\n    }\n\n    /// <summary>Credentials used to authenticate to the API.</summary>\n    /// <remarks>Must be <see cref=\"NetworkCredential\"/>.</remarks>\n    public ICredentials Credentials { get; set; } = null!;\n    /// <summary>Determines how errors are handled.</summary>\n    public ActionPreference? ErrorAction { get; set; }\n\n    /// <summary>Primary recipients.</summary>\n    public List<object> To { get; set; } = new();\n    /// <summary>Carbon copy recipients.</summary>\n    public List<object> Cc { get; set; } = new();\n    /// <summary>Blind carbon copy recipients.</summary>\n    public List<object> Bcc { get; set; } = new();\n    /// <summary>The sender address.</summary>\n    public object From { get; set; } = null!;\n    /// <summary>Reply-to address.</summary>\n    public object? ReplyTo { get; set; }\n    /// <summary>Message subject.</summary>\n    public string? Subject { get; set; }\n    /// <summary>Plain text body.</summary>\n    public string Text { get; set; } = string.Empty;\n    /// <summary>HTML body.</summary>\n    public string Html { get; set; } = string.Empty;\n    /// <summary>File paths to include as attachments.</summary>\n    public string[]? Attachment { get; set; }\n    /// <summary>File paths to include as inline attachments.</summary>\n    public string[]? InlineAttachment { get; set; }\n\n    /// <summary>Custom headers to include with the message.</summary>\n    public Dictionary<string, string>? Headers { get; set; }\n\n    /// <summary>Collector used to store log entries.</summary>\n    public LogCollector LogCollector { get; set; } = new();\n\n    /// <summary>\n    /// When set, sending is simulated and no Mailgun request is issued.\n    /// </summary>\n    public bool DryRun { get; set; }\n    /// <summary>Number of retry attempts on failure.</summary>\n    public int RetryCount { get; set; } = 0;\n    /// <summary>Base delay in milliseconds between retries.</summary>\n    public int RetryDelayMilliseconds { get; set; } = 0;\n    /// <summary>Exponential backoff multiplier for retries.</summary>\n    public double RetryDelayBackoff { get; set; } = 1.0;\n    /// <summary>Maximum delay in milliseconds between retries. 0 disables capping.</summary>\n    public int MaxDelayMilliseconds { get; set; } = 0;\n    /// <summary>Jitter window in milliseconds added to retry delay. 0 disables jitter.</summary>\n    public int JitterMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// When set to <c>true</c> the client retries sending even if the\n    /// encountered error is not transient.\n    /// </summary>\n    public bool RetryAlways { get; set; } = false;\n\n    /// <summary>URL of the webhook called after sending.</summary>\n    public string? WebhookUrl { get; set; }\n\n    /// <summary>Repository used to persist messages that need retrying.</summary>\n    public IPendingMessageRepository? PendingMessageRepository { get; set; }\n\n    /// <summary>The normalized sender email address.</summary>\n    public string SentFrom => Helpers.GetEmailAddress(From);\n    /// <summary>Comma separated list of recipients.</summary>\n    public string SentTo {\n        get {\n            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            var addresses = new List<string>();\n            if (To != null) addresses.AddRange(Helpers.UniqueAddresses(To, seen).Select(Helpers.GetEmailAddress));\n            if (Cc != null) addresses.AddRange(Helpers.UniqueAddresses(Cc, seen).Select(Helpers.GetEmailAddress));\n            if (Bcc != null) addresses.AddRange(Helpers.UniqueAddresses(Bcc, seen).Select(Helpers.GetEmailAddress));\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"MailgunClient\"/> class.\n    /// </summary>\n    public MailgunClient() {\n        Stopwatch = Stopwatch.StartNew();\n        _client = new HttpClient();\n    }\n\n    /// <summary>\n    /// Converts an address object into the format required by the Mailgun API.\n    /// </summary>\n    /// <param name=\"address\">The address object to convert.</param>\n    /// <returns>The formatted address string.</returns>\n    private static string ConvertAddress(object address) {\n        var (email, name) = Helpers.GetEmailAndName(address);\n        var emailSafe = email ?? string.Empty;\n        return string.IsNullOrWhiteSpace(name) ? emailSafe : $\"{name} <{emailSafe}>\";\n    }\n\n    private static StreamContent CreateStreamContent(string path) {\n        var stream = new FileStream(\n            path,\n            FileMode.Open,\n            FileAccess.Read,\n            FileShare.Read,\n            bufferSize: 8192,\n            options: FileOptions.Asynchronous | FileOptions.SequentialScan);\n\n        var streamContent = new StreamContent(stream);\n        streamContent.Headers.ContentType = new MediaTypeHeaderValue(\"application/octet-stream\");\n        return streamContent;\n    }\n\n    /// <summary>\n    /// Builds the multipart HTTP content used for the Mailgun API request.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Token to cancel asynchronous operations.</param>\n    /// <returns>The constructed multipart content.</returns>\n    private Task<MultipartFormDataContent> CreateContentAsync(CancellationToken cancellationToken) {\n        var content = new MultipartFormDataContent();\n        content.Add(new StringContent(ConvertAddress(From)), \"from\");\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var t in Helpers.UniqueAddresses(To, seen)) content.Add(new StringContent(ConvertAddress(t)), \"to\");\n        foreach (var c in Helpers.UniqueAddresses(Cc, seen)) content.Add(new StringContent(ConvertAddress(c)), \"cc\");\n        foreach (var b in Helpers.UniqueAddresses(Bcc, seen)) content.Add(new StringContent(ConvertAddress(b)), \"bcc\");\n        if (ReplyTo != null) content.Add(new StringContent(ConvertAddress(ReplyTo)), \"h:Reply-To\");\n        if (!string.IsNullOrWhiteSpace(Subject)) content.Add(new StringContent(Subject), \"subject\");\n        if (!string.IsNullOrWhiteSpace(Text)) content.Add(new StringContent(Text), \"text\");\n        if (!string.IsNullOrWhiteSpace(Html)) content.Add(new StringContent(Html), \"html\");\n        var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        if (Attachment != null) {\n            foreach (var path in Attachment) {\n                if (!files.Add(path)) continue;\n                if (!File.Exists(path)) {\n                    LogCollector.LogWarning($\"Send-EmailMessage - Attachment file not found: {path}\");\n                    LogCollector.LogWarning($\"Send-EmailMessage - Possible issue: Path '{path}' is invalid. Verify the file exists and the path is correct.\");\n                    continue;\n                }\n\n                var fileContent = CreateStreamContent(path);\n                content.Add(fileContent, \"attachment\", Path.GetFileName(path));\n            }\n        }\n        if (InlineAttachment != null) {\n            foreach (var path in InlineAttachment) {\n                if (!files.Add(path)) continue;\n                if (!File.Exists(path)) {\n                    LogCollector.LogWarning($\"Send-EmailMessage - Inline attachment file not found: {path}\");\n                    LogCollector.LogWarning($\"Send-EmailMessage - Possible issue: Path '{path}' is invalid. Verify the file exists and the path is correct.\");\n                    continue;\n                }\n\n                var fileContent = CreateStreamContent(path);\n                content.Add(fileContent, \"inline\", Path.GetFileName(path));\n            }\n        }\n        if (Headers != null) {\n            foreach (var kvp in Headers) {\n                content.Add(new StringContent(kvp.Value), $\"h:{kvp.Key}\");\n            }\n        }\n        return Task.FromResult(content);\n    }\n\n    private MimeMessage BuildMimeMessage() {\n        var smtp = new Smtp {\n            From = From,\n            To = To,\n            Cc = Cc,\n            Bcc = Bcc,\n            ReplyTo = ReplyTo,\n            Subject = Subject ?? string.Empty,\n            TextBody = Text,\n            HtmlBody = Html,\n            Headers = Headers\n        };\n        if (Attachment != null) {\n            smtp.Attachments = Attachment.Select(path => new FileAttachmentDescriptor(path)).Cast<AttachmentDescriptor>().ToList();\n        }\n        if (InlineAttachment != null) {\n            smtp.InlineAttachments = InlineAttachment.Select(path => new FileAttachmentDescriptor(path)).Cast<AttachmentDescriptor>().ToList();\n        }\n        smtp.CreateMessage();\n        return smtp.Message;\n    }\n\n    private async Task QueuePendingMessageAsync(CancellationToken cancellationToken) {\n        if (PendingMessageRepository == null) {\n            return;\n        }\n\n        string apiKey;\n        string domain;\n        try {\n            apiKey = ApiKey;\n            domain = EmailDomain;\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Failed to capture Mailgun credentials for retry: {ex.Message}\");\n            return;\n        }\n\n        if (string.IsNullOrEmpty(apiKey)) {\n            return;\n        }\n\n        MimeMessage message;\n        try {\n            message = BuildMimeMessage();\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Failed to serialize Mailgun message for retry: {ex.Message}\");\n            return;\n        }\n\n        var messageId = string.IsNullOrEmpty(message.MessageId)\n            ? MimeKit.Utils.MimeUtils.GenerateMessageId(domain)\n            : message.MessageId!;\n\n        using var stream = new MemoryStream();\n        await message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord {\n            MessageId = messageId,\n            MimeMessage = Convert.ToBase64String(stream.ToArray()),\n            Timestamp = now,\n            NextAttemptAt = now,\n            Provider = EmailProvider.Mailgun\n        };\n        record.ProviderData[MailgunPendingMessageSender.DomainKey] = domain;\n        var protector = CredentialProtection.Default;\n        record.ProviderData[MailgunPendingMessageSender.ApiKeyProtectedKey] = protector.Protect(apiKey);\n        record.ProviderData.Remove(MailgunPendingMessageSender.ApiKeyKey);\n        record.ProviderData.Remove(MailgunPendingMessageSender.ApiKeyBase64Key);\n\n        try {\n            await PendingMessageRepository.SaveAsync(record, cancellationToken).ConfigureAwait(false);\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Failed to persist Mailgun pending message: {ex.Message}\");\n        }\n    }\n\n    /// <summary>\n    /// Sends the email using the Mailgun REST API.\n    /// </summary>\n    /// <returns>The result of the send operation.</returns>\n    public Task<SmtpResult> SendEmailAsync() {\n        ThrowIfDisposed();\n        return SendEmailAsync(CancellationToken.None);\n    }\n\n    /// <summary>\n    /// Sends the email using the Mailgun REST API.\n    /// </summary>\n    /// <returns>The result of the send operation.</returns>\n    public async Task<SmtpResult> SendEmailAsync(CancellationToken cancellationToken) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Mailgun send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"MailgunApi\", 0, Stopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        var url = $\"https://api.mailgun.net/v3/{EmailDomain}/messages\";\n        var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($\"api:{ApiKey}\"));\n\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                using var content = await CreateContentAsync(cancellationToken).ConfigureAwait(false);\n                using var request = new HttpRequestMessage(HttpMethod.Post, url) {\n                    Content = content\n                };\n                request.Headers.Authorization = new AuthenticationHeaderValue(\"Basic\", auth);\n                using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);\n                if (response.IsSuccessStatusCode) {\n                    var statusCode = response.StatusCode.ToString();\n                    var okResult = new SmtpResult(true, EmailAction.Send, SentTo, SentFrom, \"MailgunApi\", 0, Stopwatch.Elapsed, statusCode, \"\");\n                    await Helpers.PostWebhookAsync(WebhookUrl, okResult, cancellationToken).ConfigureAwait(false);\n                    return okResult;\n                }\n#if NET5_0_OR_GREATER\n                var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n                var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                throw new HttpRequestException(error);\n            } catch (HttpRequestException ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using Mailgun: {ex.Message}\");\n                if ((!Helpers.IsTransient(ex) && !RetryAlways) || attempts >= RetryCount) {\n                    await QueuePendingMessageAsync(cancellationToken).ConfigureAwait(false);\n                    if (ErrorAction == ActionPreference.Stop) throw;\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"MailgunApi\", 0, Stopwatch.Elapsed, \"\", ex.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken).ConfigureAwait(false);\n                    return failResult;\n                }\n                var delay = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempts));\n                if (MaxDelayMilliseconds > 0 && delay > MaxDelayMilliseconds) {\n                    delay = MaxDelayMilliseconds;\n                }\n                if (JitterMilliseconds > 0 && delay > 0) {\n                    delay += GraphRetryHelperRandom.NextInt(JitterMilliseconds + 1);\n                }\n                if (delay > 0) await Task.Delay(TimeSpan.FromMilliseconds(delay), cancellationToken).ConfigureAwait(false);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        await QueuePendingMessageAsync(cancellationToken).ConfigureAwait(false);\n        var finalResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"MailgunApi\", 0, Stopwatch.Elapsed, \"\", lastException?.Message);\n        await Helpers.PostWebhookAsync(WebhookUrl, finalResult, cancellationToken).ConfigureAwait(false);\n        return finalResult;\n    }\n\n    /// <summary>\n    /// Releases resources used by the client.\n    /// </summary>\n    private void ThrowIfDisposed() {\n        if (Volatile.Read(ref _disposed) != 0) {\n            throw new ObjectDisposedException(nameof(MailgunClient));\n        }\n    }\n\n    /// <summary>Releases resources used by the client.</summary>\n    /// <param name=\"disposing\">When true, disposes managed resources as well.</param>\n    protected virtual void Dispose(bool disposing) {\n        if (Interlocked.Exchange(ref _disposed, 1) != 0) return;\n        if (disposing) {\n            _client.Dispose();\n        }\n    }\n\n    /// <summary>Finalizer that ensures unmanaged resources are released.</summary>\n    ~MailgunClient() => Dispose(false);\n\n    /// <summary>Disposes the client and suppresses finalization.</summary>\n    public void Dispose() {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Mailozaurr.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <Company>Evotec</Company>\n        <Authors>Przemyslaw Klys</Authors>\n        <VersionPrefix>2.0.7</VersionPrefix>\n        <TargetFrameworks>net472;netstandard2.0;net8.0</TargetFrameworks>\n        <AssemblyName>Mailozaurr</AssemblyName>\n        <Copyright>(c) 2011 - 2024 Przemyslaw Klys @ Evotec. All rights reserved.</Copyright>\n        <LangVersion>latest</LangVersion>\n        <PackageIcon>Mailozaurr.png</PackageIcon>\n        <PackageReadmeFile>README.MD</PackageReadmeFile>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <!-- Keep local build artifacts from being treated as SDK default items/references. -->\n        <DefaultItemExcludes>$(DefaultItemExcludes);**/artifacts/**;**/Artifacts/**</DefaultItemExcludes>\n        <!-- Required for pointer operations in Helpers/SecureStringHelper.cs -->\n        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n        <RepositoryUrl>https://github.com/EvotecIT/Mailozaurr</RepositoryUrl>\n        <PackageTags>\n            Windows;MacOS;Linux;Mail;Email;MX;SPF;DMARC;DKIM;GraphApi;SendGrid;Graph;IMAP;POP3</PackageTags>\n    </PropertyGroup>\n\n    <!-- Include referenced assemblies alongside output -->\n    <PropertyGroup>\n        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    </PropertyGroup>\n\n    <ItemGroup>\n\n        <PackageReference Include=\"EmailValidation\" Version=\"1.3.0\" />\n        <PackageReference Include=\"Google.Apis.Auth\" Version=\"1.73.0\" />\n        <PackageReference Include=\"MailKit\" Version=\"4.16.0\" />\n        <PackageReference Include=\"MimeKit\" Version=\"4.16.0\" />\n        <PackageReference Include=\"Microsoft.Identity.Client\" Version=\"4.82.1\" />\n        <PackageReference Include=\"NSspi\" Version=\"0.3.1\" />\n        <PackageReference Include=\"BouncyCastle.Cryptography\" Version=\"2.6.2\" />\n    </ItemGroup>\n\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'net472' \">\n        <PackageReference Include=\"System.Data.SQLite\" Version=\"2.0.2\" />\n        <PackageReference Include=\"System.Text.Json\" Version=\"10.0.3\" />\n    </ItemGroup>\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'netstandard2.0' \">\n        <PackageReference Include=\"System.Text.Json\" Version=\"10.0.3\" />\n    </ItemGroup>\n\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net8.0' \">\n        <PackageReference Include=\"System.Security.Cryptography.ProtectedData\" Version=\"10.0.3\" />\n    </ItemGroup>\n\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'net472' \">\n        <Reference Include=\"System.IO.Compression\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <None Include=\"..\\..\\Mailozaurr.png\" Pack=\"true\" PackagePath=\"\\\" />\n        <None Include=\"..\\..\\README.MD\" Pack=\"true\" PackagePath=\"\\\" />\n    </ItemGroup>\n\n\n    <ItemGroup>\n        <Using Include=\"MailKit\" />\n        <Using Include=\"MailKit.Net.Smtp\" />\n        <Using Include=\"MailKit.Security\" />\n        <Using Include=\"MailKit.Net.Pop3\" />\n        <Using Include=\"MailKit.Net.Imap\" />\n        <Using Include=\"MailKit.Net\" />\n        <Using Include=\"MimeKit\" />\n        <Using Include=\"MimeKit.Cryptography\" />\n        <Using Include=\"System\" />\n        <Using Include=\"System.Net\" />\n        <Using Include=\"System.Net.Http\" />\n        <Using Include=\"System.Text\" />\n        <Using Include=\"System.Text.Json\" />\n        <Using Include=\"System.Text.Json.Serialization\" />\n        <Using Include=\"System.Collections\" />\n        <Using Include=\"System.Collections.Generic\" />\n        <Using Include=\"System.IO\" />\n        <Using Include=\"System.Linq\" />\n        <Using Include=\"System.Diagnostics\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <EmbeddedResource Include=\"Resources\\disposable_email_blocklist.conf\" />\n        <EmbeddedResource Include=\"Resources\\allowlist.conf\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <AssemblyAttribute Include=\"System.Runtime.CompilerServices.InternalsVisibleTo\">\n            <_Parameter1>Mailozaurr.Tests</_Parameter1>\n        </AssemblyAttribute>\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr/Mailozaurr.csproj.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=authentication/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=definitions/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=enums/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=logging/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mailgun/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=microsoftgraph/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=sendgrid/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=smtp/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "Sources/Mailozaurr/MailozaurrOptions.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Global options for Mailozaurr behavior.\n/// </summary>\npublic static class MailozaurrOptions\n{\n    static MailozaurrOptions()\n    {\n        // Internal default for PowerShell and other hosts that rebuild or do not execute psm1 init.\n        // Can be overridden at runtime by assigning DefaultGraphPolicy.\n        DefaultGraphPolicy = GraphSendPolicy.Default;\n    }\n    /// <summary>\n    /// Default policy applied to all Graph send operations when an instance policy is not supplied.\n    /// Leave <c>null</c> to preserve legacy behavior (no policy applied by default).\n    /// </summary>\n    public static GraphSendPolicy? DefaultGraphPolicy { get; set; }\n\n    /// <summary>\n    /// Optional global factory used to construct an SMTP sender for fallback scenarios.\n    /// If set and the active <see cref=\"GraphSendPolicy\"/> enables fallback, the factory will be invoked.\n    /// </summary>\n    public static Func<Graph, Smtp?>? SmtpFallbackFactory { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/Graph.cs",
    "content": "using System;\nusing System.Buffers;\nusing System.Diagnostics;\nusing System.Net.Http.Headers;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper class for sending messages via Microsoft Graph API.\n/// </summary>\n/// <remarks>\n/// Provides convenience methods for constructing requests and\n/// uploading attachments without having to manually craft HTTP calls.\n/// </remarks>\n    public class Graph : IDisposable {\n        private readonly HttpClient _client;\n        /// <summary>\n        /// Maximum size of an attachment chunk when uploading large files (4 MiB).\n        /// </summary>\n        public const int MaxChunkSize = 4 * 1024 * 1024;\n        private const long GraphPayloadLimitBytes = 4_000_000;\n    private int _chunkSize = MaxChunkSize;\n    /// <summary>\n    /// Serialized JSON representation of the current Graph message.\n    /// </summary>\n    public string MessageJson = string.Empty;\n\n    /// <summary>\n    /// Container object used when building a Graph message.\n    /// </summary>\n    public GraphMessageContainer MessageContainer = new();\n\n    /// <summary>Measures elapsed time spent during send operations.</summary>\n    public readonly Stopwatch Stopwatch;\n\n        /// <summary>\n        /// Value indicating whether the total size of the attachments is larger than the Graph payload limit.\n        /// </summary>\n        public bool IsLargerAttachment { get; set; }\n\n    /// <summary>\n    /// Total size of all attachments in bytes (including file paths and in-memory attachments).\n    /// </summary>\n    public long TotalAttachmentSizeBytes { get; private set; }\n\n    private long _inlineAttachmentSizeBytes;\n    private int _fileAttachmentCount;\n\n    /// <summary>\n    /// List of GraphAttachment objects created from the file paths in the Attachments property.\n    /// </summary>\n    public List<GraphAttachment> ConvertedAttachments { get; set; } = new List<GraphAttachment>();\n\n    /// <summary>\n    /// Collection of attachment placeholders used for large file uploads.\n    /// </summary>\n    public List<GraphAttachmentPlaceHolder> AttachmentsPlaceHolders { get; set; } = new List<GraphAttachmentPlaceHolder>();\n\n    /// <summary>\n    /// Collection of attachments which can be file paths or GraphAttachment objects.\n    /// </summary>\n    public object[]? Attachments { get; set; }\n\n    /// <summary>\n    /// Gets or sets the sender. Can be a string (email) or a dictionary with Name and Email.\n    ///\n    /// Note: The display name (\"Name\") for the sender is controlled by Office 365 and may not reflect the value you provide here.\n    /// Office 365 will use the mailbox's configured display name for the sender, regardless of what is set in the payload.\n    /// The email address must be used for API calls and authentication.\n    /// </summary>\n    public object? From { get; set; }\n\n    /// <summary>\n    /// Gets or sets the email address to reply to.\n    /// </summary>\n    public string? ReplyTo { get; set; }\n\n    /// <summary>\n    /// Gets or sets the email addresses of the recipients.\n    /// </summary>\n    public object[]? To { get; set; }\n\n    /// <summary>\n    /// Gets or sets the email addresses of the CC recipients.\n    /// </summary>\n    public object[]? Cc { get; set; }\n\n    /// <summary>\n    /// Gets or sets the email addresses of the BCC recipients.\n    /// </summary>\n    public object[]? Bcc { get; set; }\n\n    /// <summary>\n    /// Gets or sets the subject of the email.\n    /// </summary>\n    public string Subject { get; set; } = string.Empty;\n\n    /// <summary>\n    /// HTML content of the email.\n    /// </summary>\n    public string HTML { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Priority of the message (mapped to Graph importance).\n    /// </summary>\n    public MessagePriority Priority { get; set; } = MessagePriority.Normal;\n\n    private string _contentType = \"HTML\";\n\n    /// <summary>\n    /// Content type of the email.\n    /// </summary>\n    public string ContentType {\n        get => _contentType;\n        set {\n            if (value is null) {\n                throw new ArgumentNullException(nameof(value));\n            }\n\n            if (string.Equals(value, \"HTML\", StringComparison.OrdinalIgnoreCase)) {\n                _contentType = \"HTML\";\n                return;\n            }\n\n            if (string.Equals(value, \"Text\", StringComparison.OrdinalIgnoreCase)) {\n                _contentType = \"Text\";\n                return;\n            }\n\n            throw new ArgumentException(\"ContentType must be either \\\"Text\\\" or \\\"HTML\\\".\", nameof(value));\n        }\n    }\n\n    /// <summary>\n    /// Value indicating whether the message should not be saved to the Sent Items folder.\n    /// </summary>\n    public bool DoNotSaveToSentItems { get; set; }\n\n    /// <summary>\n    /// Access token for the Graph API.\n    /// </summary>\n    public string AccessToken { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Application ID for the Graph API.\n    /// </summary>\n    private string ApplicationID { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Application key for the Graph API.\n    /// </summary>\n    private string ApplicationKey { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Tenant domain for the Graph API.\n    /// </summary>\n    private string TenantDomain { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Action to take when an error occurs based on the ErrorAction preference.\n    /// </summary>\n    public ActionPreference? ErrorAction { get; set; }\n\n    /// <summary>\n    /// Number of times to retry sending the message when an error occurs.\n    /// </summary>\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay in milliseconds between retry attempts.\n    /// </summary>\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Factor used to increase the delay for each subsequent retry. A value of\n    /// 1 disables backoff.\n    /// </summary>\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>\n    /// Timeout for HTTP operations in seconds.\n    /// </summary>\n    public int TimeoutSeconds {\n        get => (int)_client.Timeout.TotalSeconds;\n        set => _client.Timeout = TimeSpan.FromSeconds(value);\n    }\n\n    /// <summary>\n    /// When enabled, scans the HTML body for local image references and embeds\n    /// them as inline attachments.\n    /// </summary>\n    public bool AutoEmbedImages { get; set; } = false;\n\n    /// <summary>\n    /// Forces retries even when the encountered error is not classified as\n    /// transient.\n    /// </summary>\n    public bool RetryAlways { get; set; } = false;\n\n    /// <summary>\n    /// Optional policy controlling throttling/backoff and fallback behavior for Graph sends.\n    /// </summary>\n    public GraphSendPolicy? SendPolicy { get; private set; }\n\n    private Func<Smtp>? _smtpFallbackFactory;\n\n    /// <summary>Webhook invoked after sending.</summary>\n    public string? WebhookUrl { get; set; }\n\n    /// <summary>Custom headers to include with the message.</summary>\n    public Dictionary<string, string>? Headers { get; set; }\n\n    /// <summary>\n    /// Size in bytes of the chunks used when uploading attachments. Defaults to\n    /// <see cref=\"MaxChunkSize\"/> and cannot exceed this value.\n    /// </summary>\n    public int ChunkSize {\n        get => _chunkSize;\n        set => _chunkSize = value > MaxChunkSize ? MaxChunkSize : value;\n    }\n\n    /// <summary>\n    /// The type of token that was issued.\n    /// </summary>\n    public string TokenType { get; set; } = string.Empty;\n\n    /// <summary>\n    /// The email address that the message was sent from.\n    /// </summary>\n    public string SentFrom => From == null ? string.Empty : Helpers.GetEmailAddress(From);\n\n    /// <summary>\n    /// A comma-separated list of email addresses that the message was sent to.\n    /// </summary>\n    public string SentTo {\n        get {\n            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            var addresses = new List<string>();\n            if (To != null) {\n                addresses.AddRange(Helpers.UniqueAddresses(To, seen).Select(obj => Helpers.GetEmailAddress(obj)));\n            }\n            if (Cc != null) {\n                addresses.AddRange(Helpers.UniqueAddresses(Cc, seen).Select(obj => Helpers.GetEmailAddress(obj)));\n            }\n            if (Bcc != null) {\n                addresses.AddRange(Helpers.UniqueAddresses(Bcc, seen).Select(obj => Helpers.GetEmailAddress(obj)));\n            }\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>\n    /// Request a read receipt for the message.\n    /// </summary>\n    public bool RequestReadReceipt { get; set; }\n\n    /// <summary>\n    /// Request a delivery receipt for the message.\n    /// </summary>\n    public bool RequestDeliveryReceipt { get; set; }\n\n    /// <summary>Collector used to store log entries.</summary>\n    public LogCollector LogCollector { get; set; } = new();\n\n    /// <summary>\n    /// When set, sending is simulated and no Graph requests are issued.\n    /// </summary>\n    public bool DryRun { get; set; }\n\n    /// <summary>\n    /// Initializes a new instance of the Graph class.\n    /// </summary>\n    public Graph() {\n        Stopwatch = Stopwatch.StartNew();\n        _client = new HttpClient();\n        _client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds);\n        if (LogCollector == null) LogCollector = new();\n    }\n\n    /// <summary>\n    /// Apply a send policy to this instance. Also updates the global Graph concurrency limit.\n    /// </summary>\n    /// <remarks>\n    /// The concurrency limit is process-wide and affects all Graph operations within the AppDomain.\n    /// The update is performed using a thread-safe semaphore swap, but callers should be aware of\n    /// the global nature of this setting when running multiple independent pipelines in parallel.\n    /// </remarks>\n    public Graph WithSendPolicy(GraphSendPolicy policy) {\n        SendPolicy = policy ?? throw new ArgumentNullException(nameof(policy));\n        if (policy.MaxConcurrency > 0) {\n            MicrosoftGraphUtils.MaxConcurrentRequests = policy.MaxConcurrency;\n        }\n        return this;\n    }\n\n    /// <summary>\n    /// Provide an SMTP factory used for fallback when the active policy enables SMTP fallback.\n    /// </summary>\n    public Graph WithSmtpFallback(Func<Smtp> factory) {\n        _smtpFallbackFactory = factory ?? throw new ArgumentNullException(nameof(factory));\n        return this;\n    }\n\n    private void LogMissingAttachmentWarning(string attachmentPath) {\n        var pathForMessage = string.IsNullOrWhiteSpace(attachmentPath)\n            ? \"(empty path)\"\n            : attachmentPath;\n        LogCollector.LogWarning($\"Send-EmailMessage - Attachment file not found: {pathForMessage}\");\n        LogCollector.LogWarning($\"Send-EmailMessage - Possible issue: Path '{pathForMessage}' is invalid. Verify the file exists and the path is correct.\");\n    }\n\n    private Stopwatch StartOperationTimer() {\n        Stopwatch.Reset();\n        Stopwatch.Start();\n        return System.Diagnostics.Stopwatch.StartNew();\n    }\n\n    private async Task WaitForConcurrencyAsync(Stopwatch operationStopwatch, CancellationToken cancellationToken) {\n        operationStopwatch.Stop();\n        Stopwatch.Stop();\n        try {\n            await MicrosoftGraphUtils.ConcurrencySemaphore.WaitAsync(cancellationToken);\n        } finally {\n            Stopwatch.Start();\n            operationStopwatch.Start();\n        }\n    }\n\n    /// <summary>\n    /// Converts the <see cref=\"Attachments\"/> collection into <see cref=\"GraphAttachment\"/> instances.\n    /// </summary>\n    public void CreateAttachments() {\n        ConvertedAttachments.Clear();\n        TotalAttachmentSizeBytes = 0;\n        IsLargerAttachment = false;\n        _inlineAttachmentSizeBytes = 0;\n        _fileAttachmentCount = 0;\n        if (Attachments != null && Attachments.Any()) {\n            var fileAttachments = new List<string>();\n            long fileTotalBytes = 0;\n            long inMemoryTotalBytes = 0;\n\n            // First pass: compute total size without loading file contents.\n            foreach (var item in Attachments) {\n                if (item is string path) {\n                    if (!File.Exists(path)) {\n                        LogMissingAttachmentWarning(path);\n                        continue;\n                    }\n                    fileAttachments.Add(path);\n                    try {\n                        var length = new FileInfo(path).Length;\n                        fileTotalBytes += length;\n                        _fileAttachmentCount++;\n                    } catch (Exception ex) {\n                        LogCollector.LogError($\"Send-EmailMessage - Failed to read attachment '{path}': {ex.Message}\");\n                    }\n                } else if (item is GraphAttachment ga) {\n                    ConvertedAttachments.Add(ga);\n                    var size = EstimateAttachmentSize(ga);\n                    inMemoryTotalBytes += size;\n                }\n            }\n\n            _inlineAttachmentSizeBytes = inMemoryTotalBytes;\n            TotalAttachmentSizeBytes = fileTotalBytes + inMemoryTotalBytes;\n            IsLargerAttachment = TotalAttachmentSizeBytes > GraphPayloadLimitBytes;\n\n            // Only load file attachments into memory when they fit in a simple send payload.\n            if (!IsLargerAttachment && fileAttachments.Count > 0) {\n                foreach (var path in fileAttachments) {\n                    ConvertedAttachments.Add(GraphAttachment.FromFile(path));\n                }\n            }\n\n            if (_inlineAttachmentSizeBytes > GraphPayloadLimitBytes) {\n                LogCollector.LogWarning(\"Send-EmailMessage - Large in-memory attachments detected. Consider using file paths for large attachments to enable upload sessions.\");\n            }\n        }\n    }\n\n    private static long EstimateTotalSize(IEnumerable<GraphAttachment> attachments) {\n        long total = 0;\n        foreach (var attachment in attachments) {\n            total += EstimateAttachmentSize(attachment);\n        }\n        return total;\n    }\n\n    private static long EstimateAttachmentSize(GraphAttachment attachment) {\n        if (string.IsNullOrWhiteSpace(attachment.ContentBytes)) {\n            return 0;\n        }\n        var value = attachment.ContentBytes.Trim();\n        if (value.Length == 0) {\n            return 0;\n        }\n        var padding = 0;\n        if (value.EndsWith(\"==\", StringComparison.Ordinal)) {\n            padding = 2;\n        } else if (value.EndsWith(\"=\", StringComparison.Ordinal)) {\n            padding = 1;\n        }\n        var bytes = (long)value.Length * 3 / 4 - padding;\n        return bytes < 0 ? 0 : bytes;\n    }\n\n    /// <summary>\n    /// Builds the <see cref=\"GraphMessageContainer\"/> object that represents the email.\n    /// </summary>\n    public void CreateMessage() {\n        CreateAttachments();\n        if (AutoEmbedImages) {\n            var (html, paths) = HtmlUtils.ExtractLocalImagePaths(HTML);\n            HTML = html;\n            foreach (var p in paths) {\n                var att = GraphAttachment.FromFile(p);\n                att.IsInline = true;\n                att.ContentId = Path.GetFileName(p);\n                ConvertedAttachments.Add(att);\n                var size = EstimateAttachmentSize(att);\n                _inlineAttachmentSizeBytes += size;\n                TotalAttachmentSizeBytes += size;\n            }\n        }\n        if (From is null) {\n            throw new InvalidOperationException(\"From address must be specified.\");\n        }\n        // Note: The display name for the sender is controlled by Office 365 and may not reflect the value you provide here.\n        // Office 365 will use the mailbox's configured display name for the sender, regardless of what is set in the payload.\n        // Always use the email address for API calls and authentication.\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        MessageContainer = new GraphMessageContainer {\n            Message = new GraphMessage {\n                From = ConvertToGraphEmailAddress(From),\n                To = ConvertToGraphEmailAddressUnique(To, seen),\n                Cc = ConvertToGraphEmailAddressUnique(Cc, seen),\n                Bcc = ConvertToGraphEmailAddressUnique(Bcc, seen),\n                ReplyTo = string.IsNullOrWhiteSpace(ReplyTo)\n                    ? null\n                    : new List<GraphEmailAddress> { ConvertToGraphEmailAddress(ReplyTo)! },\n                Subject = Subject,\n                Body = new GraphContent { Content = HTML, Type = ContentType },\n                Importance = MapImportance(Priority),\n                IsDeliveryReceiptRequested = RequestDeliveryReceipt,\n                IsReadReceiptRequested = RequestReadReceipt\n            },\n            SaveToSentItems = !DoNotSaveToSentItems\n        };\n        if (_inlineAttachmentSizeBytes > GraphPayloadLimitBytes) {\n            throw new InvalidOperationException(\"In-memory attachments exceed the 4MB Graph payload limit. Use file path attachments or reduce attachment size.\");\n        }\n        if (!IsLargerAttachment && TotalAttachmentSizeBytes > GraphPayloadLimitBytes && _fileAttachmentCount > 0) {\n            throw new InvalidOperationException(\"Total attachment payload exceeds the 4MB Graph limit after embedding images. Use file attachments or reduce attachment size.\");\n        }\n        if (ConvertedAttachments.Count > 0) {\n            MessageContainer.Message.Attachments = ConvertedAttachments;\n        }\n        if (Headers != null && Headers.Count > 0) {\n            MessageContainer.Message.InternetMessageHeaders = Headers.Select(kvp => new GraphInternetMessageHeader { Name = kvp.Key, Value = kvp.Value }).ToList();\n        }\n\n        MessageJson = JsonSerializer.Serialize(MessageContainer, MailozaurrJsonContext.Default.GraphMessageContainer);\n        //LoggingMessages.Logger.WriteVerbose(MessageJson);\n    }\n\n    /// <summary>\n    /// Parses the provided credentials into client id, secret and tenant domain.\n    /// </summary>\n    /// <param name=\"Credentials\">The credentials to parse.</param>\n    public void Authenticate(ICredentials Credentials) {\n        if (Credentials is null) {\n            throw new ArgumentNullException(nameof(Credentials));\n        }\n\n        if (Credentials is not NetworkCredential networkCredential) {\n            throw new ArgumentException(\n                \"Credentials must be of type NetworkCredential.\",\n                nameof(Credentials));\n        }\n\n        if (string.IsNullOrWhiteSpace(networkCredential.UserName)) {\n            throw new ArgumentException(\n                \"Credential.UserName must be in the format 'clientid@directoryid'\",\n                nameof(Credentials));\n        }\n\n        var userSplit = networkCredential.UserName.Split('@');\n        if (userSplit.Length != 2 || string.IsNullOrWhiteSpace(userSplit[0]) || string.IsNullOrWhiteSpace(userSplit[1])) {\n            throw new ArgumentException(\n                \"Credential.UserName must be in the format 'clientid@directoryid'\",\n                nameof(Credentials));\n        }\n\n        ApplicationID = userSplit[0];\n        ApplicationKey = networkCredential.Password;\n        TenantDomain = userSplit[1];\n    }\n\n    private GraphEmailAddress? ConvertToGraphEmailAddress(object? email) {\n        if (email == null) {\n            return null;\n        }\n        var address = Helpers.GetEmailAddress(email);\n        return new GraphEmailAddress { Email = new GraphEmail { Address = address } };\n    }\n\n    private List<GraphEmailAddress>? ConvertToGraphEmailAddress(object[]? emails) {\n        if (emails == null) {\n            return null;\n        }\n        return emails.Select(email => new GraphEmailAddress { Email = new GraphEmail { Address = Helpers.GetEmailAddress(email) } }).ToList();\n    }\n\n    private List<GraphEmailAddress>? ConvertToGraphEmailAddressUnique(object[]? emails, HashSet<string> seen) {\n        if (emails == null) {\n            return null;\n        }\n\n        var list = Helpers.UniqueAddresses(emails, seen)\n            .Select(email => new GraphEmailAddress { Email = new GraphEmail { Address = Helpers.GetEmailAddress(email) } })\n            .ToList();\n        return list.Count == 0 ? null : list;\n    }\n\n    /// <summary>\n    /// Authenticates to Microsoft Graph using client credentials and obtains an access token.\n    /// </summary>\n    /// <returns>The result of the connection attempt.</returns>\n    public async Task<SmtpResult> ConnectO365GraphAsync(CancellationToken cancellationToken = default) {\n        var operationStopwatch = StartOperationTimer();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Graph authentication.\");\n            return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"Connection skipped (WhatIf)\");\n        }\n        string resource = \"https://graph.microsoft.com\";\n        var body = new Dictionary<string, string> {\n            { \"grant_type\", \"client_credentials\" },\n            { \"resource\", resource },\n            { \"client_id\", ApplicationID },\n            { \"client_secret\", ApplicationKey }\n        };\n\n        try {\n            await WaitForConcurrencyAsync(operationStopwatch, cancellationToken);\n            try {\n                using var requestContent = new FormUrlEncodedContent(body);\n                using var response = await _client.PostAsync($\"https://login.microsoftonline.com/{TenantDomain}/oauth2/token\", requestContent, cancellationToken);\n                var content = await response.Content.ReadAsStringAsync();\n                if (!response.IsSuccessStatusCode) {\n                    LogCollector.LogWarning($\"Send-EmailMessage - Error during connection using Graph API: {content}\");\n                    if (ErrorAction == ActionPreference.Stop) {\n                        response.EnsureSuccessStatusCode();\n                    }\n                    return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, content, content);\n                }\n\n                var authorization = JsonSerializer.Deserialize(content, MailozaurrJsonContext.Default.GraphAuthorization);\n                AccessToken = authorization?.AccessToken ?? string.Empty;\n                TokenType = authorization?.TokenType ?? string.Empty;\n                return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"\", \"\");\n            } finally {\n                MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n            }\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (TaskCanceledException ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Connection to Graph API cancelled: {ex.Message}\");\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, ex.Message);\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Error during connection using Graph API: {ex.Message}\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Sends the prepared message via the Graph API.\n    /// </summary>\n    /// <returns>The result of the send operation.</returns>\n    public async Task<SmtpResult> SendMessageAsync(CancellationToken cancellationToken = default) {\n        var operationStopwatch = StartOperationTimer();\n        // create message\n        CreateMessage();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Graph send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        LogCollector.LogVerbose(\"Send-EmailMessage - Sending email via Graph API\");\n        // Create the request URI outside the loop.\n        var requestUri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{MessageContainer.Message.From!.Email.Address}/sendMail\");\n\n        var policy = SendPolicy ?? MailozaurrOptions.DefaultGraphPolicy;\n        if (policy != null && policy.MaxConcurrency > 0) {\n            MicrosoftGraphUtils.MaxConcurrentRequests = policy.MaxConcurrency;\n        }\n\n        var policyDraft = SendPolicy ?? MailozaurrOptions.DefaultGraphPolicy;\n        if (policyDraft != null && policyDraft.MaxConcurrency > 0) {\n            MicrosoftGraphUtils.MaxConcurrentRequests = policyDraft.MaxConcurrency;\n        }\n\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                using var request = new HttpRequestMessage(HttpMethod.Post, requestUri) {\n                    Content = new StringContent(MessageJson, Encoding.UTF8, \"application/json\")\n                };\n                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(TokenType, AccessToken);\n\n                await WaitForConcurrencyAsync(operationStopwatch, cancellationToken);\n                try {\n                    using var response = await _client.SendAsync(request, cancellationToken);\n                    var content = await response.Content.ReadAsStringAsync();\n                    if (response.IsSuccessStatusCode) {\n                        var okResult = new SmtpResult(true, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, response.StatusCode.ToString(), \"\");\n                        await Helpers.PostWebhookAsync(WebhookUrl, okResult, cancellationToken);\n                        return okResult;\n                    }\n                    var error = JsonSerializer.Deserialize(content, MailozaurrJsonContext.Default.GraphApiError);\n                    var errorMessage = (error == null || error.Error == null || error.Error.InnerError == null)\n                        ? $\"Unknown error: {content}\"\n                        : $\"Error code: {error.Error.Code}, message: {error.Error.Message}, request ID: {error.Error.InnerError.RequestId}, date: {error.Error.InnerError.Date}\";\n                    var retryAfter = ParseRetryAfter(response);\n                    throw new GraphApiException(response.StatusCode, errorMessage, content, retryAfter);\n                } finally {\n                    MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n                }\n            } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                throw;\n            } catch (TaskCanceledException ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Sending via Graph API cancelled: {ex.Message}\");\n                var maxRetries = policy?.MaxRetries ?? RetryCount;\n                var shouldRetry = (policy?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, ex.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n                    return await TrySmtpFallbackAsync(policy, failResult, ex, cancellationToken);\n                }\n                await DelayWithBackoffAsync(policy, attempts, null, ex, cancellationToken);\n            } catch (Exception ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using Graph API: {ex.Message}\");\n                var maxRetries = policy?.MaxRetries ?? RetryCount;\n                var shouldRetry = (policy?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    if (ErrorAction == ActionPreference.Stop) {\n                        throw;\n                    }\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"\", ex.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n                    return await TrySmtpFallbackAsync(policy, failResult, ex, cancellationToken);\n                }\n                var retryAfter = (ex as GraphApiException)?.RetryAfter;\n                await DelayWithBackoffAsync(policy, attempts, retryAfter, ex, cancellationToken);\n            }\n            attempts++;\n        } while (attempts <= (policy?.MaxRetries ?? RetryCount));\n\n        var finalResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"\", lastException?.Message);\n        await Helpers.PostWebhookAsync(WebhookUrl, finalResult, cancellationToken);\n        return await TrySmtpFallbackAsync(policy, finalResult, lastException, cancellationToken);\n    }\n\n    /// <summary>\n    /// Sends a message by first creating a draft and then uploading attachments.\n    /// </summary>\n    /// <returns>The result of the send operation.</returns>\n    public async Task<SmtpResult> SendMessageDraftAsync(CancellationToken cancellationToken = default) {\n        var operationStopwatch = StartOperationTimer();\n        if (DryRun) {\n            CreateMessage();\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Graph draft send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        // Create the draft message using the new method\n        var draftMessage = await CreateDraftMessageAsync(cancellationToken);\n\n        // Upload attachments to the draft message\n        await UploadAttachmentsAsync(draftMessage, cancellationToken);\n\n        var policyDraft = SendPolicy ?? MailozaurrOptions.DefaultGraphPolicy;\n        if (policyDraft != null && policyDraft.MaxConcurrency > 0) {\n            MicrosoftGraphUtils.MaxConcurrentRequests = policyDraft.MaxConcurrency;\n        }\n\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                return await SendDraftMessage(draftMessage, cancellationToken);\n            } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                throw;\n            } catch (TaskCanceledException ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Sending draft via Graph API cancelled: {ex.Message}\");\n                var maxRetries = policyDraft?.MaxRetries ?? RetryCount;\n                var shouldRetry = (policyDraft?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, ex.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n                    return await TrySmtpFallbackAsync(policyDraft, failResult, ex, cancellationToken);\n                }\n                await DelayWithBackoffAsync(policyDraft, attempts, null, ex, cancellationToken);\n            } catch (Exception ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using Graph API: {ex.Message}\");\n                var maxRetries = policyDraft?.MaxRetries ?? RetryCount;\n                var shouldRetry = (policyDraft?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    if (ErrorAction == ActionPreference.Stop) {\n                        throw;\n                    }\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"\", ex.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n                    return await TrySmtpFallbackAsync(policyDraft, failResult, ex, cancellationToken);\n                }\n                var ra = (ex as GraphApiException)?.RetryAfter;\n                await DelayWithBackoffAsync(policyDraft, attempts, ra, ex, cancellationToken);\n            }\n            attempts++;\n        } while (attempts <= (policyDraft?.MaxRetries ?? RetryCount));\n\n        var finalResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, \"\", lastException?.Message);\n        await Helpers.PostWebhookAsync(WebhookUrl, finalResult, cancellationToken);\n        return await TrySmtpFallbackAsync(policyDraft, finalResult, lastException, cancellationToken);\n    }\n\n        /// <summary>\n        /// Sends a previously created draft message.\n        /// </summary>\n        /// <param name=\"draftMessage\">The draft message to send.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        /// <returns>The result of the send operation.</returns>\n        public async Task<SmtpResult> SendDraftMessage(GraphMessage draftMessage, CancellationToken cancellationToken = default) {\n        var operationStopwatch = StartOperationTimer();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Graph draft send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        // Send the draft message\n        var sendRequestUri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{MessageContainer.Message.From!.Email.Address}/messages/{draftMessage.Id!}/send\");\n        using var sendRequest = new HttpRequestMessage(HttpMethod.Post, sendRequestUri);\n\n        // Add the authorization header\n        sendRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(TokenType, AccessToken);\n\n        // Send the HTTP request for sending the draft message\n        await WaitForConcurrencyAsync(operationStopwatch, cancellationToken);\n        try {\n            using var sendResponse = await _client.SendAsync(sendRequest, cancellationToken);\n\n            // If the status code indicates success, return a successful result\n            if (sendResponse.IsSuccessStatusCode) {\n                var okResult = new SmtpResult(true, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, sendResponse.StatusCode.ToString(), \"\");\n                await Helpers.PostWebhookAsync(WebhookUrl, okResult, cancellationToken);\n                return okResult;\n            }\n\n            // If the status code indicates an error, throw an exception with the content\n            var sendContent = await sendResponse.Content.ReadAsStringAsync();\n            var sendError = JsonSerializer.Deserialize(sendContent, MailozaurrJsonContext.Default.GraphApiError);\n            var sendErrorMessage = (sendError == null || sendError.Error == null || sendError.Error.InnerError == null)\n                ? $\"Unknown error: {sendContent}\"\n                : $\"Error code: {sendError.Error.Code}, message: {sendError.Error.Message}, request ID: {sendError.Error.InnerError.RequestId}, date: {sendError.Error.InnerError.Date}\";\n            var retryAfter = ParseRetryAfter(sendResponse);\n            throw new GraphApiException(sendResponse.StatusCode, sendErrorMessage, sendContent, retryAfter);\n        } catch (GraphApiException ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using Graph API: {ex.Message}\");\n            var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, ex.ResponseContent, ex.Message);\n            await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n            throw;\n        } finally {\n            MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n        }\n    }\n\n    /// <summary>\n    /// Sends the current message using Microsoft Graph batch requests.\n    /// </summary>\n    public async Task<SmtpResult> SendMessageBatchAsync(CancellationToken cancellationToken = default) {\n        var operationStopwatch = StartOperationTimer();\n        CreateMessage();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping Graph batch send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"GraphAPI\", 0, operationStopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        var credential = new GraphCredential {\n            ClientId = ApplicationID,\n            ClientSecret = ApplicationKey,\n            DirectoryId = TenantDomain\n        };\n        var bodyObj = JsonSerializer.Deserialize(MessageJson, MailozaurrJsonContext.Default.JsonElement);\n        var request = new GraphBatchRequest {\n            Id = \"1\",\n            Method = GraphHttpMethod.POST,\n            Url = $\"/users/{MessageContainer.Message.From!.Email.Address}/sendMail\",\n            Headers = new Dictionary<string, string> { [\"Content-Type\"] = \"application/json\" },\n            Body = bodyObj\n        };\n        var results = await MicrosoftGraphUtils.SendBatchAsync(credential, new[] { request }, cancellationToken);\n        var response = results.FirstOrDefault();\n        var success = response != null && response.Status >= 200 && response.Status < 300;\n        return new SmtpResult(\n            success,\n            EmailAction.Send,\n            SentTo,\n            SentFrom,\n            \"GraphAPI\",\n            0,\n            operationStopwatch.Elapsed,\n            response?.Status.ToString() ?? string.Empty,\n            success ? string.Empty : response?.Body.ToString());\n    }\n\n    /// <summary>\n    /// Creates a draft message on the server and returns the resulting <see cref=\"GraphMessage\"/>.\n    /// </summary>\n    /// <returns>The created draft message.</returns>\n    public async Task<GraphMessage> CreateDraftMessageAsync(CancellationToken cancellationToken = default) {\n        // Create the draft message\n        CreateMessage();\n\n        //var options = new JsonSerializerOptions() {\n        //    WriteIndented = true\n        //};\n\n        //// Serialize only the GraphMessage to a JSON string, excluding the SaveToSentItems property\n        //var messageJson = JsonSerializer.Serialize(MessageContainer.Message, options);\n\n        var messageJson = CreateDraft();\n\n        var draftRequestUri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{MessageContainer.Message.From!.Email.Address}/mailfolders/drafts/messages\");\n        using var draftRequest = new HttpRequestMessage(HttpMethod.Post, draftRequestUri) {\n            Content = new StringContent(messageJson, Encoding.UTF8, \"application/json\")\n        };\n\n        // Add the authorization header\n        draftRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(TokenType, AccessToken);\n\n        // Send the HTTP request for creating the draft message\n        await MicrosoftGraphUtils.ConcurrencySemaphore.WaitAsync(cancellationToken);\n        HttpResponseMessage draftResponse;\n        try {\n            draftResponse = await _client.SendAsync(draftRequest, cancellationToken);\n        } finally {\n            MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n        }\n\n        using (draftResponse) {\n            // Read the response content\n            var draftContent = await draftResponse.Content.ReadAsStringAsync();\n\n            if (!draftResponse.IsSuccessStatusCode) {\n                var error = JsonSerializer.Deserialize(draftContent, MailozaurrJsonContext.Default.GraphApiError);\n                var errorMessage = (error == null || error.Error == null)\n                    ? $\"Unknown error: {draftContent}\"\n                    : $\"Error code: {error.Error.Code}, message: {error.Error.Message}\";\n                var retryAfter = ParseRetryAfter(draftResponse);\n                throw new GraphApiException(draftResponse.StatusCode, errorMessage, draftContent, retryAfter);\n            }\n\n            // Deserialize the draft message\n            var draftMessage = JsonSerializer.Deserialize(draftContent, MailozaurrJsonContext.Default.GraphMessage);\n\n            if (draftMessage == null) {\n                throw new InvalidOperationException(\"Failed to create draft message.\");\n            }\n\n            return draftMessage;\n        }\n    }\n\n    /// <summary>\n    /// Creates a draft message locally and returns its JSON representation.\n    /// </summary>\n    /// <returns>The JSON payload for the draft message.</returns>\n    public string CreateDraftForMg() {\n        // Create the draft message\n        CreateMessage();\n        var messageJson = CreateDraft();\n        return messageJson;\n    }\n\n    /// <summary>\n    /// Serializes the current message to JSON without saving it to the Sent Items folder.\n    /// </summary>\n    /// <returns>The JSON representation of the message.</returns>\n    public string CreateDraft() {\n        CreateMessage();\n\n        // Serialize only the GraphMessage to a JSON string, excluding the SaveToSentItems property\n        var messageJson = JsonSerializer.Serialize(MessageContainer.Message, MailozaurrJsonContext.Default.GraphMessage);\n        return messageJson;\n    }\n\n\n        /// <summary>\n        /// Creates the metadata and content placeholders required for uploading a file attachment.\n        /// </summary>\n        /// <param name=\"attachmentPath\">Path to the attachment file.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        /// <param name=\"preloadContent\">\n        /// When true, loads file chunks into memory and populates <see cref=\"GraphAttachmentPlaceHolder.Content\"/>.\n        /// When false, only metadata is prepared and chunk content is generated on demand.\n        /// </param>\n        /// <returns>The placeholder representing the attachment.</returns>\n        public Task<GraphAttachmentPlaceHolder> CreateGraphAttachment(string attachmentPath, CancellationToken cancellationToken = default, bool preloadContent = true) {\n        if (!File.Exists(attachmentPath)) {\n            LogMissingAttachmentWarning(attachmentPath);\n            throw new FileNotFoundException($\"Send-EmailMessage - Attachment file not found: {attachmentPath}\", attachmentPath);\n        }\n        var fileName = Path.GetFileName(attachmentPath);\n        var fileSize = new FileInfo(attachmentPath).Length;\n\n        var attachmentItem = new GraphAttachmentItem(\"file\", fileName, fileSize);\n\n        var attachmentItemWrapper = new GraphAttachmentItemWrapper(attachmentItem);\n        var attachmentItemJson = JsonSerializer.Serialize(attachmentItemWrapper, MailozaurrJsonContext.Default.GraphAttachmentItemWrapper);\n\n        List<StreamContent> content = preloadContent\n            ? PrepareByteArrayContentForUpload(attachmentPath, ChunkSize, cancellationToken)\n            : new List<StreamContent>();\n\n        var placeholder = new GraphAttachmentPlaceHolder {\n            Json = attachmentItemJson,\n            Content = content,\n            FilePath = attachmentPath,\n            FileSize = fileSize,\n            FileName = fileName\n        };\n\n        return Task.FromResult(placeholder);\n    }\n\n        /// <summary>\n        /// Creates an upload session for a large attachment.\n        /// </summary>\n        /// <param name=\"draftMessage\">The draft message the attachment belongs to.</param>\n        /// <param name=\"attachmentItemJson\">The serialized attachment item.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        /// <returns>The upload session URL.</returns>\n        public async Task<string> CreateUploadSession(GraphMessage draftMessage, string attachmentItemJson, CancellationToken cancellationToken = default) {\n        var uploadSessionUrl = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users('{SentFrom}')/messages/{draftMessage.Id}/attachments/createUploadSession\");\n        using var request = new HttpRequestMessage(HttpMethod.Post, uploadSessionUrl) {\n            Content = new StringContent(attachmentItemJson, Encoding.UTF8, \"application/json\")\n        };\n        request.Headers.Authorization = new AuthenticationHeaderValue(TokenType, AccessToken);\n        await MicrosoftGraphUtils.ConcurrencySemaphore.WaitAsync(cancellationToken);\n        HttpResponseMessage uploadSessionResponse;\n        try {\n            uploadSessionResponse = await _client.SendAsync(request, cancellationToken);\n        } finally {\n            MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n        }\n\n        using (uploadSessionResponse) {\n            var uploadSessionContent = await uploadSessionResponse.Content.ReadAsStringAsync();\n            if (!uploadSessionResponse.IsSuccessStatusCode) {\n                GraphApiError? error = null;\n                try {\n                    error = JsonSerializer.Deserialize(uploadSessionContent, MailozaurrJsonContext.Default.GraphApiError);\n                } catch (JsonException) {\n                    // Non-JSON error response; fall back to raw content.\n                }\n                var errorMessage = (error == null || error.Error == null)\n                    ? $\"Unknown error: {uploadSessionContent}\"\n                    : $\"Error code: {error.Error.Code}, message: {error.Error.Message}\";\n                var retryAfter = ParseRetryAfter(uploadSessionResponse);\n                throw new GraphApiException(uploadSessionResponse.StatusCode, errorMessage, uploadSessionContent, retryAfter);\n            }\n\n            return ParseUploadSessionResult(uploadSessionContent);\n        }\n    }\n\n    private static string ParseUploadSessionResult(string uploadSessionContent) {\n        var uploadSessionResult = JsonSerializer.Deserialize(uploadSessionContent, MailozaurrJsonContext.Default.GraphUploadSessionResult)\n            ?? throw new InvalidOperationException(\"Failed to deserialize the upload session response.\");\n\n        if (string.IsNullOrEmpty(uploadSessionResult.UploadUrl)) {\n            throw new InvalidOperationException(\"Upload URL not found in the session response.\");\n        }\n\n        return uploadSessionResult.UploadUrl;\n    }\n\n        /// <summary>\n        /// Splits <paramref name=\"filePath\"/> into chunks no larger than <see cref=\"MaxChunkSize\"/>.\n        /// </summary>\n        /// <param name=\"filePath\">Path to the file to split.</param>\n        /// <param name=\"chunkSize\">Desired size of each chunk in bytes.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        /// <returns>List of stream contents representing file chunks.</returns>\n        private List<StreamContent> PrepareByteArrayContentForUpload(string filePath, int chunkSize = MaxChunkSize, CancellationToken cancellationToken = default) {\n        chunkSize = Math.Min(chunkSize, MaxChunkSize);\n        var fileContents = new List<StreamContent>();\n        var fileSize = new FileInfo(filePath).Length;\n\n        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);\n        var buffer = ArrayPool<byte>.Shared.Rent(chunkSize);\n        int bytesRead;\n        long offset = 0;\n        try {\n            cancellationToken.ThrowIfCancellationRequested();\n            while ((bytesRead = fileStream.Read(buffer, 0, chunkSize)) > 0) {\n                cancellationToken.ThrowIfCancellationRequested();\n\n                var chunk = new byte[bytesRead];\n                Array.Copy(buffer, chunk, bytesRead);\n                var memoryStream = new MemoryStream(chunk, writable: false);\n                var contentRange = $\"bytes {offset}-{offset + bytesRead - 1}/{fileSize}\";\n                var streamContent = new StreamContent(memoryStream);\n                streamContent.Headers.Add(\"Content-Range\", contentRange);\n                fileContents.Add(streamContent);\n                offset += bytesRead;\n            }\n        } finally {\n            ArrayPool<byte>.Shared.Return(buffer, clearArray: true);\n        }\n\n        return fileContents;\n    }\n\n        /// <summary>\n        /// Uploads all attachments for the specified draft message.\n        /// </summary>\n        /// <param name=\"draftMessage\">The draft message to attach the files to.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        public async Task UploadAttachmentsAsync(GraphMessage draftMessage, CancellationToken cancellationToken = default) {\n        if (Attachments != null && Attachments.Length > 0) {\n            foreach (var path in Attachments.OfType<string>()) {\n                try {\n                    await UploadAttachmentWithRetryAsync(draftMessage, path, cancellationToken);\n                } catch (FileNotFoundException) {\n                    // Already logged by CreateGraphAttachment.\n                }\n            }\n        }\n    }\n\n        /// <summary>\n        /// Prepares attachments for upload by creating placeholders.\n        /// </summary>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        public async Task PrepareAttachments(CancellationToken cancellationToken = default) {\n        if (Attachments != null && Attachments.Length > 0) {\n            foreach (var path in Attachments.OfType<string>()) {\n                try {\n                    var attachmentItemJson = await CreateGraphAttachment(path, cancellationToken);\n                    AttachmentsPlaceHolders.Add(attachmentItemJson);\n                } catch (FileNotFoundException) {\n                    // Already logged by CreateGraphAttachment.\n                }\n            }\n        }\n    }\n\n        /// <summary>\n        /// Uploads all chunks of a file to the provided upload session URL.\n        /// </summary>\n        /// <param name=\"uploadUrl\">The upload session URL.</param>\n        /// <param name=\"fileChunks\">The file chunks to upload.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        public async Task SendFileChunks(string uploadUrl, IEnumerable<StreamContent> fileChunks, CancellationToken cancellationToken = default) {\n        foreach (var chunk in fileChunks) {\n            await SendAttachmentChunk(uploadUrl, chunk, cancellationToken);\n        }\n    }\n\n        /// <summary>\n        /// Uploads all chunks of a file to the provided upload session URL without buffering the entire file.\n        /// </summary>\n        public async Task SendFileChunks(string uploadUrl, string filePath, long fileSize, CancellationToken cancellationToken = default) {\n        var chunkSize = Math.Min(ChunkSize, MaxChunkSize);\n        var buffer = new byte[chunkSize];\n        long offset = 0;\n        using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);\n        int bytesRead;\n        while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) {\n            var chunk = new byte[bytesRead];\n            Buffer.BlockCopy(buffer, 0, chunk, 0, bytesRead);\n            await SendAttachmentChunkWithRetryAsync(uploadUrl, chunk, offset, fileSize, cancellationToken);\n            offset += bytesRead;\n        }\n    }\n\n        /// <summary>\n        /// Uploads a single attachment chunk to the Graph API.\n        /// </summary>\n        /// <param name=\"uploadUrl\">The upload session URL.</param>\n        /// <param name=\"byteArrayContent\">The chunk to send.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        public async Task SendAttachmentChunk(string uploadUrl, StreamContent byteArrayContent, CancellationToken cancellationToken = default) {\n        using var requestMessage = new HttpRequestMessage(HttpMethod.Put, uploadUrl) {\n            Content = byteArrayContent\n        };\n        requestMessage.Headers.Add(\"AnchorMailbox\", SentFrom);\n        await MicrosoftGraphUtils.ConcurrencySemaphore.WaitAsync(cancellationToken);\n        try {\n            using var uploadChunkResponse = await _client.SendAsync(requestMessage, cancellationToken);\n            if (!uploadChunkResponse.IsSuccessStatusCode) {\n                LogCollector.LogWarning(uploadChunkResponse.ToString());\n            }\n        } finally {\n            MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n        }\n    }\n\n    private async Task SendAttachmentChunkWithRetryAsync(string uploadUrl, byte[] chunk, long offset, long fileSize, CancellationToken cancellationToken) {\n        var policy = SendPolicy ?? MailozaurrOptions.DefaultGraphPolicy;\n        int attempts = 0;\n        Exception? lastException = null;\n        var maxRetries = policy?.MaxRetries ?? RetryCount;\n        do {\n            try {\n                await SendAttachmentChunkOnceAsync(uploadUrl, chunk, offset, fileSize, cancellationToken);\n                return;\n            } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                throw;\n            } catch (Exception ex) {\n                lastException = ex;\n                var shouldRetry = (policy?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    throw;\n                }\n                var retryAfter = (ex as GraphApiException)?.RetryAfter;\n                await DelayWithBackoffAsync(policy, attempts, retryAfter, ex, cancellationToken);\n            }\n            attempts++;\n        } while (attempts <= maxRetries);\n\n        if (lastException != null) {\n            throw lastException;\n        }\n    }\n\n    private async Task UploadAttachmentWithRetryAsync(GraphMessage draftMessage, string path, CancellationToken cancellationToken) {\n        var policy = SendPolicy ?? MailozaurrOptions.DefaultGraphPolicy;\n        int attempts = 0;\n        Exception? lastException = null;\n        var maxRetries = policy?.MaxRetries ?? RetryCount;\n        do {\n            try {\n                var attachmentItemJson = await CreateGraphAttachment(path, cancellationToken, preloadContent: false);\n                var uploadUrl = await CreateUploadSession(draftMessage, attachmentItemJson.Json, cancellationToken);\n                await SendFileChunks(uploadUrl, attachmentItemJson.FilePath, attachmentItemJson.FileSize, cancellationToken);\n                return;\n            } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                throw;\n            } catch (FileNotFoundException) {\n                throw;\n            } catch (Exception ex) {\n                lastException = ex;\n                var shouldRetry = (policy?.RetryOnTransient ?? true) ? GraphRetryHelper.IsTransient(ex) : RetryAlways;\n                if ((!shouldRetry && !RetryAlways) || attempts >= maxRetries) {\n                    throw;\n                }\n                var retryAfter = (ex as GraphApiException)?.RetryAfter;\n                await DelayWithBackoffAsync(policy, attempts, retryAfter, ex, cancellationToken);\n            }\n            attempts++;\n        } while (attempts <= maxRetries);\n\n        if (lastException != null) {\n            throw lastException;\n        }\n    }\n\n    private async Task SendAttachmentChunkOnceAsync(string uploadUrl, byte[] chunk, long offset, long fileSize, CancellationToken cancellationToken) {\n        using var content = new StreamContent(new MemoryStream(chunk, writable: false));\n        var contentRange = $\"bytes {offset}-{offset + chunk.Length - 1}/{fileSize}\";\n        content.Headers.Add(\"Content-Range\", contentRange);\n        using var requestMessage = new HttpRequestMessage(HttpMethod.Put, uploadUrl) {\n            Content = content\n        };\n        requestMessage.Headers.Add(\"AnchorMailbox\", SentFrom);\n        await MicrosoftGraphUtils.ConcurrencySemaphore.WaitAsync(cancellationToken);\n        try {\n            using var uploadChunkResponse = await _client.SendAsync(requestMessage, cancellationToken);\n            if (!uploadChunkResponse.IsSuccessStatusCode) {\n                var responseContent = await uploadChunkResponse.Content.ReadAsStringAsync();\n                GraphApiError? error = null;\n                try {\n                    error = JsonSerializer.Deserialize(responseContent, MailozaurrJsonContext.Default.GraphApiError);\n                } catch (JsonException) {\n                    // Non-JSON error response; fall back to raw content.\n                }\n                var errorMessage = (error == null || error.Error == null)\n                    ? $\"Unknown error: {responseContent}\"\n                    : $\"Error code: {error.Error.Code}, message: {error.Error.Message}\";\n                var retryAfter = ParseRetryAfter(uploadChunkResponse);\n                throw new GraphApiException(uploadChunkResponse.StatusCode, errorMessage, responseContent, retryAfter);\n            }\n        } finally {\n            MicrosoftGraphUtils.ConcurrencySemaphore.Release();\n        }\n    }\n\n    private static TimeSpan? ParseRetryAfter(HttpResponseMessage response) {\n        if (response.Headers.TryGetValues(\"Retry-After\", out var values)) {\n            var first = values.FirstOrDefault();\n            if (int.TryParse(first, out var seconds)) {\n                return TimeSpan.FromSeconds(Math.Max(0, seconds));\n            }\n            if (DateTimeOffset.TryParse(first, out var ts)) {\n                var delta = ts - DateTimeOffset.UtcNow;\n                return delta > TimeSpan.Zero ? delta : TimeSpan.Zero;\n            }\n        }\n        return null;\n    }\n\n    private async Task DelayWithBackoffAsync(GraphSendPolicy? policy, int attempts, TimeSpan? retryAfter, Exception ex, CancellationToken cancellationToken) {\n        TimeSpan delay = TimeSpan.Zero;\n        if (policy != null) {\n            delay = GraphRetryHelper.CalculateDelay(policy, attempts);\n            if (GraphRetryHelper.IsThrottled(ex) && retryAfter.HasValue && retryAfter.Value > delay) {\n                delay = retryAfter.Value;\n            }\n            if (policy.MaxDelayMs > 0 && delay > TimeSpan.FromMilliseconds(policy.MaxDelayMs)) {\n                delay = TimeSpan.FromMilliseconds(policy.MaxDelayMs);\n            }\n        } else {\n            var delayMs = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempts));\n            if (delayMs > 0) delay = TimeSpan.FromMilliseconds(delayMs);\n        }\n\n        if (delay > TimeSpan.Zero) {\n            var reason = GraphRetryHelper.IsThrottled(ex) ? \"throttling\" : \"transient\";\n            LogCollector.LogVerbose($\"Send-EmailMessage - Retry attempt {attempts + 1}, delaying {delay.TotalMilliseconds:N0} ms due to {reason}.\");\n            await Task.Delay(delay, cancellationToken);\n        }\n    }\n\n    private const string DefaultAttachmentName = \"attachment.bin\";\n\n    private async Task<SmtpResult> TrySmtpFallbackAsync(GraphSendPolicy? policy, SmtpResult current, Exception? lastException, CancellationToken cancellationToken) {\n        if (policy == null || !policy.EnableSmtpFallback) {\n            return current;\n        }\n\n        var smtp = _smtpFallbackFactory?.Invoke() ?? MailozaurrOptions.SmtpFallbackFactory?.Invoke(this);\n        if (smtp == null) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - SMTP fallback requested but no SMTP factory configured.\");\n            return current;\n        }\n\n        try {\n            smtp.From = this.From;\n            smtp.To = this.To;\n            smtp.Cc = this.Cc;\n            smtp.Bcc = this.Bcc;\n            smtp.ReplyTo = string.IsNullOrWhiteSpace(this.ReplyTo) ? null : this.ReplyTo;\n            smtp.Subject = this.Subject;\n            smtp.HtmlBody = this.HTML;\n            smtp.Headers = this.Headers;\n            smtp.WebhookUrl = this.WebhookUrl;\n            smtp.Priority = this.Priority;\n\n            if (this.ConvertedAttachments != null && this.ConvertedAttachments.Count > 0) {\n                var attachments = new List<Definitions.AttachmentDescriptor>();\n                var inline = new List<Definitions.AttachmentDescriptor>();\n                foreach (var a in this.ConvertedAttachments) {\n                    if (string.IsNullOrWhiteSpace(a.ContentBytes)) continue;\n                    try {\n                        var bytes = Convert.FromBase64String(a.ContentBytes);\n                        var d = new Definitions.ByteArrayAttachmentDescriptor(bytes, string.IsNullOrWhiteSpace(a.Name) ? DefaultAttachmentName : a.Name);\n                        if (!string.IsNullOrWhiteSpace(a.ContentId)) d.ContentId = a.ContentId;\n                        if (a.IsInline) inline.Add(d); else attachments.Add(d);\n                    } catch (FormatException fex) {\n                        LogCollector.LogWarning($\"Send-EmailMessage - SMTP fallback skipped invalid base64 attachment '{(a?.Name ?? \"(unnamed)\")}' : {fex.Message}\");\n                    }\n                }\n                if (attachments.Count > 0) smtp.Attachments = attachments;\n                if (inline.Count > 0) smtp.InlineAttachments = inline;\n            }\n\n            await smtp.CreateMessageAsync(cancellationToken).ConfigureAwait(false);\n            LogCollector.LogVerbose(\"Send-EmailMessage - Sending via SMTP fallback after Graph failure.\");\n            return await smtp.SendAsync(cancellationToken).ConfigureAwait(false);\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - SMTP fallback failed: {ex.Message}\");\n            // Preserve original Graph failure, but add fallback context to Error for diagnostics\n            var fallbackError = ex.ToString();\n            var mergedError = string.IsNullOrWhiteSpace(current.Error)\n                ? $\"Graph failed; SMTP fallback error: {fallbackError}\"\n                : $\"{current.Error} | SMTP fallback error: {fallbackError}\";\n            return new SmtpResult(current.Status, current.EmailAction, current.SentTo, current.SentFrom, current.Server, current.Port, current.TimeToExecute, current.Message, mergedError)\n            {\n                GraphError = current.GraphError,\n                MessageId = current.MessageId\n            };\n        }\n    }\n\n    private static string MapImportance(MessagePriority priority) {\n        return priority switch {\n            MessagePriority.High => \"high\",\n            MessagePriority.Low => \"low\",\n            _ => \"normal\"\n        };\n    }\n\n    /// <summary>\n    /// Releases resources used by the Graph client.\n    /// </summary>\n    public void Dispose() {\n        _client.Dispose();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiClient.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Lightweight client for interacting with Microsoft Graph REST API using a bearer access token.\n/// </summary>\n/// <remarks>\n/// This is a minimal helper focused on reusable primitives needed by apps (for example, managing webhook subscriptions).\n/// </remarks>\npublic sealed class GraphApiClient : IDisposable {\n    private readonly HttpClient _client;\n    private readonly Func<CancellationToken, Task<string>>? _refreshToken;\n    private readonly OAuthCredential? _credential;\n    private readonly bool _disposeClient;\n    private bool _disposed;\n\n    private void ThrowIfDisposed() {\n        if (_disposed) {\n            throw new ObjectDisposedException(nameof(GraphApiClient));\n        }\n    }\n\n    private void ApplyAuthHeader(HttpRequestMessage request) {\n        // Avoid mutating HttpClient.DefaultRequestHeaders.Authorization (thread-safety + token refresh semantics).\n        // If no credential was provided, we assume the caller configured auth on the HttpClient itself.\n        if (_credential != null && !string.IsNullOrWhiteSpace(_credential.AccessToken) && request.Headers.Authorization == null) {\n            request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _credential.AccessToken);\n        }\n    }\n\n    /// <summary>\n    /// Initializes the client using the provided OAuth credential.\n    /// </summary>\n    /// <param name=\"credential\">OAuth credential holding an access token.</param>\n    /// <param name=\"refreshToken\">\n    /// Optional delegate used to refresh an access token when a request returns 401/403.\n    /// Note: the client updates its Authorization header (and the credential's AccessToken, if provided), but does not retry the failed request automatically.\n    /// </param>\n    /// <param name=\"baseAddress\">Optional Graph base address (defaults to v1.0 endpoint).</param>\n    public GraphApiClient(\n        OAuthCredential credential,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        _credential = credential ?? throw new ArgumentNullException(nameof(credential));\n        _refreshToken = refreshToken;\n        _disposeClient = true;\n        _client = new HttpClient {\n            BaseAddress = baseAddress ?? new Uri(\"https://graph.microsoft.com/v1.0/\")\n        };\n    }\n\n    /// <summary>\n    /// Initializes the client using an externally managed <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <remarks>\n    /// If <paramref name=\"client\"/> does not specify <see cref=\"HttpClient.BaseAddress\"/>, it will be set to the Graph v1.0 endpoint\n    /// (or <paramref name=\"baseAddress\"/> if provided).\n    /// </remarks>\n    /// <param name=\"client\">HTTP client to use for requests.</param>\n    /// <param name=\"refreshToken\">Optional delegate used to refresh an access token when a request returns 401/403.</param>\n    /// <param name=\"credential\">Optional OAuth credential holding an access token.</param>\n    /// <param name=\"baseAddress\">Optional Graph base address used when <paramref name=\"client\"/> has no base address configured.</param>\n    /// <param name=\"ownsHttpClient\">When <c>true</c>, disposing this API client also disposes <paramref name=\"client\"/>.</param>\n    public GraphApiClient(\n        HttpClient client,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        OAuthCredential? credential = null,\n        Uri? baseAddress = null,\n        bool ownsHttpClient = false) {\n        _client = client ?? throw new ArgumentNullException(nameof(client));\n        _refreshToken = refreshToken;\n        _credential = credential;\n        _disposeClient = ownsHttpClient;\n        if (_client.BaseAddress == null) {\n            _client.BaseAddress = baseAddress ?? new Uri(\"https://graph.microsoft.com/v1.0/\");\n        }\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        if (_disposed) {\n            return;\n        }\n\n        if (_disposeClient) {\n            _client.Dispose();\n        }\n\n        _disposed = true;\n        GC.SuppressFinalize(this);\n    }\n\n    private async Task ThrowIfAuthErrorAsync(HttpResponseMessage response, CancellationToken cancellationToken) {\n        if (response.StatusCode == HttpStatusCode.Unauthorized ||\n            response.StatusCode == HttpStatusCode.Forbidden) {\n            if (_refreshToken != null) {\n                string token = await _refreshToken(cancellationToken).ConfigureAwait(false);\n                if (_credential != null) {\n                    _credential.AccessToken = token;\n                }\n            }\n#if NET5_0_OR_GREATER\n            string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new GraphApiException(response.StatusCode, \"Graph authentication failed.\", content);\n        }\n    }\n\n    private static TimeSpan? TryGetRetryAfter(HttpResponseMessage response) {\n        if (response.Headers?.RetryAfter == null) {\n            return null;\n        }\n        if (response.Headers.RetryAfter.Delta.HasValue) {\n            return response.Headers.RetryAfter.Delta.Value;\n        }\n        if (response.Headers.RetryAfter.Date.HasValue) {\n            var utc = response.Headers.RetryAfter.Date.Value.ToUniversalTime();\n            var now = DateTimeOffset.UtcNow;\n            if (utc > now) {\n                return utc - now;\n            }\n        }\n        return null;\n    }\n\n    /// <summary>\n    /// Creates a webhook subscription.\n    /// </summary>\n    public async Task<GraphSubscription> CreateSubscriptionAsync(GraphCreateSubscriptionRequest request, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n        if (string.IsNullOrWhiteSpace(request.Resource)) {\n            throw new ArgumentException(\"Resource is required.\", nameof(request));\n        }\n        if (string.IsNullOrWhiteSpace(request.ChangeType)) {\n            throw new ArgumentException(\"ChangeType is required.\", nameof(request));\n        }\n        if (string.IsNullOrWhiteSpace(request.NotificationUrl)) {\n            throw new ArgumentException(\"NotificationUrl is required.\", nameof(request));\n        }\n        if (request.ExpirationDateTime == default) {\n            throw new ArgumentException(\"ExpirationDateTime is required.\", nameof(request));\n        }\n\n        var json = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GraphCreateSubscriptionRequest);\n        using var content = new StringContent(json, Encoding.UTF8, \"application/json\");\n        using var req = new HttpRequestMessage(HttpMethod.Post, \"subscriptions\") { Content = content };\n        ApplyAuthHeader(req);\n        using var response = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new GraphApiException(response.StatusCode, $\"Graph subscription create failed ({(int)response.StatusCode}).\", body, TryGetRetryAfter(response));\n        }\n\n        GraphSubscription? result;\n        try {\n            result = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphSubscription);\n        } catch (JsonException ex) {\n            throw new InvalidDataException(\"Failed to parse Graph subscription create response.\", ex);\n        }\n        if (result is null) {\n            throw new InvalidDataException(\"Graph returned an invalid subscription response.\");\n        }\n        return result;\n    }\n\n    /// <summary>\n    /// Renews a webhook subscription by updating its expiration.\n    /// </summary>\n    public async Task<GraphSubscription> RenewSubscriptionAsync(string subscriptionId, DateTimeOffset expirationDateTime, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(subscriptionId)) {\n            throw new ArgumentException(\"subscriptionId is required.\", nameof(subscriptionId));\n        }\n\n        var encodedId = Uri.EscapeDataString(subscriptionId.Trim());\n        var request = new GraphRenewSubscriptionRequest { ExpirationDateTime = expirationDateTime };\n        var json = JsonSerializer.Serialize(request, MailozaurrJsonContext.Default.GraphRenewSubscriptionRequest);\n        var requestUri = _client.BaseAddress != null\n            ? new Uri(_client.BaseAddress, $\"subscriptions/{encodedId}\")\n            : new Uri($\"subscriptions/{encodedId}\", UriKind.Relative);\n        using var req = new HttpRequestMessage(new HttpMethod(\"PATCH\"), requestUri) {\n            Content = new StringContent(json, Encoding.UTF8, \"application/json\")\n        };\n        ApplyAuthHeader(req);\n        using var response = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new GraphApiException(response.StatusCode, $\"Graph subscription renew failed ({(int)response.StatusCode}).\", body, TryGetRetryAfter(response));\n        }\n\n        GraphSubscription? result;\n        try {\n            result = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphSubscription);\n        } catch (JsonException ex) {\n            throw new InvalidDataException(\"Failed to parse Graph subscription renew response.\", ex);\n        }\n        if (result is null) {\n            throw new InvalidDataException(\"Graph returned an invalid subscription response.\");\n        }\n        return result;\n    }\n\n    /// <summary>\n    /// Deletes a webhook subscription.\n    /// </summary>\n    public async Task DeleteSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(subscriptionId)) {\n            throw new ArgumentException(\"subscriptionId is required.\", nameof(subscriptionId));\n        }\n        var encodedId = Uri.EscapeDataString(subscriptionId.Trim());\n        using var req = new HttpRequestMessage(HttpMethod.Delete, $\"subscriptions/{encodedId}\");\n        ApplyAuthHeader(req);\n        using var response = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new GraphApiException(response.StatusCode, $\"Graph subscription delete failed ({(int)response.StatusCode}).\", body, TryGetRetryAfter(response));\n        }\n    }\n\n    /// <summary>\n    /// Lists webhook subscriptions for the current token context.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphSubscription>> ListSubscriptionsAsync(CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        using var req = new HttpRequestMessage(HttpMethod.Get, \"subscriptions\");\n        ApplyAuthHeader(req);\n        using var response = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(response, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new GraphApiException(response.StatusCode, $\"Graph subscriptions list failed ({(int)response.StatusCode}).\", body, TryGetRetryAfter(response));\n        }\n        GraphSubscriptionListResponse? result;\n        try {\n            result = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphSubscriptionListResponse);\n        } catch (JsonException ex) {\n            throw new InvalidDataException(\"Failed to parse Graph subscriptions list response.\", ex);\n        }\n        return (IReadOnlyList<GraphSubscription>?)result?.Value ?? Array.Empty<GraphSubscription>();\n    }\n\n    private static string BuildUserSegment(string userId) {\n        var u = (userId ?? string.Empty).Trim();\n        if (u.Length == 0 || u.Equals(\"me\", StringComparison.OrdinalIgnoreCase)) {\n            return \"me\";\n        }\n        return \"users/\" + Uri.EscapeDataString(u);\n    }\n\n    private static int ClampInt(int value, int min, int max) {\n        if (value < min) return min;\n        if (value > max) return max;\n        return value;\n    }\n\n    private static string EscapeODataStringLiteral(string value) => value.Replace(\"'\", \"''\");\n\n    private static string? TryGetString(JsonElement obj, string propertyName) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!obj.TryGetProperty(propertyName, out var el) || el.ValueKind != JsonValueKind.String) {\n            return null;\n        }\n        var s = el.GetString();\n        return string.IsNullOrWhiteSpace(s) ? null : s;\n    }\n\n    private static int? TryGetInt(JsonElement obj, string propertyName) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!obj.TryGetProperty(propertyName, out var el) || el.ValueKind != JsonValueKind.Number) {\n            return null;\n        }\n        return el.TryGetInt32(out var v) ? v : null;\n    }\n\n    private static bool? TryGetBool(JsonElement obj, string propertyName) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!obj.TryGetProperty(propertyName, out var el)) {\n            return null;\n        }\n        if (el.ValueKind == JsonValueKind.True) {\n            return true;\n        }\n        if (el.ValueKind == JsonValueKind.False) {\n            return false;\n        }\n        return null;\n    }\n\n    private static DateTimeOffset? TryGetDateTimeOffset(JsonElement obj, string propertyName) {\n        var s = TryGetString(obj, propertyName);\n        if (string.IsNullOrWhiteSpace(s)) {\n            return null;\n        }\n        return DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt)\n            ? dt\n            : null;\n    }\n\n    private static GraphEmailAddress? TryParseEmailAddress(JsonElement recipient) {\n        if (recipient.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!recipient.TryGetProperty(\"emailAddress\", out var emailAddress) || emailAddress.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!emailAddress.TryGetProperty(\"address\", out var addressEl) || addressEl.ValueKind != JsonValueKind.String) {\n            return null;\n        }\n        var address = addressEl.GetString();\n        if (address == null) {\n            return null;\n        }\n        address = address.Trim();\n        if (address.Length == 0) {\n            return null;\n        }\n        return new GraphEmailAddress { Email = new GraphEmail { Address = address } };\n    }\n\n    private static List<GraphEmailAddress>? TryParseEmailAddressList(JsonElement obj, string propertyName) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        if (!obj.TryGetProperty(propertyName, out var el) || el.ValueKind != JsonValueKind.Array) {\n            return null;\n        }\n        var list = new List<GraphEmailAddress>();\n        foreach (var item in el.EnumerateArray()) {\n            var addr = TryParseEmailAddress(item);\n            if (addr != null) {\n                list.Add(addr);\n            }\n        }\n        return list.Count == 0 ? null : list;\n    }\n\n    private static GraphMailMessage? TryParseMailMessage(JsonElement obj) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        var idRaw = TryGetString(obj, \"id\");\n        if (idRaw == null) {\n            return null;\n        }\n        var id = idRaw.Trim();\n        if (id.Length == 0) {\n            return null;\n        }\n        var msg = new GraphMailMessage {\n            Id = id,\n            Subject = TryGetString(obj, \"subject\"),\n            ReceivedDateTime = TryGetDateTimeOffset(obj, \"receivedDateTime\"),\n            InternetMessageId = TryGetString(obj, \"internetMessageId\"),\n            HasAttachments = TryGetBool(obj, \"hasAttachments\"),\n            IsRead = TryGetBool(obj, \"isRead\"),\n            ConversationId = TryGetString(obj, \"conversationId\")\n        };\n\n        if (obj.TryGetProperty(\"from\", out var fromEl)) {\n            msg.From = TryParseEmailAddress(fromEl);\n        }\n        msg.ToRecipients = TryParseEmailAddressList(obj, \"toRecipients\");\n\n        if (obj.TryGetProperty(\"flag\", out var flagEl) && flagEl.ValueKind == JsonValueKind.Object) {\n            var status = TryGetString(flagEl, \"flagStatus\");\n            if (status != null) {\n                var trimmed = status.Trim();\n                if (trimmed.Length > 0) {\n                    msg.Flag = new GraphMailMessageFlag { FlagStatus = trimmed };\n                }\n            }\n        }\n        return msg;\n    }\n\n    private static GraphMailFolder? TryParseMailFolder(JsonElement obj) {\n        if (obj.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n        var idRaw = TryGetString(obj, \"id\");\n        if (idRaw == null) {\n            return null;\n        }\n        var id = idRaw.Trim();\n        if (id.Length == 0) {\n            return null;\n        }\n        return new GraphMailFolder {\n            Id = id,\n            DisplayName = (TryGetString(obj, \"displayName\") ?? string.Empty).Trim(),\n            ParentFolderId = TryGetString(obj, \"parentFolderId\")?.Trim(),\n            ChildFolderCount = TryGetInt(obj, \"childFolderCount\"),\n            WellKnownName = TryGetString(obj, \"wellKnownName\")?.Trim(),\n            TotalItemCount = TryGetInt(obj, \"totalItemCount\"),\n            UnreadItemCount = TryGetInt(obj, \"unreadItemCount\")\n        };\n    }\n\n    /// <summary>\n    /// Lists mail folders for the given user, recursively expanding child folders.\n    /// </summary>\n    /// <remarks>\n    /// This performs best-effort traversal with safeguards to prevent infinite loops in pathological cases.\n    /// </remarks>\n    public async Task<IReadOnlyList<GraphMailFolder>> ListMailFoldersRecursiveAsync(\n        string userId = \"me\",\n        int top = 200,\n        string? select = \"id,displayName,parentFolderId,childFolderCount,wellKnownName,totalItemCount,unreadItemCount\",\n        int maxRequests = 250,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n\n        var safeTop = ClampInt(top, 1, 999);\n        var safeMax = ClampInt(maxRequests, 1, 5000);\n        var userSegment = BuildUserSegment(userId);\n\n        var foldersById = new Dictionary<string, GraphMailFolder>(StringComparer.Ordinal);\n        var pending = new Queue<string>();\n        var initial = new StringBuilder();\n        initial.Append(userSegment).Append(\"/mailFolders?$top=\").Append(safeTop.ToString(CultureInfo.InvariantCulture));\n        var selectValue = select == null ? null : select.Trim();\n        if (selectValue != null && selectValue.Length > 0) {\n            initial.Append(\"&$select=\").Append(Uri.EscapeDataString(selectValue));\n        }\n        pending.Enqueue(initial.ToString());\n\n        var processed = 0;\n        while (pending.Count > 0) {\n            cancellationToken.ThrowIfCancellationRequested();\n            if (processed++ >= safeMax) {\n                break;\n            }\n\n            var url = pending.Dequeue();\n            using var req = new HttpRequestMessage(HttpMethod.Get, url);\n            ApplyAuthHeader(req);\n            using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n            await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n            var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            if (!resp.IsSuccessStatusCode) {\n                throw new GraphApiException(resp.StatusCode, $\"Graph mailFolders list failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n            }\n\n            using var doc = JsonDocument.Parse(body);\n            if (doc.RootElement.TryGetProperty(\"value\", out var value) && value.ValueKind == JsonValueKind.Array) {\n                foreach (var item in value.EnumerateArray()) {\n                    var folder = TryParseMailFolder(item);\n                    if (folder == null || string.IsNullOrWhiteSpace(folder.Id)) {\n                        continue;\n                    }\n                    foldersById[folder.Id] = folder;\n\n                    if (folder.ChildFolderCount.HasValue && folder.ChildFolderCount.Value > 0) {\n                        var childUrl = new StringBuilder();\n                        childUrl.Append(userSegment)\n                            .Append(\"/mailFolders/\")\n                            .Append(Uri.EscapeDataString(folder.Id))\n                            .Append(\"/childFolders?$top=\")\n                            .Append(safeTop.ToString(CultureInfo.InvariantCulture));\n                        if (selectValue != null && selectValue.Length > 0) {\n                            childUrl.Append(\"&$select=\").Append(Uri.EscapeDataString(selectValue));\n                        }\n                        pending.Enqueue(childUrl.ToString());\n                    }\n                }\n            }\n\n            if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next) && next.ValueKind == JsonValueKind.String) {\n                var nextLink = next.GetString();\n                if (nextLink != null && nextLink.Trim().Length > 0) {\n                    pending.Enqueue(nextLink);\n                }\n            }\n        }\n\n        var output = new List<GraphMailFolder>(foldersById.Count);\n        output.AddRange(foldersById.Values);\n        output.Sort(static (a, b) => {\n            var r = string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);\n            if (r != 0) return r;\n            return string.Compare(a.Id, b.Id, StringComparison.Ordinal);\n        });\n        return output;\n    }\n\n    /// <summary>\n    /// Gets a single mail folder record.\n    /// </summary>\n    public async Task<GraphMailFolder> GetMailFolderAsync(\n        string folderIdOrWellKnownName,\n        string userId = \"me\",\n        string? select = \"id,displayName,parentFolderId,childFolderCount,wellKnownName,totalItemCount,unreadItemCount\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(folderIdOrWellKnownName)) {\n            throw new ArgumentException(\"folderIdOrWellKnownName is required.\", nameof(folderIdOrWellKnownName));\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(folderIdOrWellKnownName.Trim());\n        var url = new StringBuilder();\n        url.Append(userSegment).Append(\"/mailFolders/\").Append(selector);\n        var selectValue = select == null ? null : select.Trim();\n        if (selectValue != null && selectValue.Length > 0) {\n            url.Append(\"?$select=\").Append(Uri.EscapeDataString(selectValue));\n        }\n\n        using var req = new HttpRequestMessage(HttpMethod.Get, url.ToString());\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph mailFolder get failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        using var doc = JsonDocument.Parse(body);\n        var folder = TryParseMailFolder(doc.RootElement);\n        if (folder is null) {\n            throw new InvalidDataException(\"Graph returned an invalid mail folder response.\");\n        }\n        return folder;\n    }\n\n    /// <summary>\n    /// Lists messages within a mail folder.\n    /// </summary>\n    public async Task<GraphPage<GraphMailMessage>> ListMessagesAsync(\n        string folderIdOrWellKnownName,\n        string userId = \"me\",\n        int top = 100,\n        int? skip = null,\n        string? select = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\",\n        string? orderBy = \"receivedDateTime desc\",\n        string? filter = null,\n        string? search = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(folderIdOrWellKnownName)) {\n            throw new ArgumentException(\"folderIdOrWellKnownName is required.\", nameof(folderIdOrWellKnownName));\n        }\n\n        var safeTop = ClampInt(top, 1, 999);\n        var userSegment = BuildUserSegment(userId);\n        var folderSelector = Uri.EscapeDataString(folderIdOrWellKnownName.Trim());\n\n        var url = new StringBuilder();\n        url.Append(userSegment).Append(\"/mailFolders/\").Append(folderSelector).Append(\"/messages\");\n        url.Append(\"?$top=\").Append(safeTop.ToString(CultureInfo.InvariantCulture));\n        if (skip.HasValue && skip.Value > 0) {\n            url.Append(\"&$skip=\").Append(skip.Value.ToString(CultureInfo.InvariantCulture));\n        }\n        var orderByValue = orderBy == null ? null : orderBy.Trim();\n        if (orderByValue != null && orderByValue.Length > 0) {\n            url.Append(\"&$orderby=\").Append(Uri.EscapeDataString(orderByValue));\n        }\n        var selectValue = select == null ? null : select.Trim();\n        if (selectValue != null && selectValue.Length > 0) {\n            url.Append(\"&$select=\").Append(Uri.EscapeDataString(selectValue));\n        }\n        var filterValue = filter == null ? null : filter.Trim();\n        if (filterValue != null && filterValue.Length > 0) {\n            url.Append(\"&$filter=\").Append(Uri.EscapeDataString(filterValue));\n        }\n        var searchValue = search == null ? null : search.Trim();\n        if (searchValue != null && searchValue.Length > 0) {\n            var sanitized = searchValue.Replace(\"\\\"\", string.Empty).Trim();\n            if (sanitized.Length > 0) {\n                url.Append(\"&$search=\").Append(Uri.EscapeDataString(\"\\\"\" + sanitized + \"\\\"\"));\n            }\n        }\n\n        using var req = new HttpRequestMessage(HttpMethod.Get, url.ToString());\n        ApplyAuthHeader(req);\n        if (searchValue != null && searchValue.Length > 0) {\n            req.Headers.TryAddWithoutValidation(\"ConsistencyLevel\", \"eventual\");\n            req.Headers.TryAddWithoutValidation(\"Prefer\", \"HonorNonIndexedQueriesWarning=true\");\n        }\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph messages list failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        var items = new List<GraphMailMessage>();\n        string? nextLink = null;\n        using (var doc = JsonDocument.Parse(body)) {\n            if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next) && next.ValueKind == JsonValueKind.String) {\n                nextLink = next.GetString();\n            }\n            if (doc.RootElement.TryGetProperty(\"value\", out var value) && value.ValueKind == JsonValueKind.Array) {\n                foreach (var it in value.EnumerateArray()) {\n                    var msg = TryParseMailMessage(it);\n                    if (msg != null) {\n                        items.Add(msg);\n                    }\n                }\n            }\n        }\n\n        return new GraphPage<GraphMailMessage>(items, nextLink);\n    }\n\n    /// <summary>\n    /// Lists message ids for a Graph conversation.\n    /// </summary>\n    public async Task<IReadOnlyList<string>> ListConversationMessageIdsAsync(\n        string conversationId,\n        string userId = \"me\",\n        int top = 100,\n        int maxPages = 25,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(conversationId)) {\n            throw new ArgumentException(\"conversationId is required.\", nameof(conversationId));\n        }\n\n        var safeTop = ClampInt(top, 1, 999);\n        var safeMaxPages = ClampInt(maxPages, 1, 500);\n        var userSegment = BuildUserSegment(userId);\n\n        var ids = new List<string>();\n        var pages = 0;\n        var filter = \"conversationId eq '\" + EscapeODataStringLiteral(conversationId.Trim()) + \"'\";\n        var url = new StringBuilder();\n        url.Append(userSegment).Append(\"/messages?$select=id&$top=\").Append(safeTop.ToString(CultureInfo.InvariantCulture));\n        url.Append(\"&$filter=\").Append(Uri.EscapeDataString(filter));\n\n        string nextUrl = url.ToString();\n        while (pages++ < safeMaxPages) {\n            cancellationToken.ThrowIfCancellationRequested();\n            using var req = new HttpRequestMessage(HttpMethod.Get, nextUrl);\n            ApplyAuthHeader(req);\n            using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n            await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n            var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            if (!resp.IsSuccessStatusCode) {\n                throw new GraphApiException(resp.StatusCode, $\"Graph conversation list failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n            }\n\n            using var doc = JsonDocument.Parse(body);\n            if (doc.RootElement.TryGetProperty(\"value\", out var value) && value.ValueKind == JsonValueKind.Array) {\n                foreach (var it in value.EnumerateArray()) {\n                    var id = TryGetString(it, \"id\");\n                    if (id != null) {\n                        var trimmed = id.Trim();\n                        if (trimmed.Length > 0) {\n                            ids.Add(trimmed);\n                        }\n                    }\n                }\n            }\n            if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next) && next.ValueKind == JsonValueKind.String) {\n                var link = next.GetString();\n                if (link != null) {\n                    var trimmed = link.Trim();\n                    if (trimmed.Length > 0) {\n                        nextUrl = trimmed;\n                        continue;\n                    }\n                }\n            }\n            break;\n        }\n\n        return ids;\n    }\n\n    /// <summary>\n    /// Lists message metadata for a Graph conversation.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphMailMessage>> ListConversationMessagesAsync(\n        string conversationId,\n        string userId = \"me\",\n        int top = 100,\n        int maxPages = 25,\n        string? select = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\",\n        string? orderBy = \"receivedDateTime desc\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(conversationId)) {\n            throw new ArgumentException(\"conversationId is required.\", nameof(conversationId));\n        }\n\n        var safeTop = ClampInt(top, 1, 999);\n        var safeMaxPages = ClampInt(maxPages, 1, 500);\n        var userSegment = BuildUserSegment(userId);\n\n        var messages = new List<GraphMailMessage>();\n        var pages = 0;\n        var filter = \"conversationId eq '\" + EscapeODataStringLiteral(conversationId.Trim()) + \"'\";\n        var url = new StringBuilder();\n        url.Append(userSegment).Append(\"/messages?$top=\").Append(safeTop.ToString(CultureInfo.InvariantCulture));\n        url.Append(\"&$filter=\").Append(Uri.EscapeDataString(filter));\n\n        var selectValue = select == null ? null : select.Trim();\n        if (selectValue != null && selectValue.Length > 0) {\n            url.Append(\"&$select=\").Append(Uri.EscapeDataString(selectValue));\n        }\n\n        var orderByValue = orderBy == null ? null : orderBy.Trim();\n        if (orderByValue != null && orderByValue.Length > 0) {\n            url.Append(\"&$orderby=\").Append(Uri.EscapeDataString(orderByValue));\n        }\n\n        string nextUrl = url.ToString();\n        while (pages++ < safeMaxPages) {\n            cancellationToken.ThrowIfCancellationRequested();\n            using var req = new HttpRequestMessage(HttpMethod.Get, nextUrl);\n            ApplyAuthHeader(req);\n            using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n            await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n            var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            if (!resp.IsSuccessStatusCode) {\n                throw new GraphApiException(resp.StatusCode, $\"Graph conversation list failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n            }\n\n            using var doc = JsonDocument.Parse(body);\n            if (doc.RootElement.TryGetProperty(\"value\", out var value) && value.ValueKind == JsonValueKind.Array) {\n                foreach (var it in value.EnumerateArray()) {\n                    var msg = TryParseMailMessage(it);\n                    if (msg != null) {\n                        messages.Add(msg);\n                    }\n                }\n            }\n\n            if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next) && next.ValueKind == JsonValueKind.String) {\n                var link = next.GetString();\n                if (link != null) {\n                    var trimmed = link.Trim();\n                    if (trimmed.Length > 0) {\n                        nextUrl = trimmed;\n                        continue;\n                    }\n                }\n            }\n            break;\n        }\n\n        return messages;\n    }\n\n    /// <summary>\n    /// Gets message metadata.\n    /// </summary>\n    public async Task<GraphMailMessage> GetMessageAsync(\n        string messageId,\n        string userId = \"me\",\n        string? select = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var url = new StringBuilder();\n        url.Append(userSegment).Append(\"/messages/\").Append(selector);\n        var selectValue = select == null ? null : select.Trim();\n        if (selectValue != null && selectValue.Length > 0) {\n            url.Append(\"?$select=\").Append(Uri.EscapeDataString(selectValue));\n        }\n\n        using var req = new HttpRequestMessage(HttpMethod.Get, url.ToString());\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph message get failed for messageId '{messageId.Trim()}' ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        using var doc = JsonDocument.Parse(body);\n        var msg = TryParseMailMessage(doc.RootElement);\n        if (msg is null) {\n            throw new InvalidDataException(\"Graph returned an invalid message response.\");\n        }\n        return msg;\n    }\n\n    /// <summary>\n    /// Downloads the message MIME content via the <c>/$value</c> endpoint.\n    /// </summary>\n    public async Task<byte[]> GetMessageMimeAsync(\n        string messageId,\n        string userId = \"me\",\n        int maxBytes = 25 * 1024 * 1024,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (maxBytes <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxBytes), \"maxBytes must be > 0.\");\n        }\n        const int hardLimitBytes = 256 * 1024 * 1024;\n        if (maxBytes > hardLimitBytes) {\n            throw new ArgumentOutOfRangeException(nameof(maxBytes), $\"maxBytes must be <= {hardLimitBytes}.\");\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var url = userSegment + \"/messages/\" + selector + \"/$value\";\n\n        using var req = new HttpRequestMessage(HttpMethod.Get, url);\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n        if (!resp.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n            var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new GraphApiException(resp.StatusCode, $\"Graph MIME fetch failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n#if NET5_0_OR_GREATER\n        await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);\n#else\n        using var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);\n#endif\n        using var ms = new MemoryStream();\n        var buffer = new byte[81920];\n        while (true) {\n            cancellationToken.ThrowIfCancellationRequested();\n#if NET5_0_OR_GREATER\n            var read = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);\n#else\n            var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);\n#endif\n            if (read <= 0) {\n                break;\n            }\n            if (ms.Length + read > maxBytes) {\n                throw new InvalidDataException($\"Graph MIME content exceeds {maxBytes} bytes.\");\n            }\n#if NET5_0_OR_GREATER\n            await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);\n#else\n            ms.Write(buffer, 0, read);\n#endif\n        }\n        return ms.ToArray();\n    }\n\n    /// <summary>\n    /// Creates a message draft, optionally under a specific folder.\n    /// </summary>\n    public async Task<GraphMessage> CreateMessageAsync(\n        GraphMessage message,\n        string userId = \"me\",\n        string? folderIdOrWellKnownName = null,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        string? trimmedFolderId = null;\n        if (folderIdOrWellKnownName != null) {\n            var candidate = folderIdOrWellKnownName.Trim();\n            if (candidate.Length > 0) {\n                trimmedFolderId = candidate;\n            }\n        }\n        var url = trimmedFolderId == null\n            ? userSegment + \"/messages\"\n            : userSegment + \"/mailFolders/\" + Uri.EscapeDataString(trimmedFolderId) + \"/messages\";\n        var payload = JsonSerializer.Serialize(message, MailozaurrJsonContext.Default.GraphMessage);\n        using var req = new HttpRequestMessage(HttpMethod.Post, url) {\n            Content = new StringContent(payload, Encoding.UTF8, \"application/json\")\n        };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph message create failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        GraphMessage? created;\n        try {\n            created = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphMessage);\n        } catch (JsonException ex) {\n            throw new InvalidDataException(\"Failed to parse Graph message create response.\", ex);\n        }\n        if (created == null || string.IsNullOrWhiteSpace(created.Id)) {\n            throw new InvalidDataException(\"Graph returned an invalid created message response.\");\n        }\n        return created;\n    }\n\n    /// <summary>\n    /// Sends an existing draft message.\n    /// </summary>\n    public async Task SendDraftMessageAsync(\n        string messageId,\n        string userId = \"me\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        using var req = new HttpRequestMessage(HttpMethod.Post, userSegment + \"/messages/\" + selector + \"/send\");\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph draft send failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n    }\n\n    /// <summary>\n    /// Creates attachment upload session for an existing draft message.\n    /// </summary>\n    public async Task<GraphUploadSessionResult> CreateAttachmentUploadSessionAsync(\n        string messageId,\n        GraphAttachmentItem attachmentItem,\n        string userId = \"me\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (attachmentItem == null) {\n            throw new ArgumentNullException(nameof(attachmentItem));\n        }\n        if (string.IsNullOrWhiteSpace(attachmentItem.Name)) {\n            throw new ArgumentException(\"attachmentItem.Name is required.\", nameof(attachmentItem));\n        }\n        if (attachmentItem.Size <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(attachmentItem), \"attachmentItem.Size must be > 0.\");\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var url = userSegment + \"/messages/\" + selector + \"/attachments/createUploadSession\";\n\n        var payload = BuildCreateUploadSessionPayload(attachmentItem);\n        using var req = new HttpRequestMessage(HttpMethod.Post, url) {\n            Content = new StringContent(payload, Encoding.UTF8, \"application/json\")\n        };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph upload session create failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        GraphUploadSessionResult? result;\n        try {\n            result = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphUploadSessionResult);\n        } catch (JsonException ex) {\n            throw new InvalidDataException(\"Failed to parse Graph upload session response.\", ex);\n        }\n        if (result == null || string.IsNullOrWhiteSpace(result.UploadUrl)) {\n            throw new InvalidDataException(\"Graph returned an invalid upload session response.\");\n        }\n        return result;\n    }\n\n    /// <summary>\n    /// Uploads one attachment chunk to a Graph upload session URL.\n    /// </summary>\n    public async Task UploadAttachmentChunkAsync(\n        string uploadUrl,\n        byte[] chunk,\n        long startInclusive,\n        long endInclusive,\n        long totalLength,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(uploadUrl)) {\n            throw new ArgumentException(\"uploadUrl is required.\", nameof(uploadUrl));\n        }\n        if (chunk == null) {\n            throw new ArgumentNullException(nameof(chunk));\n        }\n        if (chunk.Length == 0) {\n            throw new ArgumentException(\"chunk must not be empty.\", nameof(chunk));\n        }\n\n        using var req = new HttpRequestMessage(HttpMethod.Put, uploadUrl);\n        req.Content = new ByteArrayContent(chunk);\n        req.Content.Headers.ContentType = new MediaTypeHeaderValue(\"application/octet-stream\");\n        req.Content.Headers.ContentRange = new ContentRangeHeaderValue(startInclusive, endInclusive, totalLength);\n\n        using var resp = await _client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (resp.IsSuccessStatusCode || (int)resp.StatusCode == 202) {\n            return;\n        }\n\n        throw new GraphApiException(resp.StatusCode, $\"Graph attachment chunk upload failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n    }\n\n    private static string BuildCreateUploadSessionPayload(GraphAttachmentItem attachmentItem) {\n        static string JsonString(string value) =>\n            \"\\\"\" + value.Replace(\"\\\\\", \"\\\\\\\\\").Replace(\"\\\"\", \"\\\\\\\"\") + \"\\\"\";\n\n        var sb = new StringBuilder();\n        sb.Append(\"{\\\"attachmentItem\\\":{\");\n        sb.Append(\"\\\"attachmentType\\\":\").Append(JsonString(string.IsNullOrWhiteSpace(attachmentItem.AttachmentType) ? \"file\" : attachmentItem.AttachmentType.Trim())).Append(',');\n        sb.Append(\"\\\"name\\\":\").Append(JsonString(attachmentItem.Name.Trim())).Append(',');\n        sb.Append(\"\\\"size\\\":\").Append(attachmentItem.Size.ToString(CultureInfo.InvariantCulture));\n        string? contentType = null;\n        if (attachmentItem.ContentType != null) {\n            var candidate = attachmentItem.ContentType.Trim();\n            if (candidate.Length > 0) {\n                contentType = candidate;\n            }\n        }\n        if (contentType != null) {\n            sb.Append(\",\\\"contentType\\\":\").Append(JsonString(contentType));\n        }\n        if (attachmentItem.IsInline.HasValue && attachmentItem.IsInline.Value) {\n            sb.Append(\",\\\"isInline\\\":true\");\n            string? contentId = null;\n            if (attachmentItem.ContentId != null) {\n                var candidate = attachmentItem.ContentId.Trim();\n                if (candidate.Length > 0) {\n                    contentId = candidate;\n                }\n            }\n            if (contentId != null) {\n                sb.Append(\",\\\"contentId\\\":\").Append(JsonString(contentId));\n            }\n        }\n        sb.Append(\"}}\");\n        return sb.ToString();\n    }\n\n    /// <summary>\n    /// Retrieves a delta page for messages in a folder.\n    /// </summary>\n    /// <remarks>\n    /// When <paramref name=\"cursor\"/> is empty, starts a new delta query for the folder.\n    /// Otherwise, continues from a previously returned nextLink/deltaLink.\n    /// </remarks>\n    public async Task<GraphDeltaPage<GraphMailMessage>> DeltaMessagesAsync(\n        string folderIdOrWellKnownName,\n        string? cursor = null,\n        string userId = \"me\",\n        int top = 100,\n        string? select = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(folderIdOrWellKnownName)) {\n            throw new ArgumentException(\"folderIdOrWellKnownName is required.\", nameof(folderIdOrWellKnownName));\n        }\n\n        var safeTop = ClampInt(top, 1, 999);\n        var userSegment = BuildUserSegment(userId);\n\n        var url = (cursor ?? string.Empty).Trim();\n        if (url.Length == 0) {\n            var folderSelector = Uri.EscapeDataString(folderIdOrWellKnownName.Trim());\n            var sb = new StringBuilder();\n            sb.Append(userSegment).Append(\"/mailFolders/\").Append(folderSelector).Append(\"/messages/delta\");\n            sb.Append(\"?$top=\").Append(safeTop.ToString(CultureInfo.InvariantCulture));\n            var selectValue = select == null ? null : select.Trim();\n            if (selectValue != null && selectValue.Length > 0) {\n                sb.Append(\"&$select=\").Append(Uri.EscapeDataString(selectValue));\n            }\n            url = sb.ToString();\n        }\n\n        using var req = new HttpRequestMessage(HttpMethod.Get, url);\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph delta failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        var upserts = new List<GraphMailMessage>();\n        var deletedIds = new List<string>();\n        string? nextLink = null;\n        string? deltaLink = null;\n\n        using (var doc = JsonDocument.Parse(body)) {\n            if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next) && next.ValueKind == JsonValueKind.String) {\n                nextLink = next.GetString();\n            }\n            if (doc.RootElement.TryGetProperty(\"@odata.deltaLink\", out var delta) && delta.ValueKind == JsonValueKind.String) {\n                deltaLink = delta.GetString();\n            }\n            if (doc.RootElement.TryGetProperty(\"value\", out var value) && value.ValueKind == JsonValueKind.Array) {\n                foreach (var item in value.EnumerateArray()) {\n                    var id = TryGetString(item, \"id\");\n                    if (id == null) {\n                        continue;\n                    }\n                    var trimmedId = id.Trim();\n                    if (trimmedId.Length == 0) {\n                        continue;\n                    }\n                    if (item.TryGetProperty(\"@removed\", out _)) {\n                        deletedIds.Add(trimmedId);\n                        continue;\n                    }\n                    var msg = TryParseMailMessage(item);\n                    if (msg != null) {\n                        upserts.Add(msg);\n                    }\n                }\n            }\n        }\n\n        return new GraphDeltaPage<GraphMailMessage>(upserts, nextLink, deltaLink, deletedIds);\n    }\n\n    /// <summary>\n    /// Moves a message to the specified destination folder id.\n    /// </summary>\n    public async Task MoveMessageAsync(\n        string messageId,\n        string destinationId,\n        string userId = \"me\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (string.IsNullOrWhiteSpace(destinationId)) {\n            throw new ArgumentException(\"destinationId is required.\", nameof(destinationId));\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var json = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = destinationId.Trim() }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n        using var content = new StringContent(json, Encoding.UTF8, \"application/json\");\n        using var req = new HttpRequestMessage(HttpMethod.Post, userSegment + \"/messages/\" + selector + \"/move\") { Content = content };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph move failed for messageId '{messageId.Trim()}' ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n    }\n\n    /// <summary>\n    /// Deletes a message.\n    /// </summary>\n    public async Task DeleteMessageAsync(string messageId, string userId = \"me\", CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        using var req = new HttpRequestMessage(HttpMethod.Delete, userSegment + \"/messages/\" + selector);\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph delete failed for messageId '{messageId.Trim()}' ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n    }\n\n    /// <summary>\n    /// Sets the read state of a message.\n    /// </summary>\n    public async Task SetMessageIsReadAsync(\n        string messageId,\n        bool isRead,\n        string userId = \"me\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var json = JsonSerializer.Serialize(new GraphMarkReadRequest { IsRead = isRead }, MailozaurrJsonContext.Default.GraphMarkReadRequest);\n        using var req = new HttpRequestMessage(new HttpMethod(\"PATCH\"), userSegment + \"/messages/\" + selector) {\n            Content = new StringContent(json, Encoding.UTF8, \"application/json\")\n        };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph set-isRead failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n    }\n\n    /// <summary>\n    /// Sets the flagged state of a message.\n    /// </summary>\n    public async Task SetMessageFlaggedAsync(\n        string messageId,\n        bool flagged,\n        string userId = \"me\",\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        var userSegment = BuildUserSegment(userId);\n        var selector = Uri.EscapeDataString(messageId.Trim());\n        var status = flagged ? \"flagged\" : \"notFlagged\";\n        var json = JsonSerializer.Serialize(\n            new GraphSetFlagRequest { Flag = new GraphSetFlagRequestFlag { FlagStatus = status } },\n            MailozaurrJsonContext.Default.GraphSetFlagRequest);\n        using var req = new HttpRequestMessage(new HttpMethod(\"PATCH\"), userSegment + \"/messages/\" + selector) {\n            Content = new StringContent(json, Encoding.UTF8, \"application/json\")\n        };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph set-flag failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n    }\n\n    /// <summary>\n    /// Moves many messages using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchMoveMessagesAsync(\n        IEnumerable<string> messageIds,\n        string destinationFolderId,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n        if (string.IsNullOrWhiteSpace(destinationFolderId)) {\n            throw new ArgumentException(\"destinationFolderId is required.\", nameof(destinationFolderId));\n        }\n\n        var ids = NormalizeBulkIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var batch = ClampInt(batchSize, 1, 20);\n        var payloadJson = JsonSerializer.Serialize(\n            new GraphDestinationRequest { DestinationId = destinationFolderId.Trim() },\n            MailozaurrJsonContext.Default.GraphDestinationRequest);\n        using var bodyDoc = JsonDocument.Parse(payloadJson);\n        var body = bodyDoc.RootElement.Clone();\n\n        return await ExecuteMessageBatchAsync(\n            ids,\n            batch,\n            messageId => new GraphBatchRequest {\n                Method = GraphHttpMethod.POST,\n                Url = userSegment + \"/messages/\" + Uri.EscapeDataString(messageId) + \"/move\",\n                Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { [\"Content-Type\"] = \"application/json\" },\n                Body = body\n            },\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes many messages using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchDeleteMessagesAsync(\n        IEnumerable<string> messageIds,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeBulkIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var batch = ClampInt(batchSize, 1, 20);\n        return await ExecuteMessageBatchAsync(\n            ids,\n            batch,\n            messageId => new GraphBatchRequest {\n                Method = GraphHttpMethod.DELETE,\n                Url = userSegment + \"/messages/\" + Uri.EscapeDataString(messageId)\n            },\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state for many messages using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchSetMessagesIsReadAsync(\n        IEnumerable<string> messageIds,\n        bool isRead,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeBulkIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var batch = ClampInt(batchSize, 1, 20);\n        var payloadJson = JsonSerializer.Serialize(\n            new GraphMarkReadRequest { IsRead = isRead },\n            MailozaurrJsonContext.Default.GraphMarkReadRequest);\n        using var bodyDoc = JsonDocument.Parse(payloadJson);\n        var body = bodyDoc.RootElement.Clone();\n\n        return await ExecuteMessageBatchAsync(\n            ids,\n            batch,\n            messageId => new GraphBatchRequest {\n                Method = GraphHttpMethod.PATCH,\n                Url = userSegment + \"/messages/\" + Uri.EscapeDataString(messageId),\n                Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { [\"Content-Type\"] = \"application/json\" },\n                Body = body\n            },\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state for many messages using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchSetMessagesFlaggedAsync(\n        IEnumerable<string> messageIds,\n        bool flagged,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeBulkIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var userSegment = BuildUserSegment(userId);\n        var batch = ClampInt(batchSize, 1, 20);\n        var payloadJson = JsonSerializer.Serialize(\n            new GraphSetFlagRequest { Flag = new GraphSetFlagRequestFlag { FlagStatus = flagged ? \"flagged\" : \"notFlagged\" } },\n            MailozaurrJsonContext.Default.GraphSetFlagRequest);\n        using var bodyDoc = JsonDocument.Parse(payloadJson);\n        var body = bodyDoc.RootElement.Clone();\n\n        return await ExecuteMessageBatchAsync(\n            ids,\n            batch,\n            messageId => new GraphBatchRequest {\n                Method = GraphHttpMethod.PATCH,\n                Url = userSegment + \"/messages/\" + Uri.EscapeDataString(messageId),\n                Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { [\"Content-Type\"] = \"application/json\" },\n                Body = body\n            },\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves all messages in each conversation using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchMoveConversationsAsync(\n        IEnumerable<string> conversationIds,\n        string destinationFolderId,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n        if (string.IsNullOrWhiteSpace(destinationFolderId)) {\n            throw new ArgumentException(\"destinationFolderId is required.\", nameof(destinationFolderId));\n        }\n\n        var conversations = NormalizeBulkIds(conversationIds);\n        if (conversations.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var output = new List<GraphBulkOperationResult>(conversations.Count);\n        foreach (var conversationId in conversations) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            IReadOnlyList<string> messageIds;\n            try {\n                messageIds = await ListConversationMessageIdsAsync(conversationId, userId: userId, cancellationToken: cancellationToken).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = ex.Message\n                });\n                continue;\n            }\n\n            if (messageIds.Count == 0) {\n                output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n                continue;\n            }\n\n            var moved = await BatchMoveMessagesAsync(\n                messageIds,\n                destinationFolderId,\n                userId: userId,\n                batchSize: batchSize,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n            var failed = FindFirstFailedBulkResult(moved);\n            if (failed is not null) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = failed.Error ?? \"Graph conversation move failed.\"\n                });\n                continue;\n            }\n            output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n        }\n\n        return output;\n    }\n\n    /// <summary>\n    /// Deletes all messages in each conversation using Graph batch requests.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> BatchDeleteConversationsAsync(\n        IEnumerable<string> conversationIds,\n        string userId = \"me\",\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n\n        var conversations = NormalizeBulkIds(conversationIds);\n        if (conversations.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var output = new List<GraphBulkOperationResult>(conversations.Count);\n        foreach (var conversationId in conversations) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            IReadOnlyList<string> messageIds;\n            try {\n                messageIds = await ListConversationMessageIdsAsync(conversationId, userId: userId, cancellationToken: cancellationToken).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = ex.Message\n                });\n                continue;\n            }\n\n            if (messageIds.Count == 0) {\n                output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n                continue;\n            }\n\n            var deleted = await BatchDeleteMessagesAsync(\n                messageIds,\n                userId: userId,\n                batchSize: batchSize,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n            var failed = FindFirstFailedBulkResult(deleted);\n            if (failed is not null) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = failed.Error ?? \"Graph conversation delete failed.\"\n                });\n                continue;\n            }\n            output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n        }\n\n        return output;\n    }\n\n    /// <summary>\n    /// Sends a Graph batch request using the current bearer token.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBatchResult>> SendBatchAsync(IEnumerable<GraphBatchRequest> requests, CancellationToken cancellationToken = default) {\n        ThrowIfDisposed();\n        if (requests == null) {\n            throw new ArgumentNullException(nameof(requests));\n        }\n\n        var payload = new GraphBatchPayload();\n        var i = 0;\n        foreach (var r in requests) {\n            if (r == null) {\n                continue;\n            }\n            var id = string.IsNullOrWhiteSpace(r.Id) ? (++i).ToString(CultureInfo.InvariantCulture) : r.Id.Trim();\n            var url = (r.Url ?? string.Empty).Trim();\n            if (url.Length == 0) {\n                throw new ArgumentException(\"GraphBatchRequest.Url is required.\", nameof(requests));\n            }\n\n            payload.Requests.Add(new GraphBatchRequestPayload {\n                Id = id,\n                Method = r.Method.ToString(),\n                Url = url.TrimStart('/'),\n                Headers = r.Headers,\n                Body = r.Body\n            });\n        }\n\n        var json = JsonSerializer.Serialize(payload, MailozaurrJsonContext.Default.GraphBatchPayload);\n        using var content = new StringContent(json, Encoding.UTF8, \"application/json\");\n        using var req = new HttpRequestMessage(HttpMethod.Post, \"$batch\") { Content = content };\n        ApplyAuthHeader(req);\n        using var resp = await _client.SendAsync(req, cancellationToken).ConfigureAwait(false);\n        await ThrowIfAuthErrorAsync(resp, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            throw new GraphApiException(resp.StatusCode, $\"Graph batch failed ({(int)resp.StatusCode}).\", body, TryGetRetryAfter(resp));\n        }\n\n        var results = new List<GraphBatchResult>();\n        using (var doc = JsonDocument.Parse(body)) {\n            if (doc.RootElement.TryGetProperty(\"responses\", out var responses) && responses.ValueKind == JsonValueKind.Array) {\n                foreach (var item in responses.EnumerateArray()) {\n                    var result = new GraphBatchResult();\n                    if (item.TryGetProperty(\"id\", out var idEl) && idEl.ValueKind == JsonValueKind.String) {\n                        result.Id = idEl.GetString() ?? string.Empty;\n                    }\n                    if (item.TryGetProperty(\"status\", out var statusEl) && statusEl.TryGetInt32(out var status)) {\n                        result.Status = status;\n                    }\n                    if (item.TryGetProperty(\"headers\", out var headersEl) && headersEl.ValueKind == JsonValueKind.Object) {\n                        var h = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n                        foreach (var prop in headersEl.EnumerateObject()) {\n                            if (prop.Value.ValueKind == JsonValueKind.String) {\n                                h[prop.Name] = prop.Value.GetString() ?? string.Empty;\n                            }\n                        }\n                        result.Headers = h;\n                    }\n                    if (item.TryGetProperty(\"body\", out var bodyEl)) {\n                        result.Body = bodyEl.Clone();\n                    }\n                    results.Add(result);\n                }\n            }\n        }\n\n        return results;\n    }\n\n    private static List<string> NormalizeBulkIds(IEnumerable<string> ids) {\n        var output = new List<string>();\n        foreach (var raw in ids) {\n            if (string.IsNullOrWhiteSpace(raw)) {\n                continue;\n            }\n            var id = raw.Trim();\n            if (id.Length == 0) {\n                continue;\n            }\n            output.Add(id);\n        }\n        return output;\n    }\n\n    private async Task<IReadOnlyList<GraphBulkOperationResult>> ExecuteMessageBatchAsync(\n        List<string> messageIds,\n        int batchSize,\n        Func<string, GraphBatchRequest> requestFactory,\n        CancellationToken cancellationToken) {\n        var output = new List<GraphBulkOperationResult>(messageIds.Count);\n        var chunkSize = ClampInt(batchSize, 1, 20);\n\n        for (var i = 0; i < messageIds.Count; i += chunkSize) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            var count = Math.Min(chunkSize, messageIds.Count - i);\n            var chunk = messageIds.GetRange(i, count);\n            var subIds = new List<string>(chunk.Count);\n            var requests = new List<GraphBatchRequest>(chunk.Count);\n            for (var j = 0; j < chunk.Count; j++) {\n                var subId = (j + 1).ToString(CultureInfo.InvariantCulture);\n                subIds.Add(subId);\n                var req = requestFactory(chunk[j]);\n                req.Id = subId;\n                requests.Add(req);\n            }\n\n            IReadOnlyList<GraphBatchResult> responses;\n            try {\n                responses = await SendBatchAsync(requests, cancellationToken).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                var err = ex.Message;\n                foreach (var id in chunk) {\n                    output.Add(new GraphBulkOperationResult { Id = id, Ok = false, Error = err });\n                }\n                continue;\n            }\n\n            var byId = new Dictionary<string, GraphBatchResult>(StringComparer.Ordinal);\n            foreach (var response in responses) {\n                if (response == null || string.IsNullOrWhiteSpace(response.Id)) {\n                    continue;\n                }\n                byId[response.Id.Trim()] = response;\n            }\n\n            for (var j = 0; j < chunk.Count; j++) {\n                var messageId = chunk[j];\n                var subId = subIds[j];\n                if (!byId.TryGetValue(subId, out var response)) {\n                    output.Add(new GraphBulkOperationResult {\n                        Id = messageId,\n                        Ok = false,\n                        Error = \"Graph batch response missing for message id.\"\n                    });\n                    continue;\n                }\n\n                if (response.Status >= 200 && response.Status <= 299) {\n                    output.Add(new GraphBulkOperationResult { Id = messageId, Ok = true });\n                    continue;\n                }\n\n                output.Add(new GraphBulkOperationResult {\n                    Id = messageId,\n                    Ok = false,\n                    Error = TryExtractBatchErrorMessage(response) ?? (\"Graph batch request failed (\" + response.Status.ToString(CultureInfo.InvariantCulture) + \").\")\n                });\n            }\n        }\n\n        return output;\n    }\n\n    private static GraphBulkOperationResult? FindFirstFailedBulkResult(IReadOnlyList<GraphBulkOperationResult> results) {\n        if (results == null) {\n            return null;\n        }\n        foreach (var result in results) {\n            if (result != null && !result.Ok) {\n                return result;\n            }\n        }\n        return null;\n    }\n\n    private static string? TryExtractBatchErrorMessage(GraphBatchResult response) {\n        if (response?.Body == null) {\n            return null;\n        }\n\n        var body = response.Body.Value;\n        if (body.ValueKind != JsonValueKind.Object) {\n            return null;\n        }\n\n        if (body.TryGetProperty(\"error\", out var error)) {\n            if (error.ValueKind == JsonValueKind.String) {\n                var text = error.GetString();\n                if (text != null) {\n                    var trimmed = text.Trim();\n                    if (trimmed.Length > 0) {\n                        return trimmed;\n                    }\n                }\n            }\n\n            if (error.ValueKind == JsonValueKind.Object &&\n                error.TryGetProperty(\"message\", out var messageEl) &&\n                messageEl.ValueKind == JsonValueKind.String) {\n                var msg = messageEl.GetString();\n                if (msg != null) {\n                    var trimmed = msg.Trim();\n                    if (trimmed.Length > 0) {\n                        return trimmed;\n                    }\n                }\n            }\n        }\n\n        if (body.TryGetProperty(\"message\", out var fallbackMessageEl) &&\n            fallbackMessageEl.ValueKind == JsonValueKind.String) {\n            var fallback = fallbackMessageEl.GetString();\n            if (fallback != null) {\n                var trimmed = fallback.Trim();\n                if (trimmed.Length > 0) {\n                    return trimmed;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /// <summary>Create subscription request payload.</summary>\n    public sealed class GraphCreateSubscriptionRequest {\n        /// <summary>\n         /// Resource to subscribe to (for example, <c>me/mailFolders('inbox')/messages</c>).\n         /// </summary>\n        [JsonPropertyName(\"resource\")]\n        public string Resource { get; set; } = string.Empty;\n\n        /// <summary>\n        /// Change types (comma-separated) to subscribe to (for example, <c>created,updated,deleted</c>).\n        /// </summary>\n        [JsonPropertyName(\"changeType\")]\n        public string ChangeType { get; set; } = string.Empty;\n\n        /// <summary>Webhook URL to receive notifications.</summary>\n        [JsonPropertyName(\"notificationUrl\")]\n        public string NotificationUrl { get; set; } = string.Empty;\n\n        /// <summary>Subscription expiration time.</summary>\n        [JsonPropertyName(\"expirationDateTime\")]\n        public DateTimeOffset ExpirationDateTime { get; set; }\n\n        /// <summary>Optional opaque state returned in notifications.</summary>\n        [JsonPropertyName(\"clientState\")]\n        public string? ClientState { get; set; }\n    }\n\n    /// <summary>Renew subscription request payload.</summary>\n    public sealed class GraphRenewSubscriptionRequest {\n        /// <summary>Updated expiration time.</summary>\n        [JsonPropertyName(\"expirationDateTime\")]\n        public DateTimeOffset ExpirationDateTime { get; set; }\n    }\n\n    /// <summary>Graph webhook subscription.</summary>\n    public sealed class GraphSubscription {\n        /// <summary>Subscription id.</summary>\n        [JsonPropertyName(\"id\")]\n        public string? Id { get; set; }\n\n        /// <summary>Subscribed resource.</summary>\n        [JsonPropertyName(\"resource\")]\n        public string? Resource { get; set; }\n\n        /// <summary>Subscribed change types.</summary>\n        [JsonPropertyName(\"changeType\")]\n        public string? ChangeType { get; set; }\n\n        /// <summary>Webhook URL.</summary>\n        [JsonPropertyName(\"notificationUrl\")]\n        public string? NotificationUrl { get; set; }\n\n        /// <summary>Subscription expiration time.</summary>\n        [JsonPropertyName(\"expirationDateTime\")]\n        public DateTimeOffset ExpirationDateTime { get; set; }\n\n        /// <summary>Optional opaque state returned in notifications.</summary>\n        [JsonPropertyName(\"clientState\")]\n        public string? ClientState { get; set; }\n    }\n\n    /// <summary>Graph subscriptions list response.</summary>\n    public sealed class GraphSubscriptionListResponse {\n        /// <summary>List of subscriptions.</summary>\n        [JsonPropertyName(\"value\")]\n        public List<GraphSubscription>? Value { get; set; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiDiagnostic.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Diagnostic information returned by the Graph API.\n/// </summary>\npublic class GraphApiDiagnostic {\n    /// <summary>Gets or sets server information details.</summary>\n    [JsonPropertyName(\"ServerInfo\")]\n    public GraphApiServerInfo ServerInfo { get; set; } = new();\n}"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiErrorDetail.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Detailed information about a Graph API error.\n/// </summary>\n/// <remarks>\n/// Included when the Graph service returns structured error information\n/// that can assist with troubleshooting.\n/// </remarks>\npublic class GraphApiErrorDetail {\n    /// <summary>\n    /// Gets or sets the error code returned by the API.\n    /// </summary>\n    [JsonPropertyName(\"code\")]\n    public string Code { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the human readable error message.\n    /// </summary>\n    [JsonPropertyName(\"message\")]\n    public string Message { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets additional error details.\n    /// </summary>\n    [JsonPropertyName(\"innerError\")]\n    public GraphApiInnerError InnerError { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiErrorHeaders.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents HTTP headers returned with a Graph API error response.\n/// </summary>\npublic class GraphApiErrorHeaders {\n    /// <summary>Gets or sets the Cache-Control header.</summary>\n    public string CacheControl { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the Strict-Transport-Security header.</summary>\n    public string StrictTransportSecurity { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the request-id header.</summary>\n    public string RequestId { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the client-request-id header.</summary>\n    public string ClientRequestId { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the Date header.</summary>\n    public DateTime? Date { get; set; }\n\n    /// <summary>Gets or sets the diagnostic header.</summary>\n    public GraphApiDiagnostic? Diagnostic { get; set; }\n\n    /// <summary>Gets or sets additional headers.</summary>\n    public Dictionary<string, string> AdditionalHeaders { get; set; } = new();\n\n    /// <summary>Gets or sets additional JSON headers.</summary>\n    public Dictionary<string, JsonElement> AdditionalJsonHeaders { get; set; } = new();\n}"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiErrorParser.cs",
    "content": "using System;\nusing System.Net;\nusing System.Globalization;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\n\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Parses raw Graph API error messages into structured objects.\n/// </summary>\npublic static class GraphApiErrorParser {\n    /// <summary>\n    /// Parses a raw error string returned by Graph API.\n    /// </summary>\n    /// <param name=\"message\">Raw error message.</param>\n    /// <returns>Parsed <see cref=\"GraphApiErrorResponse\"/> or <c>null</c> if input is empty.</returns>\n    public static GraphApiErrorResponse? Parse(string? message) {\n        if (string.IsNullOrWhiteSpace(message)) {\n            return null;\n        }\n\n        var response = new GraphApiErrorResponse { Raw = message! };\n\n        try {\n            var lines = message!.Split(new[] { '\\r', '\\n' }, StringSplitOptions.RemoveEmptyEntries);\n            if (lines.Length == 0) {\n                return response;\n            }\n\n            var index = 0;\n\n            var first = lines[index++];\n            var firstParts = first.Split(new[] { ' ' }, 2);\n            if (firstParts.Length != 2 || !Enum.TryParse<GraphHttpMethod>(firstParts[0], true, out var method)) {\n                return response;\n            }\n            response.Method = method;\n            response.Uri = firstParts[1];\n\n            if (index < lines.Length) {\n                var second = lines[index++];\n                var match = Regex.Match(second, @\"HTTP/\\S+\\s+(\\d+)\");\n                if (match.Success && int.TryParse(match.Groups[1].Value, out var status)) {\n                    response.StatusCode = (HttpStatusCode)status;\n                }\n            }\n\n            for (; index < lines.Length; index++) {\n                var line = lines[index];\n                if (line.StartsWith(\"{\", StringComparison.Ordinal)) {\n                    var body = string.Join(Environment.NewLine, lines, index, lines.Length - index);\n                    try {\n                        var error = JsonSerializer.Deserialize(body, MailozaurrJsonContext.Default.GraphApiError);\n                        response.Error = error?.Error;\n                    } catch {\n                        // ignore\n                    }\n                    break;\n                }\n\n                var separator = line.IndexOf(':');\n                if (separator <= 0) {\n                    continue;\n                }\n\n                var key = line.Substring(0, separator);\n                var value = line.Substring(separator + 1).Trim();\n                switch (key.ToLowerInvariant()) {\n                    case \"cache-control\":\n                        response.Headers.CacheControl = value;\n                        break;\n                    case \"strict-transport-security\":\n                        response.Headers.StrictTransportSecurity = value;\n                        break;\n                    case \"request-id\":\n                        response.Headers.RequestId = value;\n                        break;\n                    case \"client-request-id\":\n                        response.Headers.ClientRequestId = value;\n                        break;\n                    case \"date\":\n                        if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dt)) {\n                            response.Headers.Date = dt;\n                        }\n                        break;\n                    case \"x-ms-ags-diagnostic\":\n                        try {\n                            response.Headers.Diagnostic = JsonSerializer.Deserialize(value, MailozaurrJsonContext.Default.GraphApiDiagnostic);\n                        } catch {\n                            // ignore\n                        }\n                        break;\n                    default:\n                        try {\n                            var json = JsonSerializer.Deserialize(value, MailozaurrJsonContext.Default.JsonElement);\n                            response.Headers.AdditionalJsonHeaders[key] = json;\n                        } catch {\n                            response.Headers.AdditionalHeaders[key] = value;\n                        }\n                        break;\n                }\n            }\n        } catch {\n            // ignore failures and return raw response\n        }\n\n        return response;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiErrorResponse.cs",
    "content": "using System.Net;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Parsed details of a Graph API error message.\n/// </summary>\npublic class GraphApiErrorResponse {\n    /// <summary>Gets or sets the HTTP method.</summary>\n    public GraphHttpMethod? Method { get; set; }\n\n    /// <summary>Gets or sets the request URI.</summary>\n    public string Uri { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the status code.</summary>\n    public HttpStatusCode StatusCode { get; set; }\n\n    /// <summary>Gets or sets the headers.</summary>\n    public GraphApiErrorHeaders Headers { get; set; } = new();\n\n    /// <summary>Gets or sets the original raw error message.</summary>\n    public string Raw { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the error details.</summary>\n    public GraphApiErrorDetail? Error { get; set; }\n}"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiException.cs",
    "content": "using System;\nusing System.Net;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Exception thrown when Microsoft Graph API returns an error.\n/// </summary>\n/// <remarks>\n/// Includes the HTTP status code and the response body so that\n/// callers can inspect additional error information.\n/// </remarks>\npublic class GraphApiException : Exception {\n    /// <summary>\n    /// HTTP status code returned by the Graph API.\n    /// </summary>\n    public HttpStatusCode StatusCode { get; }\n\n    /// <summary>\n    /// Raw response content returned by the Graph API.\n    /// </summary>\n    public string ResponseContent { get; }\n\n    /// <summary>\n    /// Optional server-provided retry-after delay when throttled.\n    /// </summary>\n    public TimeSpan? RetryAfter { get; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphApiException\"/> class.\n    /// </summary>\n    /// <param name=\"statusCode\">The HTTP status code.</param>\n    /// <param name=\"message\">The error message.</param>\n    /// <param name=\"responseContent\">Raw response content from the server.</param>\n    public GraphApiException(HttpStatusCode statusCode, string message, string responseContent)\n        : this(statusCode, message, responseContent, null) { }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphApiException\"/> class.\n    /// </summary>\n    /// <param name=\"statusCode\">The HTTP status code.</param>\n    /// <param name=\"message\">The error message.</param>\n    /// <param name=\"responseContent\">Raw response content from the server.</param>\n    /// <param name=\"retryAfter\">Optional server-provided retry-after hint.</param>\n    public GraphApiException(HttpStatusCode statusCode, string message, string responseContent, TimeSpan? retryAfter)\n        : base(message) {\n        StatusCode = statusCode;\n        ResponseContent = responseContent;\n        RetryAfter = retryAfter;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiInnerError.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Additional error details returned by the Graph API.\n/// </summary>\n/// <remarks>\n/// This object is nested inside <see cref=\"GraphApiErrorDetail\"/> when\n/// the service includes extra context about a failure.\n/// </remarks>\npublic class GraphApiInnerError {\n    /// <summary>\n    /// Gets or sets the request identifier associated with the error.\n    /// </summary>\n    [JsonPropertyName(\"request-id\")]\n    public string RequestId { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the client request identifier associated with the error.\n    /// </summary>\n    [JsonPropertyName(\"client-request-id\")]\n    public string ClientRequestId { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the timestamp of when the error occurred.\n    /// </summary>\n    [JsonPropertyName(\"date\")]\n    public DateTime Date { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphApiServerInfo.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Details about the server handling the request.\n/// </summary>\npublic class GraphApiServerInfo {\n    /// <summary>Gets or sets the data center.</summary>\n    [JsonPropertyName(\"DataCenter\")]\n    public string DataCenter { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the slice.</summary>\n    [JsonPropertyName(\"Slice\")]\n    public string Slice { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the ring.</summary>\n    [JsonPropertyName(\"Ring\")]\n    public string Ring { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the scale unit.</summary>\n    [JsonPropertyName(\"ScaleUnit\")]\n    public string ScaleUnit { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the role instance.</summary>\n    [JsonPropertyName(\"RoleInstance\")]\n    public string RoleInstance { get; set; } = string.Empty;\n}"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphAttachment.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Represents a simple file attachment used when sending messages.\n/// </summary>\n/// <remarks>\n/// This is the lightweight counterpart to <c>AttachmentItem</c>\n/// used during message creation.\n/// </remarks>\npublic class GraphAttachment {\n    /// <summary>\n    /// Gets or sets the Graph type of the attachment.\n    /// </summary>\n    [JsonPropertyName(\"@odata.type\")]\n    public string ODataType { get; set; } = \"#microsoft.graph.fileAttachment\";\n\n    /// <summary>\n    /// Gets or sets the attachment file name.\n    /// </summary>\n    [JsonPropertyName(\"name\")]\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets attachment content type.\n    /// </summary>\n    [JsonPropertyName(\"contentType\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? ContentType { get; set; }\n\n    /// <summary>\n    /// Gets or sets the file content encoded as a Base64 string.\n    /// </summary>\n    [JsonPropertyName(\"contentBytes\")]\n    public string ContentBytes { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Indicates whether this attachment should be rendered inline in the\n    /// message body.\n    /// </summary>\n    [JsonPropertyName(\"isInline\")]\n    public bool IsInline { get; set; }\n\n    /// <summary>\n    /// Optional identifier used to reference the attachment via a <c>cid:</c>\n    /// URL within the HTML body.\n    /// </summary>\n    [JsonPropertyName(\"contentId\")]\n    public string? ContentId { get; set; }\n\n    /// <summary>\n    /// Creates a <see cref=\"GraphAttachment\"/> from a local file path.\n    /// </summary>\n    /// <param name=\"filePath\">Path to the file.</param>\n    /// <returns>The created attachment.</returns>\n    public static GraphAttachment FromFile(string filePath) {\n        var fileInfo = new FileInfo(filePath);\n        var fileBytes = File.ReadAllBytes(filePath);\n        var fileContentBase64 = Convert.ToBase64String(fileBytes);\n\n        return new GraphAttachment {\n            Name = fileInfo.Name,\n            ContentBytes = fileContentBase64\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphAttachmentItem.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Metadata describing an attachment for upload.\n/// </summary>\n/// <remarks>\n/// This type mirrors the <c>attachmentItem</c> resource used\n/// when creating an upload session in the Graph API.\n/// </remarks>\npublic class GraphAttachmentItem {\n    /// <summary>\n    /// Gets or sets the type of the attachment being uploaded.\n    /// </summary>\n    [JsonPropertyName(\"attachmentType\")]\n    public string AttachmentType { get; set; }\n\n    /// <summary>\n    /// Gets or sets the file name of the attachment.\n    /// </summary>\n    [JsonPropertyName(\"name\")]\n    public string Name { get; set; }\n\n    /// <summary>\n    /// Gets or sets the size of the attachment in bytes.\n    /// </summary>\n    [JsonPropertyName(\"size\")]\n    public long Size { get; set; }\n\n    /// <summary>\n    /// Gets or sets optional content type.\n    /// </summary>\n    [JsonPropertyName(\"contentType\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? ContentType { get; set; }\n\n    /// <summary>\n    /// Gets or sets whether attachment should be rendered inline.\n    /// </summary>\n    [JsonPropertyName(\"isInline\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? IsInline { get; set; }\n\n    /// <summary>\n    /// Gets or sets optional content identifier for inline attachments.\n    /// </summary>\n    [JsonPropertyName(\"contentId\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? ContentId { get; set; }\n\n    /// <summary>Creates a new attachment item.</summary>\n    /// <param name=\"attachmentType\">Type of the attachment.</param>\n    /// <param name=\"name\">File name.</param>\n    /// <param name=\"size\">File size.</param>\n    public GraphAttachmentItem(string attachmentType, string name, long size) {\n        AttachmentType = attachmentType;\n        Name = name;\n        Size = size;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphAttachmentItemWrapper.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Wrapper used when creating an upload session.\n/// </summary>\n/// <remarks>\n/// Required by the Graph API when initiating a chunked upload for\n/// large attachments.\n/// </remarks>\npublic class GraphAttachmentItemWrapper {\n    /// <summary>\n    /// Gets the attachment item used when creating the upload session.\n    /// </summary>\n    [JsonPropertyName(\"AttachmentItem\")]\n    public GraphAttachmentItem AttachmentItem { get; set; }\n\n    /// <summary>Initializes a new instance of the wrapper.</summary>\n    /// <param name=\"attachmentItem\">The attachment item.</param>\n    public GraphAttachmentItemWrapper(GraphAttachmentItem attachmentItem) {\n        AttachmentItem = attachmentItem;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphAttachmentPlaceHolder.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a placeholder for an attachment upload session.\n/// </summary>\n/// <remarks>\n/// Used when streaming large files to the Graph API in multiple\n/// requests.\n/// </remarks>\npublic class GraphAttachmentPlaceHolder {\n    /// <summary>Serialized attachment metadata.</summary>\n    public string Json { get; set; } = string.Empty;\n    /// <summary>Chunks of the file content.</summary>\n    public List<StreamContent> Content { get; set; } = [];\n    /// <summary>Path to the file.</summary>\n    public string FilePath { get; set; } = string.Empty;\n    /// <summary>Size of the file in bytes.</summary>\n    public long FileSize { get; set; }\n    /// <summary>Name of the file.</summary>\n    public string FileName { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphAuthorization.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Authorization information returned by Microsoft Graph OAuth flows.\n/// </summary>\n/// <remarks>\n/// This object is serialized from the JSON response after acquiring\n/// an access token using device code or interactive login.\n/// </remarks>\npublic class GraphAuthorization {\n    /// <summary>The type of token issued.</summary>\n    [JsonPropertyName(\"token_type\")]\n    public string TokenType { get; set; } = string.Empty;\n\n    /// <summary>The access token value.</summary>\n    [JsonPropertyName(\"access_token\")]\n    public string AccessToken { get; set; } = string.Empty;\n\n    /// <summary>Time when the access token expires.</summary>\n    [JsonPropertyName(\"expires_on\")]\n    [JsonConverter(typeof(UnixTimeSecondsDateTimeOffsetConverter))]\n    public DateTimeOffset ExpiresOn { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphBatchRequest.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json;\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a single request within a Microsoft Graph batch operation.\n/// </summary>\npublic class GraphBatchRequest {\n    /// <summary>Unique request identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n    /// <summary>HTTP method to execute.</summary>\n    public GraphHttpMethod Method { get; set; }\n    /// <summary>Relative request URL (e.g. <c>/me/messages</c>).</summary>\n    public string Url { get; set; } = string.Empty;\n    /// <summary>Optional headers to include with the request.</summary>\n    public IDictionary<string, string>? Headers { get; set; }\n    /// <summary>Optional JSON body for POST/PUT/PATCH requests.</summary>\n    public JsonElement? Body { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphBatchResult.cs",
    "content": "using System.Text.Json;\nusing System.Collections.Generic;\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents the response for a single request within a Microsoft Graph batch.\n/// </summary>\npublic class GraphBatchResult {\n    /// <summary>Identifier matching the originating request.</summary>\n    public string Id { get; set; } = string.Empty;\n    /// <summary>HTTP status code returned by the request.</summary>\n    public int Status { get; set; }\n    /// <summary>Response headers.</summary>\n    public IDictionary<string, string>? Headers { get; set; }\n    /// <summary>Raw JSON body of the response.</summary>\n    public JsonElement? Body { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphBulkOperationResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Result item for Graph bulk mailbox operations.\n/// </summary>\npublic sealed class GraphBulkOperationResult : MailboxBulkOperationResult;\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphContent.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Body content for a message.\n/// </summary>\npublic class GraphContent {\n    /// <summary>\n    /// Gets or sets the content type, such as \"Text\" or \"HTML\".\n    /// </summary>\n    [JsonPropertyName(\"contentType\")]\n    public string Type { get; set; } = \"Text\";\n\n    /// <summary>\n    /// Gets or sets the content value.\n    /// </summary>\n    [JsonPropertyName(\"content\")]\n    public string Content { get; set; } = \"\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEmail.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Simple email address container.\n/// </summary>\npublic class GraphEmail {\n    /// <summary>\n    /// Gets or sets the email address value.\n    /// </summary>\n    [JsonPropertyName(\"address\")]\n    public string Address { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the optional display name.\n    /// </summary>\n    [JsonPropertyName(\"name\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Name { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEmailAddress.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents an email address object for Graph API payloads.\n/// </summary>\n/// <remarks>\n/// Used when specifying senders or recipients in Graph requests.\n/// </remarks>\npublic class GraphEmailAddress {\n    /// <summary>\n    /// Gets or sets the email details.\n    /// </summary>\n    [JsonPropertyName(\"emailAddress\")]\n    public GraphEmail Email { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphErrors.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Container for an error returned by the Graph API.\n/// </summary>\n/// <remarks>\n/// The Graph service wraps detailed error information in this\n/// structure when a request fails.\n/// </remarks>\npublic class GraphApiError {\n    /// <summary>\n    /// Gets or sets the error details returned by the API.\n    /// </summary>\n    [JsonPropertyName(\"error\")]\n    public GraphApiErrorDetail Error { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEvent.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a calendar event used with Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// This type mirrors the <c>event</c> resource and exposes only\n/// commonly used properties.\n/// </remarks>\npublic class GraphEvent {\n    /// <summary>Event identifier.</summary>\n    [JsonPropertyName(\"id\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Id { get; set; }\n\n    /// <summary>Subject of the event.</summary>\n    [JsonPropertyName(\"subject\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Subject { get; set; }\n\n    /// <summary>Event start time.</summary>\n    [JsonPropertyName(\"start\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphEventTime? Start { get; set; }\n\n    /// <summary>Event end time.</summary>\n    [JsonPropertyName(\"end\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphEventTime? End { get; set; }\n\n    /// <summary>Body of the event.</summary>\n    [JsonPropertyName(\"body\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphContent? Body { get; set; }\n\n    /// <summary>Attendees of the event.</summary>\n    [JsonPropertyName(\"attendees\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEventAttendee>? Attendees { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEventAttendee.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents an attendee for a calendar event.\n/// </summary>\n/// <remarks>\n/// This mirrors the <c>attendee</c> resource when creating or\n/// updating events via Graph.\n/// </remarks>\npublic class GraphEventAttendee {\n    /// <summary>Email address of the attendee.</summary>\n    [JsonPropertyName(\"emailAddress\")]\n    public GraphEmailAddress EmailAddress { get; set; } = new();\n\n    /// <summary>Attendee type such as required or optional.</summary>\n    [JsonPropertyName(\"type\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Type { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEventBuilder.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Fluent builder for <see cref=\"GraphEvent\"/> objects.\n/// </summary>\npublic sealed class GraphEventBuilder {\n    private readonly GraphEvent _event = new();\n\n    /// <summary>Sets the subject.</summary>\n    public GraphEventBuilder Subject(string subject) {\n        _event.Subject = subject;\n        return this;\n    }\n\n    /// <summary>Sets the start time.</summary>\n    public GraphEventBuilder Start(DateTime dateTime, string timeZone = \"UTC\") {\n        _event.Start = new GraphEventTime { DateTime = dateTime.ToString(\"o\"), TimeZone = timeZone };\n        return this;\n    }\n\n    /// <summary>Sets the end time.</summary>\n    public GraphEventBuilder End(DateTime dateTime, string timeZone = \"UTC\") {\n        _event.End = new GraphEventTime { DateTime = dateTime.ToString(\"o\"), TimeZone = timeZone };\n        return this;\n    }\n\n    /// <summary>Sets the body content.</summary>\n    public GraphEventBuilder Body(string content, string type = \"HTML\") {\n        _event.Body = new GraphContent { Content = content, Type = type };\n        return this;\n    }\n\n    /// <summary>Adds an attendee.</summary>\n    public GraphEventBuilder Attendee(string address, string name, string type = \"required\") {\n        _event.Attendees ??= new List<GraphEventAttendee>();\n        _event.Attendees.Add(new GraphEventAttendee {\n            EmailAddress = new GraphEmailAddress { Email = new GraphEmail { Address = address } },\n            Type = type\n        });\n        return this;\n    }\n\n    /// <summary>Gets the constructed <see cref=\"GraphEvent\"/> instance.</summary>\n    internal GraphEvent Build() => _event;\n\n    /// <summary>\n    /// Allows <see cref=\"GraphEventBuilder\"/> to be used wherever\n    /// <see cref=\"GraphEvent\"/> is expected.\n    /// </summary>\n    /// <param name=\"builder\">The builder instance.</param>\n    public static implicit operator GraphEvent(GraphEventBuilder builder) => builder._event;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphEventTime.cs",
    "content": "using System.Text.Json.Serialization;\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents date/time with timezone for Graph calendar events.\n/// </summary>\n/// <remarks>\n/// Used by <see cref=\"GraphEvent\"/> when specifying the start and\n/// end times of a calendar event.\n/// </remarks>\npublic class GraphEventTime {\n    /// <summary>Date/time value in ISO 8601 format.</summary>\n    [JsonPropertyName(\"dateTime\")]\n    public string DateTime { get; set; } = string.Empty;\n\n    /// <summary>Time zone identifier.</summary>\n    [JsonPropertyName(\"timeZone\")]\n    public string TimeZone { get; set; } = \"UTC\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphHttpMethod.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// HTTP methods supported by Microsoft Graph batch requests.\n/// </summary>\npublic enum GraphHttpMethod {\n    /// <summary>HTTP GET method.</summary>\n    GET,\n    /// <summary>HTTP POST method.</summary>\n    POST,\n    /// <summary>HTTP PATCH method.</summary>\n    PATCH,\n    /// <summary>HTTP PUT method.</summary>\n    PUT,\n    /// <summary>HTTP DELETE method.</summary>\n    DELETE\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphInboxRule.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a message rule as used by Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// Only a subset of rule properties are implemented to keep the\n/// wrapper light-weight.\n/// </remarks>\npublic class GraphInboxRule\n{\n    /// <summary>The rule identifier.</summary>\n    [JsonPropertyName(\"id\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Id { get; set; }\n\n    /// <summary>Display name of the rule.</summary>\n    [JsonPropertyName(\"displayName\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? DisplayName { get; set; }\n\n    /// <summary>Sequence number of the rule.</summary>\n    [JsonPropertyName(\"sequence\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public int? Sequence { get; set; }\n\n    /// <summary>Indicates whether the rule is enabled.</summary>\n    [JsonPropertyName(\"isEnabled\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? IsEnabled { get; set; }\n\n    /// <summary>Indicates whether the rule is read-only.</summary>\n    [JsonPropertyName(\"isReadOnly\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? IsReadOnly { get; set; }\n\n    /// <summary>Indicates whether the rule encountered errors.</summary>\n    [JsonPropertyName(\"hasError\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? HasError { get; set; }\n\n    /// <summary>Conditions of the rule.</summary>\n    [JsonPropertyName(\"conditions\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphInboxRulePredicates? Conditions { get; set; }\n\n    /// <summary>Actions executed by the rule.</summary>\n    [JsonPropertyName(\"actions\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphInboxRuleActions? Actions { get; set; }\n\n    /// <summary>Exceptions for the rule.</summary>\n    [JsonPropertyName(\"exceptions\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public GraphInboxRulePredicates? Exceptions { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphInboxRuleActions.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents actions that an inbox rule performs.\n/// </summary>\npublic class GraphInboxRuleActions\n{\n    /// <summary>\n    /// Folder path the message should be moved to.\n    /// </summary>\n    [JsonPropertyName(\"moveToFolder\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? MoveToFolder { get; set; }\n\n    /// <summary>\n    /// Folder path the message should be copied to.\n    /// </summary>\n    [JsonPropertyName(\"copyToFolder\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? CopyToFolder { get; set; }\n\n    /// <summary>\n    /// Indicates whether the message should be deleted.\n    /// </summary>\n    [JsonPropertyName(\"delete\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? Delete { get; set; }\n\n    /// <summary>\n    /// Addresses the message should be forwarded to.\n    /// </summary>\n    [JsonPropertyName(\"forwardTo\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEmailAddress>? ForwardTo { get; set; }\n\n    /// <summary>\n    /// Stops further rule processing when set.\n    /// </summary>\n    [JsonPropertyName(\"stopProcessingRules\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public bool? StopProcessingRules { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphInboxRuleBuilder.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a fluent API for building <see cref=\"GraphInboxRule\"/> objects.\n/// </summary>\npublic sealed class GraphInboxRuleBuilder {\n    private readonly GraphInboxRule _rule = new();\n\n    /// <summary>Sets the display name of the rule.</summary>\n    public GraphInboxRuleBuilder DisplayName(string name) {\n        _rule.DisplayName = name;\n        return this;\n    }\n\n    /// <summary>Sets the sequence of the rule.</summary>\n    public GraphInboxRuleBuilder Sequence(int sequence) {\n        _rule.Sequence = sequence;\n        return this;\n    }\n\n    /// <summary>Enables or disables the rule.</summary>\n    public GraphInboxRuleBuilder Enabled(bool enabled = true) {\n        _rule.IsEnabled = enabled;\n        return this;\n    }\n\n    private GraphInboxRulePredicates EnsurePredicates() => _rule.Conditions ??= new GraphInboxRulePredicates();\n    private GraphInboxRuleActions EnsureActions() => _rule.Actions ??= new GraphInboxRuleActions();\n\n    /// <summary>Adds senders to match.</summary>\n    public GraphInboxRuleBuilder SenderContains(params string[] senders) {\n        var preds = EnsurePredicates();\n        preds.SenderContains = senders.ToList();\n        return this;\n    }\n\n    /// <summary>Adds recipients to match.</summary>\n    public GraphInboxRuleBuilder RecipientContains(params string[] recipients) {\n        var preds = EnsurePredicates();\n        preds.RecipientContains = recipients.ToList();\n        return this;\n    }\n\n    /// <summary>Adds subjects to match.</summary>\n    public GraphInboxRuleBuilder SubjectContains(params string[] subjects) {\n        var preds = EnsurePredicates();\n        preds.SubjectContains = subjects.ToList();\n        return this;\n    }\n\n    /// <summary>Adds body text to match.</summary>\n    public GraphInboxRuleBuilder BodyContains(params string[] body) {\n        var preds = EnsurePredicates();\n        preds.BodyContains = body.ToList();\n        return this;\n    }\n\n    /// <summary>Sets the importance to match.</summary>\n    public GraphInboxRuleBuilder Importance(string importance) {\n        var preds = EnsurePredicates();\n        preds.Importance = importance;\n        return this;\n    }\n\n    /// <summary>Moves matching messages to the specified folder.</summary>\n    public GraphInboxRuleBuilder MoveToFolder(string folder) {\n        var actions = EnsureActions();\n        actions.MoveToFolder = folder;\n        return this;\n    }\n\n    /// <summary>Copies matching messages to the specified folder.</summary>\n    public GraphInboxRuleBuilder CopyToFolder(string folder) {\n        var actions = EnsureActions();\n        actions.CopyToFolder = folder;\n        return this;\n    }\n\n    /// <summary>Marks matching messages for deletion.</summary>\n    public GraphInboxRuleBuilder Delete(bool delete = true) {\n        var actions = EnsureActions();\n        actions.Delete = delete;\n        return this;\n    }\n\n    /// <summary>Forwards matching messages to the provided recipients.</summary>\n    public GraphInboxRuleBuilder ForwardTo(params string[] addresses) {\n        var actions = EnsureActions();\n        actions.ForwardTo = addresses\n            .Select(a => new GraphEmailAddress { Email = new GraphEmail { Address = a } })\n            .ToList();\n        return this;\n    }\n\n    /// <summary>Stops processing additional rules when this rule matches.</summary>\n    public GraphInboxRuleBuilder StopProcessingRules(bool stop = true) {\n        var actions = EnsureActions();\n        actions.StopProcessingRules = stop;\n        return this;\n    }\n\n    /// <summary>Builds the <see cref=\"GraphInboxRule\"/>.</summary>\n    public GraphInboxRule Build() => _rule;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphInboxRulePredicates.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents predicate options for inbox rules.\n/// </summary>\npublic class GraphInboxRulePredicates\n{\n    /// <summary>\n    /// Gets or sets a list of strings that must appear in the sender address.\n    /// </summary>\n    [JsonPropertyName(\"senderContains\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<string>? SenderContains { get; set; }\n\n    /// <summary>\n    /// Gets or sets a list of strings that must appear in the recipient address.\n    /// </summary>\n    [JsonPropertyName(\"recipientContains\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<string>? RecipientContains { get; set; }\n\n    /// <summary>\n    /// Gets or sets a list of strings that must appear in the subject line.\n    /// </summary>\n    [JsonPropertyName(\"subjectContains\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<string>? SubjectContains { get; set; }\n\n    /// <summary>\n    /// Gets or sets a list of strings that must appear in the message body.\n    /// </summary>\n    [JsonPropertyName(\"bodyContains\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<string>? BodyContains { get; set; }\n\n    /// <summary>\n    /// Gets or sets the required importance value.\n    /// </summary>\n    [JsonPropertyName(\"importance\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Importance { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphInternetMessageHeader.cs",
    "content": "using System.Text.Json.Serialization;\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a single internet message header returned by Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// Only name and value are exposed as these are typically the\n/// only fields required when processing headers.\n/// </remarks>\npublic class GraphInternetMessageHeader {\n    /// <summary>Header name.</summary>\n    [JsonPropertyName(\"name\")]\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Header value.</summary>\n    [JsonPropertyName(\"value\")]\n    public string Value { get; set; } = string.Empty;\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphLargeAttachmentUploader.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Uploads Graph attachments via upload sessions.\n/// </summary>\npublic static class GraphLargeAttachmentUploader {\n    // Must be a multiple of 320 KiB (except last chunk).\n    private const int ChunkSize = 327_680 * 32; // 10 MiB\n\n    /// <summary>\n    /// Uploads decoded attachments to an existing draft message.\n    /// </summary>\n    /// <returns>Error text when upload fails; otherwise null.</returns>\n    public static async Task<string?> UploadAsync(\n        HttpClient client,\n        string accessToken,\n        string messageId,\n        IReadOnlyList<DecodedMimeAttachment> attachments,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(accessToken)) {\n            throw new ArgumentException(\"accessToken is required.\", nameof(accessToken));\n        }\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (attachments == null) {\n            throw new ArgumentNullException(nameof(attachments));\n        }\n\n        foreach (var attachment in attachments) {\n            if (attachment == null) {\n                continue;\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n            var uploadUrl = await CreateUploadSessionAsync(client, accessToken, messageId, attachment, cancellationToken).ConfigureAwait(false);\n            if (string.IsNullOrWhiteSpace(uploadUrl)) {\n                return \"Graph upload session creation failed (empty uploadUrl).\";\n            }\n\n            var uploadError = await UploadToSessionAsync(client, uploadUrl!, attachment, cancellationToken).ConfigureAwait(false);\n            if (!string.IsNullOrWhiteSpace(uploadError)) {\n                return uploadError;\n            }\n        }\n\n        return null;\n    }\n\n    private static async Task<string?> CreateUploadSessionAsync(\n        HttpClient client,\n        string accessToken,\n        string messageId,\n        DecodedMimeAttachment attachment,\n        CancellationToken cancellationToken) {\n        var url = \"https://graph.microsoft.com/v1.0/me/messages/\" + Uri.EscapeDataString(messageId) + \"/attachments/createUploadSession\";\n        using var req = new HttpRequestMessage(HttpMethod.Post, url);\n        req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n        req.Content = new StringContent(BuildUploadSessionRequestJson(attachment), Encoding.UTF8, \"application/json\");\n\n        using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!resp.IsSuccessStatusCode) {\n            var trimmed = (body ?? string.Empty).Replace('\\r', ' ').Replace('\\n', ' ').Trim();\n            if (trimmed.Length > 500) {\n                trimmed = trimmed.Substring(0, 500) + \"...\";\n            }\n            return $\"Graph upload session creation failed (HTTP {((int)resp.StatusCode).ToString(CultureInfo.InvariantCulture)}): {trimmed}\";\n        }\n\n        try {\n            using var doc = JsonDocument.Parse(body);\n            if (doc.RootElement.TryGetProperty(\"uploadUrl\", out var uploadUrlEl)) {\n                var uploadUrl = uploadUrlEl.GetString();\n                var uploadUrlTrimmed = uploadUrl == null ? null : uploadUrl.Trim();\n                return string.IsNullOrWhiteSpace(uploadUrlTrimmed) ? null : uploadUrlTrimmed;\n            }\n        } catch {\n            // ignore parse errors, caller gets null\n        }\n\n        return null;\n    }\n\n    private static string BuildUploadSessionRequestJson(DecodedMimeAttachment attachment) {\n        static string JsonString(string value) =>\n            \"\\\"\" + (value ?? string.Empty).Replace(\"\\\\\", \"\\\\\\\\\").Replace(\"\\\"\", \"\\\\\\\"\") + \"\\\"\";\n\n        var sb = new StringBuilder();\n        sb.Append(\"{\\\"attachmentItem\\\":{\");\n        sb.Append(\"\\\"attachmentType\\\":\\\"file\\\",\");\n        sb.Append(\"\\\"name\\\":\").Append(JsonString(attachment.Name)).Append(\",\");\n        sb.Append(\"\\\"size\\\":\").Append(attachment.Length.ToString(CultureInfo.InvariantCulture));\n        var contentType = attachment.ContentType;\n        if (contentType != null && contentType.Trim().Length > 0) {\n            sb.Append(\",\\\"contentType\\\":\").Append(JsonString(contentType));\n        }\n        if (attachment.IsInline) {\n            sb.Append(\",\\\"isInline\\\":true\");\n            var contentId = attachment.ContentId;\n            if (contentId != null && contentId.Trim().Length > 0) {\n                sb.Append(\",\\\"contentId\\\":\").Append(JsonString(contentId));\n            }\n        }\n        sb.Append(\"}}\");\n        return sb.ToString();\n    }\n\n    private static async Task<string?> UploadToSessionAsync(\n        HttpClient client,\n        string uploadUrl,\n        DecodedMimeAttachment attachment,\n        CancellationToken cancellationToken) {\n        if (attachment.Length <= 0) {\n            return $\"Graph upload failed: attachment '{attachment.Name}' length is {attachment.Length.ToString(CultureInfo.InvariantCulture)}.\";\n        }\n\n        long offset = 0;\n        using var stream = attachment.OpenRead();\n        if (!stream.CanSeek) {\n            return $\"Graph upload failed: attachment '{attachment.Name}' stream is not seekable.\";\n        }\n\n        while (offset < attachment.Length) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            var remaining = attachment.Length - offset;\n            var chunkLen = (int)Math.Min(ChunkSize, remaining);\n            var buffer = new byte[chunkLen];\n            var read = 0;\n            while (read < chunkLen) {\n#if NET5_0_OR_GREATER\n                var n = await stream.ReadAsync(buffer.AsMemory(read, chunkLen - read), cancellationToken).ConfigureAwait(false);\n#else\n                var n = await stream.ReadAsync(buffer, read, chunkLen - read, cancellationToken).ConfigureAwait(false);\n#endif\n                if (n <= 0) {\n                    break;\n                }\n                read += n;\n            }\n\n            if (read <= 0) {\n                return $\"Graph upload failed: unexpected end of stream for '{attachment.Name}' at {offset.ToString(CultureInfo.InvariantCulture)}.\";\n            }\n\n            var start = offset;\n            var end = offset + read - 1;\n            using var req = new HttpRequestMessage(HttpMethod.Put, uploadUrl);\n            req.Content = new ByteArrayContent(buffer, 0, read);\n            req.Content.Headers.ContentType = new MediaTypeHeaderValue(\"application/octet-stream\");\n            req.Content.Headers.ContentRange = new ContentRangeHeaderValue(start, end, attachment.Length);\n\n            using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);\n            if (resp.IsSuccessStatusCode || (int)resp.StatusCode == 202) {\n                offset += read;\n                continue;\n            }\n\n#if NET5_0_OR_GREATER\n            var body = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            var trimmed = (body ?? string.Empty).Replace('\\r', ' ').Replace('\\n', ' ').Trim();\n            if (trimmed.Length > 500) {\n                trimmed = trimmed.Substring(0, 500) + \"...\";\n            }\n            return $\"Graph upload failed (HTTP {(int)resp.StatusCode}) for '{attachment.Name}': {trimmed}\";\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailFolder.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a Microsoft Graph mail folder as returned by the <c>/mailFolders</c> endpoints.\n/// </summary>\npublic sealed class GraphMailFolder {\n    /// <summary>Folder identifier.</summary>\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Folder display name.</summary>\n    [JsonPropertyName(\"displayName\")]\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Parent folder identifier (may be null for top-level folders).</summary>\n    [JsonPropertyName(\"parentFolderId\")]\n    public string? ParentFolderId { get; set; }\n\n    /// <summary>Number of child folders.</summary>\n    [JsonPropertyName(\"childFolderCount\")]\n    public int? ChildFolderCount { get; set; }\n\n    /// <summary>Well-known folder name (for example, <c>inbox</c> or <c>sentitems</c>).</summary>\n    [JsonPropertyName(\"wellKnownName\")]\n    public string? WellKnownName { get; set; }\n\n    /// <summary>Total number of items in the folder.</summary>\n    [JsonPropertyName(\"totalItemCount\")]\n    public int? TotalItemCount { get; set; }\n\n    /// <summary>Number of unread items in the folder.</summary>\n    [JsonPropertyName(\"unreadItemCount\")]\n    public int? UnreadItemCount { get; set; }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailMessage.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a mail message metadata record returned by Graph mailbox endpoints.\n/// </summary>\npublic sealed class GraphMailMessage {\n    /// <summary>Message identifier.</summary>\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Subject line.</summary>\n    [JsonPropertyName(\"subject\")]\n    public string? Subject { get; set; }\n\n    /// <summary>Received date/time in UTC.</summary>\n    [JsonPropertyName(\"receivedDateTime\")]\n    public DateTimeOffset? ReceivedDateTime { get; set; }\n\n    /// <summary>Sender.</summary>\n    [JsonPropertyName(\"from\")]\n    public GraphEmailAddress? From { get; set; }\n\n    /// <summary>Primary recipients.</summary>\n    [JsonPropertyName(\"toRecipients\")]\n    public List<GraphEmailAddress>? ToRecipients { get; set; }\n\n    /// <summary>Internet message id (RFC822 Message-Id).</summary>\n    [JsonPropertyName(\"internetMessageId\")]\n    public string? InternetMessageId { get; set; }\n\n    /// <summary>True when the message has attachments.</summary>\n    [JsonPropertyName(\"hasAttachments\")]\n    public bool? HasAttachments { get; set; }\n\n    /// <summary>True when the message is marked as read.</summary>\n    [JsonPropertyName(\"isRead\")]\n    public bool? IsRead { get; set; }\n\n    /// <summary>Message flag state.</summary>\n    [JsonPropertyName(\"flag\")]\n    public GraphMailMessageFlag? Flag { get; set; }\n\n    /// <summary>Conversation identifier.</summary>\n    [JsonPropertyName(\"conversationId\")]\n    public string? ConversationId { get; set; }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailMessageFlag.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents the flag status of a Graph message.\n/// </summary>\npublic sealed class GraphMailMessageFlag {\n    /// <summary>\n    /// Flag status value (for example, <c>flagged</c> or <c>notFlagged</c>).\n    /// </summary>\n    [JsonPropertyName(\"flagStatus\")]\n    public string? FlagStatus { get; set; }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxBrowser.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// High-level delegated-token mailbox browsing helpers built on top of <see cref=\"GraphApiClient\"/>.\n/// </summary>\npublic sealed class GraphMailboxBrowser {\n    private const string SummarySelect = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\";\n    // Must be a multiple of 320 KiB (except last chunk).\n    private const int LargeAttachmentChunkSize = 327_680 * 32; // 10 MiB\n    private readonly GraphApiClient _graph;\n\n    /// <summary>\n    /// Maximum MIME payload size used by <see cref=\"GetMessageContentAsync\"/> when no explicit limit is provided.\n    /// </summary>\n    public const int DefaultMaxMimeBytes = 25 * 1024 * 1024;\n\n    /// <summary>\n    /// Maximum MIME payload size used by <see cref=\"GetThreadingMetadataAsync\"/> when no explicit limit is provided.\n    /// </summary>\n    public const int DefaultThreadingMetadataMaxMimeBytes = 2 * 1024 * 1024;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMailboxBrowser\"/> class.\n    /// </summary>\n    /// <param name=\"graph\">Graph API client.</param>\n    public GraphMailboxBrowser(GraphApiClient graph) {\n        _graph = graph ?? throw new ArgumentNullException(nameof(graph));\n    }\n\n    /// <summary>\n    /// Lists mailbox folders and returns normalized hierarchical folder names.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphMailboxFolderSummary>> ListFoldersAsync(\n        int top = 200,\n        int maxRequests = 250,\n        CancellationToken cancellationToken = default) {\n        var folders = await _graph.ListMailFoldersRecursiveAsync(\n            top: ClampInt(top, 1, 999),\n            select: \"id,displayName,parentFolderId,childFolderCount,wellKnownName,totalItemCount,unreadItemCount\",\n            maxRequests: ClampInt(maxRequests, 1, 5000),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var nodes = new Dictionary<string, GraphFolderPathNode>(StringComparer.Ordinal);\n        foreach (var folder in folders) {\n            if (folder == null) {\n                continue;\n            }\n\n            var id = string.IsNullOrWhiteSpace(folder.Id) ? null : folder.Id.Trim();\n            if (id == null) {\n                continue;\n            }\n\n            var displayName = string.IsNullOrWhiteSpace(folder.DisplayName) ? id : folder.DisplayName.Trim();\n            var parentIdRaw = folder.ParentFolderId;\n            string? parentId = null;\n            if (!string.IsNullOrWhiteSpace(parentIdRaw)) {\n                parentId = parentIdRaw!.Trim();\n            }\n            var wellKnownNameRaw = folder.WellKnownName;\n            string? wellKnownName = null;\n            if (!string.IsNullOrWhiteSpace(wellKnownNameRaw)) {\n                wellKnownName = wellKnownNameRaw!.Trim();\n            }\n            nodes[id] = new GraphFolderPathNode(\n                id,\n                displayName,\n                parentId,\n                wellKnownName,\n                folder.TotalItemCount,\n                folder.UnreadItemCount);\n        }\n\n        string BuildPath(string id) {\n            if (!nodes.TryGetValue(id, out var node)) {\n                return id;\n            }\n\n            var parts = new List<string>();\n            var current = node;\n            var guard = 0;\n            while (guard++ < 100) {\n                parts.Add(current.DisplayName);\n                var parentId = current.ParentId;\n                if (string.IsNullOrWhiteSpace(parentId)) {\n                    break;\n                }\n\n                var parentKey = parentId!.Trim();\n                if (!nodes.TryGetValue(parentKey, out current)) {\n                    break;\n                }\n            }\n            parts.Reverse();\n            return string.Join(\"/\", parts);\n        }\n\n        var output = new List<GraphMailboxFolderSummary>(nodes.Count);\n        foreach (var node in nodes.Values) {\n            output.Add(new GraphMailboxFolderSummary {\n                Id = node.Id,\n                Name = BuildPath(node.Id),\n                DisplayName = node.DisplayName,\n                ParentId = node.ParentId,\n                WellKnownName = node.WellKnownName,\n                TotalItemCount = node.TotalItemCount,\n                UnreadItemCount = node.UnreadItemCount\n            });\n        }\n\n        output.Sort(static (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));\n        return output;\n    }\n\n    /// <summary>\n    /// Lists messages in a Graph folder.\n    /// </summary>\n    public async Task<GraphMailboxListResult> ListMessagesAsync(\n        string? folder,\n        int limit,\n        int offset,\n        CancellationToken cancellationToken = default) {\n        var folderSelector = ResolveFolderSelector(folder);\n        var top = ClampInt(limit, 1, 1000);\n        var skip = Math.Max(0, offset);\n\n        var folderInfo = await _graph.GetMailFolderAsync(folderSelector, select: \"totalItemCount\", cancellationToken: cancellationToken).ConfigureAwait(false);\n        var totalCount = folderInfo.TotalItemCount ?? 0;\n\n        var page = await _graph.ListMessagesAsync(\n            folderSelector,\n            top: top,\n            skip: skip,\n            select: SummarySelect,\n            orderBy: \"receivedDateTime desc\",\n            filter: null,\n            search: null,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new GraphMailboxListResult {\n            FolderSelector = folderSelector,\n            TotalCount = totalCount,\n            Messages = MapSummaries(page.Items)\n        };\n    }\n\n    /// <summary>\n    /// Lists messages in a Graph conversation.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphMailboxMessageSummary>> ListConversationMessagesAsync(\n        string conversationId,\n        int top = 100,\n        int maxPages = 25,\n        int maxItems = 2000,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(conversationId)) {\n            throw new ArgumentException(\"conversationId is required.\", nameof(conversationId));\n        }\n\n        var items = await _graph.ListConversationMessagesAsync(\n            conversationId.Trim(),\n            top: ClampInt(top, 1, 1000),\n            maxPages: ClampInt(maxPages, 1, 100),\n            select: SummarySelect,\n            orderBy: \"receivedDateTime desc\",\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var output = MapSummaries(items);\n        output.Sort(static (a, b) => b.DateUtc.CompareTo(a.DateUtc));\n        var cap = ClampInt(maxItems, 1, 10000);\n        if (output.Count > cap) {\n            output = output.GetRange(0, cap);\n        }\n        return output;\n    }\n\n    /// <summary>\n    /// Lists a paged slice of messages in a Graph conversation.\n    /// </summary>\n    public async Task<GraphMailboxConversationListResult> ListConversationMessagesPageAsync(\n        string conversationId,\n        int limit,\n        int offset,\n        int top = 100,\n        int maxPages = 25,\n        int maxItems = 2000,\n        CancellationToken cancellationToken = default) {\n        var messages = await ListConversationMessagesAsync(\n            conversationId,\n            top: top,\n            maxPages: maxPages,\n            maxItems: maxItems,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var total = messages.Count;\n        var skip = Math.Max(0, offset);\n        var take = ClampInt(limit, 1, 1000);\n        var page = messages.Skip(skip).Take(take).ToList();\n\n        return new GraphMailboxConversationListResult {\n            ConversationId = conversationId.Trim(),\n            TotalCount = total,\n            Messages = page\n        };\n    }\n\n    /// <summary>\n    /// Searches messages in a Graph folder.\n    /// </summary>\n    public async Task<GraphMailboxSearchResult> SearchMessagesAsync(\n        GraphMailboxSearchRequest request,\n        int max,\n        CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var limit = ClampInt(max, 1, 1000);\n        var folderSelector = ResolveFolderSelector(request.Folder);\n        var hasText = !string.IsNullOrWhiteSpace(request.Query) ||\n                      !string.IsNullOrWhiteSpace(request.SubjectContains) ||\n                      !string.IsNullOrWhiteSpace(request.FromContains) ||\n                      !string.IsNullOrWhiteSpace(request.ToContains) ||\n                      !string.IsNullOrWhiteSpace(request.BodyContains);\n\n        var filterParts = new List<string>();\n        if (!hasText) {\n            if (request.UnseenOnly) {\n                filterParts.Add(\"isRead eq false\");\n            }\n            if (request.HasAttachment) {\n                filterParts.Add(\"hasAttachments eq true\");\n            }\n            if (request.SinceUtc.HasValue) {\n                filterParts.Add(\"receivedDateTime ge \" + request.SinceUtc.Value.ToUniversalTime().ToString(\"o\", CultureInfo.InvariantCulture));\n            }\n            if (request.BeforeUtc.HasValue) {\n                filterParts.Add(\"receivedDateTime lt \" + request.BeforeUtc.Value.ToUniversalTime().ToString(\"o\", CultureInfo.InvariantCulture));\n            }\n        }\n\n        string? filter = filterParts.Count == 0 ? null : string.Join(\" and \", filterParts);\n        string? search = null;\n        if (hasText) {\n            var tokens = new List<string>();\n            AddSearchToken(tokens, request.Query);\n            AddSearchToken(tokens, request.SubjectContains);\n            AddSearchToken(tokens, request.FromContains);\n            AddSearchToken(tokens, request.ToContains);\n            AddSearchToken(tokens, request.BodyContains);\n            search = tokens.Count == 0 ? null : string.Join(\" \", tokens);\n        }\n\n        var page = await _graph.ListMessagesAsync(\n            folderSelector,\n            top: limit,\n            skip: null,\n            select: SummarySelect,\n            orderBy: \"receivedDateTime desc\",\n            filter: filter,\n            search: search,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var messages = MapSummaries(page.Items);\n        messages.Sort(static (a, b) => b.DateUtc.CompareTo(a.DateUtc));\n        if (messages.Count > limit) {\n            messages = messages.GetRange(0, limit);\n        }\n        return new GraphMailboxSearchResult {\n            FolderSelector = folderSelector,\n            Messages = messages\n        };\n    }\n\n    /// <summary>\n    /// Performs Graph mailbox delta query for a folder.\n    /// </summary>\n    public async Task<GraphMailboxDeltaResult> DeltaMessagesAsync(\n        string folder,\n        string? cursor,\n        int max,\n        CancellationToken cancellationToken = default) {\n        var folderSelector = ResolveFolderSelector(folder);\n        var delta = await _graph.DeltaMessagesAsync(\n            folderSelector,\n            cursor: cursor,\n            top: ClampInt(max, 1, 1000),\n            select: SummarySelect,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var deleted = delta.DeletedIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToList()\n                      ?? new List<string>();\n        return new GraphMailboxDeltaResult {\n            FolderSelector = folderSelector,\n            Cursor = delta.Cursor,\n            Upserts = MapSummaries(delta.Items),\n            DeletedNativeIds = deleted\n        };\n    }\n\n    /// <summary>\n    /// Gets one message summary.\n    /// </summary>\n    public async Task<GraphMailboxMessageSummary> GetMessageSummaryAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var message = await _graph.GetMessageAsync(\n            messageId.Trim(),\n            select: SummarySelect,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapSummary(message);\n    }\n\n    /// <summary>\n    /// Imports a MIME message into a Graph folder (typically <c>sentitems</c>).\n    /// </summary>\n    /// <param name=\"message\">MIME message to import.</param>\n    /// <param name=\"folder\">Folder alias/id. Defaults to <c>Sent Items</c>.</param>\n    /// <param name=\"maxInlineAttachmentBytes\">Maximum inline-attachment budget in bytes.</param>\n    /// <param name=\"idempotencyHeaderName\">Optional idempotency header name to preserve.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Import result.</returns>\n    public async Task<GraphMailboxImportResult> ImportMessageAsync(\n        MimeMessage message,\n        string? folder = \"Sent Items\",\n        int maxInlineAttachmentBytes = GraphMimePreparation.DefaultMaxInlineAttachmentBytes,\n        string? idempotencyHeaderName = null,\n        CancellationToken cancellationToken = default) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (maxInlineAttachmentBytes < 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxInlineAttachmentBytes), \"maxInlineAttachmentBytes must be zero or greater.\");\n        }\n\n        var folderSelector = ResolveFolderSelector(folder);\n        var prepared = GraphMimePreparation.PrepareMessage(\n            message,\n            maxInlineAttachmentBytes: maxInlineAttachmentBytes,\n            idempotencyHeaderName: idempotencyHeaderName);\n        try {\n            var created = await _graph.CreateMessageAsync(\n                prepared.Message,\n                folderIdOrWellKnownName: folderSelector,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n            var nativeId = NormalizeOptional(created.Id);\n            if (nativeId == null) {\n                throw new InvalidDataException(\"Graph returned an invalid created message response (missing id).\");\n            }\n\n            if (prepared.UploadAttachments.Count > 0) {\n                await UploadLargeAttachmentsAsync(\n                    nativeId,\n                    prepared.UploadAttachments,\n                    cancellationToken).ConfigureAwait(false);\n            }\n\n            return new GraphMailboxImportResult {\n                FolderSelector = folderSelector,\n                NativeId = nativeId,\n                MessageId = NormalizeMessageIdValue(message.MessageId)\n            };\n        } finally {\n            foreach (var attachment in prepared.UploadAttachments) {\n                attachment.Dispose();\n            }\n        }\n    }\n\n    /// <summary>\n    /// Sends a MIME message by creating a Graph draft and dispatching it.\n    /// </summary>\n    /// <param name=\"message\">MIME message to send.</param>\n    /// <param name=\"maxInlineAttachmentBytes\">Maximum inline-attachment budget in bytes.</param>\n    /// <param name=\"idempotencyHeaderName\">Optional idempotency header name to preserve.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Send result metadata.</returns>\n    public async Task<GraphMailboxSendResult> SendMessageAsync(\n        MimeMessage message,\n        int maxInlineAttachmentBytes = GraphMimePreparation.DefaultMaxInlineAttachmentBytes,\n        string? idempotencyHeaderName = null,\n        CancellationToken cancellationToken = default) {\n        var draft = await ImportMessageAsync(\n            message,\n            folder: \"Drafts\",\n            maxInlineAttachmentBytes: maxInlineAttachmentBytes,\n            idempotencyHeaderName: idempotencyHeaderName,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var draftId = NormalizeOptional(draft.NativeId);\n        if (draftId == null) {\n            throw new InvalidDataException(\"Graph draft created but response did not include message id.\");\n        }\n\n        await _graph.SendDraftMessageAsync(draftId, cancellationToken: cancellationToken).ConfigureAwait(false);\n        return new GraphMailboxSendResult {\n            DraftId = draftId,\n            MessageId = draft.MessageId\n        };\n    }\n\n    /// <summary>\n    /// Probes a Graph folder for a message with a matching RFC822 <c>Message-Id</c> token.\n    /// </summary>\n    /// <param name=\"messageIdToken\">Message-Id token (with or without angle brackets).</param>\n    /// <param name=\"folder\">Folder alias/id. Defaults to <c>Sent Items</c>.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Duplicate probe result.</returns>\n    public async Task<GraphMailboxDuplicateProbeResult> FindMessageByInternetMessageIdAsync(\n        string messageIdToken,\n        string? folder = \"Sent Items\",\n        CancellationToken cancellationToken = default) {\n        var normalizedToken = NormalizeMessageIdValue(messageIdToken);\n        if (normalizedToken == null) {\n            throw new ArgumentException(\"messageIdToken is required.\", nameof(messageIdToken));\n        }\n\n        var folderSelector = ResolveFolderSelector(folder);\n        var bracketedToken = \"<\" + normalizedToken + \">\";\n        var filter = \"internetMessageId eq '\" + EscapeODataStringLiteral(bracketedToken) +\n                     \"' or internetMessageId eq '\" + EscapeODataStringLiteral(normalizedToken) + \"'\";\n        var page = await _graph.ListMessagesAsync(\n            folderSelector,\n            top: 1,\n            skip: null,\n            select: \"id,internetMessageId\",\n            orderBy: null,\n            filter: filter,\n            search: null,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var match = page.Items.FirstOrDefault(x =>\n            string.Equals(\n                NormalizeMessageIdValue(x.InternetMessageId),\n                normalizedToken,\n                StringComparison.OrdinalIgnoreCase));\n        if (match == null) {\n            return new GraphMailboxDuplicateProbeResult {\n                IsMatch = false,\n                FolderSelector = folderSelector\n            };\n        }\n\n        return new GraphMailboxDuplicateProbeResult {\n            IsMatch = true,\n            FolderSelector = folderSelector,\n            NativeId = NormalizeOptional(match.Id),\n            MessageId = NormalizeMessageIdValue(match.InternetMessageId)\n        };\n    }\n\n    /// <summary>\n    /// Creates a Graph webhook subscription for message changes in a selected folder.\n    /// </summary>\n    public async Task<GraphMailboxSubscriptionResult> CreateMessageSubscriptionAsync(\n        string notificationUrl,\n        string folder = \"INBOX\",\n        DateTimeOffset? expirationDateTime = null,\n        string changeType = \"created,updated,deleted\",\n        string? clientState = null,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(notificationUrl)) {\n            throw new ArgumentException(\"notificationUrl is required.\", nameof(notificationUrl));\n        }\n        if (string.IsNullOrWhiteSpace(changeType)) {\n            throw new ArgumentException(\"changeType is required.\", nameof(changeType));\n        }\n\n        var resource = BuildMessageSubscriptionResource(folder);\n        var request = new GraphApiClient.GraphCreateSubscriptionRequest {\n            ChangeType = changeType.Trim(),\n            NotificationUrl = notificationUrl.Trim(),\n            Resource = resource,\n            ExpirationDateTime = expirationDateTime ?? DateTimeOffset.UtcNow.AddHours(8),\n            ClientState = string.IsNullOrWhiteSpace(clientState) ? null : clientState!.Trim()\n        };\n\n        var created = await _graph.CreateSubscriptionAsync(request, cancellationToken).ConfigureAwait(false);\n        return MapSubscription(created, resource);\n    }\n\n    /// <summary>\n    /// Renews an existing Graph webhook subscription.\n    /// </summary>\n    public async Task<GraphMailboxSubscriptionResult> RenewSubscriptionAsync(\n        string subscriptionId,\n        DateTimeOffset expirationDateTime,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(subscriptionId)) {\n            throw new ArgumentException(\"subscriptionId is required.\", nameof(subscriptionId));\n        }\n\n        var renewed = await _graph.RenewSubscriptionAsync(\n            subscriptionId.Trim(),\n            expirationDateTime,\n            cancellationToken).ConfigureAwait(false);\n        return MapSubscription(renewed, NormalizeOptional(renewed.Resource));\n    }\n\n    /// <summary>\n    /// Renews an existing Graph webhook subscription with stale-remote handling.\n    /// </summary>\n    public async Task<GraphMailboxSubscriptionRenewResult> RenewSubscriptionSafeAsync(\n        string subscriptionId,\n        DateTimeOffset expirationDateTime,\n        bool treatMissingAsStale = true,\n        CancellationToken cancellationToken = default) {\n        try {\n            var renewed = await RenewSubscriptionAsync(subscriptionId, expirationDateTime, cancellationToken).ConfigureAwait(false);\n            return new GraphMailboxSubscriptionRenewResult {\n                Renewed = true,\n                Missing = false,\n                Subscription = renewed\n            };\n        } catch (GraphApiException ex) when (treatMissingAsStale &&\n                                             (ex.StatusCode == HttpStatusCode.NotFound || ex.StatusCode == HttpStatusCode.Gone)) {\n            return new GraphMailboxSubscriptionRenewResult {\n                Renewed = false,\n                Missing = true,\n                Subscription = null\n            };\n        }\n    }\n\n    /// <summary>\n    /// Deletes an existing Graph webhook subscription.\n    /// </summary>\n    public async Task<GraphMailboxSubscriptionDeleteResult> DeleteSubscriptionAsync(\n        string subscriptionId,\n        bool treatMissingAsSuccess = true,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(subscriptionId)) {\n            throw new ArgumentException(\"subscriptionId is required.\", nameof(subscriptionId));\n        }\n\n        try {\n            await _graph.DeleteSubscriptionAsync(subscriptionId.Trim(), cancellationToken).ConfigureAwait(false);\n            return new GraphMailboxSubscriptionDeleteResult { Deleted = true };\n        } catch (GraphApiException ex) when (treatMissingAsSuccess &&\n                                             (ex.StatusCode == HttpStatusCode.NotFound || ex.StatusCode == HttpStatusCode.Gone)) {\n            return new GraphMailboxSubscriptionDeleteResult {\n                Deleted = true,\n                AlreadyDeleted = true\n            };\n        }\n    }\n\n    /// <summary>\n    /// Builds Graph subscription resource for folder message notifications.\n    /// </summary>\n    public static string BuildMessageSubscriptionResource(string folder) {\n        var selector = ResolveFolderSelector(folder);\n        return \"me/mailFolders('\" + EscapeGraphLiteral(selector) + \"')/messages\";\n    }\n\n    /// <summary>\n    /// Reads provider threading metadata for a single message.\n    /// </summary>\n    /// <param name=\"messageId\">Graph message id.</param>\n    /// <param name=\"maxMimeBytes\">Maximum MIME payload size to read.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Threading metadata parsed from MIME headers.</returns>\n    public async Task<GraphMailboxThreadingMetadataResult> GetThreadingMetadataAsync(\n        string messageId,\n        int maxMimeBytes = DefaultThreadingMetadataMaxMimeBytes,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (maxMimeBytes <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxMimeBytes), \"maxMimeBytes must be greater than zero.\");\n        }\n\n        var mimeBytes = await _graph.GetMessageMimeAsync(\n            messageId.Trim(),\n            maxBytes: maxMimeBytes,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        MimeMessage message;\n        try {\n            message = MimeMessage.Load(new MemoryStream(mimeBytes, writable: false));\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to parse Graph MIME message.\", ex);\n        }\n\n        return new GraphMailboxThreadingMetadataResult {\n            MessageId = NormalizeMessageIdValue(message.MessageId),\n            ReplyTo = NormalizeOptional(message.ReplyTo?.ToString()),\n            Cc = NormalizeOptional(message.Cc?.ToString()),\n            InReplyTo = NormalizeMessageIdValue(message.InReplyTo),\n            References = NormalizeMessageIdValues(message.References)\n        };\n    }\n\n    /// <summary>\n    /// Gets one message content by downloading MIME payload and parsing it to <see cref=\"MimeMessage\"/>.\n    /// </summary>\n    public async Task<GraphMailboxGetResult> GetMessageContentAsync(\n        string messageId,\n        int maxMimeBytes = DefaultMaxMimeBytes,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n        if (maxMimeBytes <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxMimeBytes), \"maxMimeBytes must be greater than zero.\");\n        }\n\n        var normalizedId = messageId.Trim();\n        var meta = await _graph.GetMessageAsync(normalizedId, select: \"id,isRead,flag,conversationId\", cancellationToken: cancellationToken).ConfigureAwait(false);\n        if (meta == null) {\n            throw new InvalidDataException(\"Graph message metadata was not returned.\");\n        }\n        var mimeBytes = await _graph.GetMessageMimeAsync(normalizedId, maxBytes: maxMimeBytes, cancellationToken: cancellationToken).ConfigureAwait(false);\n        string? conversationId = null;\n        var trimmedConversationId = meta.ConversationId?.Trim();\n        if (!string.IsNullOrWhiteSpace(trimmedConversationId)) {\n            conversationId = trimmedConversationId;\n        }\n        try {\n            return new GraphMailboxGetResult {\n                Seen = meta.IsRead,\n                Flagged = meta.Flag == null ? null : IsFlagged(meta.Flag),\n                NativeThreadId = conversationId,\n                Message = MimeMessage.Load(new MemoryStream(mimeBytes, writable: false))\n            };\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to parse Graph MIME message.\", ex);\n        }\n    }\n\n    /// <summary>\n    /// Sets message read/unread state.\n    /// </summary>\n    public async Task SetMessageSeenAsync(\n        string messageId,\n        bool seen,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _graph.SetMessageIsReadAsync(\n            messageId.Trim(),\n            seen,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets message flagged/unflagged state.\n    /// </summary>\n    public async Task SetMessageFlaggedAsync(\n        string messageId,\n        bool flagged,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _graph.SetMessageFlaggedAsync(\n            messageId.Trim(),\n            flagged,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves a message to a target folder alias/id.\n    /// </summary>\n    public async Task MoveMessageAsync(\n        string messageId,\n        string targetFolder,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        var destinationId = await ResolveFolderIdAsync(targetFolder, cancellationToken).ConfigureAwait(false);\n        await _graph.MoveMessageAsync(\n            messageId.Trim(),\n            destinationId,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives a single message.\n    /// </summary>\n    public Task ArchiveMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        return MoveMessageAsync(messageId, \"Archive\", cancellationToken);\n    }\n\n    /// <summary>\n    /// Moves a single message to trash.\n    /// </summary>\n    public Task TrashMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        return MoveMessageAsync(messageId, \"Trash\", cancellationToken);\n    }\n\n    /// <summary>\n    /// Deletes a message.\n    /// </summary>\n    public async Task DeleteMessageAsync(\n        string messageId,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"messageId is required.\", nameof(messageId));\n        }\n\n        await _graph.DeleteMessageAsync(\n            messageId.Trim(),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many messages to a target folder alias/id.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> MoveMessagesAsync(\n        IEnumerable<string> messageIds,\n        string targetFolder,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var ids = NormalizeBulkIds(messageIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var destinationId = await ResolveFolderIdAsync(targetFolder, cancellationToken).ConfigureAwait(false);\n        return await _graph.BatchMoveMessagesAsync(\n            ids,\n            destinationId,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives many messages.\n    /// </summary>\n    public Task<IReadOnlyList<GraphBulkOperationResult>> ArchiveMessagesAsync(\n        IEnumerable<string> messageIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        return MoveMessagesAsync(messageIds, \"Archive\", batchSize, cancellationToken);\n    }\n\n    /// <summary>\n    /// Moves many messages to trash.\n    /// </summary>\n    public Task<IReadOnlyList<GraphBulkOperationResult>> TrashMessagesAsync(\n        IEnumerable<string> messageIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        return MoveMessagesAsync(messageIds, \"Trash\", batchSize, cancellationToken);\n    }\n\n    /// <summary>\n    /// Deletes many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> DeleteMessagesAsync(\n        IEnumerable<string> messageIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await _graph.BatchDeleteMessagesAsync(\n            messageIds,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state on many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> SetMessagesSeenAsync(\n        IEnumerable<string> messageIds,\n        bool seen,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await _graph.BatchSetMessagesIsReadAsync(\n            messageIds,\n            seen,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on many messages.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> SetMessagesFlaggedAsync(\n        IEnumerable<string> messageIds,\n        bool flagged,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        return await _graph.BatchSetMessagesFlaggedAsync(\n            messageIds,\n            flagged,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets read/unread state on many conversations.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> SetConversationsSeenAsync(\n        IEnumerable<string> conversationIds,\n        bool seen,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n\n        return await ExecuteConversationMessageActionAsync(\n            conversationIds,\n            (messageIds, token) => _graph.BatchSetMessagesIsReadAsync(\n                messageIds,\n                seen,\n                batchSize: batchSize,\n                cancellationToken: token),\n            \"Graph conversation set-seen failed.\",\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets flagged/unflagged state on many conversations.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> SetConversationsFlaggedAsync(\n        IEnumerable<string> conversationIds,\n        bool flagged,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n\n        return await ExecuteConversationMessageActionAsync(\n            conversationIds,\n            (messageIds, token) => _graph.BatchSetMessagesFlaggedAsync(\n                messageIds,\n                flagged,\n                batchSize: batchSize,\n                cancellationToken: token),\n            \"Graph conversation set-flagged failed.\",\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves many conversations to a target folder alias/id.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> MoveConversationsAsync(\n        IEnumerable<string> conversationIds,\n        string targetFolder,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n\n        var ids = NormalizeBulkIds(conversationIds);\n        if (ids.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var destinationId = await ResolveFolderIdAsync(targetFolder, cancellationToken).ConfigureAwait(false);\n        return await _graph.BatchMoveConversationsAsync(\n            ids,\n            destinationId,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Archives many conversations.\n    /// </summary>\n    public Task<IReadOnlyList<GraphBulkOperationResult>> ArchiveConversationsAsync(\n        IEnumerable<string> conversationIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        return MoveConversationsAsync(conversationIds, \"Archive\", batchSize, cancellationToken);\n    }\n\n    /// <summary>\n    /// Moves many conversations to trash.\n    /// </summary>\n    public Task<IReadOnlyList<GraphBulkOperationResult>> TrashConversationsAsync(\n        IEnumerable<string> conversationIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        return MoveConversationsAsync(conversationIds, \"Trash\", batchSize, cancellationToken);\n    }\n\n    /// <summary>\n    /// Deletes many conversations.\n    /// </summary>\n    public async Task<IReadOnlyList<GraphBulkOperationResult>> DeleteConversationsAsync(\n        IEnumerable<string> conversationIds,\n        int batchSize = 20,\n        CancellationToken cancellationToken = default) {\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n\n        return await _graph.BatchDeleteConversationsAsync(\n            conversationIds,\n            batchSize: batchSize,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<IReadOnlyList<GraphBulkOperationResult>> ExecuteConversationMessageActionAsync(\n        IEnumerable<string> conversationIds,\n        Func<IReadOnlyList<string>, CancellationToken, Task<IReadOnlyList<GraphBulkOperationResult>>> operationAsync,\n        string fallbackError,\n        CancellationToken cancellationToken) {\n        if (conversationIds == null) {\n            throw new ArgumentNullException(nameof(conversationIds));\n        }\n        if (operationAsync == null) {\n            throw new ArgumentNullException(nameof(operationAsync));\n        }\n\n        var conversations = NormalizeConversationIds(conversationIds);\n        if (conversations.Count == 0) {\n            return Array.Empty<GraphBulkOperationResult>();\n        }\n\n        var output = new List<GraphBulkOperationResult>(conversations.Count);\n        foreach (var conversationId in conversations) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            IReadOnlyList<string> messageIds;\n            try {\n                messageIds = await _graph.ListConversationMessageIdsAsync(\n                    conversationId,\n                    cancellationToken: cancellationToken).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = ex.Message\n                });\n                continue;\n            }\n\n            if (messageIds.Count == 0) {\n                output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n                continue;\n            }\n\n            var results = await operationAsync(messageIds, cancellationToken).ConfigureAwait(false);\n            var failed = results.FirstOrDefault(result => result != null && !result.Ok);\n            if (failed is not null) {\n                output.Add(new GraphBulkOperationResult {\n                    Id = conversationId,\n                    Ok = false,\n                    Error = failed.Error ?? fallbackError\n                });\n                continue;\n            }\n\n            output.Add(new GraphBulkOperationResult { Id = conversationId, Ok = true });\n        }\n\n        return output;\n    }\n\n    private static List<string> NormalizeConversationIds(IEnumerable<string> conversationIds) {\n        var output = new List<string>();\n        var seen = new HashSet<string>(StringComparer.Ordinal);\n        foreach (var raw in conversationIds) {\n            if (string.IsNullOrWhiteSpace(raw)) {\n                continue;\n            }\n\n            var trimmed = raw.Trim();\n            if (trimmed.Length == 0 || !seen.Add(trimmed)) {\n                continue;\n            }\n\n            output.Add(trimmed);\n        }\n\n        return output;\n    }\n\n    private async Task UploadLargeAttachmentsAsync(\n        string messageId,\n        IReadOnlyList<DecodedMimeAttachment> attachments,\n        CancellationToken cancellationToken) {\n        foreach (var attachment in attachments) {\n            if (attachment == null) {\n                continue;\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n            await UploadLargeAttachmentAsync(messageId, attachment, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private async Task UploadLargeAttachmentAsync(\n        string messageId,\n        DecodedMimeAttachment attachment,\n        CancellationToken cancellationToken) {\n        if (attachment.Length <= 0) {\n            throw new InvalidDataException(\n                $\"Graph upload failed: attachment '{attachment.Name}' length is {attachment.Length.ToString(CultureInfo.InvariantCulture)}.\");\n        }\n\n        var uploadSession = await _graph.CreateAttachmentUploadSessionAsync(\n            messageId,\n            BuildAttachmentItem(attachment),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        var uploadUrl = NormalizeOptional(uploadSession.UploadUrl);\n        if (uploadUrl == null) {\n            throw new InvalidDataException(\"Graph upload session creation failed (empty uploadUrl).\");\n        }\n\n        using var stream = attachment.OpenRead();\n        long offset = 0;\n        while (offset < attachment.Length) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var remaining = attachment.Length - offset;\n            var chunkLen = (int)Math.Min(LargeAttachmentChunkSize, remaining);\n            var buffer = new byte[chunkLen];\n\n            var read = 0;\n            while (read < chunkLen) {\n#if NET5_0_OR_GREATER\n                var count = await stream.ReadAsync(buffer.AsMemory(read, chunkLen - read), cancellationToken).ConfigureAwait(false);\n#else\n                var count = await stream.ReadAsync(buffer, read, chunkLen - read, cancellationToken).ConfigureAwait(false);\n#endif\n                if (count <= 0) {\n                    break;\n                }\n\n                read += count;\n            }\n\n            if (read <= 0) {\n                throw new InvalidDataException(\n                    $\"Graph upload failed: unexpected end of stream for '{attachment.Name}' at {offset.ToString(CultureInfo.InvariantCulture)}.\");\n            }\n\n            var start = offset;\n            var end = offset + read - 1;\n            if (read == buffer.Length) {\n                await _graph.UploadAttachmentChunkAsync(\n                    uploadUrl,\n                    buffer,\n                    start,\n                    end,\n                    attachment.Length,\n                    cancellationToken).ConfigureAwait(false);\n            } else {\n                var trimmed = new byte[read];\n                Buffer.BlockCopy(buffer, 0, trimmed, 0, read);\n                await _graph.UploadAttachmentChunkAsync(\n                    uploadUrl,\n                    trimmed,\n                    start,\n                    end,\n                    attachment.Length,\n                    cancellationToken).ConfigureAwait(false);\n            }\n\n            offset += read;\n        }\n    }\n\n    private static GraphAttachmentItem BuildAttachmentItem(DecodedMimeAttachment attachment) {\n        var item = new GraphAttachmentItem(\"file\", attachment.Name, attachment.Length) {\n            ContentType = NormalizeOptional(attachment.ContentType)\n        };\n        if (attachment.IsInline) {\n            item.IsInline = true;\n            item.ContentId = NormalizeOptional(attachment.ContentId);\n        }\n        return item;\n    }\n\n    private async Task<string> ResolveFolderIdAsync(string targetFolder, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(targetFolder)) {\n            throw new ArgumentException(\"targetFolder is required.\", nameof(targetFolder));\n        }\n\n        var folderSelector = ResolveFolderSelector(targetFolder);\n        var folder = await _graph.GetMailFolderAsync(\n            folderSelector,\n            select: \"id\",\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        var id = (folder.Id ?? string.Empty).Trim();\n        if (id.Length == 0) {\n            throw new InvalidOperationException($\"Graph folder id resolution returned an empty id for selector '{folderSelector}'.\");\n        }\n        return id;\n    }\n\n    private static int ClampInt(int value, int min, int max) {\n        if (value < min) return min;\n        if (value > max) return max;\n        return value;\n    }\n\n    private static void AddSearchToken(List<string> tokens, string? value) {\n        if (value != null) {\n            var trimmed = value.Trim();\n            if (trimmed.Length > 0) {\n                tokens.Add(trimmed);\n            }\n        }\n    }\n\n    private static bool IsFlagged(GraphMailMessageFlag? flag) =>\n        string.Equals(flag?.FlagStatus, \"flagged\", StringComparison.OrdinalIgnoreCase);\n\n    private sealed class GraphFolderPathNode {\n        public GraphFolderPathNode(\n            string id,\n            string displayName,\n            string? parentId,\n            string? wellKnownName,\n            int? totalItemCount,\n            int? unreadItemCount) {\n            Id = id;\n            DisplayName = displayName;\n            ParentId = parentId;\n            WellKnownName = wellKnownName;\n            TotalItemCount = totalItemCount;\n            UnreadItemCount = unreadItemCount;\n        }\n\n        public string Id { get; }\n        public string DisplayName { get; }\n        public string? ParentId { get; }\n        public string? WellKnownName { get; }\n        public int? TotalItemCount { get; }\n        public int? UnreadItemCount { get; }\n    }\n\n    private static string JoinRecipients(IReadOnlyList<GraphEmailAddress>? recipients) {\n        if (recipients == null || recipients.Count == 0) {\n            return string.Empty;\n        }\n        var items = recipients\n            .Select(r => r?.Email?.Address)\n            .Where(x => !string.IsNullOrWhiteSpace(x))\n            .Select(x => x!.Trim())\n            .ToList();\n        return items.Count == 0 ? string.Empty : string.Join(\", \", items);\n    }\n\n    private static string? NormalizeOptional(string? raw) {\n        var trimmed = (raw ?? string.Empty).Trim();\n        return trimmed.Length == 0 ? null : trimmed;\n    }\n\n    private static List<string> NormalizeBulkIds(IEnumerable<string> values) {\n        var normalized = new List<string>();\n        foreach (var value in values) {\n            var trimmed = NormalizeOptional(value);\n            if (trimmed != null) {\n                normalized.Add(trimmed);\n            }\n        }\n        return normalized;\n    }\n\n    private static string EscapeGraphLiteral(string value) =>\n        (value ?? string.Empty).Replace(\"'\", \"''\");\n\n    private static string EscapeODataStringLiteral(string value) =>\n        (value ?? string.Empty).Replace(\"'\", \"''\");\n\n    private static string? NormalizeMessageIdValue(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var normalized = value == null ? string.Empty : value.Trim();\n        if (normalized.StartsWith(\"<\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(1);\n        }\n        if (normalized.EndsWith(\">\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(0, normalized.Length - 1);\n        }\n        normalized = normalized.Trim();\n        return normalized.Length == 0 ? null : normalized;\n    }\n\n    private static List<string> NormalizeMessageIdValues(IEnumerable<string>? values) {\n        if (values == null) {\n            return new List<string>();\n        }\n\n        var output = new List<string>();\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var value in values) {\n            var normalized = NormalizeMessageIdValue(value);\n            if (normalized == null || !seen.Add(normalized)) {\n                continue;\n            }\n            output.Add(normalized);\n        }\n        return output;\n    }\n\n    private static GraphMailboxMessageSummary MapSummary(GraphMailMessage message) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        return new GraphMailboxMessageSummary {\n            NativeId = message.Id,\n            NativeThreadId = string.IsNullOrWhiteSpace(message.ConversationId) ? null : (message.ConversationId ?? string.Empty).Trim(),\n            MessageId = NormalizeMessageIdValue(message.InternetMessageId),\n            From = message.From?.Email?.Address ?? string.Empty,\n            To = JoinRecipients(message.ToRecipients),\n            Subject = message.Subject,\n            DateUtc = message.ReceivedDateTime?.UtcDateTime ?? DateTime.UtcNow,\n            HasAttachments = message.HasAttachments ?? false,\n            Seen = message.IsRead ?? false,\n            Flagged = IsFlagged(message.Flag)\n        };\n    }\n\n    private static List<GraphMailboxMessageSummary> MapSummaries(IReadOnlyList<GraphMailMessage>? messages) {\n        if (messages == null || messages.Count == 0) {\n            return new List<GraphMailboxMessageSummary>();\n        }\n\n        var output = new List<GraphMailboxMessageSummary>(messages.Count);\n        foreach (var message in messages) {\n            if (message == null) {\n                continue;\n            }\n            output.Add(MapSummary(message));\n        }\n        return output;\n    }\n\n    private static GraphMailboxSubscriptionResult MapSubscription(GraphApiClient.GraphSubscription subscription, string? resource) {\n        if (subscription == null) {\n            throw new ArgumentNullException(nameof(subscription));\n        }\n\n        return new GraphMailboxSubscriptionResult {\n            SubscriptionId = NormalizeOptional(subscription.Id),\n            Resource = NormalizeOptional(resource ?? subscription.Resource),\n            ClientState = NormalizeOptional(subscription.ClientState),\n            ExpirationDateTime = subscription.ExpirationDateTime\n        };\n    }\n\n    /// <summary>\n    /// Resolves user folder aliases to Graph well-known folder selectors.\n    /// </summary>\n    public static string ResolveFolderSelector(string? folderRaw) {\n        var folder = (folderRaw ?? string.Empty).Trim();\n        if (folder.Length == 0) {\n            return \"inbox\";\n        }\n\n        if (folder.Equals(\"INBOX\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"Inbox\", StringComparison.OrdinalIgnoreCase)) {\n            return \"inbox\";\n        }\n\n        if (folder.Equals(\"Sent\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"Sent Items\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"SentItems\", StringComparison.OrdinalIgnoreCase)) {\n            return \"sentitems\";\n        }\n\n        if (folder.Equals(\"Drafts\", StringComparison.OrdinalIgnoreCase)) {\n            return \"drafts\";\n        }\n\n        if (folder.Equals(\"Archive\", StringComparison.OrdinalIgnoreCase)) {\n            return \"archive\";\n        }\n\n        if (folder.Equals(\"Junk\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"Junk Email\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"Spam\", StringComparison.OrdinalIgnoreCase)) {\n            return \"junkemail\";\n        }\n\n        if (folder.Equals(\"Trash\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"Deleted Items\", StringComparison.OrdinalIgnoreCase) ||\n            folder.Equals(\"DeletedItems\", StringComparison.OrdinalIgnoreCase)) {\n            return \"deleteditems\";\n        }\n\n        // Allow callers to pass a Graph folder id directly.\n        return folder;\n    }\n\n    /// <summary>\n    /// Graph mailbox folder summary.\n    /// </summary>\n    public sealed class GraphMailboxFolderSummary {\n        /// <summary>Graph folder identifier.</summary>\n        public string Id { get; set; } = string.Empty;\n\n        /// <summary>Hierarchical display path (for example, <c>Inbox/Projects</c>).</summary>\n        public string Name { get; set; } = string.Empty;\n\n        /// <summary>Folder display name.</summary>\n        public string DisplayName { get; set; } = string.Empty;\n\n        /// <summary>Parent folder id, when available.</summary>\n        public string? ParentId { get; set; }\n\n        /// <summary>Graph well-known folder name, when available.</summary>\n        public string? WellKnownName { get; set; }\n\n        /// <summary>Total item count, when available.</summary>\n        public int? TotalItemCount { get; set; }\n\n        /// <summary>Unread item count, when available.</summary>\n        public int? UnreadItemCount { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox list result.\n    /// </summary>\n    public sealed class GraphMailboxListResult {\n        /// <summary>Resolved Graph folder selector used for listing.</summary>\n        public string FolderSelector { get; set; } = string.Empty;\n\n        /// <summary>Total item count as reported by Graph folder metadata.</summary>\n        public int TotalCount { get; set; }\n\n        /// <summary>Message summaries.</summary>\n        public List<GraphMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Graph mailbox conversation list result.\n    /// </summary>\n    public sealed class GraphMailboxConversationListResult {\n        /// <summary>Graph conversation id used for listing.</summary>\n        public string ConversationId { get; set; } = string.Empty;\n\n        /// <summary>Total number of messages available in the conversation slice source.</summary>\n        public int TotalCount { get; set; }\n\n        /// <summary>Paged conversation message summaries.</summary>\n        public List<GraphMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Graph mailbox search request.\n    /// </summary>\n    public sealed class GraphMailboxSearchRequest {\n        /// <summary>Folder filter.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Free-form Graph search query string.</summary>\n        public string? Query { get; set; }\n\n        /// <summary>Subject contains filter.</summary>\n        public string? SubjectContains { get; set; }\n\n        /// <summary>From contains filter.</summary>\n        public string? FromContains { get; set; }\n\n        /// <summary>To contains filter.</summary>\n        public string? ToContains { get; set; }\n\n        /// <summary>Body contains filter.</summary>\n        public string? BodyContains { get; set; }\n\n        /// <summary>Unseen-only filter.</summary>\n        public bool UnseenOnly { get; set; }\n\n        /// <summary>Has-attachment filter.</summary>\n        public bool HasAttachment { get; set; }\n\n        /// <summary>Lower bound for received date/time (UTC).</summary>\n        public DateTime? SinceUtc { get; set; }\n\n        /// <summary>Upper bound for received date/time (UTC).</summary>\n        public DateTime? BeforeUtc { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox search result.\n    /// </summary>\n    public sealed class GraphMailboxSearchResult {\n        /// <summary>Resolved Graph folder selector used for search.</summary>\n        public string FolderSelector { get; set; } = string.Empty;\n\n        /// <summary>Matched messages.</summary>\n        public List<GraphMailboxMessageSummary> Messages { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Graph mailbox import result.\n    /// </summary>\n    public sealed class GraphMailboxImportResult {\n        /// <summary>Resolved Graph folder selector used for import.</summary>\n        public string FolderSelector { get; set; } = string.Empty;\n\n        /// <summary>Created Graph native message id.</summary>\n        public string? NativeId { get; set; }\n\n        /// <summary>Normalized RFC822 Message-Id used for import.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox send result.\n    /// </summary>\n    public sealed class GraphMailboxSendResult {\n        /// <summary>Graph draft id that was sent.</summary>\n        public string? DraftId { get; set; }\n\n        /// <summary>Normalized RFC822 Message-Id used for send.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox duplicate probe result.\n    /// </summary>\n    public sealed class GraphMailboxDuplicateProbeResult {\n        /// <summary>True when a matching message was found.</summary>\n        public bool IsMatch { get; set; }\n\n        /// <summary>Resolved Graph folder selector used for probing.</summary>\n        public string? FolderSelector { get; set; }\n\n        /// <summary>Matched Graph native message id, when available.</summary>\n        public string? NativeId { get; set; }\n\n        /// <summary>Matched normalized RFC822 Message-Id.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox webhook subscription result.\n    /// </summary>\n    public sealed class GraphMailboxSubscriptionResult {\n        /// <summary>Graph subscription id.</summary>\n        public string? SubscriptionId { get; set; }\n\n        /// <summary>Subscription resource path.</summary>\n        public string? Resource { get; set; }\n\n        /// <summary>Client-state value when provided.</summary>\n        public string? ClientState { get; set; }\n\n        /// <summary>Subscription expiration value.</summary>\n        public DateTimeOffset ExpirationDateTime { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox webhook delete result.\n    /// </summary>\n    public sealed class GraphMailboxSubscriptionDeleteResult {\n        /// <summary>True when delete operation succeeded.</summary>\n        public bool Deleted { get; set; }\n\n        /// <summary>True when delete succeeded because subscription was already gone.</summary>\n        public bool AlreadyDeleted { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox webhook renew result.\n    /// </summary>\n    public sealed class GraphMailboxSubscriptionRenewResult {\n        /// <summary>True when renew operation succeeded.</summary>\n        public bool Renewed { get; set; }\n\n        /// <summary>True when renew failed because subscription was already missing.</summary>\n        public bool Missing { get; set; }\n\n        /// <summary>Renewed subscription payload when available.</summary>\n        public GraphMailboxSubscriptionResult? Subscription { get; set; }\n    }\n\n    /// <summary>\n    /// Graph mailbox threading metadata.\n    /// </summary>\n    public sealed class GraphMailboxThreadingMetadataResult {\n        /// <summary>Normalized RFC822 Message-Id.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Reply-To header value.</summary>\n        public string? ReplyTo { get; set; }\n\n        /// <summary>Cc header value.</summary>\n        public string? Cc { get; set; }\n\n        /// <summary>Normalized RFC822 In-Reply-To value.</summary>\n        public string? InReplyTo { get; set; }\n\n        /// <summary>Normalized RFC822 References tokens.</summary>\n        public List<string> References { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Graph mailbox delta result.\n    /// </summary>\n    public sealed class GraphMailboxDeltaResult {\n        /// <summary>Resolved Graph folder selector used for delta query.</summary>\n        public string FolderSelector { get; set; } = string.Empty;\n\n        /// <summary>Graph delta cursor (next or delta link).</summary>\n        public string? Cursor { get; set; }\n\n        /// <summary>Messages to upsert.</summary>\n        public List<GraphMailboxMessageSummary> Upserts { get; set; } = new();\n\n        /// <summary>Deleted message ids.</summary>\n        public List<string> DeletedNativeIds { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Graph mailbox get-message result.\n    /// </summary>\n    public sealed class GraphMailboxGetResult {\n        /// <summary>Parsed MIME message.</summary>\n        public MimeMessage Message { get; set; } = new MimeMessage();\n\n        /// <summary>Read state from Graph metadata.</summary>\n        public bool? Seen { get; set; }\n\n        /// <summary>Flagged state from Graph metadata.</summary>\n        public bool? Flagged { get; set; }\n\n        /// <summary>Graph conversation id.</summary>\n        public string? NativeThreadId { get; set; }\n    }\n\n    /// <summary>\n    /// Provider-agnostic Graph mailbox message summary.\n    /// </summary>\n    public sealed class GraphMailboxMessageSummary {\n        /// <summary>Graph message id.</summary>\n        public string NativeId { get; set; } = string.Empty;\n\n        /// <summary>Graph conversation id.</summary>\n        public string? NativeThreadId { get; set; }\n\n        /// <summary>Normalized message-id header value.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Sender address.</summary>\n        public string From { get; set; } = string.Empty;\n\n        /// <summary>Joined recipient list.</summary>\n        public string To { get; set; } = string.Empty;\n\n        /// <summary>Subject line.</summary>\n        public string? Subject { get; set; }\n\n        /// <summary>Message received date/time (UTC).</summary>\n        public DateTime DateUtc { get; set; }\n\n        /// <summary>True when message has attachments.</summary>\n        public bool HasAttachments { get; set; }\n\n        /// <summary>True when message is seen.</summary>\n        public bool Seen { get; set; }\n\n        /// <summary>True when message is flagged.</summary>\n        public bool Flagged { get; set; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxFolderStatistics.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Statistics for a single mail folder.\n/// </summary>\n/// <remarks>\n/// Retrieved via the <c>/mailFolders/{id}/messages</c> endpoints\n/// to provide quick insights into mailbox usage.\n/// </remarks>\npublic class GraphMailboxFolderStatistics {\n    /// <summary>Folder identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Display name of the folder.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Optional well-known name such as 'inbox' or 'sentitems'.</summary>\n    public string? WellKnownName { get; set; }\n\n    /// <summary>Total number of items in this folder.</summary>\n    public int TotalItemCount { get; set; }\n\n    /// <summary>Number of unread items in this folder.</summary>\n    public int UnreadItemCount { get; set; }\n\n    /// <summary>Number of subfolders contained within this folder.</summary>\n    public int ChildFolderCount { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxGrantee.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents the entity receiving mailbox permissions.\n/// </summary>\n/// <remarks>\n/// Only minimal details are stored for the grantee as typically\n/// only the user principal name is required.\n/// </remarks>\npublic class GraphMailboxGrantee {\n    /// <summary>User principal name of the grantee.</summary>\n    [JsonPropertyName(\"user\")]\n    public string? User { get; set; }\n\n    /// <summary>Creates an instance from a dictionary.</summary>\n    public static GraphMailboxGrantee FromDictionary(IDictionary<string, object> dict) {\n        var g = new GraphMailboxGrantee();\n        if (dict.TryGetValue(\"user\", out var u)) g.User = u?.ToString();\n        return g;\n    }\n\n\n    /// <summary>Creates an instance from a PowerShell hashtable.</summary>\n    public static GraphMailboxGrantee FromHashtable(Hashtable table) {\n        var dict = table.Cast<DictionaryEntry>().ToDictionary(e => (string)e.Key, e => (object)(e.Value ?? string.Empty));\n        return FromDictionary(dict);\n    }\n\n    /// <summary>Converts the grantee to a dictionary.</summary>\n    public Dictionary<string, object> ToDictionary() {\n        var dict = new Dictionary<string, object>();\n        if (User != null) dict[\"user\"] = User!;\n        return dict;\n    }\n\n    /// <inheritdoc />\n    public override string ToString() => User ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxPermission.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a mailbox permission entry returned by Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// Provides parsing helpers to convert between Graph responses and\n/// strongly typed permission data.\n/// </remarks>\npublic class GraphMailboxPermission {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMailboxPermission\"/> class.\n    /// </summary>\n    public GraphMailboxPermission() {\n        Raw = new Dictionary<string, object>();\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMailboxPermission\"/> class from a dictionary.\n    /// </summary>\n    /// <param name=\"raw\">Dictionary with Graph permission fields.</param>\n    /// <param name=\"userPrincipalName\">Mailbox owner.</param>\n    public GraphMailboxPermission(Dictionary<string, object> raw, string? userPrincipalName = null) {\n        var source = raw ?? new Dictionary<string, object>();\n        Raw = source;\n        UserPrincipalName = userPrincipalName;\n        if (source.TryGetValue(\"id\", out var idObj)) Id = idObj as string;\n        if (source.TryGetValue(\"roles\", out var rolesObj) && rolesObj is object[] arr)\n            Roles = arr.Select(r => Enum.TryParse<GraphMailboxRole>(r?.ToString(), ignoreCase: true, out var role) ? role : GraphMailboxRole.Custom).ToArray();\n        if (source.TryGetValue(\"grantedTo\", out var granted) && granted is Dictionary<string, object> gdict)\n            GrantedTo = GraphMailboxGrantee.FromDictionary(gdict);\n    }\n\n    /// <summary>Unique permission identifier.</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Mailbox owning the permission.</summary>\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Roles assigned by the permission.</summary>\n    public GraphMailboxRole[]? Roles { get; set; }\n\n    /// <summary>Information about the grantee.</summary>\n    public GraphMailboxGrantee? GrantedTo { get; set; }\n\n    /// <summary>Raw dictionary returned by Graph.</summary>\n    public Dictionary<string, object> Raw { get; }\n\n    /// <summary>\n    /// Creates an instance from PowerShell hashtable input.\n    /// </summary>\n    /// <param name=\"table\">Hashtable describing the permission.</param>\n    /// <param name=\"userPrincipalName\">Mailbox owner.</param>\n    /// <returns>Created permission object.</returns>\n    public static GraphMailboxPermission FromHashtable(Hashtable table, string? userPrincipalName = null) {\n        var dict = table.Cast<DictionaryEntry>().ToDictionary(e => (string)e.Key, e => (object)(e.Value ?? string.Empty));\n        return new GraphMailboxPermission(dict, userPrincipalName);\n    }\n\n    // Intentionally no overload with nullable dictionary to avoid nullability warnings in callers.\n\n    /// <summary>\n    /// Converts the permission to a dictionary suitable for Graph requests.\n    /// </summary>\n    public Dictionary<string, object> ToDictionary() {\n        var dict = new Dictionary<string, object>(Raw);\n        if (Id != null) dict[\"id\"] = Id;\n        if (Roles != null)\n            dict[\"roles\"] = Roles.Select(r => r.ToString().ToLowerInvariant()).ToArray();\n        if (GrantedTo != null)\n            dict[\"grantedTo\"] = GrantedTo.ToDictionary();\n        return dict;\n    }\n\n    /// <summary>\n    /// Adds this permission to the associated mailbox.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential.</param>\n    public async Task AddAsync(GraphCredential credential) {\n        await AddAsync(credential, dryRun: false).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Adds this permission to the associated mailbox, optionally simulating the change.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential.</param>\n    /// <param name=\"dryRun\">When set, no Graph request is issued.</param>\n    public async Task AddAsync(GraphCredential credential, bool dryRun) {\n        if (UserPrincipalName is null) throw new InvalidOperationException(\"UserPrincipalName not set.\");\n        var body = JsonSerializer.Serialize(ToDictionary(), MailozaurrJsonContext.Default.DictionaryStringObject);\n        await MicrosoftGraphUtils.AddMailboxPermissionAsync(credential, UserPrincipalName, body, dryRun).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Removes this permission from the associated mailbox.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential.</param>\n    public async Task RemoveAsync(GraphCredential credential) {\n        await RemoveAsync(credential, dryRun: false).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Removes this permission from the associated mailbox, optionally simulating the change.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential.</param>\n    /// <param name=\"dryRun\">When set, no Graph request is issued.</param>\n    public async Task RemoveAsync(GraphCredential credential, bool dryRun) {\n        if (UserPrincipalName is null) throw new InvalidOperationException(\"UserPrincipalName not set.\");\n        if (Id is null) throw new InvalidOperationException(\"Id not set.\");\n        await MicrosoftGraphUtils.RemoveMailboxPermissionAsync(credential, UserPrincipalName, Id, dryRun).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public override string ToString() => Id ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxPermissionBuilder.cs",
    "content": "using System;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a fluent API for building <see cref=\"GraphMailboxPermission\"/> objects.\n/// </summary>\npublic sealed class GraphMailboxPermissionBuilder {\n    private readonly GraphMailboxPermission _permission = new();\n\n    /// <summary>Sets the permission identifier.</summary>\n    public GraphMailboxPermissionBuilder Id(string id) {\n        _permission.Id = id;\n        return this;\n    }\n\n    /// <summary>Sets the mailbox owner UPN.</summary>\n    public GraphMailboxPermissionBuilder UserPrincipalName(string upn) {\n        _permission.UserPrincipalName = upn;\n        return this;\n    }\n\n    /// <summary>Sets the roles for the permission.</summary>\n    public GraphMailboxPermissionBuilder Roles(params GraphMailboxRole[] roles) {\n        _permission.Roles = roles;\n        return this;\n    }\n\n    /// <summary>Sets the grantee user.</summary>\n    public GraphMailboxPermissionBuilder GrantedToUser(string user) {\n        _permission.GrantedTo = new GraphMailboxGrantee { User = user };\n        return this;\n    }\n\n    /// <summary>Builds the permission object.</summary>\n    public GraphMailboxPermission Build() => _permission;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMailboxStatistics.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Aggregated mailbox statistics retrieved from Microsoft Graph.\n/// </summary>\n/// <remarks>\n/// Useful for reporting or monitoring mailbox usage trends over time.\n/// </remarks>\npublic class GraphMailboxStatistics {\n    /// <summary>User principal name of the mailbox.</summary>\n    public string UserPrincipalName { get; set; } = string.Empty;\n\n    /// <summary>Total number of messages across all folders.</summary>\n    public int MessageCount { get; set; }\n\n    /// <summary>Total number of messages that contain attachments.</summary>\n    public int MessagesWithAttachments { get; set; }\n\n    /// <summary>Total size of all attachments in bytes.</summary>\n    public long TotalAttachmentSize { get; set; }\n\n    /// <summary>Total number of folders in the mailbox.</summary>\n    public int TotalFolders { get; set; }\n\n    /// <summary>Statistics for individual folders.</summary>\n    public List<GraphMailboxFolderStatistics> FolderStatistics { get; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMessage.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Represents an email message for use with the Graph API.\n/// </summary>\n/// <remarks>\n/// Includes only a subset of message properties commonly used when\n/// sending mail via Microsoft Graph.\n/// </remarks>\npublic class GraphMessage {\n    /// <summary>\n    /// Gets or sets the message identifier.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    [JsonPropertyName(\"id\")]\n    public string? Id { get; set; }\n    /// <summary>\n    /// Gets or sets the sender address.\n    /// </summary>\n    [JsonPropertyName(\"from\")]\n    public GraphEmailAddress? From { get; set; }\n\n    /// <summary>\n    /// Gets or sets the primary recipients of the message.\n    /// </summary>\n    [JsonPropertyName(\"toRecipients\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEmailAddress>? To { get; set; }\n\n    /// <summary>\n    /// Gets or sets the CC recipients of the message.\n    /// </summary>\n    [JsonPropertyName(\"ccRecipients\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEmailAddress>? Cc { get; set; }\n\n    /// <summary>\n    /// Gets or sets the BCC recipients of the message.\n    /// </summary>\n    [JsonPropertyName(\"bccRecipients\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEmailAddress>? Bcc { get; set; }\n\n    /// <summary>\n    /// Gets or sets reply-to addresses for the message.\n    /// </summary>\n    [JsonPropertyName(\"replyTo\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphEmailAddress>? ReplyTo { get; set; }\n\n    /// <summary>\n    /// Gets or sets the subject line of the message.\n    /// </summary>\n    [JsonPropertyName(\"subject\")]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Gets or sets the message body.\n    /// </summary>\n    [JsonPropertyName(\"body\")]\n    public GraphContent? Body { get; set; }\n\n    /// <summary>\n    /// Gets or sets the importance of the message.\n    /// </summary>\n    [JsonPropertyName(\"importance\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Importance { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether a read receipt is requested.\n    /// </summary>\n    [JsonPropertyName(\"isReadReceiptRequested\")]\n    public bool IsReadReceiptRequested { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether a delivery receipt is requested.\n    /// </summary>\n    [JsonPropertyName(\"isDeliveryReceiptRequested\")]\n    public bool IsDeliveryReceiptRequested { get; set; }\n\n    /// <summary>\n    /// Gets or sets attachments included with the message.\n    /// </summary>\n    [JsonPropertyName(\"attachments\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphAttachment>? Attachments { get; set; }\n\n    /// <summary>\n    /// Collection of MIME headers associated with the message.\n    /// </summary>\n    [JsonPropertyName(\"internetMessageHeaders\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<GraphInternetMessageHeader>? InternetMessageHeaders { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMessageAction.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents actions that can be performed on a Microsoft Graph message.\n/// </summary>\n/// <remarks>\n/// These are simplified descriptions of common message operations\n/// available in the Graph API.\n/// </remarks>\npublic enum GraphMessageAction {\n    /// <summary>Move a message to another folder.</summary>\n    Move,\n    /// <summary>Copy a message to another folder.</summary>\n    Copy,\n    /// <summary>Delete a message.</summary>\n    Delete\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMessageContainer.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Wrapper used when serializing a message for Graph API calls.\n/// </summary>\n/// <remarks>\n/// Allows additional properties like <c>saveToSentItems</c> to be\n/// specified alongside the message body.\n/// </remarks>\npublic class GraphMessageContainer {\n    /// <summary>\n    /// Gets or sets the message payload.\n    /// </summary>\n    [JsonPropertyName(\"message\")]\n    public GraphMessage Message { get; set; } = new GraphMessage();\n\n    /// <summary>\n    /// Gets or sets a value indicating whether the message should be saved to the Sent Items folder.\n    /// </summary>\n    [JsonPropertyName(\"saveToSentItems\")]\n    public bool SaveToSentItems { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMessageInfo.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a convenient view over a Graph email message.\n/// </summary>\n/// <remarks>\n/// Parses JSON dictionaries returned by the Graph API into strongly\n/// typed properties that are easier to consume.\n/// </remarks>\npublic class GraphMessageInfo {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMessageInfo\"/> class.\n    /// </summary>\n    public GraphMessageInfo() {\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMessageInfo\"/> class.\n    /// </summary>\n    /// <param name=\"raw\">Dictionary representing the Graph message.</param>\n    /// <param name=\"userPrincipalName\">Mailbox owner.</param>\n    /// <param name=\"summary\">Optional search result snippet.</param>\n    public GraphMessageInfo(Dictionary<string, object> raw, string? userPrincipalName = null, string? summary = null) {\n        Raw = raw ?? throw new ArgumentNullException(nameof(raw));\n        UserPrincipalName = userPrincipalName;\n        Summary = summary;\n        if (raw.TryGetValue(\"id\", out var idObj)) Id = idObj as string;\n        if (raw.TryGetValue(\"subject\", out var subjectObj)) Subject = subjectObj as string;\n        if (raw.TryGetValue(\"from\", out var fromObj)) From = ExtractAddress(fromObj);\n        if (raw.TryGetValue(\"toRecipients\", out var toObj)) To = ExtractAddresses(toObj);\n        if (raw.TryGetValue(\"sentDateTime\", out var dateObj) && DateTimeOffset.TryParse(dateObj?.ToString(), out var dt)) {\n            Date = dt.DateTime;\n        }\n        if (raw.TryGetValue(\"bodyPreview\", out var previewObj)) BodyPreview = previewObj as string;\n        if (raw.TryGetValue(\"body\", out var bodyObj)) {\n            ContentRaw = bodyObj;\n            if (bodyObj is Dictionary<string, object> bodyDict &&\n                bodyDict.TryGetValue(\"content\", out var contentObj)) {\n                Content = contentObj as string;\n            }\n        }\n        if (raw.TryGetValue(\"isRead\", out var readObj) && bool.TryParse(readObj?.ToString(), out var read)) {\n            IsRead = read;\n        }\n        if (raw.TryGetValue(\"importance\", out var importanceObj) &&\n            Enum.TryParse<GraphImportance>(importanceObj?.ToString(), true, out var importance)) {\n            Importance = importance;\n        }\n    }\n\n    /// <summary>The dictionary returned from Microsoft Graph.</summary>\n    public Dictionary<string, object>? Raw { get; }\n\n    /// <summary>User principal name of the mailbox containing the message.</summary>\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Unique identifier of the message.</summary>\n    public string? Id { get; set; }\n\n    /// <summary>Sender address.</summary>\n    public string? From { get; set; }\n\n    /// <summary>Recipient addresses.</summary>\n    public string? To { get; set; }\n\n    /// <summary>Subject of the message.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Date the message was sent.</summary>\n    public DateTime? Date { get; set; }\n\n    /// <summary>Preview text of the body.</summary>\n    public string? BodyPreview { get; set; }\n\n    /// <summary>Raw body content as returned by Graph.</summary>\n    public object? ContentRaw { get; set; }\n\n    /// <summary>Body content as plain string if available.</summary>\n    public string? Content { get; set; }\n\n    /// <summary>Whether the message is marked as read.</summary>\n    public bool? IsRead { get; set; }\n\n    /// <summary>Message importance.</summary>\n    public GraphImportance? Importance { get; set; }\n\n    /// <summary>Snippet extracted by the search service highlighting the match.</summary>\n    public string? Summary { get; set; }\n\n    /// <inheritdoc />\n    public override string ToString() => Subject ?? string.Empty;\n\n    private static string? ExtractAddress(object obj) {\n        if (obj is Dictionary<string, object> dict &&\n            dict.TryGetValue(\"emailAddress\", out var emailObj) &&\n            emailObj is Dictionary<string, object> email &&\n            email.TryGetValue(\"address\", out var addr)) {\n            return addr as string;\n        }\n        return null;\n    }\n\n    private static string? ExtractAddresses(object obj) {\n        if (obj is object[] arr) {\n            var list = new List<string>();\n            foreach (var item in arr) {\n                var addr = ExtractAddress(item);\n                if (addr != null) list.Add(addr);\n            }\n            return string.Join(\", \", list);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMimeMessageSender.cs",
    "content": "using System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends MIME messages through Microsoft Graph draft and attachment APIs.\n/// </summary>\npublic static class GraphMimeMessageSender {\n    /// <summary>\n    /// Sends a MIME message by creating a Graph draft, uploading large attachments, and sending the draft.\n    /// </summary>\n    public static async Task<GraphMessage> SendAsync(\n        GraphApiClient client,\n        string userId,\n        MimeMessage message,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var prepared = GraphMimePreparation.PrepareMessage(message);\n        try {\n            var created = await client.CreateMessageAsync(\n                prepared.Message,\n                userId: userId,\n                folderIdOrWellKnownName: null,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            await UploadAttachmentsAsync(client, userId, created, prepared.UploadAttachments, cancellationToken).ConfigureAwait(false);\n            await client.SendDraftMessageAsync(created.Id!, userId, cancellationToken).ConfigureAwait(false);\n            return created;\n        } finally {\n            foreach (var uploadAttachment in prepared.UploadAttachments) {\n                uploadAttachment.Dispose();\n            }\n        }\n    }\n\n    private static async Task UploadAttachmentsAsync(\n        GraphApiClient client,\n        string userId,\n        GraphMessage draft,\n        IReadOnlyList<DecodedMimeAttachment> attachments,\n        CancellationToken cancellationToken) {\n        if (attachments.Count == 0 || string.IsNullOrWhiteSpace(draft.Id)) {\n            return;\n        }\n\n        foreach (var attachment in attachments) {\n            var uploadSession = await client.CreateAttachmentUploadSessionAsync(\n                draft.Id!,\n                new GraphAttachmentItem(\"file\", attachment.Name, attachment.Length) {\n                    ContentType = attachment.ContentType,\n                    IsInline = attachment.IsInline,\n                    ContentId = attachment.ContentId\n                },\n                userId: userId,\n                cancellationToken: cancellationToken).ConfigureAwait(false);\n\n            await UploadAttachmentAsync(client, uploadSession, attachment, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private static async Task UploadAttachmentAsync(\n        GraphApiClient client,\n        GraphUploadSessionResult uploadSession,\n        DecodedMimeAttachment attachment,\n        CancellationToken cancellationToken) {\n        const int chunkSize = Graph.MaxChunkSize;\n        using var stream = attachment.OpenRead();\n        var buffer = new byte[chunkSize];\n        long position = 0;\n\n        while (true) {\n            var read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);\n            if (read <= 0) {\n                break;\n            }\n\n            var chunk = new byte[read];\n            Buffer.BlockCopy(buffer, 0, chunk, 0, read);\n            var startInclusive = position;\n            var endInclusive = position + read - 1L;\n\n            await client.UploadAttachmentChunkAsync(\n                uploadSession.UploadUrl!,\n                chunk,\n                startInclusive,\n                endInclusive,\n                attachment.Length,\n                cancellationToken).ConfigureAwait(false);\n\n            position += read;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphMimePreparation.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helpers for preparing MimeKit messages for Microsoft Graph APIs.\n/// </summary>\npublic static class GraphMimePreparation {\n    /// <summary>\n    /// Default maximum attachment bytes kept inline in Graph JSON payload.\n    /// </summary>\n    public const int DefaultMaxInlineAttachmentBytes = 3 * 1024 * 1024;\n\n    /// <summary>\n    /// Encodes bytes into base64url.\n    /// </summary>\n    public static string Base64UrlEncode(byte[] bytes) {\n        var base64 = Convert.ToBase64String(bytes);\n        return base64.TrimEnd('=').Replace('+', '-').Replace('/', '_');\n    }\n\n    /// <summary>\n    /// Prepares a MIME message for Graph create-message APIs.\n    /// </summary>\n    public static GraphPreparedMessage PrepareMessage(\n        MimeMessage message,\n        int maxInlineAttachmentBytes = DefaultMaxInlineAttachmentBytes,\n        string? idempotencyHeaderName = null) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (maxInlineAttachmentBytes < 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxInlineAttachmentBytes));\n        }\n\n        var inlineAttachments = new List<GraphAttachment>();\n        var uploadAttachments = new List<DecodedMimeAttachment>();\n        var inlineBytes = 0L;\n\n        foreach (var entity in message.Attachments) {\n            if (entity is not MimePart part) {\n                continue;\n            }\n\n            var decoded = DecodedMimeAttachment.DecodeToTempFile(part);\n            if (decoded.Length <= maxInlineAttachmentBytes &&\n                inlineBytes + decoded.Length <= maxInlineAttachmentBytes) {\n                byte[] bytes;\n                try {\n                    bytes = decoded.ReadAllBytes();\n                } finally {\n                    decoded.Dispose();\n                }\n\n                inlineBytes += bytes.Length;\n                inlineAttachments.Add(new GraphAttachment {\n                    ODataType = \"#microsoft.graph.fileAttachment\",\n                    Name = decoded.Name,\n                    ContentType = decoded.ContentType,\n                    ContentBytes = Convert.ToBase64String(bytes),\n                    IsInline = decoded.IsInline,\n                    ContentId = decoded.IsInline ? decoded.ContentId : null\n                });\n            } else {\n                uploadAttachments.Add(decoded);\n            }\n        }\n\n        var graphMessage = ConvertToGraphMessage(\n            message,\n            inlineAttachments.Count == 0 ? null : inlineAttachments,\n            idempotencyHeaderName);\n        return new GraphPreparedMessage(graphMessage, uploadAttachments);\n    }\n\n    /// <summary>\n    /// Converts a MIME message to Graph message payload.\n    /// </summary>\n    public static GraphMessage ConvertToGraphMessage(\n        MimeMessage message,\n        List<GraphAttachment>? attachments = null,\n        string? idempotencyHeaderName = null) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var html = message.HtmlBody;\n        var text = message.TextBody;\n        var bodyContentType = string.IsNullOrWhiteSpace(html) ? \"Text\" : \"HTML\";\n        var bodyContent = string.IsNullOrWhiteSpace(html) ? (text ?? string.Empty) : html!;\n\n        var headers = new List<GraphInternetMessageHeader>();\n        AddHeader(headers, \"Message-Id\", message.MessageId);\n        AddHeader(headers, \"In-Reply-To\", message.InReplyTo);\n        if (message.References != null && message.References.Count > 0) {\n            AddHeader(headers, \"References\", string.Join(\" \", message.References));\n        }\n\n        if (idempotencyHeaderName != null) {\n            var trimmedIdempotencyHeaderName = idempotencyHeaderName.Trim();\n            if (trimmedIdempotencyHeaderName.Length > 0) {\n                AddHeader(headers, trimmedIdempotencyHeaderName, message.Headers[trimmedIdempotencyHeaderName]);\n            }\n        }\n\n        return new GraphMessage {\n            Subject = message.Subject ?? string.Empty,\n            Body = new GraphContent { Type = bodyContentType, Content = bodyContent },\n            To = ConvertRecipients(message.To),\n            Cc = ConvertRecipients(message.Cc),\n            Bcc = ConvertRecipients(message.Bcc),\n            ReplyTo = ConvertRecipients(message.ReplyTo),\n            InternetMessageHeaders = headers.Count == 0 ? null : headers,\n            Attachments = attachments\n        };\n    }\n\n    private static List<GraphEmailAddress>? ConvertRecipients(InternetAddressList? list) {\n        if (list == null) {\n            return null;\n        }\n\n        var recipients = new List<GraphEmailAddress>();\n        foreach (var mailbox in list.Mailboxes) {\n            var address = (mailbox.Address ?? string.Empty).Trim();\n            if (address.Length == 0) {\n                continue;\n            }\n\n            recipients.Add(new GraphEmailAddress {\n                Email = new GraphEmail {\n                    Address = address,\n                    Name = string.IsNullOrWhiteSpace(mailbox.Name) ? null : mailbox.Name!.Trim()\n                }\n            });\n        }\n\n        return recipients.Count == 0 ? null : recipients;\n    }\n\n    private static void AddHeader(List<GraphInternetMessageHeader> headers, string name, string? value) {\n        var trimmed = (value ?? string.Empty).Trim();\n        if (trimmed.Length == 0) {\n            return;\n        }\n\n        headers.Add(new GraphInternetMessageHeader {\n            Name = name,\n            Value = trimmed\n        });\n    }\n}\n\n/// <summary>\n/// Result of graph MIME preparation.\n/// </summary>\npublic sealed record GraphPreparedMessage(\n    GraphMessage Message,\n    IReadOnlyList<DecodedMimeAttachment> UploadAttachments);\n\n/// <summary>\n/// Decoded attachment materialized into a temp file.\n/// </summary>\npublic sealed class DecodedMimeAttachment : IDisposable {\n    private readonly string _tempPath;\n\n    private DecodedMimeAttachment(\n        string tempPath,\n        string name,\n        string? contentType,\n        bool isInline,\n        string? contentId,\n        long length) {\n        _tempPath = tempPath;\n        Name = name;\n        ContentType = contentType;\n        IsInline = isInline;\n        ContentId = contentId;\n        Length = length;\n    }\n\n    /// <summary>Attachment display name.</summary>\n    public string Name { get; }\n    /// <summary>Optional MIME content type.</summary>\n    public string? ContentType { get; }\n    /// <summary>True when attachment is inline.</summary>\n    public bool IsInline { get; }\n    /// <summary>Inline content id (cid).</summary>\n    public string? ContentId { get; }\n    /// <summary>Attachment length in bytes.</summary>\n    public long Length { get; }\n\n    /// <summary>\n    /// Decodes a MIME part into a temp-file backed attachment.\n    /// </summary>\n    public static DecodedMimeAttachment DecodeToTempFile(MimePart part) {\n        if (part == null) {\n            throw new ArgumentNullException(nameof(part));\n        }\n\n        var fileName = (part.FileName ?? string.Empty).Trim();\n        if (fileName.Length == 0) {\n            fileName = \"attachment\";\n        }\n\n        var isInline = part.ContentDisposition?.Disposition?.Equals(ContentDisposition.Inline, StringComparison.OrdinalIgnoreCase) == true;\n        var contentType = part.ContentType?.MimeType;\n        var contentId = isInline ? part.ContentId : null;\n        var tempPath = Path.Combine(Path.GetTempPath(), $\"mailozaurr-mime-{Guid.NewGuid():N}.bin\");\n\n        try {\n            using var fs = new FileStream(tempPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);\n            if (part.Content != null) {\n                part.Content.DecodeTo(fs);\n            } else {\n                part.WriteTo(fs);\n            }\n            fs.Flush(true);\n            return new DecodedMimeAttachment(tempPath, fileName, contentType, isInline, contentId, fs.Length);\n        } catch {\n            TryDelete(tempPath);\n            throw;\n        }\n    }\n\n    /// <summary>\n    /// Opens attachment content as a readable stream.\n    /// </summary>\n    public Stream OpenRead() {\n        return new FileStream(_tempPath, FileMode.Open, FileAccess.Read, FileShare.Read);\n    }\n\n    /// <summary>\n    /// Reads all attachment bytes.\n    /// </summary>\n    public byte[] ReadAllBytes() {\n        return File.ReadAllBytes(_tempPath);\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        TryDelete(_tempPath);\n        GC.SuppressFinalize(this);\n    }\n\n    private static void TryDelete(string path) {\n        try {\n            if (File.Exists(path)) {\n                File.Delete(path);\n            }\n        } catch {\n            // best-effort cleanup\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphPages.cs",
    "content": "using System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a single page of results returned by Microsoft Graph.\n/// </summary>\npublic class GraphPage<T> {\n    /// <summary>Items returned in the current page.</summary>\n    public IReadOnlyList<T> Items { get; }\n\n    /// <summary>Link to the next page (when present).</summary>\n    public string? NextLink { get; }\n\n    /// <summary>Creates a page instance.</summary>\n    public GraphPage(IReadOnlyList<T> items, string? nextLink) {\n        Items = items ?? new List<T>();\n        if (nextLink == null) {\n            NextLink = null;\n        } else {\n            var trimmed = nextLink.Trim();\n            NextLink = trimmed.Length == 0 ? null : trimmed;\n        }\n    }\n}\n\n/// <summary>\n/// Represents a delta page returned by Graph delta endpoints.\n/// </summary>\npublic sealed class GraphDeltaPage<T> : GraphPage<T> {\n    /// <summary>Link to the delta cursor (when returned by Graph).</summary>\n    public string? DeltaLink { get; }\n\n    /// <summary>Identifiers of deleted items (tombstones) returned in the current page.</summary>\n    public IReadOnlyList<string> DeletedIds { get; }\n\n    /// <summary>The recommended cursor to persist (nextLink when present, otherwise deltaLink).</summary>\n    public string? Cursor => NextLink ?? DeltaLink;\n\n    /// <summary>Creates a delta page instance.</summary>\n    public GraphDeltaPage(IReadOnlyList<T> items, string? nextLink, string? deltaLink, IReadOnlyList<string> deletedIds)\n        : base(items, nextLink) {\n        if (deltaLink == null) {\n            DeltaLink = null;\n        } else {\n            var trimmed = deltaLink.Trim();\n            DeltaLink = trimmed.Length == 0 ? null : trimmed;\n        }\n        DeletedIds = deletedIds ?? new List<string>();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphRetryHelper.cs",
    "content": "using System.Net;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Classification and backoff helpers for Microsoft Graph send operations.\n/// </summary>\ninternal static class GraphRetryHelper\n{\n    private const int StatusRequestTimeout = 408;\n    private const int StatusTooManyRequests = 429;\n    private const int StatusServerErrorMin = 500;\n    private const int StatusServerErrorMax = 599;\n    private static readonly string[] ThrottleMarkers = new[]\n    {\n        \"ApplicationThrottled\",\n        \"MailboxConcurrency\",\n        \"TooManyRequests\",\n        \"Throttled\"\n    };\n\n    internal static bool IsThrottled(Exception ex)\n    {\n        if (ex is GraphApiException gex)\n        {\n            if ((int)gex.StatusCode == StatusTooManyRequests)\n            {\n                return true;\n            }\n            var parsed = GraphApiErrorParser.Parse(gex.ResponseContent);\n            var code = parsed?.Error?.Code ?? string.Empty;\n            foreach (var marker in ThrottleMarkers)\n            {\n                if (code.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0)\n                    return true;\n            }\n            if (!string.IsNullOrWhiteSpace(parsed?.Raw))\n            {\n                foreach (var marker in ThrottleMarkers)\n                {\n                    if (parsed!.Raw.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0)\n                        return true;\n                }\n            }\n        }\n\n        if (ex is HttpRequestException httpEx)\n        {\n#if NET5_0_OR_GREATER\n            if (httpEx.StatusCode.HasValue && (int)httpEx.StatusCode.Value == StatusTooManyRequests)\n                return true;\n#endif\n        }\n\n        return false;\n    }\n\n    internal static bool IsTransient(Exception ex)\n    {\n        if (IsThrottled(ex))\n        {\n            return true;\n        }\n\n        if (ex is GraphApiException gex)\n        {\n            var code = (int)gex.StatusCode;\n            if (code == StatusRequestTimeout || code == StatusTooManyRequests)\n                return true;\n            if (code >= StatusServerErrorMin && code <= StatusServerErrorMax)\n                return true;\n\n            var parsed = GraphApiErrorParser.Parse(gex.ResponseContent);\n            var message = parsed?.Error?.Message ?? parsed?.Raw ?? string.Empty;\n            if (message.IndexOf(\"timeout\", StringComparison.OrdinalIgnoreCase) >= 0) return true;\n            if (message.IndexOf(\"temporarily\", StringComparison.OrdinalIgnoreCase) >= 0) return true;\n            if (message.IndexOf(\"gateway\", StringComparison.OrdinalIgnoreCase) >= 0) return true;\n            if (message.IndexOf(\"503\", StringComparison.OrdinalIgnoreCase) >= 0) return true;\n        }\n\n        // Fall back to generic library-wide transient classification.\n        return Helpers.IsTransient(ex);\n    }\n\n    /// <summary>\n    /// Calculate exponential backoff delay with optional jitter and cap.\n    /// </summary>\n    internal static TimeSpan CalculateDelay(GraphSendPolicy policy, int attempt)\n    {\n        if (attempt < 0) attempt = 0;\n        var baseDelay = (double)Math.Max(0, policy.BaseDelayMs);\n        var delay = baseDelay * Math.Pow(2, attempt);\n        var max = Math.Max(0, policy.MaxDelayMs);\n        if (max > 0)\n        {\n            delay = Math.Min(delay, max);\n        }\n\n        var jitterWindow = Math.Max(0, policy.JitterMs);\n        if (jitterWindow > 0)\n        {\n            var jitter = GraphRetryHelperRandom.NextInt(jitterWindow + 1);\n            delay += jitter;\n        }\n\n        return TimeSpan.FromMilliseconds(Math.Max(0, (int)Math.Round(delay)));\n    }\n}\n\ninternal static class GraphRetryHelperRandom\n{\n#if !NET5_0_OR_GREATER\n    [ThreadStatic]\n    private static Random? s_random;\n#endif\n\n    internal static int NextInt(int maxExclusive)\n    {\n        if (maxExclusive <= 1) return 0;\n#if NET5_0_OR_GREATER\n        return System.Security.Cryptography.RandomNumberGenerator.GetInt32(0, maxExclusive);\n#else\n        var rnd = s_random ??= new Random(unchecked(Environment.TickCount * 31 + Thread.CurrentThread.ManagedThreadId));\n        return rnd.Next(0, maxExclusive);\n#endif\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphSendPolicy.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Policy controlling how Graph send operations are throttled and retried.\n/// </summary>\npublic sealed class GraphSendPolicy\n{\n    /// <summary>Maximum number of concurrent Graph HTTP requests. Applies globally.</summary>\n    public int MaxConcurrency { get; set; } = 2;\n\n    /// <summary>Maximum number of retries upon transient errors.</summary>\n    public int MaxRetries { get; set; } = 4;\n\n    /// <summary>Base delay in milliseconds for backoff calculation.</summary>\n    public int BaseDelayMs { get; set; } = 1000;\n\n    /// <summary>Maximum delay in milliseconds between retries.</summary>\n    public int MaxDelayMs { get; set; } = 30000;\n\n    /// <summary>Jitter window in milliseconds added to each delay.</summary>\n    public int JitterMs { get; set; } = 500;\n\n    /// <summary>Retry only when the error is classified as transient or throttling related.</summary>\n    public bool RetryOnTransient { get; set; } = true;\n\n    /// <summary>When true and SMTP is configured, fallback to SMTP after Graph retries are exhausted.</summary>\n    public bool EnableSmtpFallback { get; set; } = false;\n\n    /// <summary>Returns a conservative default policy suitable for most workloads.</summary>\n    public static GraphSendPolicy Default => new GraphSendPolicy();\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraph/GraphUploadSessionResult.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Result returned when creating an upload session for large attachments.\n/// </summary>\n/// <remarks>\n/// The <see cref=\"UploadUrl\"/> is valid for a limited time and is\n/// used to PUT the attachment bytes to the service.\n/// </remarks>\npublic class GraphUploadSessionResult {\n    /// <summary>\n    /// Gets or sets the URL that should be used to upload the attachment bytes.\n    /// </summary>\n    [JsonPropertyName(\"uploadUrl\")]\n    public string UploadUrl { get; set; } = string.Empty;\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/MicrosoftGraphUtils.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Linq;\nusing System.IO;\nusing System.Text.Json.Serialization;\nusing System.Collections.Concurrent;\nusing System.Threading;\nusing MimeKit;\n\nnamespace Mailozaurr {\n\n    /// <summary>\n    /// Utility helpers for working with the Microsoft Graph API.\n    /// </summary>\n    public static class MicrosoftGraphUtils {\n        private static readonly HttpClient HttpClient;\n        private static readonly ConcurrentDictionary<string, GraphAuthorization> TokenCache = new();\n        internal static Func<string, string, string, string, IEnumerable<string>?, Task<GraphAuthorization>> AcquireGraphCertificateTokenAsyncFunc { get; set; } = AcquireGraphCertificateTokenAsyncDefault;\n        internal static Func<string, string, byte[], string, IEnumerable<string>?, Task<GraphAuthorization>> AcquireGraphCertificateBytesTokenAsyncFunc { get; set; } = AcquireGraphCertificateBytesTokenAsyncDefault;\n        internal static Func<string, string, string, IEnumerable<string>?, Task<GraphAuthorization>> AcquireGraphCertificatePemTokenAsyncFunc { get; set; } = AcquireGraphCertificatePemTokenAsyncDefault;\n        private static SemaphoreSlim _concurrencySemaphore = new(5, 5);\n        private static int _maxConcurrentRequests = 5;\n\n        /// <summary>\n        /// Gets or sets the maximum number of concurrent HTTP requests allowed.\n        /// </summary>\n        /// <remarks>\n        /// This is a process-wide limit. Setting this property affects all Graph operations in the\n        /// current AppDomain. The underlying semaphore is swapped using a thread-safe exchange to\n        /// ensure safe updates under concurrency.\n        /// </remarks>\n        public static int MaxConcurrentRequests {\n            get => _maxConcurrentRequests;\n            set {\n                if (value <= 0) {\n                    throw new ArgumentOutOfRangeException(nameof(MaxConcurrentRequests));\n                }\n                var newSem = new SemaphoreSlim(value, value);\n                var old = Interlocked.Exchange(ref _concurrencySemaphore, newSem);\n                old.Dispose();\n                _maxConcurrentRequests = value;\n            }\n        }\n\n        internal static SemaphoreSlim ConcurrencySemaphore => _concurrencySemaphore;\n\n        internal static string GetEndpointBase(GraphEndpoint endpoint) =>\n            endpoint switch {\n                GraphEndpoint.V1 => \"https://graph.microsoft.com/v1.0\",\n                GraphEndpoint.Beta => \"https://graph.microsoft.com/beta\",\n                _ => throw new ArgumentOutOfRangeException(nameof(endpoint))\n            };\n\n        private static TimeSpan GetRetryAfterDelay(HttpResponseMessage response) {\n            if (response.Headers.TryGetValues(\"Retry-After\", out var values)) {\n                var value = System.Linq.Enumerable.FirstOrDefault(values);\n                if (int.TryParse(value, out var seconds)) {\n                    return TimeSpan.FromSeconds(seconds);\n                }\n                if (DateTimeOffset.TryParse(value, out var date)) {\n                    var diff = date - DateTimeOffset.UtcNow;\n                    return diff > TimeSpan.Zero ? diff : TimeSpan.Zero;\n                }\n            }\n            return TimeSpan.Zero;\n        }\n\n        /// <summary>\n        /// Gets or sets the timeout for HTTP operations in seconds.\n        /// </summary>\n        public static int TimeoutSeconds {\n            get => (int)HttpClient.Timeout.TotalSeconds;\n            set => HttpClient.Timeout = TimeSpan.FromSeconds(value);\n        }\n\n        static MicrosoftGraphUtils() {\n            HttpClient = new HttpClient();\n            HttpClient.Timeout = TimeSpan.FromSeconds(TimeoutSeconds);\n            AppDomain.CurrentDomain.ProcessExit += (_, _) => HttpClient.Dispose();\n        }\n\n        internal static void ResetOAuthHelperOverrides() {\n            AcquireGraphCertificateTokenAsyncFunc = AcquireGraphCertificateTokenAsyncDefault;\n            AcquireGraphCertificateBytesTokenAsyncFunc = AcquireGraphCertificateBytesTokenAsyncDefault;\n            AcquireGraphCertificatePemTokenAsyncFunc = AcquireGraphCertificatePemTokenAsyncDefault;\n        }\n\n        private static Task<GraphAuthorization> AcquireGraphCertificateTokenAsyncDefault(string clientId, string tenantDomain, string certificatePath, string certificatePassword, IEnumerable<string>? scopes) =>\n            OAuthHelpers.AcquireGraphCertificateTokenAsync(clientId, tenantDomain, certificatePath, certificatePassword, scopes);\n\n        private static Task<GraphAuthorization> AcquireGraphCertificateBytesTokenAsyncDefault(string clientId, string tenantDomain, byte[] certificateBytes, string certificatePassword, IEnumerable<string>? scopes) =>\n            OAuthHelpers.AcquireGraphCertificateTokenAsync(clientId, tenantDomain, certificateBytes, certificatePassword, scopes);\n\n        private static Task<GraphAuthorization> AcquireGraphCertificatePemTokenAsyncDefault(string clientId, string tenantDomain, string pemPath, IEnumerable<string>? scopes) =>\n            OAuthHelpers.AcquireGraphCertificatePemTokenAsync(clientId, tenantDomain, pemPath, scopes);\n\n        private static async Task CacheGraphAuthorizationAsync(string key, string clientId, GraphAuthorization authorization) {\n            TokenCache[key] = authorization;\n            await OAuthTokenCache.SetAsync($\"graph:{key}\", new OAuthCredential {\n                UserName = clientId,\n                AccessToken = authorization.AccessToken,\n                ExpiresOn = authorization.ExpiresOn\n            }).ConfigureAwait(false);\n        }\n        /// <summary>\n        /// Converts a credential string (username@directory) and secret to a GraphCredential object.\n        /// </summary>\n        public static GraphCredential ConvertFromGraphCredential(string username, string password) {\n            if (username == null) {\n                throw new ArgumentNullException(nameof(username));\n            }\n\n            if (string.IsNullOrWhiteSpace(username)) {\n                throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(username));\n            }\n\n            if (password == null) {\n                throw new ArgumentNullException(nameof(password));\n            }\n\n            if (string.IsNullOrWhiteSpace(password)) {\n                throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(password));\n            }\n\n            username = username.Trim();\n            var parts = username.Split('@');\n            if (parts.Length != 2) {\n                throw new ArgumentException(\"Invalid credential format. Expected 'clientid@directoryid'.\");\n            }\n\n            return new GraphCredential {\n                ClientId = parts[0],\n                DirectoryId = parts[1],\n                ClientSecret = password\n            };\n        }\n\n        /// <summary>\n        /// Connects to O365 Graph and returns the Authorization header value (\"Bearer ...\").\n        /// </summary>\n        public static async Task<string> ConnectO365GraphAsync(GraphCredential credential, string tenantDomain, string resource = \"https://manage.office.com\", CancellationToken cancellationToken = default) {\n            var delegatedAccessToken = credential.AccessToken;\n            if (delegatedAccessToken != null && delegatedAccessToken.Trim().Length > 0) {\n                var accessToken = delegatedAccessToken.Trim();\n                return accessToken.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase)\n                    ? accessToken\n                    : $\"Bearer {accessToken}\";\n            }\n\n            var key = $\"{credential.ClientId}|{tenantDomain}|{credential.CertificatePath}|{credential.ClientSecret}|{resource}\";\n            if (TokenCache.TryGetValue(key, out var cached) && cached.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5)) {\n                return $\"{cached.TokenType} {cached.AccessToken}\";\n            }\n            var cachedFile = await OAuthTokenCache.GetAsync($\"graph:{key}\").ConfigureAwait(false);\n            if (cachedFile != null && cachedFile.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5)) {\n                TokenCache[key] = new GraphAuthorization { AccessToken = cachedFile.AccessToken, TokenType = \"Bearer\", ExpiresOn = cachedFile.ExpiresOn };\n                return $\"Bearer {cachedFile.AccessToken}\";\n            }\n            if (!string.IsNullOrWhiteSpace(credential.CertificatePath)) {\n                var scopes = new[] { $\"{resource}/.default\" };\n                var auth = await AcquireGraphCertificateTokenAsyncFunc(\n                    credential.ClientId,\n                    tenantDomain,\n                    credential.CertificatePath!,\n                    credential.CertificatePassword ?? string.Empty,\n                    scopes).ConfigureAwait(false);\n                await CacheGraphAuthorizationAsync(key, credential.ClientId, auth).ConfigureAwait(false);\n                return $\"{auth.TokenType} {auth.AccessToken}\";\n            }\n            if (credential.CertificateBytes != null) {\n                var scopes = new[] { $\"{resource}/.default\" };\n                var auth = await AcquireGraphCertificateBytesTokenAsyncFunc(\n                    credential.ClientId,\n                    tenantDomain,\n                    credential.CertificateBytes,\n                    credential.CertificatePassword ?? string.Empty,\n                    scopes).ConfigureAwait(false);\n                await CacheGraphAuthorizationAsync(key, credential.ClientId, auth).ConfigureAwait(false);\n                return $\"{auth.TokenType} {auth.AccessToken}\";\n            }\n            if (!string.IsNullOrWhiteSpace(credential.CertificatePemPath)) {\n                var scopes = new[] { $\"{resource}/.default\" };\n                var auth = await AcquireGraphCertificatePemTokenAsyncFunc(\n                    credential.ClientId,\n                    tenantDomain,\n                    credential.CertificatePemPath!,\n                    scopes).ConfigureAwait(false);\n                await CacheGraphAuthorizationAsync(key, credential.ClientId, auth).ConfigureAwait(false);\n                return $\"{auth.TokenType} {auth.AccessToken}\";\n            }\n\n            var body = new Dictionary<string, string>\n            {\n                { \"grant_type\", \"client_credentials\" },\n                { \"resource\", resource },\n                { \"client_id\", credential.ClientId }\n            };\n            if (!string.IsNullOrEmpty(credential.ClientSecret)) {\n                body.Add(\"client_secret\", credential.ClientSecret!);\n            }\n            var content = new FormUrlEncodedContent(body);\n            var url = $\"https://login.microsoftonline.com/{tenantDomain}/oauth2/token\";\n            await ConcurrencySemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);\n            HttpResponseMessage? response = null;\n            try {\n                response = await HttpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);\n                if ((int)response.StatusCode == 429) {\n                    var delay = GetRetryAfterDelay(response);\n                    response.Dispose();\n                    if (delay > TimeSpan.Zero) {\n                        await Task.Delay(delay, cancellationToken).ConfigureAwait(false);\n                    }\n                    response = await HttpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);\n                }\n#if NET5_0_OR_GREATER\n                var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n                var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                if (!response.IsSuccessStatusCode) {\n                    throw new GraphApiException(\n                        response.StatusCode,\n                        $\"ConnectO365GraphAsync - Error: {json}\",\n                        json);\n                }\n                var token = System.Text.Json.JsonDocument.Parse(json);\n                var accessToken = token.RootElement.GetProperty(\"access_token\").GetString();\n                var tokenType = token.RootElement.GetProperty(\"token_type\").GetString();\n                var expiresOn = DateTimeOffset.UtcNow.AddHours(1);\n                if (token.RootElement.TryGetProperty(\"expires_in\", out var expIn)) {\n                    expiresOn = DateTimeOffset.UtcNow.AddSeconds(expIn.GetInt32());\n                }\n                if (token.RootElement.TryGetProperty(\"expires_on\", out var expOn)) {\n                    if (long.TryParse(expOn.GetString(), out var expSeconds)) {\n                        expiresOn = DateTimeOffset.FromUnixTimeSeconds(expSeconds);\n                    }\n                }\n                TokenCache[key] = new GraphAuthorization { AccessToken = accessToken ?? string.Empty, TokenType = tokenType ?? string.Empty, ExpiresOn = expiresOn };\n                await OAuthTokenCache.SetAsync($\"graph:{key}\", new OAuthCredential {\n                    UserName = credential.ClientId,\n                    AccessToken = accessToken ?? string.Empty,\n                    ExpiresOn = expiresOn\n                }).ConfigureAwait(false);\n                return $\"{tokenType} {accessToken}\";\n            } finally {\n                response?.Dispose();\n                ConcurrencySemaphore.Release();\n            }\n        }\n\n        /// <summary>\n        /// Connects to O365 Graph with retry logic and returns the Authorization header value.\n        /// </summary>\n        public static async Task<string> ConnectO365GraphWithRetryAsync(\n            GraphCredential credential,\n            string tenantDomain,\n            int retryCount,\n            int retryDelayMilliseconds,\n            double retryDelayBackoff,\n            string resource = \"https://manage.office.com\",\n            CancellationToken cancellationToken = default) {\n            int attempts = 0;\n            Exception? lastException = null;\n            do {\n                try {\n                    return await ConnectO365GraphAsync(credential, tenantDomain, resource, cancellationToken).ConfigureAwait(false);\n                } catch (Exception ex) {\n                    lastException = ex;\n                    LoggingMessages.Logger.WriteWarning($\"Connect-EmailGraph - {ex.Message}\");\n                    if ((!Helpers.IsTransient(ex)) || attempts >= retryCount) {\n                        throw;\n                    }\n                    var delay = (int)Math.Round(retryDelayMilliseconds * Math.Pow(retryDelayBackoff, attempts));\n                    if (delay > 0) {\n                        await Task.Delay(delay, cancellationToken).ConfigureAwait(false);\n                    }\n                }\n                attempts++;\n            } while (attempts <= retryCount);\n            throw lastException ?? new InvalidOperationException(\"Operation failed without exception\");\n        }\n\n        /// <summary>\n        /// Builds a full URI from base, path, and query parameters.\n        /// </summary>\n        public static string BuildGraphUri(GraphEndpoint endpoint, string path, IDictionary<string, string>? queryParameters = null) =>\n            BuildGraphUri(GetEndpointBase(endpoint), path, queryParameters);\n\n        /// <summary>\n        /// Builds a full URI from base, path, and query parameters.\n        /// </summary>\n        public static string BuildGraphUri(string baseUri, string path, IDictionary<string, string>? queryParameters = null) {\n            var uriBuilder = new StringBuilder();\n            uriBuilder.Append(baseUri.TrimEnd('/'));\n            if (!string.IsNullOrWhiteSpace(path)) {\n                if (!path.StartsWith(\"/\")) uriBuilder.Append('/');\n                uriBuilder.Append(path);\n            }\n            if (queryParameters != null && queryParameters.Count > 0) {\n                var first = true;\n                foreach (var kvp in queryParameters) {\n                    if (string.IsNullOrWhiteSpace(kvp.Value)) continue;\n                    uriBuilder.Append(first ? '?' : '&');\n                    uriBuilder.Append(Uri.EscapeDataString(kvp.Key));\n                    uriBuilder.Append('=');\n                    uriBuilder.Append(Uri.EscapeDataString(kvp.Value));\n                    first = false;\n                }\n            }\n            return uriBuilder.ToString();\n        }\n\n        /// <summary>\n        /// Invokes a REST API call to Microsoft Graph and returns the result as a JsonDocument.\n        /// </summary>\n        public static async Task<JsonDocument> InvokeGraphApiAsync(\n            string method,\n            string uri,\n            IDictionary<string, string>? headers = null,\n            string? body = null,\n            CancellationToken cancellationToken = default) {\n            var request = new HttpRequestMessage(new HttpMethod(method), uri);\n            if (headers != null) {\n                foreach (var kvp in headers) {\n                    request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);\n                }\n            }\n            if (!string.IsNullOrWhiteSpace(body) && (method == \"POST\" || method == \"PUT\" || method == \"PATCH\")) {\n                request.Content = new StringContent(body, Encoding.UTF8, \"application/json\");\n            }\n            await ConcurrencySemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);\n            HttpResponseMessage? response = null;\n            try {\n                response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);\n                if ((int)response.StatusCode == 429) {\n                    var delay = GetRetryAfterDelay(response);\n                    response.Dispose();\n                    if (delay > TimeSpan.Zero) {\n                        await Task.Delay(delay, cancellationToken).ConfigureAwait(false);\n                    }\n                    response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);\n                }\n#if NET5_0_OR_GREATER\n                var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n                var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                if (!response.IsSuccessStatusCode) {\n                    throw new GraphApiException(\n                        response.StatusCode,\n                        $\"InvokeGraphApiAsync - Error: {response.StatusCode} - {responseContent}\",\n                        responseContent);\n                }\n                return JsonDocument.Parse(responseContent);\n            } finally {\n                response?.Dispose();\n                ConcurrencySemaphore.Release();\n            }\n        }\n\n        /// <summary>\n        /// Sends multiple requests to Microsoft Graph in a single batch.\n        /// </summary>\n        public static async Task<IReadOnlyList<GraphBatchResult>> SendBatchAsync(GraphCredential credential, IEnumerable<GraphBatchRequest> requests, CancellationToken cancellationToken = default) {\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\", cancellationToken).ConfigureAwait(false);\n            var headers = new Dictionary<string, string> { { \"Authorization\", token } };\n            var batchPayload = new GraphBatchPayload {\n                Requests = requests.Select(r => new GraphBatchRequestPayload {\n                    Id = r.Id,\n                    Method = r.Method.ToString(),\n                    Url = r.Url.TrimStart('/'),\n                    Headers = r.Headers,\n                    Body = r.Body\n                }).ToList()\n            };\n            var jsonBody = JsonSerializer.Serialize(batchPayload, MailozaurrJsonContext.Default.GraphBatchPayload);\n            var batchUri = BuildGraphUri(GraphEndpoint.V1, \"/$batch\");\n            var doc = await InvokeGraphApiAsync(\"POST\", batchUri, headers, jsonBody, cancellationToken).ConfigureAwait(false);\n            var results = new List<GraphBatchResult>();\n            if (doc.RootElement.TryGetProperty(\"responses\", out var responses) && responses.ValueKind == JsonValueKind.Array) {\n                foreach (var item in responses.EnumerateArray()) {\n                    var result = new GraphBatchResult();\n                    if (item.TryGetProperty(\"id\", out var idEl) && idEl.ValueKind == JsonValueKind.String) result.Id = idEl.GetString() ?? string.Empty;\n                    if (item.TryGetProperty(\"status\", out var statusEl) && statusEl.TryGetInt32(out var status)) result.Status = status;\n                    if (item.TryGetProperty(\"headers\", out var headersEl) && headersEl.ValueKind == JsonValueKind.Object) {\n                        var h = new Dictionary<string, string>();\n                        foreach (var prop in headersEl.EnumerateObject()) {\n                            if (prop.Value.ValueKind == JsonValueKind.String) h[prop.Name] = prop.Value.GetString() ?? string.Empty;\n                        }\n                        result.Headers = h;\n                    }\n                    if (item.TryGetProperty(\"body\", out var bodyEl)) {\n                        result.Body = bodyEl;\n                    }\n                    results.Add(result);\n                }\n            }\n            return results;\n        }\n\n        /// <summary>\n        /// Removes empty values from a dictionary recursively (null, empty string, empty array, empty dictionary).\n        /// </summary>\n        public static void RemoveEmptyValues(IDictionary<string, object> dict, HashSet<string>? exclude = null, bool recursive = true, int rerun = 0) {\n            exclude ??= new HashSet<string>();\n            var keys = dict.Keys.ToList();\n            foreach (var key in keys) {\n                if (exclude.Contains(key)) continue;\n                var value = dict[key];\n                if (recursive && value is IDictionary<string, object> subDict) {\n                    if (subDict.Count == 0) {\n                        dict.Remove(key);\n                    } else {\n                        RemoveEmptyValues(subDict, exclude, recursive, rerun);\n                        if (subDict.Count == 0) dict.Remove(key);\n                    }\n                } else if (value == null) {\n                    dict.Remove(key);\n                } else if (value is string s && string.IsNullOrWhiteSpace(s)) {\n                    dict.Remove(key);\n                } else if (value is System.Collections.IList list && list.Count == 0) {\n                    dict.Remove(key);\n                } else if (value is IDictionary<string, object> d && d.Count == 0) {\n                    dict.Remove(key);\n                }\n            }\n            if (rerun > 0) {\n                for (int i = 0; i < rerun; i++) {\n                    RemoveEmptyValues(dict, exclude, recursive, 0);\n                }\n            }\n        }\n\n        /// <summary>\n        /// Joins a base URI, optional relative URI, and query parameters into a full URI string.\n        /// </summary>\n        public static string JoinUriQuery(GraphEndpoint endpoint, string? relativeOrAbsoluteUri = null, IDictionary<string, object>? queryParameters = null, bool escapeUriString = false) =>\n            JoinUriQuery(GetEndpointBase(endpoint), relativeOrAbsoluteUri, queryParameters, escapeUriString);\n\n        /// <summary>\n        /// Joins a base URI, optional relative URI, and query parameters into a full URI string.\n        /// </summary>\n        public static string JoinUriQuery(string baseUri, string? relativeOrAbsoluteUri = null, IDictionary<string, object>? queryParameters = null, bool escapeUriString = false) {\n            if (baseUri == null) {\n                throw new ArgumentNullException(nameof(baseUri));\n            }\n            string url = baseUri.TrimEnd('/');\n            if (!string.IsNullOrWhiteSpace(relativeOrAbsoluteUri)) {\n                url += \"/\" + relativeOrAbsoluteUri!.TrimStart('/');\n            }\n            var uriBuilder = new UriBuilder(url);\n            if (queryParameters != null && queryParameters.Count > 0) {\n                var query = new StringBuilder();\n                bool first = true;\n                foreach (var kvp in queryParameters) {\n                    if (kvp.Value == null) {\n                        continue;\n                    }\n\n                    if (!first) {\n                        query.Append('&');\n                    }\n\n                    query.Append(Uri.EscapeDataString(kvp.Key));\n                    query.Append('=');\n                    var valueString = kvp.Value.ToString() ?? string.Empty;\n                    query.Append(Uri.EscapeDataString(valueString));\n                    first = false;\n                }\n                uriBuilder.Query = query.ToString();\n            }\n            var result = uriBuilder.Uri.AbsoluteUri;\n            if (escapeUriString) {\n                if (Uri.TryCreate(result, UriKind.Absolute, out var uri)) {\n                    result = uri.GetComponents(UriComponents.AbsoluteUri, UriFormat.UriEscaped);\n                } else {\n                    result = Uri.EscapeDataString(result);\n                }\n            }\n            return result;\n        }\n\n        private static object? ConvertJsonElementToNativeObject(JsonElement element) {\n            switch (element.ValueKind) {\n                case JsonValueKind.Object:\n                    var dict = new Dictionary<string, object>();\n                    foreach (var prop in element.EnumerateObject())\n                        dict[prop.Name] = ConvertJsonElementToNativeObject(prop.Value)!;\n                    return dict;\n                case JsonValueKind.Array:\n                    var list = new List<object>();\n                    foreach (var item in element.EnumerateArray())\n                        list.Add(ConvertJsonElementToNativeObject(item)!);\n                    return list.ToArray();\n                case JsonValueKind.String:\n                    return element.GetString();\n                case JsonValueKind.Number:\n                    if (element.TryGetInt64(out var l)) return l;\n                    if (element.TryGetDouble(out var d)) return d;\n                    return element.GetRawText();\n                case JsonValueKind.True:\n                case JsonValueKind.False:\n                    return element.GetBoolean();\n                case JsonValueKind.Null:\n                default:\n                    return null;\n            }\n        }\n\n        /// <summary>\n        /// Retrieves mail messages for the specified user.\n        /// </summary>\n        public static async Task<List<Dictionary<string, object>>> GetMailMessagesAsync(GraphCredential credential, string userPrincipalName, IEnumerable<string>? properties = null, string? filter = null, int? limit = null, CancellationToken cancellationToken = default) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\", cancellationToken).ConfigureAwait(false);\n            headers[\"Authorization\"] = token ?? string.Empty;\n            var queryParams = new Dictionary<string, object>();\n            if (!string.IsNullOrWhiteSpace(filter)) queryParams[\"$filter\"] = filter!;\n            if (properties != null && properties.Any()) queryParams[\"$select\"] = string.Join(\",\", properties);\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages\", queryParams);\n            var messages = new List<Dictionary<string, object>>();\n            while (!string.IsNullOrEmpty(uri)) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var doc = await InvokeGraphApiAsync(\"GET\", uri!, headers, cancellationToken: cancellationToken).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                    foreach (var item in valueElement.EnumerateArray()) {\n                        cancellationToken.ThrowIfCancellationRequested();\n                        var native = ConvertJsonElementToNativeObject(item) as Dictionary<string, object>;\n                        if (native != null) {\n                            messages.Add(native);\n                            if (limit.HasValue && messages.Count >= limit.Value) {\n                                return messages;\n                            }\n                        }\n                    }\n                }\n                if (!doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var nextLinkElement)) {\n                    break;\n                }\n                var nextLink = nextLinkElement.GetString();\n                if (string.IsNullOrEmpty(nextLink)) {\n                    break;\n                }\n                uri = nextLink;\n            }\n            return messages;\n        }\n\n        /// <summary>\n        /// Retrieves attachments for a specific message.\n        /// </summary>\n        public static async Task<List<Attachment>> GetMailMessageAttachmentsAsync(GraphCredential credential, string userPrincipalName, string messageId, IEnumerable<string>? properties = null, CancellationToken cancellationToken = default) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\", cancellationToken).ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var queryParams = new Dictionary<string, object>();\n            if (properties != null && properties.Any()) queryParams[\"$select\"] = string.Join(\",\", properties);\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages/{messageId}/attachments\", queryParams);\n            var attachments = new List<Attachment>();\n            while (!string.IsNullOrEmpty(uri)) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var doc = await InvokeGraphApiAsync(\"GET\", uri!, headers, cancellationToken: cancellationToken).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                    foreach (var item in valueElement.EnumerateArray()) {\n                        cancellationToken.ThrowIfCancellationRequested();\n                        var att = JsonSerializer.Deserialize(item.GetRawText(), MailozaurrJsonContext.Default.Attachment);\n                        if (att != null) {\n                            attachments.Add(att);\n                        }\n                    }\n                }\n\n                if (!doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var nextLinkElement)) {\n                    break;\n                }\n                var nextLink = nextLinkElement.GetString();\n                if (string.IsNullOrEmpty(nextLink)) {\n                    break;\n                }\n                uri = nextLink;\n            }\n            return attachments;\n        }\n\n        /// <summary>\n        /// Lists mail folders for the specified user.\n        /// </summary>\n        public static async Task<List<JsonElement>> GetMailFoldersAsync(GraphCredential credential, string userPrincipalName, CancellationToken cancellationToken = default) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\", cancellationToken).ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders\");\n            var folders = new List<JsonElement>();\n            while (!string.IsNullOrEmpty(uri)) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var doc = await InvokeGraphApiAsync(\"GET\", uri!, headers, cancellationToken: cancellationToken).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                    foreach (var item in valueElement.EnumerateArray()) {\n                        cancellationToken.ThrowIfCancellationRequested();\n                        folders.Add(item);\n                    }\n                }\n\n                if (!doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var nextLinkElement)) {\n                    break;\n                }\n                var nextLink = nextLinkElement.GetString();\n                if (string.IsNullOrEmpty(nextLink)) {\n                    break;\n                }\n                uri = nextLink;\n            }\n            return folders;\n        }\n\n        /// <summary>\n        /// Saves the bodies of messages to disk as HTML files.\n        /// </summary>\n        public static void SaveMailMessages(IEnumerable<EmailGraphMessage> messages, string path) {\n            var resolvedPath = Path.GetFullPath(path);\n            if (!Directory.Exists(resolvedPath)) Directory.CreateDirectory(resolvedPath);\n            foreach (var m in messages) {\n                if (m?.Body is not null) {\n                    var randomFileName = Path.ChangeExtension(Path.GetRandomFileName(), \"html\");\n                    var filePath = Path.Combine(resolvedPath, randomFileName);\n                    try {\n                        var content = m.Body is JsonElement je && je.TryGetProperty(\"Content\", out var c) ? c.GetString() : m.Body.ToString();\n                        File.WriteAllText(filePath, content);\n                    } catch (IOException ex) {\n                        // Log or handle error\n                        LoggingMessages.Logger.WriteWarning($\"SaveMailMessage - Couldn't save file to {filePath}. Error: {ex.Message}\");\n                        LoggingMessages.Logger.WriteWarning($\"SaveMailMessage - Possible issue: Ensure the directory '{resolvedPath}' exists and you have write permissions.\");\n                    }\n                }\n            }\n        }\n\n        /// <summary>\n        /// Saves attachments to the specified directory.\n        /// </summary>\n        public static void SaveAttachments(IEnumerable<Attachment> attachments, string path) {\n            var resolvedPath = Path.GetFullPath(path);\n            if (!Directory.Exists(resolvedPath)) Directory.CreateDirectory(resolvedPath);\n            foreach (var att in attachments) {\n                if (!string.IsNullOrWhiteSpace(att.ContentBytes) && !string.IsNullOrWhiteSpace(att.Name)) {\n                    var filePath = Path.Combine(resolvedPath, att.Name);\n                    try {\n                        var bytes = Convert.FromBase64String(att.ContentBytes);\n                        File.WriteAllBytes(filePath, bytes);\n                    } catch (FormatException fex) {\n                        // Invalid Base64 content\n                        LoggingMessages.Logger.WriteWarning($\"SaveAttachment - Invalid base64 content for {att.Name}. Error: {fex.Message}\");\n                        LoggingMessages.Logger.WriteWarning($\"SaveAttachment - Possible issue: The attachment '{att.Name}' may be corrupted.\");\n                    } catch (IOException ex) {\n                        // Log or handle other errors\n                        LoggingMessages.Logger.WriteWarning($\"SaveAttachment - Couldn't save file to {filePath}. Error: {ex.Message}\");\n                        LoggingMessages.Logger.WriteWarning($\"SaveAttachment - Possible issue: Verify the path '{filePath}' exists and you have write permissions.\");\n                    }\n                }\n            }\n        }\n\n        /// <summary>\n        /// Executes a search query across one or more mailboxes.\n        /// </summary>\n        public static async Task<List<GraphMessageInfo>> SearchMailboxesAsync(\n            GraphCredential credential,\n            IEnumerable<string> userPrincipalNames,\n            string queryString,\n            int from = 0,\n            int size = 25) {\n            var mailboxList = userPrincipalNames as IList<string> ?? userPrincipalNames.ToList();\n            if (mailboxList.Count == 0) {\n                return new List<GraphMessageInfo>();\n            }\n\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n\n            var searchPayload = new GraphSearchPayload();\n            foreach (var upn in mailboxList) {\n                searchPayload.Requests.Add(new GraphSearchRequest {\n                    EntityTypes = new[] { \"message\" },\n                    From = from,\n                    Size = size,\n                    Query = new GraphSearchQuery { QueryString = queryString },\n                    UserScopes = new[] { upn }\n                });\n            }\n\n            var body = JsonSerializer.Serialize(searchPayload, MailozaurrJsonContext.Default.GraphSearchPayload);\n            var searchUri = BuildGraphUri(GraphEndpoint.V1, \"/search/query\");\n            var doc = await InvokeGraphApiAsync(\"POST\", searchUri, headers, body).ConfigureAwait(false);\n\n            var results = new List<GraphMessageInfo>();\n            int index = 0;\n            if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                foreach (var item in valueElement.EnumerateArray()) {\n                    if (index >= mailboxList.Count) {\n                        break;\n                    }\n\n                    var upn = mailboxList[index++];\n                    if (item.TryGetProperty(\"hitsContainers\", out var containers) && containers.ValueKind == JsonValueKind.Array) {\n                        foreach (var container in containers.EnumerateArray()) {\n                            if (container.TryGetProperty(\"hits\", out var hits) && hits.ValueKind == JsonValueKind.Array) {\n                                foreach (var hit in hits.EnumerateArray()) {\n                                    string? summary = null;\n                                    if (hit.TryGetProperty(\"summary\", out var sumEl)) summary = sumEl.GetString();\n                                    if (hit.TryGetProperty(\"resource\", out var res) && res.ValueKind == JsonValueKind.Object) {\n                                        var dict = ConvertJsonElementToNativeObject(res) as Dictionary<string, object>;\n                                        if (dict != null) {\n                                            results.Add(new GraphMessageInfo(dict, upn, summary));\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            return results;\n        }\n\n        /// <summary>\n        /// Performs an action on a mail message.\n        /// </summary>\n        public static async Task ExecuteMailMessageActionAsync(GraphCredential credential, string userPrincipalName, string messageId, GraphMessageAction action, string? destinationFolderId = null) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n\n            string method;\n            string uri;\n            string? body = null;\n\n            switch (action) {\n                case GraphMessageAction.Move:\n                    if (string.IsNullOrWhiteSpace(destinationFolderId)) throw new ArgumentNullException(nameof(destinationFolderId));\n                    method = \"POST\";\n                    uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages/{messageId}/move\");\n                    body = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = destinationFolderId }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n                    break;\n                case GraphMessageAction.Copy:\n                    if (string.IsNullOrWhiteSpace(destinationFolderId)) throw new ArgumentNullException(nameof(destinationFolderId));\n                    method = \"POST\";\n                    uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages/{messageId}/copy\");\n                    body = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = destinationFolderId }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n                    break;\n                case GraphMessageAction.Delete:\n                    method = \"DELETE\";\n                    uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages/{messageId}\");\n                    break;\n                default:\n                    throw new ArgumentOutOfRangeException(nameof(action), action, null);\n            }\n\n            await InvokeGraphApiAsync(method, uri, headers, body).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Moves a mail message to another folder.\n        /// </summary>\n        public static Task MoveMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, string destinationFolderId) =>\n            MoveMailMessageAsync(credential, userPrincipalName, messageId, destinationFolderId, dryRun: false);\n\n        /// <summary>\n        /// Moves a mail message to another folder, optionally simulating the change.\n        /// </summary>\n        public static async Task MoveMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, string destinationFolderId, bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            await ExecuteMailMessageActionAsync(credential, userPrincipalName, messageId, GraphMessageAction.Move, destinationFolderId).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Copies a mail message to another folder.\n        /// </summary>\n        public static async Task CopyMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, string destinationFolderId) {\n            await ExecuteMailMessageActionAsync(credential, userPrincipalName, messageId, GraphMessageAction.Copy, destinationFolderId).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Sets the read state for a mail message.\n        /// </summary>\n        public static Task SetMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, bool isRead) =>\n            SetMailMessageAsync(credential, userPrincipalName, messageId, isRead, dryRun: false);\n\n        /// <summary>\n        /// Sets the read state for a mail message, optionally simulating the change.\n        /// </summary>\n        public static async Task SetMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, bool isRead, bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages/{messageId}\");\n            var body = JsonSerializer.Serialize(new GraphMarkReadRequest { IsRead = isRead }, MailozaurrJsonContext.Default.GraphMarkReadRequest);\n            await InvokeGraphApiAsync(\"PATCH\", uri, headers, body).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Deletes a mail message.\n        /// </summary>\n        public static Task DeleteMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId) =>\n            DeleteMailMessageAsync(credential, userPrincipalName, messageId, dryRun: false);\n\n        /// <summary>\n        /// Deletes a mail message, optionally simulating the change.\n        /// </summary>\n        public static async Task DeleteMailMessageAsync(GraphCredential credential, string userPrincipalName, string messageId, bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            await ExecuteMailMessageActionAsync(credential, userPrincipalName, messageId, GraphMessageAction.Delete).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Retrieves the raw MIME content of a mail message.\n        /// </summary>\n        public static async Task<MimeMessage> GetMailMessageMimeAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string messageId,\n            CancellationToken cancellationToken = default) {\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\", cancellationToken).ConfigureAwait(false);\n            var request = new HttpRequestMessage(HttpMethod.Get, $\"https://graph.microsoft.com/v1.0/users/{userPrincipalName}/messages/{messageId}/$value\");\n            request.Headers.TryAddWithoutValidation(\"Authorization\", token);\n            await ConcurrencySemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);\n            try {\n                using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);\n                response.EnsureSuccessStatusCode();\n                using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);\n                return await MimeMessage.LoadAsync(stream, cancellationToken).ConfigureAwait(false);\n            } finally {\n                ConcurrencySemaphore.Release();\n            }\n        }\n\n        /// <summary>\n        /// Deletes all messages from the Junk Email folder.\n        /// </summary>\n        public static Task ClearJunkMailAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            IEnumerable<string>? skipIds = null,\n            IEnumerable<string>? skipFrom = null,\n            IEnumerable<string>? skipTo = null,\n            IEnumerable<string>? skipSubjectContains = null,\n            bool skipHasAttachment = false,\n            IEnumerable<string>? skipAttachmentExtension = null) =>\n            ClearJunkMailAsync(credential, userPrincipalName, dryRun: false, skipIds, skipFrom, skipTo, skipSubjectContains, skipHasAttachment, skipAttachmentExtension);\n\n        /// <summary>\n        /// Deletes all messages from the Junk Email folder, optionally simulating the change.\n        /// </summary>\n        public static async Task ClearJunkMailAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            bool dryRun,\n            IEnumerable<string>? skipIds = null,\n            IEnumerable<string>? skipFrom = null,\n            IEnumerable<string>? skipTo = null,\n            IEnumerable<string>? skipSubjectContains = null,\n            bool skipHasAttachment = false,\n            IEnumerable<string>? skipAttachmentExtension = null) {\n            if (dryRun) {\n                return;\n            }\n            var properties = new List<string> { \"id\" };\n            if (skipFrom != null) properties.Add(\"from\");\n            if (skipTo != null) properties.Add(\"toRecipients\");\n            if (skipSubjectContains != null) properties.Add(\"subject\");\n            if (skipHasAttachment || skipAttachmentExtension != null) properties.Add(\"hasAttachments\");\n\n            var messages = await GetJunkMailMessagesAsync(\n                credential,\n                userPrincipalName,\n                properties,\n                skipIds,\n                skipFrom,\n                skipTo,\n                skipSubjectContains,\n                skipHasAttachment,\n                skipAttachmentExtension).ConfigureAwait(false);\n\n            foreach (var msg in messages) {\n                var id = msg[\"id\"] as string;\n                if (string.IsNullOrWhiteSpace(id)) continue;\n                await DeleteMailMessageAsync(credential, userPrincipalName, id!).ConfigureAwait(false);\n            }\n        }\n\n        /// <summary>\n        /// Filters a collection of messages using provided skip criteria.\n        /// </summary>\n        /// <param name=\"messages\">Messages to filter.</param>\n        /// <param name=\"skipIds\">IDs of messages to exclude.</param>\n        /// <param name=\"skipFrom\">Sender addresses to exclude.</param>\n        /// <param name=\"skipTo\">Recipient addresses to exclude.</param>\n        /// <param name=\"skipSubjectContains\">Subject substrings to exclude.</param>\n        /// <param name=\"skipHasAttachment\">Exclude messages that have attachments.</param>\n        /// <returns>List of messages that are not considered junk.</returns>\n        public static List<Dictionary<string, object>> FilterJunkMessages(\n            IEnumerable<Dictionary<string, object>> messages,\n            IEnumerable<string>? skipIds = null,\n            IEnumerable<string>? skipFrom = null,\n            IEnumerable<string>? skipTo = null,\n            IEnumerable<string>? skipSubjectContains = null,\n            bool skipHasAttachment = false) {\n            var result = new List<Dictionary<string, object>>();\n            var skipIdsSet = skipIds != null ? new HashSet<string>(skipIds, StringComparer.OrdinalIgnoreCase) : null;\n            var skipFromSet = skipFrom != null ? new HashSet<string>(skipFrom, StringComparer.OrdinalIgnoreCase) : null;\n            var skipToSet = skipTo != null ? new HashSet<string>(skipTo, StringComparer.OrdinalIgnoreCase) : null;\n            var skipSubjectTokens = skipSubjectContains?\n                .Where(s => !string.IsNullOrWhiteSpace(s))\n                .ToArray();\n            foreach (var msg in messages) {\n                var id = msg.TryGetValue(\"id\", out var idObj) ? idObj as string : null;\n                if (!string.IsNullOrWhiteSpace(id) && skipIdsSet != null && skipIdsSet.Contains(id!)) {\n                    continue;\n                }\n\n                if (skipFromSet != null &&\n                    msg.TryGetValue(\"from\", out var fromObj) &&\n                    fromObj is Dictionary<string, object> fDict &&\n                    fDict.TryGetValue(\"emailAddress\", out var addrObj) &&\n                    addrObj is Dictionary<string, object> addr &&\n                    addr.TryGetValue(\"address\", out var fromAddrObj) &&\n                    fromAddrObj is string fromAddr &&\n                    skipFromSet.Contains(fromAddr)) {\n                    continue;\n                }\n\n                if (skipToSet != null &&\n                    msg.TryGetValue(\"toRecipients\", out var toObj) &&\n                    toObj is object[] arr &&\n                    arr.OfType<Dictionary<string, object>>().Any(rec =>\n                        rec.TryGetValue(\"emailAddress\", out var tAddrObj) &&\n                        tAddrObj is Dictionary<string, object> tAddr &&\n                        tAddr.TryGetValue(\"address\", out var addrVal) &&\n                        addrVal is string addrStr &&\n                        skipToSet.Contains(addrStr))) {\n                    continue;\n                }\n\n                if (skipSubjectTokens != null &&\n                    msg.TryGetValue(\"subject\", out var subjObj) &&\n                    subjObj is string subj &&\n                    skipSubjectTokens.Any(s => subj.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0)) {\n                    continue;\n                }\n\n                if (skipHasAttachment &&\n                    msg.TryGetValue(\"hasAttachments\", out var hasObj) &&\n                    hasObj is bool hasAtt && hasAtt) {\n                    continue;\n                }\n\n                result.Add(msg);\n            }\n            return result;\n        }\n\n        /// <summary>\n        /// Retrieves messages from the Junk Email folder.\n        /// </summary>\n        public static async Task<List<Dictionary<string, object>>> GetJunkMailMessagesAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            IEnumerable<string>? properties = null,\n            IEnumerable<string>? skipIds = null,\n            IEnumerable<string>? skipFrom = null,\n            IEnumerable<string>? skipTo = null,\n            IEnumerable<string>? skipSubjectContains = null,\n            bool skipHasAttachment = false,\n            IEnumerable<string>? skipAttachmentExtension = null) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var props = properties != null ? new List<string>(properties) : new List<string>();\n            HashSet<string>? skipAttachmentExtensions = null;\n            if (skipAttachmentExtension != null) {\n                skipAttachmentExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n                foreach (var ext in skipAttachmentExtension) {\n                    if (string.IsNullOrWhiteSpace(ext)) {\n                        continue;\n                    }\n\n                    var normalized = ext.Trim().TrimStart('.');\n                    if (normalized.Length > 0) {\n                        skipAttachmentExtensions.Add(normalized);\n                    }\n                }\n            }\n            if (skipHasAttachment || (skipAttachmentExtensions?.Count > 0)) {\n                if (!props.Contains(\"hasAttachments\")) props.Add(\"hasAttachments\");\n            }\n            var query = new Dictionary<string, object>();\n            if (props.Count > 0) query[\"$select\"] = string.Join(\",\", props);\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/junkemail/messages\", query);\n            var messages = new List<Dictionary<string, object>>();\n            while (!string.IsNullOrWhiteSpace(uri)) {\n                var doc = await InvokeGraphApiAsync(\"GET\", uri!, headers).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                    foreach (var item in valueElement.EnumerateArray()) {\n                        var native = ConvertJsonElementToNativeObject(item) as Dictionary<string, object>;\n                        if (native != null) messages.Add(native);\n                    }\n                }\n                uri = null;\n                if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next)) {\n                    uri = next.GetString();\n                }\n            }\n            if (skipIds != null || skipFrom != null || skipTo != null || skipSubjectContains != null || skipHasAttachment) {\n                messages = FilterJunkMessages(messages, skipIds, skipFrom, skipTo, skipSubjectContains, skipHasAttachment);\n            }\n\n            if (skipAttachmentExtensions?.Count > 0) {\n                var result = new List<Dictionary<string, object>>();\n                foreach (var msg in messages) {\n                    if (!msg.TryGetValue(\"id\", out var idObj) || idObj is not string id) continue;\n                    if (msg.TryGetValue(\"hasAttachments\", out var hasObj) && hasObj is bool hasAtt && hasAtt) {\n                        var atts = await GetMailMessageAttachmentsAsync(\n                            credential,\n                            userPrincipalName,\n                            id,\n                            new[] { \"name\" }).ConfigureAwait(false);\n                        if (atts.Any(att =>\n                                skipAttachmentExtensions.Contains(\n                                    System.IO.Path.GetExtension(att.Name ?? string.Empty).TrimStart('.')))) {\n                            continue;\n                        }\n                    }\n                    result.Add(msg);\n                }\n                messages = result;\n            }\n            return messages;\n        }\n\n        /// <summary>\n        /// Moves a mail folder to another location.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mail folder.</param>\n        /// <param name=\"folderId\">Identifier of the folder to move.</param>\n        /// <param name=\"destinationFolderId\">Identifier of the new parent folder.</param>\n        /// <returns>A task representing the asynchronous operation.</returns>\n        public static Task MoveFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId,\n            string destinationFolderId) =>\n            MoveFolderAsync(credential, userPrincipalName, folderId, destinationFolderId, dryRun: false);\n\n        /// <summary>\n        /// Moves a mail folder to another location, optionally simulating the change.\n        /// </summary>\n        public static async Task MoveFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId,\n            string destinationFolderId,\n            bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/{folderId}/move\");\n            var body = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = destinationFolderId }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n            await InvokeGraphApiAsync(\"POST\", uri, headers, body).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Renames a mail folder.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mail folder.</param>\n        /// <param name=\"folderId\">Identifier of the folder to rename.</param>\n        /// <param name=\"newDisplayName\">New display name for the folder.</param>\n        /// <returns>A task representing the asynchronous operation.</returns>\n        public static Task RenameFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId,\n            string newDisplayName) =>\n            RenameFolderAsync(credential, userPrincipalName, folderId, newDisplayName, dryRun: false);\n\n        /// <summary>\n        /// Renames a mail folder, optionally simulating the change.\n        /// </summary>\n        public static async Task RenameFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId,\n            string newDisplayName,\n            bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/{folderId}\");\n            var body = JsonSerializer.Serialize(new GraphFolderRenameRequest { DisplayName = newDisplayName }, MailozaurrJsonContext.Default.GraphFolderRenameRequest);\n            await InvokeGraphApiAsync(\"PATCH\", uri, headers, body).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Removes a mail folder.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mail folder.</param>\n        /// <param name=\"folderId\">Identifier of the folder to remove.</param>\n        /// <returns>A task representing the asynchronous operation.</returns>\n        public static Task RemoveFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId) =>\n            RemoveFolderAsync(credential, userPrincipalName, folderId, dryRun: false);\n\n        /// <summary>\n        /// Removes a mail folder, optionally simulating the change.\n        /// </summary>\n        public static async Task RemoveFolderAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string folderId,\n            bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/{folderId}\");\n            await InvokeGraphApiAsync(\"DELETE\", uri, headers).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Retrieves mailbox permissions for a user.\n        /// </summary>\n        public static async Task<List<GraphMailboxPermission>> GetMailboxPermissionsAsync(GraphCredential credential, string userPrincipalName) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/permissions\");\n            var doc = await InvokeGraphApiAsync(\"GET\", uri, headers).ConfigureAwait(false);\n            var result = new List<GraphMailboxPermission>();\n            if (doc.RootElement.TryGetProperty(\"value\", out var val) && val.ValueKind == JsonValueKind.Array) {\n                foreach (var item in val.EnumerateArray()) {\n                    var dict = ConvertJsonElementToNativeObject(item) as Dictionary<string, object>;\n                    if (dict != null) result.Add(new GraphMailboxPermission(dict, userPrincipalName));\n                }\n            }\n            return result;\n        }\n\n        /// <summary>\n        /// Adds a mailbox permission.\n        /// </summary>\n        public static Task AddMailboxPermissionAsync(GraphCredential credential, string userPrincipalName, string body) =>\n            AddMailboxPermissionAsync(credential, userPrincipalName, body, dryRun: false);\n\n        /// <summary>\n        /// Adds a mailbox permission, optionally simulating the change.\n        /// </summary>\n        public static async Task AddMailboxPermissionAsync(GraphCredential credential, string userPrincipalName, string body, bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/permissions\");\n            await InvokeGraphApiAsync(\"POST\", uri, headers, body).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Removes a mailbox permission.\n        /// </summary>\n        public static Task RemoveMailboxPermissionAsync(GraphCredential credential, string userPrincipalName, string permissionId) =>\n            RemoveMailboxPermissionAsync(credential, userPrincipalName, permissionId, dryRun: false);\n\n        /// <summary>\n        /// Removes a mailbox permission, optionally simulating the change.\n        /// </summary>\n        public static async Task RemoveMailboxPermissionAsync(GraphCredential credential, string userPrincipalName, string permissionId, bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/permissions/{permissionId}\");\n            await InvokeGraphApiAsync(\"DELETE\", uri, headers).ConfigureAwait(false);\n        }\n      \n        /// <summary>     \n        /// Retrieves aggregated mailbox statistics including message count and total attachment size.\n        /// </summary>\n        public static async Task<GraphMailboxStatistics> GetMailboxStatisticsAsync(\n            GraphCredential credential,\n            string userPrincipalName) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n\n            var folderQuery = new Dictionary<string, object> {\n                { \"$select\", \"id,displayName,wellKnownName,totalItemCount,unreadItemCount,childFolderCount\" },\n                { \"$top\", \"100\" }\n            };\n            var folderUri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders\", folderQuery);\n            int messageCount = 0;\n            int folderCount = 0;\n            var foldersStats = new List<GraphMailboxFolderStatistics>();\n            while (!string.IsNullOrWhiteSpace(folderUri)) {\n                var doc = await InvokeGraphApiAsync(\"GET\", folderUri!, headers).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var folders) && folders.ValueKind == JsonValueKind.Array) {\n                    foreach (var item in folders.EnumerateArray()) {\n                        var stat = new GraphMailboxFolderStatistics {\n                            Id = item.GetProperty(\"id\").GetString() ?? string.Empty,\n                            DisplayName = item.GetProperty(\"displayName\").GetString() ?? string.Empty,\n                            WellKnownName = item.TryGetProperty(\"wellKnownName\", out var wn) ? wn.GetString() : null,\n                            TotalItemCount = item.TryGetProperty(\"totalItemCount\", out var tic) && tic.TryGetInt32(out var c) ? c : 0,\n                            UnreadItemCount = item.TryGetProperty(\"unreadItemCount\", out var uic) && uic.TryGetInt32(out var u) ? u : 0,\n                            ChildFolderCount = item.TryGetProperty(\"childFolderCount\", out var cfc) && cfc.TryGetInt32(out var cf) ? cf : 0\n                        };\n                        messageCount += stat.TotalItemCount;\n                        folderCount++;\n                        foldersStats.Add(stat);\n                    }\n                }\n                folderUri = null;\n                if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var next)) {\n                    folderUri = next.GetString();\n                }\n            }\n\n            long attachmentSize = 0;\n            int messagesWithAttachments = 0;\n            var msgQuery = new Dictionary<string, object> {\n                { \"$select\", \"id,hasAttachments\" },\n                { \"$top\", \"50\" }\n            };\n            var msgUri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/messages\", msgQuery);\n            while (!string.IsNullOrWhiteSpace(msgUri)) {\n                var doc = await InvokeGraphApiAsync(\"GET\", msgUri!, headers).ConfigureAwait(false);\n                if (doc.RootElement.TryGetProperty(\"value\", out var msgs) && msgs.ValueKind == JsonValueKind.Array) {\n                    foreach (var msg in msgs.EnumerateArray()) {\n                        if (!msg.TryGetProperty(\"id\", out var idEl) || idEl.ValueKind != JsonValueKind.String) {\n                            continue;\n                        }\n                        var hasAtt = msg.TryGetProperty(\"hasAttachments\", out var ha) && ha.GetBoolean();\n                        if (!hasAtt) {\n                            continue;\n                        }\n                        messagesWithAttachments++;\n                        var id = idEl.GetString();\n                        if (string.IsNullOrWhiteSpace(id)) {\n                            continue;\n                        }\n                        var atts = await GetMailMessageAttachmentsAsync(\n                            credential,\n                            userPrincipalName,\n                            id!,\n                            new[] { \"size\" }).ConfigureAwait(false);\n                        foreach (var att in atts) {\n                            attachmentSize += att.Size;\n                        }\n                    }\n                }\n                msgUri = null;\n                if (doc.RootElement.TryGetProperty(\"@odata.nextLink\", out var nextMsg)) {\n                    msgUri = nextMsg.GetString();\n                }\n            }\n\n            var result = new GraphMailboxStatistics {\n                UserPrincipalName = userPrincipalName,\n                MessageCount = messageCount,\n                MessagesWithAttachments = messagesWithAttachments,\n                TotalAttachmentSize = attachmentSize,\n                TotalFolders = folderCount\n            };\n            result.FolderStatistics.AddRange(foldersStats);\n            return result;\n        }\n        \n        /// <summary>\n        /// Retrieves inbox rules for the specified user.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mailbox.</param>\n        /// <param name=\"filter\">Optional OData filter to apply to the query.</param>\n        /// <returns>List of inbox rules represented as dictionaries.</returns>\n        public static async Task<List<GraphInboxRule>> GetRulesAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string? filter = null) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            Dictionary<string, object>? qp = null;\n            if (!string.IsNullOrWhiteSpace(filter)) qp = new Dictionary<string, object> { [\"$filter\"] = filter! };\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/inbox/messageRules\", qp);\n            var doc = await InvokeGraphApiAsync(\"GET\", uri, headers).ConfigureAwait(false);\n            var rules = new List<GraphInboxRule>();\n            if (doc.RootElement.TryGetProperty(\"value\", out var valueElement) && valueElement.ValueKind == JsonValueKind.Array) {\n                foreach (var item in valueElement.EnumerateArray()) {\n                    var rule = JsonSerializer.Deserialize(item.GetRawText(), MailozaurrJsonContext.Default.GraphInboxRule);\n                    if (rule != null) rules.Add(rule);\n                }\n            }\n            return rules;\n        }\n\n        /// <summary>\n        /// Creates a new inbox rule.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mailbox.</param>\n        /// <param name=\"rule\">Dictionary describing the rule to create.</param>\n        /// <returns>The created rule as a dictionary.</returns>\n        public static Task<GraphInboxRule> NewRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            GraphInboxRule rule) =>\n            NewRuleAsync(credential, userPrincipalName, rule, dryRun: false);\n\n        /// <summary>\n        /// Creates a new inbox rule, optionally simulating the change.\n        /// </summary>\n        public static async Task<GraphInboxRule> NewRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            GraphInboxRule rule,\n            bool dryRun) {\n            if (dryRun) {\n                return rule;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var body = JsonSerializer.Serialize(rule, MailozaurrJsonContext.Default.GraphInboxRule);\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/inbox/messageRules\");\n            var doc = await InvokeGraphApiAsync(\"POST\", uri, headers, body).ConfigureAwait(false);\n            var created = JsonSerializer.Deserialize(doc.RootElement.GetRawText(), MailozaurrJsonContext.Default.GraphInboxRule);\n            if (created is null) {\n                throw new InvalidDataException(\"Microsoft Graph returned an invalid inbox rule response.\");\n            }\n            return created;\n        }\n\n        /// <summary>\n        /// Updates an existing inbox rule.\n        /// </summary>\n        public static Task<GraphInboxRule> UpdateRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string ruleId,\n            GraphInboxRule rule) =>\n            UpdateRuleAsync(credential, userPrincipalName, ruleId, rule, dryRun: false);\n\n        /// <summary>\n        /// Updates an existing inbox rule, optionally simulating the change.\n        /// </summary>\n        public static async Task<GraphInboxRule> UpdateRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string ruleId,\n            GraphInboxRule rule,\n            bool dryRun) {\n            if (dryRun) {\n                return rule;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var body = JsonSerializer.Serialize(rule, MailozaurrJsonContext.Default.GraphInboxRule);\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/inbox/messageRules/{ruleId}\");\n            var doc = await InvokeGraphApiAsync(\"PATCH\", uri, headers, body).ConfigureAwait(false);\n            var updated = JsonSerializer.Deserialize(doc.RootElement.GetRawText(), MailozaurrJsonContext.Default.GraphInboxRule);\n            if (updated is null) {\n                throw new InvalidDataException(\"Microsoft Graph returned an invalid inbox rule response.\");\n            }\n            return updated;\n        }\n\n        /// <summary>\n        /// Removes the specified inbox rule.\n        /// </summary>\n        /// <param name=\"credential\">Credential used to authenticate to Microsoft Graph.</param>\n        /// <param name=\"userPrincipalName\">User principal name owning the mailbox.</param>\n        /// <param name=\"ruleId\">Identifier of the rule to remove.</param>\n        /// <returns>A task representing the asynchronous operation.</returns>\n        public static Task RemoveRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string ruleId) =>\n            RemoveRuleAsync(credential, userPrincipalName, ruleId, dryRun: false);\n\n        /// <summary>\n        /// Removes the specified inbox rule, optionally simulating the change.\n        /// </summary>\n        public static async Task RemoveRuleAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string ruleId,\n            bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(GraphEndpoint.V1, $\"/users/{userPrincipalName}/mailFolders/inbox/messageRules/{ruleId}\");\n            await InvokeGraphApiAsync(\"DELETE\", uri, headers).ConfigureAwait(false);\n        }\n\n        /// <summary>\n        /// Retrieves calendar events for the specified user.\n        /// </summary>\n        public static async Task<List<Dictionary<string, object>>> GetEventsAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            IEnumerable<string>? properties = null,\n            string? filter = null,\n            int? limit = null) {\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var qp = new Dictionary<string, object>();\n            if (!string.IsNullOrWhiteSpace(filter)) qp[\"$filter\"] = filter!;\n            if (properties != null && properties.Any()) qp[\"$select\"] = string.Join(\",\", properties);\n            var uri = JoinUriQuery(\"https://graph.microsoft.com/v1.0\", $\"/users/{userPrincipalName}/events\", qp);\n            var doc = await InvokeGraphApiAsync(\"GET\", uri, headers).ConfigureAwait(false);\n            var events = new List<Dictionary<string, object>>();\n            if (doc.RootElement.TryGetProperty(\"value\", out var val) && val.ValueKind == JsonValueKind.Array) {\n                foreach (var item in val.EnumerateArray()) {\n                    if (limit.HasValue && events.Count >= limit.Value) break;\n                    var dict = ConvertJsonElementToNativeObject(item) as Dictionary<string, object>;\n                    if (dict != null) events.Add(dict);\n                }\n            }\n            return events;\n        }\n\n        /// <summary>\n        /// Creates a new calendar event.\n        /// </summary>\n        public static Task<GraphEvent> NewEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            GraphEvent ev) =>\n            NewEventAsync(credential, userPrincipalName, ev, dryRun: false);\n\n        /// <summary>\n        /// Creates a new calendar event, optionally simulating the change.\n        /// </summary>\n        public static async Task<GraphEvent> NewEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            GraphEvent ev,\n            bool dryRun) {\n            if (dryRun) {\n                return ev;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var body = JsonSerializer.Serialize(ev, MailozaurrJsonContext.Default.GraphEvent);\n            var uri = JoinUriQuery(\"https://graph.microsoft.com/v1.0\", $\"/users/{userPrincipalName}/events\");\n            var doc = await InvokeGraphApiAsync(\"POST\", uri, headers, body).ConfigureAwait(false);\n            var created = JsonSerializer.Deserialize(doc.RootElement.GetRawText(), MailozaurrJsonContext.Default.GraphEvent);\n            if (created is null) {\n                throw new InvalidDataException(\"Microsoft Graph returned an invalid event response.\");\n            }\n            return created;\n        }\n\n        /// <summary>\n        /// Updates an existing calendar event.\n        /// </summary>\n        public static Task<GraphEvent> UpdateEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string eventId,\n            GraphEvent ev) =>\n            UpdateEventAsync(credential, userPrincipalName, eventId, ev, dryRun: false);\n\n        /// <summary>\n        /// Updates an existing calendar event, optionally simulating the change.\n        /// </summary>\n        public static async Task<GraphEvent> UpdateEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string eventId,\n            GraphEvent ev,\n            bool dryRun) {\n            if (dryRun) {\n                return ev;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var body = JsonSerializer.Serialize(ev, MailozaurrJsonContext.Default.GraphEvent);\n            var uri = JoinUriQuery(\"https://graph.microsoft.com/v1.0\", $\"/users/{userPrincipalName}/events/{eventId}\");\n            var doc = await InvokeGraphApiAsync(\"PATCH\", uri, headers, body).ConfigureAwait(false);\n            var updated = JsonSerializer.Deserialize(doc.RootElement.GetRawText(), MailozaurrJsonContext.Default.GraphEvent);\n            if (updated is null) {\n                throw new InvalidDataException(\"Microsoft Graph returned an invalid event response.\");\n            }\n            return updated;\n        }\n\n        /// <summary>\n        /// Removes the specified calendar event.\n        /// </summary>\n        public static Task RemoveEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string eventId) =>\n            RemoveEventAsync(credential, userPrincipalName, eventId, dryRun: false);\n\n        /// <summary>\n        /// Removes the specified calendar event, optionally simulating the change.\n        /// </summary>\n        public static async Task RemoveEventAsync(\n            GraphCredential credential,\n            string userPrincipalName,\n            string eventId,\n            bool dryRun) {\n            if (dryRun) {\n                return;\n            }\n            var headers = new Dictionary<string, string>();\n            var token = await ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\").ConfigureAwait(false);\n            headers[\"Authorization\"] = token;\n            var uri = JoinUriQuery(\"https://graph.microsoft.com/v1.0\", $\"/users/{userPrincipalName}/events/{eventId}\");\n            await InvokeGraphApiAsync(\"DELETE\", uri, headers).ConfigureAwait(false);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MimeKitUtils.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\nusing MimeKit;\nusing System.Collections.Generic;\nusing System.IO;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Utility methods for working with <see cref=\"MimeKit\"/> objects.\n/// </summary>\npublic static class MimeKitUtils {\n    /// <summary>\n    /// Saves the provided MIME attachments to the specified directory.\n    /// </summary>\n    /// <param name=\"attachments\">Collection of MIME entities representing attachments.</param>\n    /// <param name=\"path\">Directory path where attachments should be saved.</param>\n    public static void SaveAttachments(IEnumerable<MimeEntity> attachments, string path) {\n        var resolved = Path.GetFullPath(path);\n        if (!Directory.Exists(resolved)) Directory.CreateDirectory(resolved);\n        foreach (var attachment in attachments) {\n            if (attachment is MimePart mp) {\n                var file = Path.Combine(resolved, mp.FileName ?? Path.GetRandomFileName());\n                using var fs = File.Create(file);\n                if (mp.Content != null) {\n                    mp.Content.DecodeTo(fs);\n                } else {\n                    mp.WriteTo(fs);\n                }\n            } else if (attachment is MessagePart msgPart) {\n                var name = msgPart.ContentDisposition?.FileName ?? msgPart.ContentType.Name ?? Path.GetRandomFileName();\n                var file = Path.Combine(resolved, name);\n                if (msgPart.Message != null) {\n                    msgPart.Message.WriteTo(file);\n                } else {\n                    msgPart.WriteTo(file);\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// Attempts to extract all <see cref=\"NonDeliveryReport\"/> instances from a message.\n    /// </summary>\n    public static IList<NonDeliveryReport> GetNonDeliveryReports(MimeMessage message) {\n        var reports = new List<NonDeliveryReport>();\n        if (message == null) {\n            return reports;\n        }\n\n        var status = FindDeliveryStatus(message.Body);\n        if (status == null && !SubjectIndicatesNdr(message.Subject)) {\n            return reports;\n        }\n\n        if (status != null) {\n            foreach (var group in status.StatusGroups) {\n                var headers = new Dictionary<string, string>(System.StringComparer.OrdinalIgnoreCase);\n                foreach (var header in group) {\n                    headers.TryAdd(header.Field, header.Value);\n                }\n\n                var report = headers.Count > 0 ? NonDeliveryReport.FromHeaders(headers) : new NonDeliveryReport();\n                if (report.Timestamp == System.DateTimeOffset.MinValue) {\n                    report.Timestamp = message.Date;\n                }\n                reports.Add(report);\n            }\n        } else {\n            reports.Add(new NonDeliveryReport { Timestamp = message.Date });\n        }\n\n        return reports;\n    }\n\n    /// <summary>Attempts to extract the first <see cref=\"NonDeliveryReport\"/> from a message.</summary>\n    public static NonDeliveryReport? GetNonDeliveryReport(MimeMessage message) {\n        var reports = GetNonDeliveryReports(message);\n        return reports.Count > 0 ? reports[0] : null;\n    }\n\n    private static MessageDeliveryStatus? FindDeliveryStatus(MimeEntity? entity) {\n        if (entity == null) {\n            return null;\n        }\n\n        if (entity is MessageDeliveryStatus mds) {\n            return mds;\n        }\n        if (entity is Multipart multipart) {\n            foreach (var part in multipart) {\n                var found = FindDeliveryStatus(part);\n                if (found != null) {\n                    return found;\n                }\n            }\n        }\n        return null;\n    }\n\n    private static bool SubjectIndicatesNdr(string? subject) {\n        if (string.IsNullOrWhiteSpace(subject)) {\n            return false;\n        }\n        foreach (var p in NonDeliveryReportSubjectPatterns.Values) {\n            if (subject!.IndexOf(p, System.StringComparison.OrdinalIgnoreCase) >= 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /// <summary>Determines the encryption or signing type of a message.</summary>\n    public static EmailEncryption GetEncryption(MimeMessage message) {\n        if (message.Body is MultipartEncrypted encrypted) {\n            var protocol = encrypted.ContentType.Parameters[\"protocol\"];\n            if (!string.IsNullOrEmpty(protocol) && protocol!.Equals(\"application/pgp-encrypted\", System.StringComparison.OrdinalIgnoreCase)) {\n                return EmailEncryption.PgpEncrypted;\n            }\n        }\n\n        if (message.Body is ApplicationPkcs7Mime pkcs7 && pkcs7.SecureMimeType == SecureMimeType.EnvelopedData) {\n            return EmailEncryption.SmimeEncrypted;\n        }\n\n        if (message.Body is MultipartSigned signed) {\n            var mimeType = signed.Count > 1 ? signed[1].ContentType?.MimeType : null;\n            if (string.Equals(mimeType, \"application/pgp-signature\", System.StringComparison.OrdinalIgnoreCase)) {\n                return EmailEncryption.PgpSigned;\n            }\n            if (string.Equals(mimeType, \"application/pkcs7-signature\", System.StringComparison.OrdinalIgnoreCase) ||\n                string.Equals(mimeType, \"application/x-pkcs7-signature\", System.StringComparison.OrdinalIgnoreCase)) {\n                return EmailEncryption.SmimeSigned;\n            }\n        }\n\n        return EmailEncryption.None;\n    }\n\n    /// <summary>Decrypts a PGP encrypted message using the specified private key.</summary>\n    public static MimeMessage DecryptPgp(MimeMessage message, string privateKeyPath, string password) {\n        if (GetEncryption(message) != EmailEncryption.PgpEncrypted) return message;\n        using var ctx = new EphemeralOpenPgpContext(password);\n        using (var sec = File.OpenRead(privateKeyPath))\n            ctx.Import(new Org.BouncyCastle.Bcpg.OpenPgp.PgpSecretKeyRingBundle(new Org.BouncyCastle.Bcpg.ArmoredInputStream(sec)));\n        if (message.Body is not MultipartEncrypted encrypted) {\n            return message;\n        }\n\n        var decrypted = encrypted.Decrypt(ctx);\n        var result = new MimeMessage(message.Headers);\n        result.Body = decrypted;\n        return result;\n    }\n\n    /// <summary>Decrypts an S/MIME encrypted message using the provided certificate.</summary>\n    public static MimeMessage DecryptSmime(MimeMessage message, System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) {\n        if (GetEncryption(message) != EmailEncryption.SmimeEncrypted) return message;\n        using var ctx = new MimeKit.Cryptography.TemporarySecureMimeContext();\n        ctx.Import(certificate);\n        if (message.Body is not ApplicationPkcs7Mime pkcs7) {\n            return message;\n        }\n\n        var decrypted = pkcs7.Decrypt(ctx);\n        var result = new MimeMessage(message.Headers);\n        result.Body = decrypted;\n        return result;\n    }\n\n    /// <summary>Verifies a PGP signed message using the given public key.</summary>\n    public static bool VerifyPgpSignature(MimeMessage message, string publicKeyPath) {\n        if (GetEncryption(message) != EmailEncryption.PgpSigned) return false;\n        using var ctx = new EphemeralOpenPgpContext();\n        using (var pub = File.OpenRead(publicKeyPath))\n            ctx.Import(pub);\n        if (message.Body is not MultipartSigned signed) {\n            return false;\n        }\n\n        var signatures = signed.Verify(ctx);\n        foreach (var sig in signatures) sig.Verify();\n        return true;\n    }\n\n    /// <summary>Verifies an S/MIME signed message.</summary>\n    public static bool VerifySmimeSignature(MimeMessage message, params System.Security.Cryptography.X509Certificates.X509Certificate2[] certificates) {\n        if (GetEncryption(message) != EmailEncryption.SmimeSigned) return false;\n        using var ctx = new MimeKit.Cryptography.TemporarySecureMimeContext();\n        foreach (var cert in certificates) ctx.Import(cert);\n        if (message.Body is not MultipartSigned signed) {\n            return false;\n        }\n\n        var signatures = signed.Verify(ctx);\n        foreach (var sig in signatures) sig.Verify(true);\n        return true;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/MimeMessageContent.cs",
    "content": "namespace Mailozaurr;\n\nusing MimeKit;\n\n/// <summary>\n/// Represents text and HTML bodies extracted from a MIME message.\n/// </summary>\n/// <remarks>\n/// Useful when converting between different message formats or when\n/// sanitizing HTML before sending.\n/// </remarks>\npublic class MimeMessageContent {\n    /// <summary>Creates a new instance from the specified message.</summary>\n    /// <param name=\"message\">Source MIME message.</param>\n    public MimeMessageContent(MimeMessage message) {\n        TextBody = message.TextBody;\n        HtmlBody = message.HtmlBody;\n    }\n\n    /// <summary>Plain text body.</summary>\n    public string? TextBody { get; }\n\n    /// <summary>HTML body.</summary>\n    public string? HtmlBody { get; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NativeMailboxBrowserSessions.cs",
    "content": "using System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Owns a <see cref=\"GraphApiClient\"/> instance and exposes a <see cref=\"GraphMailboxBrowser\"/> built on top of it.\n/// </summary>\npublic sealed class GraphMailboxBrowserSession : IDisposable {\n    private readonly GraphApiClient _graph;\n\n    /// <summary>\n    /// Initializes a new session with an internally managed <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <param name=\"credential\">OAuth credential used for Graph requests.</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Graph API base address.</param>\n    public GraphMailboxBrowserSession(\n        OAuthCredential credential,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        _graph = new GraphApiClient(credential, refreshToken, baseAddress);\n        Browser = new GraphMailboxBrowser(_graph);\n    }\n\n    /// <summary>\n    /// Initializes a new session with an externally provided <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <param name=\"client\">HTTP client to use for Graph requests.</param>\n    /// <param name=\"credential\">OAuth credential used for Graph requests.</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Graph API base address used when <paramref name=\"client\"/> has no base address configured.</param>\n    public GraphMailboxBrowserSession(\n        HttpClient client,\n        OAuthCredential credential,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        _graph = new GraphApiClient(client, refreshToken, credential, baseAddress);\n        Browser = new GraphMailboxBrowser(_graph);\n    }\n\n    /// <summary>\n    /// Gets the high-level mailbox browser.\n    /// </summary>\n    public GraphMailboxBrowser Browser { get; }\n\n    /// <summary>\n    /// Creates a session from a raw OAuth access token.\n    /// </summary>\n    /// <param name=\"accessToken\">OAuth access token.</param>\n    /// <param name=\"client\">Optional HTTP client instance.</param>\n    /// <param name=\"userName\">Credential user name (defaults to <c>me</c>).</param>\n    /// <param name=\"expiresOn\">Credential expiration time (defaults to <see cref=\"DateTimeOffset.MaxValue\"/>).</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Graph API base address.</param>\n    /// <returns>A new <see cref=\"GraphMailboxBrowserSession\"/>.</returns>\n    public static GraphMailboxBrowserSession CreateWithAccessToken(\n        string accessToken,\n        HttpClient? client = null,\n        string userName = \"me\",\n        DateTimeOffset? expiresOn = null,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (string.IsNullOrWhiteSpace(accessToken)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(accessToken));\n        }\n\n        var credential = new OAuthCredential {\n            UserName = string.IsNullOrWhiteSpace(userName) ? \"me\" : userName.Trim(),\n            AccessToken = accessToken.Trim(),\n            ExpiresOn = expiresOn ?? DateTimeOffset.MaxValue\n        };\n\n        return client == null\n            ? new GraphMailboxBrowserSession(credential, refreshToken, baseAddress)\n            : new GraphMailboxBrowserSession(client, credential, refreshToken, baseAddress);\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        _graph.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n\n/// <summary>\n/// Owns a <see cref=\"GmailApiClient\"/> instance and exposes a <see cref=\"GmailMailboxBrowser\"/> built on top of it.\n/// </summary>\npublic sealed class GmailMailboxBrowserSession : IDisposable {\n    private readonly GmailApiClient _gmail;\n\n    /// <summary>\n    /// Initializes a new session with an internally managed <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <param name=\"credential\">OAuth credential used for Gmail requests.</param>\n    /// <param name=\"userId\">Mailbox user id (defaults to <c>me</c>).</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Gmail API base address.</param>\n    public GmailMailboxBrowserSession(\n        OAuthCredential credential,\n        string userId = \"me\",\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        _gmail = baseAddress == null\n            ? new GmailApiClient(credential, refreshToken)\n            : new GmailApiClient(new HttpClient(), refreshToken, credential, baseAddress, ownsHttpClient: true);\n        Browser = new GmailMailboxBrowser(_gmail, userId);\n    }\n\n    /// <summary>\n    /// Initializes a new session with an externally provided <see cref=\"HttpClient\"/>.\n    /// </summary>\n    /// <param name=\"client\">HTTP client to use for Gmail requests.</param>\n    /// <param name=\"credential\">OAuth credential used for Gmail requests.</param>\n    /// <param name=\"userId\">Mailbox user id (defaults to <c>me</c>).</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Gmail API base address used when <paramref name=\"client\"/> has no base address configured.</param>\n    public GmailMailboxBrowserSession(\n        HttpClient client,\n        OAuthCredential credential,\n        string userId = \"me\",\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (credential == null) {\n            throw new ArgumentNullException(nameof(credential));\n        }\n\n        _gmail = new GmailApiClient(client, refreshToken, credential, baseAddress);\n        Browser = new GmailMailboxBrowser(_gmail, userId);\n    }\n\n    /// <summary>\n    /// Gets the high-level mailbox browser.\n    /// </summary>\n    public GmailMailboxBrowser Browser { get; }\n\n    /// <summary>\n    /// Creates a session from a raw OAuth access token.\n    /// </summary>\n    /// <param name=\"accessToken\">OAuth access token.</param>\n    /// <param name=\"client\">Optional HTTP client instance.</param>\n    /// <param name=\"userId\">Mailbox user id (defaults to <c>me</c>).</param>\n    /// <param name=\"expiresOn\">Credential expiration time (defaults to <see cref=\"DateTimeOffset.MaxValue\"/>).</param>\n    /// <param name=\"refreshToken\">Optional token refresh delegate.</param>\n    /// <param name=\"baseAddress\">Optional Gmail API base address.</param>\n    /// <returns>A new <see cref=\"GmailMailboxBrowserSession\"/>.</returns>\n    public static GmailMailboxBrowserSession CreateWithAccessToken(\n        string accessToken,\n        HttpClient? client = null,\n        string userId = \"me\",\n        DateTimeOffset? expiresOn = null,\n        Func<CancellationToken, Task<string>>? refreshToken = null,\n        Uri? baseAddress = null) {\n        if (string.IsNullOrWhiteSpace(accessToken)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(accessToken));\n        }\n\n        var normalizedUserId = string.IsNullOrWhiteSpace(userId) ? \"me\" : userId.Trim();\n        var credential = new OAuthCredential {\n            UserName = normalizedUserId,\n            AccessToken = accessToken.Trim(),\n            ExpiresOn = expiresOn ?? DateTimeOffset.MaxValue\n        };\n\n        return client == null\n            ? new GmailMailboxBrowserSession(credential, normalizedUserId, refreshToken, baseAddress)\n            : new GmailMailboxBrowserSession(client, credential, normalizedUserId, refreshToken, baseAddress);\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        _gmail.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NativeMailboxThreadingMetadataOperations.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Shared normalization/retrieval helpers for native-provider threading metadata.\n/// </summary>\npublic static class NativeMailboxThreadingMetadataOperations {\n    /// <summary>\n    /// Provider-agnostic threading metadata shape.\n    /// </summary>\n    public sealed class NativeMailboxThreadingMetadataResult {\n        /// <summary>Normalized RFC822 Message-Id.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Reply-To header value.</summary>\n        public string? ReplyTo { get; set; }\n\n        /// <summary>Cc header value.</summary>\n        public string? Cc { get; set; }\n\n        /// <summary>Normalized RFC822 In-Reply-To value.</summary>\n        public string? InReplyTo { get; set; }\n\n        /// <summary>Normalized RFC822 References tokens.</summary>\n        public List<string>? References { get; set; }\n    }\n\n    /// <summary>\n    /// Reads and normalizes Graph threading metadata for a message.\n    /// </summary>\n    public static async Task<NativeMailboxThreadingMetadataResult> GetGraphThreadingMetadataAsync(\n        GraphMailboxBrowser browser,\n        string nativeId,\n        int maxMimeBytes = GraphMailboxBrowser.DefaultThreadingMetadataMaxMimeBytes,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n        if (string.IsNullOrWhiteSpace(nativeId)) {\n            throw new ArgumentException(\"nativeId is required.\", nameof(nativeId));\n        }\n\n        var metadata = await browser.GetThreadingMetadataAsync(\n            nativeId.Trim(),\n            maxMimeBytes: maxMimeBytes,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        return Normalize(metadata);\n    }\n\n    /// <summary>\n    /// Reads and normalizes Gmail threading metadata for a message.\n    /// </summary>\n    public static async Task<NativeMailboxThreadingMetadataResult> GetGmailThreadingMetadataAsync(\n        GmailMailboxBrowser browser,\n        string nativeId,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n        if (string.IsNullOrWhiteSpace(nativeId)) {\n            throw new ArgumentException(\"nativeId is required.\", nameof(nativeId));\n        }\n\n        var metadata = await browser.GetThreadingMetadataAsync(\n            nativeId.Trim(),\n            cancellationToken).ConfigureAwait(false);\n        return Normalize(metadata);\n    }\n\n    /// <summary>\n    /// Normalizes provider-agnostic threading metadata values.\n    /// </summary>\n    public static NativeMailboxThreadingMetadataResult Normalize(\n        string? messageId,\n        string? replyTo,\n        string? cc,\n        string? inReplyTo,\n        IEnumerable<string>? references) {\n        var normalizedReferences = NormalizeMessageIdList(references);\n        return new NativeMailboxThreadingMetadataResult {\n            MessageId = ImapSentMessageOperations.NormalizeMessageIdToken(messageId),\n            ReplyTo = NormalizeOptional(replyTo),\n            Cc = NormalizeOptional(cc),\n            InReplyTo = ImapSentMessageOperations.NormalizeMessageIdToken(inReplyTo),\n            References = normalizedReferences\n        };\n    }\n\n    /// <summary>\n    /// Normalizes Graph threading metadata values.\n    /// </summary>\n    public static NativeMailboxThreadingMetadataResult Normalize(\n        GraphMailboxBrowser.GraphMailboxThreadingMetadataResult metadata) {\n        if (metadata == null) {\n            throw new ArgumentNullException(nameof(metadata));\n        }\n\n        return Normalize(\n            metadata.MessageId,\n            metadata.ReplyTo,\n            metadata.Cc,\n            metadata.InReplyTo,\n            metadata.References);\n    }\n\n    /// <summary>\n    /// Normalizes Gmail threading metadata values.\n    /// </summary>\n    public static NativeMailboxThreadingMetadataResult Normalize(\n        GmailMailboxBrowser.GmailMailboxThreadingMetadataResult metadata) {\n        if (metadata == null) {\n            throw new ArgumentNullException(nameof(metadata));\n        }\n\n        return Normalize(\n            metadata.MessageId,\n            metadata.ReplyTo,\n            metadata.Cc,\n            metadata.InReplyTo,\n            metadata.References);\n    }\n\n    private static List<string>? NormalizeMessageIdList(IEnumerable<string>? references) {\n        if (references == null) {\n            return null;\n        }\n\n        var output = new List<string>();\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var reference in references) {\n            var normalized = ImapSentMessageOperations.NormalizeMessageIdToken(reference);\n            if (normalized == null || !seen.Add(normalized)) {\n                continue;\n            }\n            output.Add(normalized);\n        }\n\n        return output.Count == 0 ? null : output;\n    }\n\n    private static string? NormalizeOptional(string? value) {\n        var trimmed = (value ?? string.Empty).Trim();\n        return trimmed.Length == 0 ? null : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/DsnDiagnosticCode.cs",
    "content": "namespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Represents the diagnostic code from a Delivery Status Notification.\n/// </summary>\npublic sealed class DsnDiagnosticCode {\n    /// <summary>The diagnostic code type, e.g. \"smtp\".</summary>\n    public string Type { get; }\n\n    /// <summary>The textual diagnostic information.</summary>\n    public string Text { get; }\n\n    private DsnDiagnosticCode(string type, string text) {\n        Type = type;\n        Text = text;\n    }\n\n    /// <summary>Parses a diagnostic code string in the form \"smtp; 550 5.1.1\".</summary>\n    public static DsnDiagnosticCode Parse(string value) {\n        var parts = value.Split(new[] { ';' }, 2);\n        var type = parts[0].Trim();\n        var text = parts.Length > 1 ? parts[1].Trim() : string.Empty;\n        return new DsnDiagnosticCode(type, text);\n    }\n\n    /// <inheritdoc />\n    public override string ToString() => string.IsNullOrEmpty(Text) ? Type : $\"{Type}; {Text}\";\n}"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/DsnStatus.cs",
    "content": "namespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Represents a Delivery Status Notification status code.\n/// </summary>\npublic sealed class DsnStatus {\n    /// <summary>Classification of the DSN status code.</summary>\n    public DsnStatusClass Class { get; }\n\n    /// <summary>Subject subcode of the DSN status.</summary>\n    public int Subject { get; }\n\n    /// <summary>Detail subcode of the DSN status.</summary>\n    public int Detail { get; }\n\n    private DsnStatus(DsnStatusClass statusClass, int subject, int detail) {\n        Class = statusClass;\n        Subject = subject;\n        Detail = detail;\n    }\n\n    /// <summary>Parses a DSN status string (e.g. \"5.1.1\").</summary>\n    public static DsnStatus Parse(string value) {\n        if (!TryParse(value, out var status)) {\n            throw new FormatException($\"Invalid DSN status '{value}'.\");\n        }\n        return status;\n    }\n\n    /// <summary>Attempts to parse a DSN status string.</summary>\n    public static bool TryParse(string? value, out DsnStatus status) {\n        status = null!;\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n        var parts = value!.Trim().Split('.');\n        if (parts.Length != 3 || !int.TryParse(parts[0], out var cls) || !int.TryParse(parts[1], out var subj) || !int.TryParse(parts[2], out var detail)) {\n            return false;\n        }\n        if (!Enum.IsDefined(typeof(DsnStatusClass), cls)) {\n            return false;\n        }\n        status = new DsnStatus((DsnStatusClass)cls, subj, detail);\n        return true;\n    }\n\n    /// <inheritdoc />\n    public override string ToString() => $\"{(int)Class}.{Subject}.{Detail}\";\n}"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/DsnStatusClass.cs",
    "content": "namespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Top level DSN status classes as defined in RFC 3463.\n/// </summary>\npublic enum DsnStatusClass {\n    /// <summary>Success (2.x.x).</summary>\n    Success = 2,\n    /// <summary>Persistent transient failure (4.x.x).</summary>\n    PersistentTransientFailure = 4,\n    /// <summary>Permanent failure (5.x.x).</summary>\n    PermanentFailure = 5\n}"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/GmailNonDeliveryReportService.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Retrieves non-delivery reports using the Gmail API.\n/// </summary>\npublic sealed class GmailNonDeliveryReportService : NonDeliveryReportServiceBase {\n    private readonly GmailApiClient client;\n    private readonly string userId;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailNonDeliveryReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">Gmail API client used to access the mailbox.</param>\n    /// <param name=\"userId\">Gmail account identifier.</param>\n    /// <param name=\"resolver\">Resolver used to match reports with sent messages.</param>\n    public GmailNonDeliveryReportService(GmailApiClient client, string userId, SendLogResolver resolver) : base(resolver) {\n        this.client = client;\n        this.userId = userId;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<NonDeliveryReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchNonDeliveryReportsAsync(client, userId, since, before, recipientContains, messageId, maxResults, cancellationToken: cancellationToken);\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/GraphNonDeliveryReportService.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Retrieves non-delivery reports using the Microsoft Graph API.\n/// </summary>\npublic sealed class GraphNonDeliveryReportService : NonDeliveryReportServiceBase {\n    private readonly GraphCredential credential;\n    private readonly string userPrincipalName;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphNonDeliveryReportService\"/> class.\n    /// </summary>\n    /// <param name=\"credential\">Graph authentication credential.</param>\n    /// <param name=\"userPrincipalName\">User principal name to search the mailbox for.</param>\n    /// <param name=\"resolver\">Resolver used to match reports with sent messages.</param>\n    public GraphNonDeliveryReportService(GraphCredential credential, string userPrincipalName, SendLogResolver resolver) : base(resolver) {\n        this.credential = credential;\n        this.userPrincipalName = userPrincipalName;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<NonDeliveryReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchNonDeliveryReportsAsync(credential, userPrincipalName, since, before, recipientContains, messageId, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/ImapNonDeliveryReportService.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Retrieves non-delivery reports from an IMAP mailbox.\n/// </summary>\npublic sealed class ImapNonDeliveryReportService : NonDeliveryReportServiceBase {\n    private readonly ImapClient client;\n    private readonly string? folder;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ImapNonDeliveryReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">IMAP client used to access the mailbox.</param>\n    /// <param name=\"resolver\">Resolver used to match reports with sent messages.</param>\n    /// <param name=\"folder\">Optional folder name to search within.</param>\n    public ImapNonDeliveryReportService(ImapClient client, SendLogResolver resolver, string? folder = null) : base(resolver) {\n        this.client = client;\n        this.folder = folder;\n    }\n\n    /// <inheritdoc />\n    protected override Task<IList<NonDeliveryReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchNonDeliveryReportsAsync(client, folder, since, before, recipientContains, messageId, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/NonDeliveryReport.cs",
    "content": "using MimeKit.Utils;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Represents a parsed Non-Delivery Report (Delivery Status Notification).\n/// </summary>\npublic sealed class NonDeliveryReport {\n    /// <summary>Original recipient as specified in the NDR.</summary>\n    public string? OriginalRecipient { get; set; }\n\n    /// <summary>Normalized address extracted from <see cref=\"OriginalRecipient\"/>.</summary>\n    public string? OriginalRecipientAddress { get; set; }\n\n    /// <summary>Final recipient that the report refers to.</summary>\n    public string? FinalRecipient { get; set; }\n\n    /// <summary>Normalized address extracted from <see cref=\"FinalRecipient\"/>.</summary>\n    public string? FinalRecipientAddress { get; set; }\n\n    /// <summary>Identifier of the original message associated with this report.</summary>\n    public string? OriginalMessageId { get; set; }\n\n    /// <summary>Reporting mail transfer agent.</summary>\n    public string? ReportingMta { get; set; }\n\n    /// <summary>Action taken by the server for the delivery attempt.</summary>\n    public string? Action { get; set; }\n\n    /// <summary>Remote mail transfer agent involved in the delivery.</summary>\n    public string? RemoteMta { get; set; }\n\n    /// <summary>Date of the last delivery attempt.</summary>\n    public DateTimeOffset? LastAttemptDate { get; set; }\n\n    /// <summary>Identifier of the final log entry.</summary>\n    public string? FinalLogId { get; set; }\n\n    /// <summary>Diagnostic code explaining the failure.</summary>\n    public DsnDiagnosticCode? DiagnosticCode { get; set; }\n\n    /// <summary>Status code for the delivery attempt.</summary>\n    public DsnStatus? Status { get; set; }\n\n    /// <summary>The time the message originally arrived at the reporting MTA.</summary>\n    public DateTimeOffset Timestamp { get; set; }\n\n    /// <summary>Determines the NDR type derived from the status code.</summary>\n    public NonDeliveryReportType Type => GetReportType(Status);\n\n    /// <summary>Creates a <see cref=\"NonDeliveryReport\"/> instance from DSN headers.</summary>\n    public static NonDeliveryReport FromHeaders(IDictionary<string, string> headers) {\n        headers.TryGetValue(\"Original-Recipient\", out var originalRecipient);\n        headers.TryGetValue(\"Final-Recipient\", out var finalRecipient);\n        headers.TryGetValue(\"Reporting-MTA\", out var reportingMta);\n        headers.TryGetValue(\"Action\", out var action);\n        headers.TryGetValue(\"Remote-MTA\", out var remoteMta);\n        headers.TryGetValue(\"Last-Attempt-Date\", out var lastAttemptDate);\n        headers.TryGetValue(\"Final-Log-ID\", out var finalLogId);\n        headers.TryGetValue(\"Original-Message-ID\", out var originalMessageId);\n        headers.TryGetValue(\"Diagnostic-Code\", out var diagnosticCode);\n        headers.TryGetValue(\"Status\", out var statusCode);\n        headers.TryGetValue(\"Arrival-Date\", out var arrivalDate);\n\n        var ndr = new NonDeliveryReport {\n            OriginalRecipient = originalRecipient,\n            OriginalRecipientAddress = ExtractAddress(originalRecipient),\n            FinalRecipient = finalRecipient,\n            FinalRecipientAddress = ExtractAddress(finalRecipient),\n            OriginalMessageId = NormalizeMessageId(originalMessageId),\n            ReportingMta = reportingMta,\n            Action = action,\n            RemoteMta = remoteMta,\n            LastAttemptDate = TryParseTimestamp(lastAttemptDate),\n            FinalLogId = finalLogId,\n            DiagnosticCode = diagnosticCode is not null ? DsnDiagnosticCode.Parse(diagnosticCode) : null,\n            Status = statusCode is not null && DsnStatus.TryParse(statusCode, out var status) ? status : null,\n            Timestamp = TryParseTimestamp(arrivalDate) ?? TryParseTimestamp(lastAttemptDate) ?? DateTimeOffset.MinValue\n        };\n        return ndr;\n    }\n\n    private static string? ExtractAddress(string? header) {\n        if (string.IsNullOrWhiteSpace(header)) {\n            return null;\n        }\n        var idx = header!.IndexOf(';');\n        return idx >= 0 ? header.Substring(idx + 1).Trim() : header.Trim();\n    }\n\n    internal static string? NormalizeMessageId(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n        var trimmed = value!.Trim();\n        if (trimmed.Length > 1 && trimmed[0] == '<' && trimmed[trimmed.Length - 1] == '>') {\n            trimmed = trimmed.Substring(1, trimmed.Length - 2);\n        }\n        return trimmed;\n    }\n\n    private static DateTimeOffset? TryParseTimestamp(string? value)\n        => DateUtils.TryParse(value, out var dt) ? dt : null;\n\n    /// <summary>Maps a <see cref=\"DsnStatus\"/> to an <see cref=\"NonDeliveryReportType\"/>.</summary>\n    public static NonDeliveryReportType GetReportType(DsnStatus? status) {\n        if (status is null) {\n            return NonDeliveryReportType.Unknown;\n        }\n        return status switch {\n            { Subject: 1, Detail: 1 } => NonDeliveryReportType.UnknownRecipient,\n            { Subject: 2, Detail: 2 } => NonDeliveryReportType.MailboxFull,\n            { Subject: 7 } => NonDeliveryReportType.PolicyBlock,\n            { Subject: 6 } => NonDeliveryReportType.ContentRejected,\n            { Subject: 4 } => NonDeliveryReportType.DnsFailure,\n            { Class: DsnStatusClass.PermanentFailure } => NonDeliveryReportType.HardBounce,\n            { Class: DsnStatusClass.PersistentTransientFailure } => NonDeliveryReportType.SoftBounce,\n            _ => NonDeliveryReportType.Unknown\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/NonDeliveryReportResult.cs",
    "content": "using Mailozaurr;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Represents a non-delivery report paired with an optional sent message record.\n/// </summary>\npublic sealed class NonDeliveryReportResult {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"NonDeliveryReportResult\"/> class.\n    /// </summary>\n    /// <param name=\"report\">Parsed non-delivery report details.</param>\n    /// <param name=\"sentMessage\">Associated sent message record, if available.</param>\n    public NonDeliveryReportResult(NonDeliveryReport report, SentMessageRecord? sentMessage) {\n        Report = report;\n        SentMessage = sentMessage;\n    }\n\n    /// <summary>Parsed non-delivery report details.</summary>\n    public NonDeliveryReport Report { get; }\n\n    /// <summary>Sent message record corresponding to the report if resolved; otherwise <c>null</c>.</summary>\n    public SentMessageRecord? SentMessage { get; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/NonDeliveryReportServiceBase.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Provides common functionality for non-delivery report services.\n/// </summary>\npublic abstract class NonDeliveryReportServiceBase : INonDeliveryReportService {\n    private readonly SendLogResolver resolver;\n    private readonly SemaphoreSlim gate = new(1, 1);\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"NonDeliveryReportServiceBase\"/> class.\n    /// </summary>\n    /// <param name=\"resolver\">Resolver used to match reports with sent messages.</param>\n    protected NonDeliveryReportServiceBase(SendLogResolver resolver) => this.resolver = resolver;\n\n    /// <summary>\n    /// Searches for non-delivery reports using implementation specific logic.\n    /// </summary>\n    /// <param name=\"since\">Only reports after this date are considered.</param>\n    /// <param name=\"before\">Only reports before this date are considered.</param>\n    /// <param name=\"recipientContains\">Filters reports by recipient substring.</param>\n    /// <param name=\"messageId\">Filters reports by message identifier.</param>\n    /// <param name=\"maxResults\">Maximum number of reports to return.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>List of found reports paired with optional sent message records.</returns>\n    public async Task<IList<NonDeliveryReportResult>> SearchAsync(\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        IList<NonDeliveryReport> reports;\n        try {\n            reports = await SearchInternalAsync(since, before, recipientContains, messageId, maxResults, cancellationToken).ConfigureAwait(false);\n        }\n        finally {\n            gate.Release();\n        }\n        var results = new List<NonDeliveryReportResult>(reports.Count);\n        foreach (NonDeliveryReport report in reports) {\n            SentMessageRecord? record = await resolver.ResolveAsync(report, cancellationToken).ConfigureAwait(false);\n            results.Add(new NonDeliveryReportResult(report, record));\n        }\n        return results;\n    }\n\n    /// <summary>\n    /// Performs the actual search for non-delivery reports.\n    /// </summary>\n    /// <param name=\"since\">Only reports after this date are considered.</param>\n    /// <param name=\"before\">Only reports before this date are considered.</param>\n    /// <param name=\"recipientContains\">Filters reports by recipient substring.</param>\n    /// <param name=\"messageId\">Filters reports by message identifier.</param>\n    /// <param name=\"maxResults\">Maximum number of reports to return.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Collection of parsed non-delivery reports.</returns>\n    protected abstract Task<IList<NonDeliveryReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId,\n        int maxResults,\n        CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/NonDeliveryReportSubjectPatterns.cs",
    "content": "namespace Mailozaurr.NonDeliveryReports;\n\ninternal static class NonDeliveryReportSubjectPatterns {\n    internal static readonly string[] Values = {\n        \"Undelivered Mail Returned to Sender\",\n        \"Delivery Status Notification\",\n        \"Mail delivery failed\",\n        \"Mail Delivery Subsystem\",\n        \"Failure Notice\",\n        \"Delivery failure\",\n        \"Undeliverable:\",\n        \"Delivery has failed to these recipients or groups\",\n        \"Undeliverable message\",\n        \"Undeliverable mail\",\n        \"Your message couldn't be delivered\",\n    };\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/NonDeliveryReportType.cs",
    "content": "namespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Describes common Non-Delivery Report types.\n/// </summary>\npublic enum NonDeliveryReportType {\n    /// <summary>Type of the failure couldn't be determined.</summary>\n    Unknown,\n    /// <summary>The message was permanently rejected.</summary>\n    HardBounce,\n    /// <summary>The message failed temporarily and may be retried.</summary>\n    SoftBounce,\n    /// <summary>The recipient's mailbox is full.</summary>\n    MailboxFull,\n    /// <summary>The recipient does not exist.</summary>\n    UnknownRecipient,\n    /// <summary>The message was blocked due to policy or security settings.</summary>\n    PolicyBlock,\n    /// <summary>The message content was rejected or unsupported.</summary>\n    ContentRejected,\n    /// <summary>The message could not be delivered due to DNS or routing failures.</summary>\n    DnsFailure\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/NonDeliveryReports/Pop3NonDeliveryReportService.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Pop3;\n\nnamespace Mailozaurr.NonDeliveryReports;\n\n/// <summary>\n/// Retrieves non-delivery reports using the POP3 protocol.\n/// </summary>\npublic sealed class Pop3NonDeliveryReportService : NonDeliveryReportServiceBase {\n    private readonly Pop3Client client;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Pop3NonDeliveryReportService\"/> class.\n    /// </summary>\n    /// <param name=\"client\">POP3 client used to access the mailbox.</param>\n    /// <param name=\"resolver\">Resolver used to match reports with sent messages.</param>\n    public Pop3NonDeliveryReportService(Pop3Client client, SendLogResolver resolver) : base(resolver) => this.client = client;\n\n    /// <inheritdoc />\n    protected override Task<IList<NonDeliveryReport>> SearchInternalAsync(\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId,\n        int maxResults,\n        CancellationToken cancellationToken) =>\n        MailboxSearcher.SearchNonDeliveryReportsAsync(client, since, before, recipientContains, messageId, maxResults, cancellationToken: cancellationToken);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Pop3EmailMessage.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a POP3 email message along with its index.\n/// </summary>\n/// <remarks>\n/// Similar to <see cref=\"ImapEmailMessage\"/> but used when retrieving\n/// messages from a POP3 server.\n/// </remarks>\npublic class Pop3EmailMessage {\n    /// <summary>\n    /// Creates a new instance of <see cref=\"Pop3EmailMessage\"/>.\n    /// </summary>\n    /// <param name=\"index\">Index of the message within the mailbox.</param>\n    /// <param name=\"message\">The actual MIME message.</param>\n    /// <param name=\"nonDeliveryReports\">Optional parsed NDRs associated with the message.</param>\n    public Pop3EmailMessage(int index, MimeMessage message, IList<NonDeliveryReport>? nonDeliveryReports = null) {\n        Index = index;\n        Message = message;\n        Encryption = MimeKitUtils.GetEncryption(message);\n        NonDeliveryReports = nonDeliveryReports ?? MimeKitUtils.GetNonDeliveryReports(message);\n    }\n\n    /// <summary>Index of the message within the mailbox.</summary>\n    public int Index { get; }\n\n    /// <summary>The underlying <see cref=\"MimeMessage\"/>.</summary>\n    public MimeMessage Message { get; }\n\n    /// <summary>Detected encryption or signature type.</summary>\n    public EmailEncryption Encryption { get; }\n\n    /// <summary>Parsed Non-Delivery Report details, if available.</summary>\n    public IList<NonDeliveryReport> NonDeliveryReports { get; }\n\n    /// <inheritdoc />\n    public override string ToString() => Message.Subject ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Pop3MessageInfo.cs",
    "content": "using System;\nusing System.Linq;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides a user friendly view over a POP3 email message.\n/// </summary>\n/// <remarks>\n/// Exposes common message properties to simplify scripting scenarios\n/// when dealing with POP3 servers.\n/// </remarks>\npublic class Pop3MessageInfo {\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Pop3MessageInfo\"/> class.\n    /// </summary>\n    /// <param name=\"message\">The underlying POP3 email message.</param>\n    public Pop3MessageInfo(Pop3EmailMessage message) {\n        Raw = message;\n    }\n\n    /// <summary>The wrapped <see cref=\"Pop3EmailMessage\"/>.</summary>\n    public Pop3EmailMessage Raw { get; }\n\n    /// <summary>Index of the message in the mailbox.</summary>\n    public int Index => Raw.Index;\n\n    /// <summary>Sender addresses.</summary>\n    public string From => string.Join(\", \", Raw.Message.From.Mailboxes.Select(m => m.ToString()));\n\n    /// <summary>Recipient addresses.</summary>\n    public string To => string.Join(\", \", Raw.Message.To.Mailboxes.Select(m => m.ToString()));\n\n    /// <summary>Subject of the message.</summary>\n    public string? Subject => Raw.Message.Subject;\n\n    /// <summary>Date the message was sent.</summary>\n    public DateTime Date => Raw.Message.Date.DateTime;\n\n    /// <summary>Plain text body.</summary>\n    public string? TextBody => Raw.Message.TextBody;\n\n    /// <summary>HTML body.</summary>\n    public string? HtmlBody => Raw.Message.HtmlBody;\n\n    /// <summary>Message priority.</summary>\n    public MessagePriority Priority => Raw.Message.Priority switch {\n        MimeKit.MessagePriority.Urgent => MessagePriority.High,\n        MimeKit.MessagePriority.NonUrgent => MessagePriority.Low,\n        _ => MessagePriority.Normal,\n    };\n\n    /// <summary>Whether the message has any attachments.</summary>\n    public bool HasAttachments => Raw.Message.Attachments.Any();\n\n    /// <summary>Encryption or signature detected.</summary>\n    public EmailEncryption Encryption => Raw.Encryption;\n\n    /// <inheritdoc />\n    public override string ToString() => Subject ?? string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/README.md",
    "content": "# Mailozaurr\n\nMailozaurr is a powerful .NET library for sending emails using various providers (SMTP, SendGrid, Mailgun, Amazon SES, Microsoft Graph, etc.), supporting advanced features like attachments, HTML bodies, and authentication.\n\n## Features\n- Send emails via SMTP, SendGrid, Mailgun, Amazon SES, Microsoft Graph, and more\n- Support for attachments, HTML, and plain text\n- OAuth2 and basic authentication\n- Delivery notifications and advanced options\n- Designed for integration in C# and PowerShell projects\n\n## Installation\n\nAdd a reference to the `Mailozaurr` project or its compiled DLL in your C# project:\n\n```\n// If using .csproj\n<ProjectReference Include=\"../Mailozaurr/Mailozaurr.csproj\" />\n```\nOr, if you have the DLL:\n```\n// In your .csproj\n<ItemGroup>\n  <Reference Include=\"Mailozaurr\">\n    <HintPath>path/to/Mailozaurr.dll</HintPath>\n  </Reference>\n</ItemGroup>\n```\n\n## Usage in C#\n\n### Basic Example: Sending an Email via SMTP\n\n```csharp\nusing Mailozaurr;\nusing MailKit.Security;\n\nvar smtp = new Smtp();\nsmtp.From = \"sender@example.com\";\nsmtp.To = new[] { \"recipient@example.com\" };\nsmtp.Subject = \"Test Email\";\nsmtp.HtmlBody = \"<b>Hello from Mailozaurr!</b>\";\nsmtp.Server = \"smtp.example.com\";\nsmtp.Port = 587;\nsmtp.SecureSocketOptions = SecureSocketOptions.StartTls;\n\n// Optional: authentication\nsmtp.Authenticate(\"username\", \"password\");\n\n// Build the MIME message\nsmtp.CreateMessage();\n\n// Send the email\nvar result = smtp.Send();\nif (result.Status)\n{\n    Console.WriteLine(\"Email sent successfully!\");\n}\nelse\n{\nConsole.WriteLine($\"Failed: {result.ErrorMessage}\");\n}\n```\n\n> **Tip**\n> You can set `smtp.AutoCreateMessage = true` to build the MIME message\n> automatically before sending if you forget to call `CreateMessage()`.\n\n> **Note**\n> When `Connect` is called with the `useSsl` flag and\n> `SecureSocketOptions` left as `Auto`, the library automatically uses\n> `SecureSocketOptions.StartTls`. Provide an explicit option if a\n> different behaviour is required.\n\n### Using Microsoft Graph\n\n```csharp\nusing Mailozaurr;\n\nusing var graph = new Graph();\ngraph.From = \"sender@example.com\";\ngraph.To = new[] { \"recipient@example.com\" };\ngraph.Subject = \"Graph API Email\";\ngraph.HTML = \"<p>Sent via Microsoft Graph!</p>\";\n// ... set up authentication ...\n// graph.Authenticate(...);\n// graph.SendMessageAsync();\n```\n\n## Key Classes\n- `Smtp` - for SMTP email sending\n- `Graph` - for Microsoft Graph API\n- `SendGridClient` - for SendGrid\n- `MailgunClient` - for Mailgun\n- `SesClient` - for Amazon SES\n- `SmtpResult` - result object for send operations\n\n## Building from Source\n\n1. Clone the repository\n2. Open the solution in Visual Studio or run `dotnet build` in the root directory\n3. Reference the built DLL in your project\n\n## License\n\n(c) 2011 - 2024 Przemyslaw Klys @ Evotec. All rights reserved.\n\n---\n\n*For more advanced usage, see the source code and examples in the repository.*\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/FolderOperations.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for common IMAP folder operations.\n/// </summary>\npublic static class FolderOperations {\n    /// <summary>\n    /// Moves an IMAP folder to a different parent folder.\n    /// </summary>\n    /// <param name=\"client\">Active IMAP client instance.</param>\n    /// <param name=\"sourceFolder\">Name of the folder to move.</param>\n    /// <param name=\"destinationFolder\">Name of the destination folder. Pass <c>null</c> or an empty string to move to the root.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the asynchronous operation.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    public static async Task MoveFolderAsync(\n        ImapClient client,\n        string sourceFolder,\n        string? destinationFolder,\n        CancellationToken cancellationToken = default) {\n        await MoveFolderAsync(client, sourceFolder, destinationFolder, dryRun: false, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves an IMAP folder to a different parent folder, optionally simulating the change.\n    /// </summary>\n    public static async Task MoveFolderAsync(\n        ImapClient client,\n        string sourceFolder,\n        string? destinationFolder,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var source = client.GetCachedFolder(sourceFolder, FolderAccess.ReadWrite);\n        IMailFolder destParent;\n        if (string.IsNullOrWhiteSpace(destinationFolder)) {\n            destParent = client.PersonalNamespaces.Count > 0\n                ? client.GetFolder(client.PersonalNamespaces[0])\n                : client.GetFolder(string.Empty);\n            if (!destParent.IsOpen)\n                destParent.Open(FolderAccess.ReadWrite);\n        } else {\n            destParent = client.GetCachedFolder(destinationFolder, FolderAccess.ReadWrite);\n        }\n        try {\n            await source.RenameAsync(destParent, source.Name, cancellationToken).ConfigureAwait(false);\n        } finally {\n            if (source.IsOpen)\n                await source.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n            if (destParent.IsOpen)\n                await destParent.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n            client.ClearFolderCache();\n        }\n    }\n\n    /// <summary>\n    /// Renames an existing IMAP folder.\n    /// </summary>\n    /// <param name=\"client\">Active IMAP client instance.</param>\n    /// <param name=\"folder\">Name of the folder to rename.</param>\n    /// <param name=\"newName\">New name for the folder.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the asynchronous operation.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    public static async Task RenameFolderAsync(\n        ImapClient client,\n        string folder,\n        string newName,\n        CancellationToken cancellationToken = default) {\n        await RenameFolderAsync(client, folder, newName, dryRun: false, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Renames an existing IMAP folder, optionally simulating the change.\n    /// </summary>\n    public static async Task RenameFolderAsync(\n        ImapClient client,\n        string folder,\n        string newName,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var src = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n        try {\n            var parent = src.ParentFolder;\n            if (parent is null) {\n                throw new InvalidOperationException($\"Cannot rename folder '{folder}' because its parent folder could not be resolved.\");\n            }\n\n            await src.RenameAsync(parent, newName, cancellationToken).ConfigureAwait(false);\n        } finally {\n            if (src.IsOpen)\n                await src.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n            client.ClearFolderCache();\n        }\n    }\n\n    /// <summary>\n    /// Permanently deletes a folder.\n    /// </summary>\n    /// <param name=\"client\">Active IMAP client instance.</param>\n    /// <param name=\"folder\">Name of the folder to remove.</param>\n    /// <param name=\"recursive\">When set to <c>true</c>, removes all subfolders recursively.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the asynchronous operation.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    public static async Task RemoveFolderAsync(\n        ImapClient client,\n        string folder,\n        bool recursive = false,\n        CancellationToken cancellationToken = default) {\n        await RemoveFolderAsync(client, folder, recursive, dryRun: false, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Permanently deletes a folder, optionally simulating the change.\n    /// </summary>\n    public static async Task RemoveFolderAsync(\n        ImapClient client,\n        string folder,\n        bool recursive,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var src = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n        try {\n            if (recursive) {\n                foreach (var sub in await src.GetSubfoldersAsync(false, cancellationToken).ConfigureAwait(false))\n                    await RemoveFolderAsync(client, sub.FullName, true, dryRun, cancellationToken).ConfigureAwait(false);\n\n                if (src.IsOpen)\n                    await src.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n                src = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n            }\n\n            await src.DeleteAsync(cancellationToken).ConfigureAwait(false);\n        } finally {\n            if (src.IsOpen)\n                await src.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n            client.ClearFolderCache();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/GraphMessageListener.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Polls Microsoft Graph for new messages and raises events when they arrive.\n/// </summary>\n/// <remarks>\n/// The listener keeps track of message IDs to avoid raising duplicate\n/// notifications during polling.\n/// </remarks>\npublic class GraphMessageListener : IDisposable {\n    private readonly GraphCredential _credential;\n    private readonly string _userPrincipalName;\n    private readonly HashSet<string> _seenIds = new();\n    private CancellationTokenSource? _cancel;\n    private Task? _pollTask;\n    private readonly TimeSpan _interval;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GraphMessageListener\"/> class.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential to use.</param>\n    /// <param name=\"userPrincipalName\">User principal name to monitor.</param>\n    /// <param name=\"interval\">Polling interval.</param>\n    public GraphMessageListener(GraphCredential credential, string userPrincipalName, TimeSpan? interval = null) {\n        _credential = credential ?? throw new ArgumentNullException(nameof(credential));\n        _userPrincipalName = userPrincipalName ?? throw new ArgumentNullException(nameof(userPrincipalName));\n        _interval = interval ?? TimeSpan.FromMinutes(1);\n    }\n\n    /// <summary>\n    /// Occurs when a new message arrives.\n    /// </summary>\n    public event EventHandler<Dictionary<string, object>>? MessageArrived;\n\n    /// <summary>\n    /// Occurs when an error is encountered while polling.\n    /// </summary>\n    public event EventHandler<Exception>? PollError;\n\n    /// <summary>\n    /// Starts listening for new messages.\n    /// </summary>\n    public async Task StartAsync(CancellationToken cancellationToken = default) {\n        if (_cancel != null) {\n            throw new InvalidOperationException(\"Listener already started.\");\n        }\n\n        _cancel = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        try {\n            var initial = await MicrosoftGraphUtils.GetMailMessagesAsync(_credential, _userPrincipalName, cancellationToken: _cancel.Token).ConfigureAwait(false);\n            foreach (var msg in initial) {\n                if (msg.TryGetValue(\"id\", out var idObj) && idObj is string id) {\n                    _seenIds.Add(id);\n                }\n            }\n\n            _pollTask = PollLoopAsync();\n        } catch {\n            _cancel.Dispose();\n            _cancel = null;\n            _pollTask = null;\n            throw;\n        }\n    }\n\n    /// <summary>\n    /// Stops listening for new messages.\n    /// </summary>\n    public void Stop() {\n        if (_cancel == null) {\n            return;\n        }\n\n        _cancel.Cancel();\n        try {\n            _pollTask?.GetAwaiter().GetResult();\n        } catch (OperationCanceledException) {\n            // ignored\n        }\n\n        _cancel.Dispose();\n        _cancel = null;\n        _pollTask = null;\n    }\n\n    private async Task PollLoopAsync() {\n        while (!_cancel!.IsCancellationRequested) {\n            try {\n                await Task.Delay(_interval, _cancel.Token).ConfigureAwait(false);\n                var messages = await MicrosoftGraphUtils.GetMailMessagesAsync(_credential, _userPrincipalName, cancellationToken: _cancel.Token).ConfigureAwait(false);\n                foreach (var msg in messages) {\n                    if (msg.TryGetValue(\"id\", out var idObj) && idObj is string id && !_seenIds.Contains(id)) {\n                        _seenIds.Add(id);\n                        MessageArrived?.Invoke(this, msg);\n                    }\n                }\n            } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                break;\n            } catch (Exception ex) {\n                PollError?.Invoke(this, ex);\n                try {\n                    await Task.Delay(TimeSpan.FromSeconds(5), _cancel.Token).ConfigureAwait(false);\n                } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                    break;\n                }\n            }\n        }\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        Stop();\n        _cancel?.Dispose();\n        _cancel = null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapBulkFlagOperations.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Executes bulk IMAP flag updates with per-item fallback when server bulk operations fail.\n/// </summary>\npublic static class ImapBulkFlagOperations {\n    /// <summary>\n    /// Abstraction over IMAP flag operations for testability.\n    /// </summary>\n    public interface IImapFolder {\n        /// <summary>Adds flags for a batch of UIDs.</summary>\n        Task AddFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n\n        /// <summary>Removes flags for a batch of UIDs.</summary>\n        Task RemoveFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n\n        /// <summary>Adds flags for one UID.</summary>\n        Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n\n        /// <summary>Removes flags for one UID.</summary>\n        Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n    }\n\n    private sealed class MailKitFolderAdapter : IImapFolder {\n        private readonly IMailFolder _folder;\n\n        internal MailKitFolderAdapter(IMailFolder folder) {\n            _folder = folder ?? throw new ArgumentNullException(nameof(folder));\n        }\n\n        public Task AddFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.AddFlagsAsync(new List<UniqueId>(uids), flags, silent, cancellationToken);\n\n        public Task RemoveFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.RemoveFlagsAsync(new List<UniqueId>(uids), flags, silent, cancellationToken);\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.AddFlagsAsync(uid, flags, silent, cancellationToken);\n\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.RemoveFlagsAsync(uid, flags, silent, cancellationToken);\n    }\n\n    /// <summary>\n    /// One bulk-flag item result.\n    /// </summary>\n    public sealed class ImapBulkFlagResultItem {\n        /// <summary>Message UID.</summary>\n        public UniqueId Uid { get; set; }\n\n        /// <summary>True when update succeeded.</summary>\n        public bool Ok { get; set; }\n\n        /// <summary>Error text for failed updates.</summary>\n        public string? Error { get; set; }\n    }\n\n    /// <summary>\n    /// Bulk-flag operation result.\n    /// </summary>\n    public sealed class ImapBulkFlagResult {\n        /// <summary>Total unique UIDs requested.</summary>\n        public int Requested { get; set; }\n\n        /// <summary>Count of successful updates.</summary>\n        public int Updated { get; set; }\n\n        /// <summary>Per-item results in request order.</summary>\n        public List<ImapBulkFlagResultItem> Results { get; set; } = new();\n\n        /// <summary>UIDs successfully updated.</summary>\n        public List<UniqueId> SuccessfulUids { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Sets or clears IMAP flags for many UIDs with per-item fallback when bulk call fails.\n    /// </summary>\n    /// <param name=\"folder\">Opened IMAP folder with write access.</param>\n    /// <param name=\"uids\">Message UIDs to update.</param>\n    /// <param name=\"flags\">Flags to set/clear.</param>\n    /// <param name=\"add\">True to add flags, false to remove flags.</param>\n    /// <param name=\"sanitizeError\">Optional error sanitizer for per-item failures.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Bulk operation result.</returns>\n    public static async Task<ImapBulkFlagResult> SetFlagsAsync(\n        IMailFolder folder,\n        IReadOnlyCollection<UniqueId> uids,\n        MessageFlags flags,\n        bool add,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n\n        return await SetFlagsAsync(\n            new MailKitFolderAdapter(folder),\n            uids,\n            flags,\n            add,\n            sanitizeError,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Sets or clears IMAP flags for many UIDs with per-item fallback when bulk call fails.\n    /// </summary>\n    public static async Task<ImapBulkFlagResult> SetFlagsAsync(\n        IImapFolder folder,\n        IReadOnlyCollection<UniqueId> uids,\n        MessageFlags flags,\n        bool add,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n        if (uids == null) {\n            throw new ArgumentNullException(nameof(uids));\n        }\n\n        var unique = Deduplicate(uids);\n        var result = new ImapBulkFlagResult {\n            Requested = unique.Count,\n            Results = new List<ImapBulkFlagResultItem>(unique.Count),\n            SuccessfulUids = new List<UniqueId>(unique.Count)\n        };\n        if (unique.Count == 0) {\n            return result;\n        }\n\n        try {\n            if (add) {\n                await folder.AddFlagsAsync(unique, flags, silent: true, cancellationToken).ConfigureAwait(false);\n            } else {\n                await folder.RemoveFlagsAsync(unique, flags, silent: true, cancellationToken).ConfigureAwait(false);\n            }\n\n            foreach (var uid in unique) {\n                result.Updated++;\n                result.SuccessfulUids.Add(uid);\n                result.Results.Add(new ImapBulkFlagResultItem { Uid = uid, Ok = true });\n            }\n            return result;\n        } catch (OperationCanceledException) {\n            throw;\n        } catch {\n            // Fallback path handled below.\n        }\n\n        foreach (var uid in unique) {\n            try {\n                if (add) {\n                    await folder.AddFlagsAsync(uid, flags, silent: true, cancellationToken).ConfigureAwait(false);\n                } else {\n                    await folder.RemoveFlagsAsync(uid, flags, silent: true, cancellationToken).ConfigureAwait(false);\n                }\n\n                result.Updated++;\n                result.SuccessfulUids.Add(uid);\n                result.Results.Add(new ImapBulkFlagResultItem { Uid = uid, Ok = true });\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                var error = ex.Message;\n                if (sanitizeError != null) {\n                    try {\n                        error = sanitizeError(error);\n                    } catch {\n                        // Ignore sanitizer failures.\n                    }\n                }\n                result.Results.Add(new ImapBulkFlagResultItem { Uid = uid, Ok = false, Error = error });\n            }\n        }\n\n        return result;\n    }\n\n    private static List<UniqueId> Deduplicate(IReadOnlyCollection<UniqueId> uids) {\n        var output = new List<UniqueId>(uids.Count);\n        var seen = new HashSet<uint>();\n        foreach (var uid in uids) {\n            var id = uid.Id;\n            if (id == 0 || !seen.Add(id)) {\n                continue;\n            }\n            output.Add(uid);\n        }\n        return output;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapClientFolderCache.cs",
    "content": "using System.Collections.Concurrent;\nusing System.Runtime.CompilerServices;\nusing MailKit;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides folder caching for <see cref=\"ImapClient\"/> instances.\n/// </summary>\npublic static class ImapClientFolderCache {\n    private static readonly ConditionalWeakTable<ImapClient, ConcurrentDictionary<string, IMailFolder>> Cache = new();\n\n    /// <summary>\n    /// Retrieves a folder, caching it on first access and ensuring it is opened with the specified access.\n    /// </summary>\n    /// <param name=\"client\">IMAP client instance.</param>\n    /// <param name=\"folder\">Folder name or null for the inbox.</param>\n    /// <param name=\"access\">Folder access mode.</param>\n    /// <returns>The opened folder.</returns>\n    public static IMailFolder GetCachedFolder(this ImapClient client, string? folder, FolderAccess access) {\n        var map = Cache.GetOrCreateValue(client);\n        var inbox = client.Inbox ?? throw new InvalidOperationException(\"The IMAP client does not expose an inbox folder.\");\n        var inboxName = inbox.FullName ?? inbox.Name ?? \"INBOX\";\n        var name = string.IsNullOrWhiteSpace(folder) ? inboxName : folder!;\n\n        if (!map.TryGetValue(name, out var mailFolder)) {\n            if (name.Equals(inboxName, System.StringComparison.OrdinalIgnoreCase)) {\n                mailFolder = inbox;\n            } else {\n                try {\n                    mailFolder = client.GetFolder(name);\n                } catch (FolderNotFoundException) {\n                    if (client.PersonalNamespaces.Count == 0) {\n                        throw;\n                    }\n\n                    mailFolder = client.GetFolder(client.PersonalNamespaces[0]).GetSubfolder(name);\n                }\n            }\n            map[name] = mailFolder;\n        }\n\n        if (!mailFolder.IsOpen || mailFolder.Access != access) {\n            mailFolder.Open(access);\n        }\n\n        return mailFolder;\n    }\n\n    /// <summary>\n    /// Clears cached folders for the specified client.\n    /// </summary>\n    /// <param name=\"client\">IMAP client instance.</param>\n    public static void ClearFolderCache(this ImapClient client) => Cache.Remove(client);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapDeleteOperations.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Executes IMAP delete operations with per-item results and optional expunge.\n/// </summary>\npublic static class ImapDeleteOperations {\n    /// <summary>\n    /// Abstraction over IMAP delete operations for testability.\n    /// </summary>\n    public interface IImapDeleteFolder : ImapBulkFlagOperations.IImapFolder {\n        /// <summary>Folder full name.</summary>\n        string FullName { get; }\n\n        /// <summary>Expunges messages flagged as deleted.</summary>\n        Task ExpungeAsync(CancellationToken cancellationToken = default);\n    }\n\n    private sealed class MailKitDeleteFolderAdapter : IImapDeleteFolder {\n        private readonly IMailFolder _folder;\n\n        internal MailKitDeleteFolderAdapter(IMailFolder folder) {\n            _folder = folder ?? throw new ArgumentNullException(nameof(folder));\n        }\n\n        public string FullName => _folder.FullName;\n\n        public Task AddFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.AddFlagsAsync(new List<UniqueId>(uids), flags, silent, cancellationToken);\n\n        public Task RemoveFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.RemoveFlagsAsync(new List<UniqueId>(uids), flags, silent, cancellationToken);\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.AddFlagsAsync(uid, flags, silent, cancellationToken);\n\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.RemoveFlagsAsync(uid, flags, silent, cancellationToken);\n\n        public Task ExpungeAsync(CancellationToken cancellationToken = default) =>\n            _folder.ExpungeAsync(cancellationToken);\n    }\n\n    /// <summary>\n    /// Per-message delete result.\n    /// </summary>\n    public sealed class ImapDeleteResultItem {\n        /// <summary>Message UID.</summary>\n        public UniqueId Uid { get; set; }\n\n        /// <summary>True when delete flag update succeeded.</summary>\n        public bool Ok { get; set; }\n\n        /// <summary>Error text when <see cref=\"Ok\"/> is false.</summary>\n        public string? Error { get; set; }\n    }\n\n    /// <summary>\n    /// Aggregate delete result.\n    /// </summary>\n    public sealed class ImapDeleteResult {\n        /// <summary>Resolved folder full name.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Total unique UIDs requested.</summary>\n        public int Requested { get; set; }\n\n        /// <summary>Count of successful delete flag updates.</summary>\n        public int Deleted { get; set; }\n\n        /// <summary>True when expunge was executed.</summary>\n        public bool Expunged { get; set; }\n\n        /// <summary>Per-item results in request order.</summary>\n        public List<ImapDeleteResultItem> Results { get; set; } = new();\n\n        /// <summary>UIDs successfully marked deleted.</summary>\n        public List<UniqueId> DeletedUids { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Marks messages deleted and optionally expunges the folder.\n    /// </summary>\n    public static Task<ImapDeleteResult> DeleteAsync(\n        IMailFolder folder,\n        IReadOnlyCollection<UniqueId> uids,\n        bool expunge,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n\n        return DeleteAsync(new MailKitDeleteFolderAdapter(folder), uids, expunge, sanitizeError, cancellationToken);\n    }\n\n    /// <summary>\n    /// Marks messages deleted and optionally expunges the folder.\n    /// </summary>\n    public static async Task<ImapDeleteResult> DeleteAsync(\n        IImapDeleteFolder folder,\n        IReadOnlyCollection<UniqueId> uids,\n        bool expunge,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n        if (uids == null) {\n            throw new ArgumentNullException(nameof(uids));\n        }\n\n        var operation = await ImapBulkFlagOperations.SetFlagsAsync(\n            folder,\n            uids,\n            MessageFlags.Deleted,\n            add: true,\n            sanitizeError: sanitizeError,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        var result = new ImapDeleteResult {\n            Folder = folder.FullName,\n            Requested = operation.Requested,\n            Deleted = operation.Updated,\n            Results = new List<ImapDeleteResultItem>(operation.Results.Count),\n            DeletedUids = new List<UniqueId>(operation.SuccessfulUids.Count)\n        };\n\n        foreach (var item in operation.Results) {\n            result.Results.Add(new ImapDeleteResultItem {\n                Uid = item.Uid,\n                Ok = item.Ok,\n                Error = item.Error\n            });\n        }\n\n        result.DeletedUids.AddRange(operation.SuccessfulUids);\n\n        if (expunge && result.Deleted > 0) {\n            await folder.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n            result.Expunged = true;\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapIdleListener.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Listens for new mail using the IMAP IDLE command and raises events when messages arrive.\n/// </summary>\n/// <remarks>\n/// An internal cache of <see cref=\"IMessageSummary\"/> objects is maintained\n/// to ensure each message is reported only once per session.\n/// </remarks>\npublic class ImapIdleListener : IDisposable, IAsyncDisposable {\n    private readonly ImapClient _client;\n    private readonly string? _folderName;\n    private readonly List<IMessageSummary> _summaries = new();\n    private readonly HashSet<UniqueId> _known = new();\n    private readonly FetchRequest _fetchRequest = new(MessageSummaryItems.Full | MessageSummaryItems.UniqueId);\n    private readonly SearchQuery? _searchQuery;\n    private IMailFolder? _folder;\n    private CancellationTokenSource? _cancel;\n    private CancellationTokenSource? _done;\n    private bool _messagesArrived;\n    private Task? _idleTask;\n    private bool _disposed;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ImapIdleListener\"/> class.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Folder to monitor or <c>null</c> for the inbox.</param>\n    /// <param name=\"searchQuery\">Optional search query to filter incoming messages.</param>\n    public ImapIdleListener(ImapClient client, string? folder = null, SearchQuery? searchQuery = null) {\n        _client = client ?? throw new ArgumentNullException(nameof(client));\n        _folderName = folder;\n        _searchQuery = searchQuery;\n    }\n\n    /// <summary>\n    /// Occurs when a new message arrives.\n    /// </summary>\n    public event EventHandler<ImapEmailMessage>? MessageArrived;\n\n    /// <summary>\n    /// Occurs when an error is encountered while idling.\n    /// </summary>\n    public event EventHandler<Exception>? IdleError;\n\n    /// <summary>\n    /// Starts listening for new messages.\n    /// </summary>\n    public async Task StartAsync(CancellationToken cancellationToken = default) {\n        if (_disposed) {\n            throw new ObjectDisposedException(nameof(ImapIdleListener));\n        }\n\n        if (_cancel != null) {\n            throw new InvalidOperationException(\"Listener already started.\");\n        }\n\n        _cancel = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        try {\n            _folder = _client.GetCachedFolder(_folderName, FolderAccess.ReadOnly);\n            await _folder.OpenAsync(FolderAccess.ReadOnly, _cancel.Token).ConfigureAwait(false);\n\n            var search = _searchQuery ?? SearchQuery.All;\n            var initialUids = await _folder.SearchAsync(search, _cancel.Token).ConfigureAwait(false);\n            var initial = await _folder.FetchAsync(initialUids, _fetchRequest, _cancel.Token).ConfigureAwait(false);\n            foreach (var summary in initial) {\n                _summaries.Add(summary);\n                _known.Add(summary.UniqueId);\n            }\n\n            _folder.CountChanged += OnCountChanged;\n            _folder.MessageExpunged += OnMessageExpunged;\n\n            _idleTask = IdleLoopAsync();\n        } catch {\n            if (_folder?.IsOpen == true) {\n                try {\n                    await _folder.CloseAsync(expunge: false, CancellationToken.None).ConfigureAwait(false);\n                } catch {\n                }\n            }\n\n            _summaries.Clear();\n            _known.Clear();\n            Cleanup();\n            _idleTask = null;\n            throw;\n        }\n    }\n\n    /// <summary>\n    /// Stops listening for new messages.\n    /// </summary>\n    public void Stop() => _cancel?.Cancel();\n\n    /// <summary>\n    /// Stops listening for new messages and waits for the listener loop to finish.\n    /// </summary>\n    public async Task StopAsync() {\n        var idleTask = _idleTask;\n        var cancel = _cancel;\n\n        Stop();\n\n        if (idleTask == null) {\n            Cleanup();\n            return;\n        }\n\n        try {\n            await idleTask.ConfigureAwait(false);\n        } catch (OperationCanceledException) when (cancel?.IsCancellationRequested == true) {\n            // Listener shutdown requested cancellation while the loop was unwinding.\n        } finally {\n            Cleanup();\n            if (ReferenceEquals(_idleTask, idleTask)) {\n                _idleTask = null;\n            }\n        }\n    }\n\n    private async Task IdleLoopAsync() {\n        try {\n            while (!_cancel!.IsCancellationRequested) {\n                try {\n                    await WaitForNewMessagesAsync().ConfigureAwait(false);\n\n                    if (_messagesArrived) {\n                        await FetchNewMessagesAsync().ConfigureAwait(false);\n                        _messagesArrived = false;\n                    }\n                } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                    break;\n                } catch (Exception ex) {\n                    IdleError?.Invoke(this, ex);\n                    try {\n                        await Task.Delay(TimeSpan.FromSeconds(5), _cancel.Token).ConfigureAwait(false);\n                    } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                        break;\n                    }\n                }\n            }\n        } finally {\n            Cleanup();\n        }\n    }\n\n    private async Task WaitForNewMessagesAsync() {\n        if (_client.Capabilities.HasFlag(ImapCapabilities.Idle)) {\n            _done = new CancellationTokenSource(TimeSpan.FromMinutes(9));\n            try {\n                await _client.IdleAsync(_done.Token, _cancel!.Token).ConfigureAwait(false);\n            } finally {\n                _done.Dispose();\n                _done = null;\n            }\n        } else {\n            await Task.Delay(TimeSpan.FromMinutes(1), _cancel!.Token).ConfigureAwait(false);\n            await _client.NoOpAsync(_cancel.Token).ConfigureAwait(false);\n        }\n    }\n\n    private async Task FetchNewMessagesAsync() {\n        var search = _searchQuery ?? SearchQuery.All;\n        var uids = await _folder!.SearchAsync(search, _cancel!.Token).ConfigureAwait(false);\n        var newUids = new List<UniqueId>();\n        foreach (var uid in uids) {\n            if (_known.Add(uid)) {\n                newUids.Add(uid);\n            }\n        }\n\n        if (newUids.Count == 0) return;\n\n        var fetched = await _folder.FetchAsync(newUids, _fetchRequest, _cancel.Token).ConfigureAwait(false);\n        foreach (var summary in fetched) {\n            var message = await _folder.GetMessageAsync(summary.UniqueId, _cancel.Token).ConfigureAwait(false);\n            _summaries.Add(summary);\n            MessageArrived?.Invoke(this, new ImapEmailMessage(summary.UniqueId, message));\n        }\n    }\n\n    private void OnCountChanged(object? sender, EventArgs e) {\n        _messagesArrived = true;\n        _done?.Cancel();\n    }\n\n    private void OnMessageExpunged(object? sender, MessageEventArgs e) {\n        if (e.UniqueId.HasValue) {\n            _known.Remove(e.UniqueId.Value);\n            _summaries.RemoveAll(s => s.UniqueId == e.UniqueId.Value);\n        } else if (e.Index < _summaries.Count) {\n            var removed = _summaries[e.Index];\n            _summaries.RemoveAt(e.Index);\n            _known.Remove(removed.UniqueId);\n        }\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        if (_disposed) {\n            return;\n        }\n\n        _disposed = true;\n\n        try {\n            Stop();\n\n            var idleTask = _idleTask;\n            if (idleTask != null) {\n                try {\n                    idleTask.GetAwaiter().GetResult();\n                } catch (OperationCanceledException) when (_cancel?.IsCancellationRequested == true) {\n                    // Listener shutdown requested cancellation while the loop was unwinding.\n                }\n            }\n        } finally {\n            Cleanup();\n            _idleTask = null;\n            GC.SuppressFinalize(this);\n        }\n    }\n\n    /// <inheritdoc />\n    public async ValueTask DisposeAsync() {\n        if (_disposed) {\n            return;\n        }\n\n        _disposed = true;\n\n        try {\n            await StopAsync().ConfigureAwait(false);\n        } finally {\n            _idleTask = null;\n            GC.SuppressFinalize(this);\n        }\n    }\n\n    private void Cleanup() {\n        if (_folder != null) {\n            _folder.CountChanged -= OnCountChanged;\n            _folder.MessageExpunged -= OnMessageExpunged;\n            _folder = null;\n        }\n\n        _done?.Dispose();\n        _done = null;\n\n        _cancel?.Dispose();\n        _cancel = null;\n\n        _messagesArrived = false;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapMailboxSearchQueryBuilder.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing MailKit.Search;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Builds IMAP <see cref=\"SearchQuery\"/> instances from common mailbox search filters.\n/// </summary>\npublic static class ImapMailboxSearchQueryBuilder {\n    /// <summary>\n    /// Builds IMAP search query with optional field filters and free-text tokens.\n    /// </summary>\n    public static SearchQuery Build(\n        bool unseenOnly = false,\n        string? subjectContains = null,\n        string? fromContains = null,\n        string? toContains = null,\n        string? bodyContains = null,\n        DateTime? sinceUtc = null,\n        DateTime? beforeUtc = null,\n        string? query = null) {\n        var search = SearchQuery.All;\n        if (unseenOnly) {\n            search = search.And(SearchQuery.NotSeen);\n        }\n\n        var subject = NormalizeOptional(subjectContains);\n        if (subject != null) {\n            search = search.And(SearchQuery.SubjectContains(subject));\n        }\n\n        var from = NormalizeOptional(fromContains);\n        if (from != null) {\n            search = search.And(SearchQuery.FromContains(from));\n        }\n\n        var to = NormalizeOptional(toContains);\n        if (to != null) {\n            search = search.And(SearchQuery.ToContains(to));\n        }\n\n        var body = NormalizeOptional(bodyContains);\n        if (body != null) {\n            search = search.And(SearchQuery.BodyContains(body));\n        }\n\n        if (sinceUtc.HasValue) {\n            search = search.And(SearchQuery.SentSince(sinceUtc.Value));\n        }\n        if (beforeUtc.HasValue) {\n            search = search.And(SearchQuery.SentBefore(beforeUtc.Value));\n        }\n\n        var tokens = Tokenize(query);\n        foreach (var token in tokens) {\n            // Pragmatic OR across common fields, AND'ed across tokens.\n            var tokenQuery = SearchQuery.SubjectContains(token)\n                .Or(SearchQuery.FromContains(token))\n                .Or(SearchQuery.ToContains(token))\n                .Or(SearchQuery.BodyContains(token));\n            search = search.And(tokenQuery);\n        }\n\n        return search;\n    }\n\n    /// <summary>\n    /// Splits free-text query into non-empty whitespace-delimited terms.\n    /// </summary>\n    public static IReadOnlyList<string> Tokenize(string? query) {\n        var normalized = NormalizeOptional(query);\n        if (normalized == null) {\n            return Array.Empty<string>();\n        }\n\n        return normalized.Split(\n            new[] { ' ', '\\t', '\\r', '\\n' },\n            StringSplitOptions.RemoveEmptyEntries);\n    }\n\n    private static string? NormalizeOptional(string? value) {\n        var trimmed = (value ?? string.Empty).Trim();\n        return trimmed.Length == 0 ? null : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapMessageReader.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Request model for reading a single IMAP message by UID.\n/// </summary>\npublic sealed record ImapMessageReadRequest(\n    UniqueId Uid,\n    string? Folder,\n    long MaxBodyBytes);\n\n/// <summary>\n/// Attachment projection returned from <see cref=\"ImapMessageReader\"/>.\n/// </summary>\npublic sealed record ImapMessageAttachmentInfo(\n    string FileName,\n    string ContentType);\n\n/// <summary>\n/// Flattened IMAP message projection returned from <see cref=\"ImapMessageReader\"/>.\n/// </summary>\npublic sealed record ImapMessageReadResult(\n    long Uid,\n    string Folder,\n    string Subject,\n    string From,\n    string To,\n    DateTimeOffset DateUtc,\n    string TextBody,\n    bool TextTruncated,\n    string HtmlBody,\n    bool HtmlTruncated,\n    bool HasAttachments,\n    IReadOnlyList<ImapMessageAttachmentInfo> Attachments);\n\n/// <summary>\n/// Reads a single IMAP message with reusable folder resolution and truncation semantics.\n/// </summary>\npublic static class ImapMessageReader\n{\n    /// <summary>\n    /// Reads and projects a single IMAP message by UID.\n    /// </summary>\n    public static async Task<ImapMessageReadResult> ReadAsync(\n        ImapClient client,\n        ImapMessageReadRequest request,\n        CancellationToken cancellationToken = default)\n    {\n        if (client == null)\n        {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (request == null)\n        {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var mailFolder = client.GetCachedFolder(request.Folder, FolderAccess.ReadOnly);\n        var message = await mailFolder.GetMessageAsync(request.Uid, cancellationToken).ConfigureAwait(false);\n\n        var attachments = message.Attachments.Select(static attachment => attachment is MimePart part\n            ? new ImapMessageAttachmentInfo(\n                FileName: part.FileName ?? string.Empty,\n                ContentType: part.ContentType?.MimeType ?? string.Empty)\n            : new ImapMessageAttachmentInfo(\n                FileName: string.Empty,\n                ContentType: attachment.ContentType?.MimeType ?? string.Empty)).ToArray();\n\n        var text = TruncateUtf8(message.TextBody, request.MaxBodyBytes, out var textTruncated);\n        var html = TruncateUtf8(message.HtmlBody, request.MaxBodyBytes, out var htmlTruncated);\n\n        return new ImapMessageReadResult(\n            Uid: request.Uid.Id,\n            Folder: mailFolder.FullName,\n            Subject: message.Subject ?? string.Empty,\n            From: string.Join(\", \", message.From.Mailboxes.Select(static mailbox => mailbox.ToString())),\n            To: string.Join(\", \", message.To.Mailboxes.Select(static mailbox => mailbox.ToString())),\n            DateUtc: message.Date.ToUniversalTime(),\n            TextBody: text ?? string.Empty,\n            TextTruncated: textTruncated,\n            HtmlBody: html ?? string.Empty,\n            HtmlTruncated: htmlTruncated,\n            HasAttachments: attachments.Length > 0,\n            Attachments: attachments);\n    }\n\n    private static string? TruncateUtf8(string? value, long maxBytes, out bool truncated)\n    {\n        truncated = false;\n        if (string.IsNullOrEmpty(value))\n        {\n            return value;\n        }\n\n        var bytes = Encoding.UTF8.GetBytes(value);\n        if (bytes.LongLength <= maxBytes)\n        {\n            return value;\n        }\n\n        truncated = true;\n        var truncatedBytes = new byte[(int)Math.Min(maxBytes, int.MaxValue)];\n        Array.Copy(bytes, truncatedBytes, truncatedBytes.Length);\n        return Encoding.UTF8.GetString(truncatedBytes);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapMoveOperations.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Executes IMAP move operations with portable fallback for servers without MOVE support.\n/// </summary>\npublic static class ImapMoveOperations {\n    /// <summary>\n    /// Abstraction over IMAP folder move operations for testability.\n    /// </summary>\n    public interface IImapMoveFolder {\n        /// <summary>Folder full name.</summary>\n        string FullName { get; }\n\n        /// <summary>True when folder is open.</summary>\n        bool IsOpen { get; }\n\n        /// <summary>Current folder access when opened.</summary>\n        FolderAccess Access { get; }\n\n        /// <summary>Opens the folder with requested access.</summary>\n        Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default);\n\n        /// <summary>Closes the folder.</summary>\n        Task CloseAsync(bool expunge, CancellationToken cancellationToken = default);\n\n        /// <summary>Fetches raw Message-Id header value for a message UID.</summary>\n        Task<string?> GetMessageIdHeaderAsync(UniqueId uid, CancellationToken cancellationToken = default);\n\n        /// <summary>Moves one UID to destination folder.</summary>\n        Task MoveToAsync(UniqueId uid, IImapMoveFolder destination, CancellationToken cancellationToken = default);\n\n        /// <summary>Copies UIDs to destination and returns UID mapping when available.</summary>\n        Task<IDictionary<UniqueId, UniqueId>?> CopyToWithMapAsync(IReadOnlyCollection<UniqueId> uids, IImapMoveFolder destination, CancellationToken cancellationToken = default);\n\n        /// <summary>Copies one UID to destination folder.</summary>\n        Task CopyToAsync(UniqueId uid, IImapMoveFolder destination, CancellationToken cancellationToken = default);\n\n        /// <summary>Adds flags to one UID.</summary>\n        Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n\n        /// <summary>Expunges the folder.</summary>\n        Task ExpungeAsync(CancellationToken cancellationToken = default);\n\n        /// <summary>Searches destination for Message-Id matches.</summary>\n        Task<IList<UniqueId>> SearchByMessageIdAsync(string messageId, CancellationToken cancellationToken = default);\n    }\n\n    private sealed class MailKitMoveFolderAdapter : IImapMoveFolder {\n        private readonly IMailFolder _folder;\n\n        internal MailKitMoveFolderAdapter(IMailFolder folder) {\n            _folder = folder ?? throw new ArgumentNullException(nameof(folder));\n        }\n\n        public string FullName => _folder.FullName;\n\n        public bool IsOpen => _folder.IsOpen;\n\n        public FolderAccess Access => _folder.Access;\n\n        public Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default) =>\n            _folder.OpenAsync(access, cancellationToken);\n\n        public Task CloseAsync(bool expunge, CancellationToken cancellationToken = default) =>\n            _folder.CloseAsync(expunge, cancellationToken);\n\n        public async Task<string?> GetMessageIdHeaderAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            var headers = await _folder.GetHeadersAsync(uid, cancellationToken).ConfigureAwait(false);\n            return headers[HeaderId.MessageId];\n        }\n\n        public Task MoveToAsync(UniqueId uid, IImapMoveFolder destination, CancellationToken cancellationToken = default) =>\n            _folder.MoveToAsync(uid, AsMailKitFolder(destination), cancellationToken);\n\n        public async Task<IDictionary<UniqueId, UniqueId>?> CopyToWithMapAsync(IReadOnlyCollection<UniqueId> uids, IImapMoveFolder destination, CancellationToken cancellationToken = default) {\n            var map = await _folder.CopyToAsync(new List<UniqueId>(uids), AsMailKitFolder(destination), cancellationToken).ConfigureAwait(false);\n            if (map == null) {\n                return null;\n            }\n\n            var resolved = new Dictionary<UniqueId, UniqueId>(uids.Count);\n            foreach (var uid in uids) {\n                if (map.TryGetValue(uid, out var mapped)) {\n                    resolved[uid] = mapped;\n                }\n            }\n            return resolved;\n        }\n\n        public Task CopyToAsync(UniqueId uid, IImapMoveFolder destination, CancellationToken cancellationToken = default) =>\n            _folder.CopyToAsync(uid, AsMailKitFolder(destination), cancellationToken);\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            _folder.AddFlagsAsync(uid, flags, silent, cancellationToken);\n\n        public Task ExpungeAsync(CancellationToken cancellationToken = default) =>\n            _folder.ExpungeAsync(cancellationToken);\n\n        public Task<IList<UniqueId>> SearchByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            _folder.SearchAsync(SearchQuery.HeaderContains(\"Message-Id\", messageId), cancellationToken);\n\n        private static IMailFolder AsMailKitFolder(IImapMoveFolder folder) {\n            if (folder is not MailKitMoveFolderAdapter adapter) {\n                throw new ArgumentException(\"folder adapter type is not supported for MailKit operations.\", nameof(folder));\n            }\n\n            return adapter._folder;\n        }\n    }\n\n    /// <summary>\n    /// Per-message result item for IMAP move operations.\n    /// </summary>\n    public sealed class ImapMoveResultItem {\n        /// <summary>Source message UID.</summary>\n        public UniqueId Uid { get; set; }\n\n        /// <summary>True when move succeeded for this UID.</summary>\n        public bool Ok { get; set; }\n\n        /// <summary>Destination UID when available.</summary>\n        public long? TargetUid { get; set; }\n\n        /// <summary>Normalized Message-Id used for destination lookup.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>True when MOVE fell back to COPY + DELETE.</summary>\n        public bool UsedCopyFallback { get; set; }\n\n        /// <summary>Failure reason when <see cref=\"Ok\"/> is false.</summary>\n        public string? Error { get; set; }\n    }\n\n    /// <summary>\n    /// Aggregate result for IMAP move operations.\n    /// </summary>\n    public sealed class ImapMoveResult {\n        /// <summary>Resolved source folder full name.</summary>\n        public string? SourceFolder { get; set; }\n\n        /// <summary>Resolved target folder full name.</summary>\n        public string? TargetFolder { get; set; }\n\n        /// <summary>Total unique UIDs requested.</summary>\n        public int Requested { get; set; }\n\n        /// <summary>Count of successful moves.</summary>\n        public int Moved { get; set; }\n\n        /// <summary>Per-item results in request order.</summary>\n        public List<ImapMoveResultItem> Results { get; set; } = new();\n    }\n\n    /// <summary>\n    /// Moves one or many UIDs between folders using an IMAP client.\n    /// </summary>\n    public static async Task<ImapMoveResult> MoveAsync(\n        ImapClient client,\n        string sourceFolder,\n        string targetFolder,\n        IReadOnlyCollection<UniqueId> uids,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(sourceFolder)) {\n            throw new ArgumentException(\"sourceFolder is required.\", nameof(sourceFolder));\n        }\n        if (string.IsNullOrWhiteSpace(targetFolder)) {\n            throw new ArgumentException(\"targetFolder is required.\", nameof(targetFolder));\n        }\n\n        var source = new MailKitMoveFolderAdapter(client.GetFolder(sourceFolder));\n        var destination = new MailKitMoveFolderAdapter(client.GetFolder(targetFolder));\n        return await MoveAsync(source, destination, uids, sanitizeError, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Moves one or many UIDs between folders using abstract folders.\n    /// </summary>\n    public static async Task<ImapMoveResult> MoveAsync(\n        IImapMoveFolder source,\n        IImapMoveFolder destination,\n        IReadOnlyCollection<UniqueId> uids,\n        Func<string, string>? sanitizeError = null,\n        CancellationToken cancellationToken = default) {\n        if (source == null) {\n            throw new ArgumentNullException(nameof(source));\n        }\n        if (destination == null) {\n            throw new ArgumentNullException(nameof(destination));\n        }\n        if (uids == null) {\n            throw new ArgumentNullException(nameof(uids));\n        }\n\n        var unique = Deduplicate(uids);\n        var result = new ImapMoveResult {\n            SourceFolder = source.FullName,\n            TargetFolder = destination.FullName,\n            Requested = unique.Count,\n            Results = new List<ImapMoveResultItem>(unique.Count)\n        };\n\n        if (unique.Count == 0) {\n            return result;\n        }\n\n        if (source.FullName.Equals(destination.FullName, StringComparison.OrdinalIgnoreCase)) {\n            foreach (var uid in unique) {\n                result.Moved++;\n                result.Results.Add(new ImapMoveResultItem {\n                    Uid = uid,\n                    Ok = true,\n                    TargetUid = uid.Id,\n                    MessageId = null,\n                    UsedCopyFallback = false\n                });\n            }\n            return result;\n        }\n\n        await EnsureFolderAccessAsync(source, FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false);\n        await EnsureFolderAccessAsync(destination, FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false);\n\n        result.SourceFolder = source.FullName;\n        result.TargetFolder = destination.FullName;\n\n        var sourceNeedsExpunge = false;\n        foreach (var uid in unique) {\n            string? messageId = null;\n            long? targetUid = null;\n            var usedCopyFallback = false;\n\n            try {\n                try {\n                    var raw = await source.GetMessageIdHeaderAsync(uid, cancellationToken).ConfigureAwait(false);\n                    messageId = NormalizeMessageIdHeader(raw);\n                } catch {\n                    // best-effort\n                    messageId = null;\n                }\n\n                try {\n                    await source.MoveToAsync(uid, destination, cancellationToken).ConfigureAwait(false);\n                } catch (NotSupportedException) {\n                    usedCopyFallback = true;\n\n                    try {\n                        var map = await source.CopyToWithMapAsync(new[] { uid }, destination, cancellationToken).ConfigureAwait(false);\n                        if (map != null && map.TryGetValue(uid, out var mapped)) {\n                            targetUid = mapped.Id;\n                        }\n                    } catch {\n                        await source.CopyToAsync(uid, destination, cancellationToken).ConfigureAwait(false);\n                    }\n\n                    await source.AddFlagsAsync(uid, MessageFlags.Deleted, silent: true, cancellationToken).ConfigureAwait(false);\n                    sourceNeedsExpunge = true;\n                }\n\n                if (!targetUid.HasValue && !string.IsNullOrWhiteSpace(messageId)) {\n                    try {\n                        var lookupMessageId = messageId!;\n                        var matches = await destination.SearchByMessageIdAsync(lookupMessageId, cancellationToken).ConfigureAwait(false);\n                        if (matches.Count > 0) {\n                            targetUid = matches[matches.Count - 1].Id;\n                        }\n                    } catch {\n                        // best-effort\n                    }\n                }\n\n                result.Moved++;\n                result.Results.Add(new ImapMoveResultItem {\n                    Uid = uid,\n                    Ok = true,\n                    TargetUid = targetUid,\n                    MessageId = messageId,\n                    UsedCopyFallback = usedCopyFallback\n                });\n            } catch (OperationCanceledException) {\n                throw;\n            } catch (Exception ex) {\n                var error = ex.Message;\n                if (sanitizeError != null) {\n                    try {\n                        error = sanitizeError(error);\n                    } catch {\n                        // Ignore sanitizer failures.\n                    }\n                }\n\n                result.Results.Add(new ImapMoveResultItem {\n                    Uid = uid,\n                    Ok = false,\n                    TargetUid = targetUid,\n                    MessageId = messageId,\n                    UsedCopyFallback = usedCopyFallback,\n                    Error = error\n                });\n            }\n        }\n\n        if (sourceNeedsExpunge) {\n            try {\n                await source.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n            } catch {\n                // best-effort: copies already exist in destination.\n            }\n        }\n\n        return result;\n    }\n\n    private static async Task EnsureFolderAccessAsync(IImapMoveFolder folder, FolderAccess access, CancellationToken cancellationToken) {\n        if (folder.IsOpen && folder.Access != access) {\n            try {\n                await folder.CloseAsync(expunge: false, cancellationToken).ConfigureAwait(false);\n            } catch {\n                // best-effort\n            }\n        }\n\n        if (!folder.IsOpen || folder.Access != access) {\n            await folder.OpenAsync(access, cancellationToken).ConfigureAwait(false);\n        }\n\n        if (!folder.IsOpen || folder.Access != access) {\n            throw new InvalidOperationException($\"Folder is not currently open in {access} mode.\");\n        }\n    }\n\n    private static List<UniqueId> Deduplicate(IReadOnlyCollection<UniqueId> uids) {\n        var output = new List<UniqueId>(uids.Count);\n        var seen = new HashSet<uint>();\n        foreach (var uid in uids) {\n            var id = uid.Id;\n            if (id == 0 || !seen.Add(id)) {\n                continue;\n            }\n\n            output.Add(uid);\n        }\n\n        return output;\n    }\n\n    private static string? NormalizeMessageIdHeader(string? rawHeaderValue) {\n        if (string.IsNullOrWhiteSpace(rawHeaderValue)) {\n            return null;\n        }\n\n        var first = ExtractFirstMessageId(rawHeaderValue);\n        return ImapSentMessageOperations.NormalizeMessageIdToken(first);\n    }\n\n    private static string? ExtractFirstMessageId(string? raw) {\n        if (string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n\n        var parts = raw!.Split(\n            new[] { ' ', '\\t', '\\r', '\\n' },\n            StringSplitOptions.RemoveEmptyEntries);\n        return parts.Length == 0 ? null : parts[0];\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapRootFolderEnumerator.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for enumerating top-level IMAP folders.\n/// </summary>\npublic static class ImapRootFolderEnumerator {\n    /// <summary>\n    /// Asynchronously enumerates top-level folders for the given IMAP client.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static async IAsyncEnumerable<IMailFolder> EnumerateAsync(\n        ImapClient client,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n        var root = client.PersonalNamespaces.Count > 0\n            ? client.GetFolder(client.PersonalNamespaces[0])\n            : client.GetFolder(\"\");\n\n        foreach (var folder in await root.GetSubfoldersAsync(false, cancellationToken).ConfigureAwait(false)) {\n            yield return folder;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapSentFolderResolver.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Resolves IMAP Sent-folder names using explicit overrides, server attributes, and name heuristics.\n/// </summary>\npublic static class ImapSentFolderResolver {\n    /// <summary>\n    /// Lightweight IMAP folder metadata used by resolver heuristics.\n    /// </summary>\n    public sealed class ImapFolderInfo {\n        /// <summary>Folder full name as used by IMAP.</summary>\n        public string FullName { get; set; } = string.Empty;\n\n        /// <summary>Folder display name.</summary>\n        public string Name { get; set; } = string.Empty;\n\n        /// <summary>True when folder has the IMAP <see cref=\"FolderAttributes.Sent\"/> attribute.</summary>\n        public bool IsSent { get; set; }\n\n        /// <summary>True when folder has the IMAP <see cref=\"FolderAttributes.Trash\"/> attribute.</summary>\n        public bool IsTrash { get; set; }\n\n        /// <summary>True when folder has the IMAP <see cref=\"FolderAttributes.Archive\"/> attribute.</summary>\n        public bool IsArchive { get; set; }\n\n        /// <summary>True when folder has the IMAP <see cref=\"FolderAttributes.Junk\"/> attribute.</summary>\n        public bool IsJunk { get; set; }\n\n        /// <summary>True when folder has the IMAP <see cref=\"FolderAttributes.Drafts\"/> attribute.</summary>\n        public bool IsDrafts { get; set; }\n    }\n\n    /// <summary>\n    /// Resolved special-folder mappings for IMAP.\n    /// </summary>\n    public sealed class ImapSpecialFolderMappings {\n        /// <summary>Inbox folder name.</summary>\n        public string Inbox { get; set; } = \"INBOX\";\n\n        /// <summary>Sent folder name.</summary>\n        public string Sent { get; set; } = \"Sent\";\n\n        /// <summary>Drafts folder name, when resolved.</summary>\n        public string? Drafts { get; set; }\n\n        /// <summary>Trash folder name, when resolved.</summary>\n        public string? Trash { get; set; }\n\n        /// <summary>Archive folder name, when resolved.</summary>\n        public string? Archive { get; set; }\n\n        /// <summary>Junk folder name, when resolved.</summary>\n        public string? Junk { get; set; }\n    }\n\n    /// <summary>\n    /// Resolves Sent folder name by enumerating folders from an IMAP client.\n    /// </summary>\n    public static async Task<string> ResolveSentFolderNameAsync(\n        ImapClient client,\n        string? requestedFolder,\n        string? configuredFolder = null,\n        string fallbackFolder = \"Sent\",\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n\n        var requested = NormalizeOptionalFolder(requestedFolder);\n        if (requested != null) {\n            return requested;\n        }\n\n        var configured = NormalizeOptionalFolder(configuredFolder);\n        if (configured != null) {\n            return configured;\n        }\n\n        try {\n            var folders = await GetFoldersInfoAsync(client, cancellationToken).ConfigureAwait(false);\n            return ResolveSentFolderName(\n                requestedFolder: null,\n                configuredFolder: null,\n                folders: folders,\n                fallbackFolder: fallbackFolder);\n        } catch {\n            // Best-effort fallback for servers/providers that fail folder enumeration.\n            return NormalizeOptionalFolder(fallbackFolder) ?? \"Sent\";\n        }\n    }\n\n    /// <summary>\n    /// Resolves Sent folder name from known folder metadata.\n    /// </summary>\n    public static string ResolveSentFolderName(\n        string? requestedFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string fallbackFolder = \"Sent\") {\n        return ResolveSentFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: null,\n            folders: folders,\n            fallbackFolder: fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Sent folder name from known folder metadata.\n    /// </summary>\n    public static string ResolveSentFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string fallbackFolder = \"Sent\") {\n        var resolved = ResolveSpecialFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: configuredFolder,\n            folders: folders,\n            attributePredicate: x => x.IsSent,\n            looksLikePredicate: LooksLikeSentFolder,\n            heuristicScoreSelector: SentFolderHeuristicScore,\n            fallbackFolder: fallbackFolder);\n        return resolved ?? ResolveFolderName(null, null, fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Drafts folder name from known folder metadata.\n    /// </summary>\n    public static string? ResolveDraftsFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string? fallbackFolder = null) {\n        return ResolveSpecialFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: configuredFolder,\n            folders: folders,\n            attributePredicate: x => x.IsDrafts,\n            looksLikePredicate: LooksLikeDraftsFolder,\n            heuristicScoreSelector: DraftsFolderHeuristicScore,\n            fallbackFolder: fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Trash folder name from known folder metadata.\n    /// </summary>\n    public static string? ResolveTrashFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string? fallbackFolder = null) {\n        return ResolveSpecialFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: configuredFolder,\n            folders: folders,\n            attributePredicate: x => x.IsTrash,\n            looksLikePredicate: LooksLikeTrashFolder,\n            heuristicScoreSelector: TrashFolderHeuristicScore,\n            fallbackFolder: fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Archive folder name from known folder metadata.\n    /// </summary>\n    public static string? ResolveArchiveFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string? fallbackFolder = null) {\n        return ResolveSpecialFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: configuredFolder,\n            folders: folders,\n            attributePredicate: x => x.IsArchive,\n            looksLikePredicate: LooksLikeArchiveFolder,\n            heuristicScoreSelector: ArchiveFolderHeuristicScore,\n            fallbackFolder: fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Junk folder name from known folder metadata.\n    /// </summary>\n    public static string? ResolveJunkFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string? fallbackFolder = null) {\n        return ResolveSpecialFolderName(\n            requestedFolder: requestedFolder,\n            configuredFolder: configuredFolder,\n            folders: folders,\n            attributePredicate: x => x.IsJunk,\n            looksLikePredicate: LooksLikeJunkFolder,\n            heuristicScoreSelector: JunkFolderHeuristicScore,\n            fallbackFolder: fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves all primary special folders using configuration overrides, attributes, and heuristics.\n    /// </summary>\n    public static ImapSpecialFolderMappings ResolveSpecialFolderMappings(\n        string? configuredInboxFolder,\n        string? configuredSentFolder,\n        string? configuredDraftsFolder,\n        string? configuredTrashFolder,\n        string? configuredArchiveFolder,\n        string? configuredJunkFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        string? configuredImapFolder) {\n        var inboxFallback = ResolveFolderName(null, configuredImapFolder, \"INBOX\");\n        return new ImapSpecialFolderMappings {\n            Inbox = ResolveFolderName(requestedFolder: null, configuredFolder: configuredInboxFolder, fallbackFolder: inboxFallback),\n            Sent = ResolveSentFolderName(requestedFolder: null, configuredFolder: configuredSentFolder, folders: folders, fallbackFolder: \"Sent\"),\n            Drafts = ResolveDraftsFolderName(requestedFolder: null, configuredFolder: configuredDraftsFolder, folders: folders),\n            Trash = ResolveTrashFolderName(requestedFolder: null, configuredFolder: configuredTrashFolder, folders: folders),\n            Archive = ResolveArchiveFolderName(requestedFolder: null, configuredFolder: configuredArchiveFolder, folders: folders),\n            Junk = ResolveJunkFolderName(requestedFolder: null, configuredFolder: configuredJunkFolder, folders: folders)\n        };\n    }\n\n    /// <summary>\n    /// Lists IMAP folders with metadata suitable for special-folder resolution.\n    /// </summary>\n    public static async Task<IReadOnlyList<ImapFolderInfo>> ListFoldersInfoAsync(\n        ImapClient client,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n\n        var folders = await GetFoldersInfoAsync(client, cancellationToken).ConfigureAwait(false);\n        folders.Sort((a, b) => string.Compare(a.FullName, b.FullName, StringComparison.OrdinalIgnoreCase));\n        return folders;\n    }\n\n    /// <summary>\n    /// Lists IMAP folder full names.\n    /// </summary>\n    public static async Task<IReadOnlyList<string>> ListFoldersAsync(\n        ImapClient client,\n        CancellationToken cancellationToken = default) {\n        var folders = await ListFoldersInfoAsync(client, cancellationToken).ConfigureAwait(false);\n        var output = new List<string>(folders.Count);\n        foreach (var folder in folders) {\n            if (string.IsNullOrWhiteSpace(folder.FullName)) {\n                continue;\n            }\n\n            output.Add(folder.FullName);\n        }\n        return output;\n    }\n\n    private static async Task<List<ImapFolderInfo>> GetFoldersInfoAsync(ImapClient client, CancellationToken cancellationToken) {\n        var output = new List<ImapFolderInfo>();\n        if (client.PersonalNamespaces.Count > 0) {\n            foreach (var ns in client.PersonalNamespaces) {\n                var root = client.GetFolder(ns);\n                await CollectFoldersInfoAsync(root, output, cancellationToken).ConfigureAwait(false);\n            }\n        } else {\n            var inbox = client.Inbox;\n            if (inbox != null) {\n                await CollectFoldersInfoAsync(inbox, output, cancellationToken).ConfigureAwait(false);\n            }\n        }\n        return output;\n    }\n\n    private static async Task CollectFoldersInfoAsync(IMailFolder folder, List<ImapFolderInfo> output, CancellationToken cancellationToken) {\n        if (folder == null) {\n            return;\n        }\n\n        try {\n            var full = folder.FullName ?? folder.Name ?? string.Empty;\n            if (!string.IsNullOrWhiteSpace(full) &&\n                !output.Exists(x => string.Equals(x.FullName, full, StringComparison.OrdinalIgnoreCase))) {\n                output.Add(new ImapFolderInfo {\n                    FullName = full,\n                    Name = folder.Name ?? full,\n                    IsSent = folder.Attributes.HasFlag(FolderAttributes.Sent),\n                    IsTrash = folder.Attributes.HasFlag(FolderAttributes.Trash),\n                    IsArchive = folder.Attributes.HasFlag(FolderAttributes.Archive),\n                    IsJunk = folder.Attributes.HasFlag(FolderAttributes.Junk),\n                    IsDrafts = folder.Attributes.HasFlag(FolderAttributes.Drafts)\n                });\n            }\n        } catch {\n            // Best effort.\n        }\n\n        IList<IMailFolder> subfolders;\n        try {\n            subfolders = await folder.GetSubfoldersAsync(false, cancellationToken).ConfigureAwait(false);\n        } catch {\n            return;\n        }\n\n        foreach (var sub in subfolders) {\n            await CollectFoldersInfoAsync(sub, output, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private static bool LooksLikeSentFolder(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var lower = (value ?? string.Empty).Trim().ToLowerInvariant();\n        if (lower.Length == 0) {\n            return false;\n        }\n        if (lower.Contains(\"draft\", StringComparison.Ordinal) ||\n            lower.Contains(\"trash\", StringComparison.Ordinal) ||\n            lower.Contains(\"junk\", StringComparison.Ordinal) ||\n            lower.Contains(\"spam\", StringComparison.Ordinal) ||\n            lower.Contains(\"archive\", StringComparison.Ordinal)) {\n            return false;\n        }\n\n        var normalized = new string(lower.Select(ch => char.IsLetterOrDigit(ch) ? ch : ' ').ToArray());\n        var tokens = normalized\n            .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)\n            .Select(token => token.Trim())\n            .Where(token => token.Length > 0);\n        foreach (var token in tokens) {\n            if (token == \"sent\" || token == \"sentitems\" || token == \"sentmail\") {\n                return true;\n            }\n        }\n\n        return lower.Contains(\"/sent\", StringComparison.Ordinal) ||\n               lower.Contains(\".sent\", StringComparison.Ordinal) ||\n               lower.Contains(\"sent\", StringComparison.Ordinal);\n    }\n\n    private static int SentFolderHeuristicScore(ImapFolderInfo folder) {\n        var name = (folder.Name ?? string.Empty).Trim().ToLowerInvariant();\n        var full = (folder.FullName ?? string.Empty).Trim().ToLowerInvariant();\n\n        if (name == \"sent\" ||\n            full == \"sent\" ||\n            full.EndsWith(\"/sent\", StringComparison.Ordinal) ||\n            full.EndsWith(\".sent\", StringComparison.Ordinal)) {\n            return 0;\n        }\n\n        if (name is \"sent items\" or \"sent mail\" ||\n            full.Contains(\"/sent items\", StringComparison.Ordinal) ||\n            full.Contains(\"/sent mail\", StringComparison.Ordinal)) {\n            return 1;\n        }\n\n        if (full.Contains(\"sent\", StringComparison.Ordinal) || name.Contains(\"sent\", StringComparison.Ordinal)) {\n            return 2;\n        }\n\n        return 1000;\n    }\n\n    private static int DraftsFolderHeuristicScore(ImapFolderInfo folder) {\n        var name = (folder.Name ?? string.Empty).Trim().ToLowerInvariant();\n        var full = (folder.FullName ?? string.Empty).Trim().ToLowerInvariant();\n\n        if (name == \"drafts\" ||\n            full == \"drafts\" ||\n            full.EndsWith(\"/drafts\", StringComparison.Ordinal) ||\n            full.EndsWith(\".drafts\", StringComparison.Ordinal)) {\n            return 0;\n        }\n        if (name == \"draft\" ||\n            full.EndsWith(\"/draft\", StringComparison.Ordinal) ||\n            full.EndsWith(\".draft\", StringComparison.Ordinal)) {\n            return 1;\n        }\n        if (name.Contains(\"draft\", StringComparison.Ordinal) || full.Contains(\"draft\", StringComparison.Ordinal)) {\n            return 2;\n        }\n\n        return 1000;\n    }\n\n    private static int TrashFolderHeuristicScore(ImapFolderInfo folder) {\n        var name = (folder.Name ?? string.Empty).Trim().ToLowerInvariant();\n        var full = (folder.FullName ?? string.Empty).Trim().ToLowerInvariant();\n\n        if (name == \"trash\" ||\n            name == \"bin\" ||\n            full == \"trash\" ||\n            full.EndsWith(\"/trash\", StringComparison.Ordinal) ||\n            full.EndsWith(\".trash\", StringComparison.Ordinal)) {\n            return 0;\n        }\n        if (name is \"deleted items\" or \"deleted messages\" ||\n            full.Contains(\"/deleted items\", StringComparison.Ordinal) ||\n            full.Contains(\"/deleted messages\", StringComparison.Ordinal)) {\n            return 1;\n        }\n        if (name.Contains(\"trash\", StringComparison.Ordinal) || full.Contains(\"trash\", StringComparison.Ordinal) ||\n            name.Contains(\"deleted\", StringComparison.Ordinal) || full.Contains(\"deleted\", StringComparison.Ordinal) ||\n            name.Contains(\"bin\", StringComparison.Ordinal) || full.Contains(\"/bin\", StringComparison.Ordinal)) {\n            return 2;\n        }\n\n        return 1000;\n    }\n\n    private static int ArchiveFolderHeuristicScore(ImapFolderInfo folder) {\n        var name = (folder.Name ?? string.Empty).Trim().ToLowerInvariant();\n        var full = (folder.FullName ?? string.Empty).Trim().ToLowerInvariant();\n\n        if (name == \"archive\" ||\n            full == \"archive\" ||\n            full.EndsWith(\"/archive\", StringComparison.Ordinal) ||\n            full.EndsWith(\".archive\", StringComparison.Ordinal)) {\n            return 0;\n        }\n        if (name == \"all mail\" ||\n            full.EndsWith(\"/all mail\", StringComparison.Ordinal) ||\n            full.EndsWith(\".all mail\", StringComparison.Ordinal)) {\n            return 1;\n        }\n        if (name.Contains(\"archive\", StringComparison.Ordinal) || full.Contains(\"archive\", StringComparison.Ordinal) ||\n            name.Contains(\"all mail\", StringComparison.Ordinal) || full.Contains(\"all mail\", StringComparison.Ordinal)) {\n            return 2;\n        }\n\n        return 1000;\n    }\n\n    private static int JunkFolderHeuristicScore(ImapFolderInfo folder) {\n        var name = (folder.Name ?? string.Empty).Trim().ToLowerInvariant();\n        var full = (folder.FullName ?? string.Empty).Trim().ToLowerInvariant();\n\n        if (name == \"junk\" ||\n            name == \"spam\" ||\n            full == \"junk\" ||\n            full.EndsWith(\"/junk\", StringComparison.Ordinal) ||\n            full.EndsWith(\".junk\", StringComparison.Ordinal)) {\n            return 0;\n        }\n        if (name == \"junk e-mail\" || full.Contains(\"/junk e-mail\", StringComparison.Ordinal)) {\n            return 1;\n        }\n        if (name.Contains(\"junk\", StringComparison.Ordinal) || full.Contains(\"junk\", StringComparison.Ordinal) ||\n            name.Contains(\"spam\", StringComparison.Ordinal) || full.Contains(\"spam\", StringComparison.Ordinal) ||\n            name.Contains(\"bulk\", StringComparison.Ordinal) || full.Contains(\"bulk\", StringComparison.Ordinal)) {\n            return 2;\n        }\n\n        return 1000;\n    }\n\n    private static bool LooksLikeDraftsFolder(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var lower = (value ?? string.Empty).Trim().ToLowerInvariant();\n        if (lower.Length == 0) {\n            return false;\n        }\n        if (lower.Contains(\"sent\", StringComparison.Ordinal) ||\n            lower.Contains(\"trash\", StringComparison.Ordinal) ||\n            lower.Contains(\"junk\", StringComparison.Ordinal) ||\n            lower.Contains(\"spam\", StringComparison.Ordinal) ||\n            lower.Contains(\"archive\", StringComparison.Ordinal)) {\n            return false;\n        }\n\n        return lower.Contains(\"draft\", StringComparison.Ordinal);\n    }\n\n    private static bool LooksLikeTrashFolder(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var lower = (value ?? string.Empty).Trim().ToLowerInvariant();\n        if (lower.Length == 0) {\n            return false;\n        }\n        if (lower.Contains(\"sent\", StringComparison.Ordinal) ||\n            lower.Contains(\"draft\", StringComparison.Ordinal) ||\n            lower.Contains(\"junk\", StringComparison.Ordinal) ||\n            lower.Contains(\"spam\", StringComparison.Ordinal) ||\n            lower.Contains(\"archive\", StringComparison.Ordinal)) {\n            return false;\n        }\n\n        return lower.Contains(\"trash\", StringComparison.Ordinal) ||\n               lower.Contains(\"deleted\", StringComparison.Ordinal) ||\n               lower.Contains(\"bin\", StringComparison.Ordinal) ||\n               lower.Contains(\"waste\", StringComparison.Ordinal);\n    }\n\n    private static bool LooksLikeArchiveFolder(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var lower = (value ?? string.Empty).Trim().ToLowerInvariant();\n        if (lower.Length == 0) {\n            return false;\n        }\n        if (lower.Contains(\"sent\", StringComparison.Ordinal) ||\n            lower.Contains(\"draft\", StringComparison.Ordinal) ||\n            lower.Contains(\"trash\", StringComparison.Ordinal) ||\n            lower.Contains(\"junk\", StringComparison.Ordinal) ||\n            lower.Contains(\"spam\", StringComparison.Ordinal)) {\n            return false;\n        }\n\n        return lower.Contains(\"archive\", StringComparison.Ordinal) ||\n               lower.Contains(\"all mail\", StringComparison.Ordinal);\n    }\n\n    private static bool LooksLikeJunkFolder(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var lower = (value ?? string.Empty).Trim().ToLowerInvariant();\n        if (lower.Length == 0) {\n            return false;\n        }\n        if (lower.Contains(\"sent\", StringComparison.Ordinal) ||\n            lower.Contains(\"draft\", StringComparison.Ordinal) ||\n            lower.Contains(\"trash\", StringComparison.Ordinal) ||\n            lower.Contains(\"archive\", StringComparison.Ordinal)) {\n            return false;\n        }\n\n        return lower.Contains(\"junk\", StringComparison.Ordinal) ||\n               lower.Contains(\"spam\", StringComparison.Ordinal) ||\n               lower.Contains(\"bulk\", StringComparison.Ordinal);\n    }\n\n    private static string? ResolveSpecialFolderName(\n        string? requestedFolder,\n        string? configuredFolder,\n        IEnumerable<ImapFolderInfo>? folders,\n        Func<ImapFolderInfo, bool> attributePredicate,\n        Func<string?, bool> looksLikePredicate,\n        Func<ImapFolderInfo, int> heuristicScoreSelector,\n        string? fallbackFolder) {\n        var requested = NormalizeOptionalFolder(requestedFolder);\n        if (requested != null) {\n            return requested;\n        }\n\n        var configured = NormalizeOptionalFolder(configuredFolder);\n        if (configured != null) {\n            return configured;\n        }\n\n        var available = folders?\n            .Where(x => !string.IsNullOrWhiteSpace(x.FullName))\n            .Select(x => new ImapFolderInfo {\n                FullName = x.FullName.Trim(),\n                Name = (x.Name ?? string.Empty).Trim(),\n                IsSent = x.IsSent,\n                IsTrash = x.IsTrash,\n                IsArchive = x.IsArchive,\n                IsJunk = x.IsJunk,\n                IsDrafts = x.IsDrafts\n            })\n            .ToList() ?? new List<ImapFolderInfo>();\n\n        if (available.Count == 0) {\n            return NormalizeOptionalFolder(fallbackFolder);\n        }\n\n        var byAttribute = available.FirstOrDefault(attributePredicate);\n        if (byAttribute != null) {\n            return byAttribute.FullName;\n        }\n\n        var byHeuristic = available\n            .Where(x => looksLikePredicate(x.Name) || looksLikePredicate(x.FullName))\n            .OrderBy(heuristicScoreSelector)\n            .ThenBy(x => x.FullName.Length)\n            .FirstOrDefault();\n        if (byHeuristic != null) {\n            return byHeuristic.FullName;\n        }\n\n        return NormalizeOptionalFolder(fallbackFolder);\n    }\n\n    private static string ResolveFolderName(string? requestedFolder, string? configuredFolder, string fallbackFolder = \"INBOX\") {\n        var requested = NormalizeOptionalFolder(requestedFolder);\n        if (requested != null) {\n            return requested;\n        }\n\n        var configured = NormalizeOptionalFolder(configuredFolder);\n        if (configured != null) {\n            return configured;\n        }\n\n        var fallback = NormalizeOptionalFolder(fallbackFolder);\n        return fallback ?? \"INBOX\";\n    }\n\n    private static string? NormalizeOptionalFolder(string? value) {\n        var trimmed = value?.Trim();\n        return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/ImapSentMessageOperations.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// IMAP helper operations used by send/idempotency pipelines around Sent-folder behavior.\n/// </summary>\npublic static class ImapSentMessageOperations {\n    /// <summary>\n    /// Abstraction over IMAP sent-folder operations for testability.\n    /// </summary>\n    public interface IImapSentFolder {\n        /// <summary>Folder full name.</summary>\n        string FullName { get; }\n\n        /// <summary>True when folder is open.</summary>\n        bool IsOpen { get; }\n\n        /// <summary>Current folder access when opened.</summary>\n        FolderAccess Access { get; }\n\n        /// <summary>Opens the folder with requested access.</summary>\n        Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default);\n\n        /// <summary>Closes the folder.</summary>\n        Task CloseAsync(bool expunge, CancellationToken cancellationToken = default);\n\n        /// <summary>Appends a MIME message.</summary>\n        Task AppendAsync(MimeMessage message, MessageFlags flags, CancellationToken cancellationToken = default);\n\n        /// <summary>Searches for matching message UIDs.</summary>\n        Task<IList<UniqueId>> SearchAsync(SearchQuery query, CancellationToken cancellationToken = default);\n\n        /// <summary>Fetches envelope message-id for a single UID.</summary>\n        Task<string?> FetchEnvelopeMessageIdAsync(UniqueId uid, CancellationToken cancellationToken = default);\n\n        /// <summary>Fetches threading metadata for a single UID.</summary>\n        Task<ImapThreadingMetadataResult?> FetchThreadingMetadataAsync(UniqueId uid, CancellationToken cancellationToken = default);\n    }\n\n    private sealed class MailKitSentFolderAdapter : IImapSentFolder {\n        private readonly IMailFolder _folder;\n\n        internal MailKitSentFolderAdapter(IMailFolder folder) {\n            _folder = folder ?? throw new ArgumentNullException(nameof(folder));\n        }\n\n        public string FullName => _folder.FullName;\n\n        public bool IsOpen => _folder.IsOpen;\n\n        public FolderAccess Access => _folder.Access;\n\n        public Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default) =>\n            _folder.OpenAsync(access, cancellationToken);\n\n        public Task CloseAsync(bool expunge, CancellationToken cancellationToken = default) =>\n            _folder.CloseAsync(expunge, cancellationToken);\n\n        public Task AppendAsync(MimeMessage message, MessageFlags flags, CancellationToken cancellationToken = default) =>\n            _folder.AppendAsync(message, flags, cancellationToken);\n\n        public Task<IList<UniqueId>> SearchAsync(SearchQuery query, CancellationToken cancellationToken = default) =>\n            _folder.SearchAsync(query, cancellationToken);\n\n        public async Task<string?> FetchEnvelopeMessageIdAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            var summaries = await _folder.FetchAsync(new[] { uid }, MessageSummaryItems.Envelope, cancellationToken).ConfigureAwait(false);\n            if (summaries == null || summaries.Count == 0) {\n                return null;\n            }\n\n            return NormalizeOptional(summaries[0].Envelope?.MessageId);\n        }\n\n        public async Task<ImapThreadingMetadataResult?> FetchThreadingMetadataAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            var summaries = await _folder.FetchAsync(\n                new[] { uid },\n                MessageSummaryItems.Envelope | MessageSummaryItems.References | MessageSummaryItems.UniqueId,\n                cancellationToken).ConfigureAwait(false);\n            if (summaries == null || summaries.Count == 0) {\n                return null;\n            }\n\n            var summary = summaries[0];\n            var env = summary.Envelope;\n            return new ImapThreadingMetadataResult {\n                MessageId = NormalizeOptional(env?.MessageId),\n                ReplyTo = NormalizeOptional(env?.ReplyTo?.ToString()),\n                Cc = NormalizeOptional(env?.Cc?.ToString()),\n                InReplyTo = NormalizeOptional(env?.InReplyTo),\n                References = summary.References is null ? null : new List<string>(summary.References)\n            };\n        }\n    }\n\n    /// <summary>\n    /// Result of Sent-folder append attempt.\n    /// </summary>\n    public sealed class ImapSentAppendResult {\n        /// <summary>True when append succeeded.</summary>\n        public bool Appended { get; set; }\n\n        /// <summary>Resolved folder name where message was appended.</summary>\n        public string? Folder { get; set; }\n    }\n\n    /// <summary>\n    /// Result of duplicate probe in Sent folder.\n    /// </summary>\n    public sealed class ImapSentDuplicateProbeResult {\n        /// <summary>True when duplicate was found.</summary>\n        public bool IsMatch { get; set; }\n\n        /// <summary>Resolved folder name that was probed.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Matched message-id when available.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Threading metadata fetched from IMAP summary headers.\n    /// </summary>\n    public sealed class ImapThreadingMetadataResult {\n        /// <summary>Message-Id value.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Reply-To value.</summary>\n        public string? ReplyTo { get; set; }\n\n        /// <summary>Cc value.</summary>\n        public string? Cc { get; set; }\n\n        /// <summary>In-Reply-To value.</summary>\n        public string? InReplyTo { get; set; }\n\n        /// <summary>References values.</summary>\n        public List<string>? References { get; set; }\n    }\n\n    /// <summary>\n    /// Appends a MIME message to Sent using resolved Sent-folder selection.\n    /// </summary>\n    public static async Task<ImapSentAppendResult> AppendToSentAsync(\n        ImapClient client,\n        MimeMessage message,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        string fallbackFolder = \"Sent\",\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var sentFolder = await ImapSentFolderResolver.ResolveSentFolderNameAsync(\n            client,\n            requestedSentFolder,\n            configuredSentFolder,\n            fallbackFolder,\n            cancellationToken).ConfigureAwait(false);\n        var folder = new MailKitSentFolderAdapter(client.GetCachedFolder(sentFolder, FolderAccess.ReadOnly));\n        return await AppendToSentAsync(folder, message, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Appends a MIME message to Sent using an abstract folder.\n    /// </summary>\n    public static async Task<ImapSentAppendResult> AppendToSentAsync(\n        IImapSentFolder folder,\n        MimeMessage message,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        await EnsureFolderAccessAsync(folder, FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false);\n        await folder.AppendAsync(message, MessageFlags.Seen, cancellationToken).ConfigureAwait(false);\n        return new ImapSentAppendResult {\n            Appended = true,\n            Folder = folder.FullName\n        };\n    }\n\n    /// <summary>\n    /// Probes Sent for duplicate send marker by idempotency header, with optional Message-Id fallback.\n    /// </summary>\n    public static async Task<ImapSentDuplicateProbeResult> FindSentDuplicateAsync(\n        ImapClient client,\n        string idempotencyHeaderName,\n        string idempotencyKey,\n        string? messageIdToken,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        string fallbackFolder = \"Sent\",\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n\n        var sentFolder = await ImapSentFolderResolver.ResolveSentFolderNameAsync(\n            client,\n            requestedSentFolder,\n            configuredSentFolder,\n            fallbackFolder,\n            cancellationToken).ConfigureAwait(false);\n        var folder = new MailKitSentFolderAdapter(client.GetCachedFolder(sentFolder, FolderAccess.ReadOnly));\n        var result = await FindSentDuplicateAsync(\n            folder,\n            idempotencyHeaderName,\n            idempotencyKey,\n            messageIdToken,\n            cancellationToken).ConfigureAwait(false);\n        result.Folder = sentFolder;\n        return result;\n    }\n\n    /// <summary>\n    /// Probes Sent for duplicate send marker by idempotency header, with optional Message-Id fallback.\n    /// </summary>\n    public static async Task<ImapSentDuplicateProbeResult> FindSentDuplicateAsync(\n        IImapSentFolder folder,\n        string idempotencyHeaderName,\n        string idempotencyKey,\n        string? messageIdToken,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyHeaderName)) {\n            throw new ArgumentException(\"idempotencyHeaderName is required.\", nameof(idempotencyHeaderName));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyKey)) {\n            throw new ArgumentException(\"idempotencyKey is required.\", nameof(idempotencyKey));\n        }\n\n        await EnsureFolderAccessAsync(folder, FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);\n\n        var uids = await folder.SearchAsync(\n            SearchQuery.HeaderContains(idempotencyHeaderName.Trim(), idempotencyKey.Trim()),\n            cancellationToken).ConfigureAwait(false);\n        if (uids.Count == 0) {\n            var normalizedMessageId = NormalizeMessageIdToken(messageIdToken);\n            if (!string.IsNullOrWhiteSpace(normalizedMessageId)) {\n                uids = await folder.SearchAsync(\n                    SearchQuery.HeaderContains(\"Message-Id\", normalizedMessageId!),\n                    cancellationToken).ConfigureAwait(false);\n            }\n        }\n\n        if (uids.Count == 0) {\n            return new ImapSentDuplicateProbeResult();\n        }\n\n        var matchedMessageId = await folder.FetchEnvelopeMessageIdAsync(uids[0], cancellationToken).ConfigureAwait(false);\n        return new ImapSentDuplicateProbeResult {\n            IsMatch = true,\n            Folder = folder.FullName,\n            MessageId = string.IsNullOrWhiteSpace(matchedMessageId) ? messageIdToken : matchedMessageId\n        };\n    }\n\n    /// <summary>\n    /// Gets threading metadata for a message identified by folder + UID.\n    /// </summary>\n    public static async Task<ImapThreadingMetadataResult?> GetThreadingMetadataAsync(\n        ImapClient client,\n        string folder,\n        uint uid,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (string.IsNullOrWhiteSpace(folder)) {\n            throw new ArgumentException(\"folder is required.\", nameof(folder));\n        }\n        if (uid == 0) {\n            throw new ArgumentOutOfRangeException(nameof(uid), \"uid must be greater than zero.\");\n        }\n\n        var mailFolder = new MailKitSentFolderAdapter(client.GetCachedFolder(folder, FolderAccess.ReadOnly));\n        return await GetThreadingMetadataAsync(mailFolder, new UniqueId(uid), cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Gets threading metadata for a message in an abstract folder.\n    /// </summary>\n    public static async Task<ImapThreadingMetadataResult?> GetThreadingMetadataAsync(\n        IImapSentFolder folder,\n        UniqueId uid,\n        CancellationToken cancellationToken = default) {\n        if (folder == null) {\n            throw new ArgumentNullException(nameof(folder));\n        }\n\n        await EnsureFolderAccessAsync(folder, FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);\n        return await folder.FetchThreadingMetadataAsync(uid, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Normalizes a message-id token by trimming whitespace and angle brackets.\n    /// </summary>\n    public static string? NormalizeMessageIdToken(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var token = value!.Trim();\n        if (token.StartsWith(\"<\", StringComparison.Ordinal)) {\n            token = token.Substring(1);\n        }\n        if (token.EndsWith(\">\", StringComparison.Ordinal)) {\n            token = token.Substring(0, token.Length - 1);\n        }\n        token = token.Trim();\n        return token.Length == 0 ? null : token;\n    }\n\n    private static async Task EnsureFolderAccessAsync(\n        IImapSentFolder folder,\n        FolderAccess access,\n        CancellationToken cancellationToken) {\n        if (folder.IsOpen && folder.Access != access) {\n            try {\n                await folder.CloseAsync(expunge: false, cancellationToken).ConfigureAwait(false);\n            } catch {\n                // best-effort\n            }\n        }\n\n        if (!folder.IsOpen || folder.Access != access) {\n            await folder.OpenAsync(access, cancellationToken).ConfigureAwait(false);\n        }\n\n        if (!folder.IsOpen || folder.Access != access) {\n            throw new InvalidOperationException($\"Folder is not currently open in {access} mode.\");\n        }\n    }\n\n    private static string? NormalizeOptional(string? value) {\n        var trimmed = (value ?? string.Empty).Trim();\n        return trimmed.Length == 0 ? null : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/JunkCleaner.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Helper methods for clearing junk mail folders.\n/// </summary>\npublic static class JunkCleaner {\n    private sealed class JunkSkipCriteria {\n        public HashSet<string>? MessageIds { get; init; }\n        public HashSet<uint>? Uids { get; init; }\n        public HashSet<string>? From { get; init; }\n        public HashSet<string>? To { get; init; }\n        public string[]? SubjectTokens { get; init; }\n        public HashSet<string>? AttachmentExtensions { get; init; }\n    }\n\n    /// <summary>\n    /// Deletes all messages from the specified junk folder.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Folder name or <c>null</c> for \"Junk\".</param>\n    /// <param name=\"skipFrom\">Sender addresses to exclude.</param>\n    /// <param name=\"skipTo\">Recipient addresses to exclude.</param>\n    /// <param name=\"skipSubjectContains\">Subjects that, if contained, will exclude the message.</param>\n    /// <param name=\"skipMessageId\">Message IDs to exclude.</param>\n    /// <param name=\"skipUid\">Message UIDs to exclude.</param>\n    /// <param name=\"skipHasAttachment\">When set, skip messages that contain attachments.</param>\n    /// <param name=\"skipAttachmentExtension\">Attachment extensions that, if present, will cause skipping.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static async Task ClearImapJunkAsync(\n        ImapClient client,\n        string? folder = null,\n        IEnumerable<string>? skipFrom = null,\n        IEnumerable<string>? skipTo = null,\n        IEnumerable<string>? skipSubjectContains = null,\n        IEnumerable<string>? skipMessageId = null,\n        IEnumerable<uint>? skipUid = null,\n        bool skipHasAttachment = false,\n        IEnumerable<string>? skipAttachmentExtension = null,\n        CancellationToken cancellationToken = default) {\n        await ClearImapJunkAsync(\n            client,\n            folder,\n            skipFrom,\n            skipTo,\n            skipSubjectContains,\n            skipMessageId,\n            skipUid,\n            skipHasAttachment,\n            skipAttachmentExtension,\n            dryRun: false,\n            cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes all messages from the specified junk folder, optionally simulating the change.\n    /// </summary>\n    public static async Task ClearImapJunkAsync(\n        ImapClient client,\n        string? folder,\n        IEnumerable<string>? skipFrom,\n        IEnumerable<string>? skipTo,\n        IEnumerable<string>? skipSubjectContains,\n        IEnumerable<string>? skipMessageId,\n        IEnumerable<uint>? skipUid,\n        bool skipHasAttachment,\n        IEnumerable<string>? skipAttachmentExtension,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var junk = client.GetCachedFolder(folder ?? \"Junk\", FolderAccess.ReadWrite);\n        var uids = await junk.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);\n        if (uids.Count == 0) {\n            return;\n        }\n\n        var criteria = CreateSkipCriteria(skipFrom, skipTo, skipSubjectContains, skipMessageId, skipUid, skipAttachmentExtension);\n        var toDelete = new List<UniqueId>(uids.Count);\n        foreach (var uid in uids) {\n            if (criteria.Uids != null && criteria.Uids.Contains(uid.Id)) continue;\n            var message = await junk.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n            if (ShouldSkipMessage(message, skipHasAttachment, criteria)) {\n                continue;\n            }\n            toDelete.Add(uid);\n        }\n\n        if (toDelete.Count == 0) return;\n\n        await junk.AddFlagsAsync(toDelete, MessageFlags.Deleted, true, cancellationToken).ConfigureAwait(false);\n        await junk.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Retrieves messages from the specified junk folder without deleting them.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Folder name or <c>null</c> for \"Junk\".</param>\n    /// <param name=\"skipFrom\">Sender addresses to exclude.</param>\n    /// <param name=\"skipTo\">Recipient addresses to exclude.</param>\n    /// <param name=\"skipSubjectContains\">Subjects that, if contained, will exclude the message.</param>\n    /// <param name=\"skipMessageId\">Message IDs to exclude.</param>\n    /// <param name=\"skipUid\">Message UIDs to exclude.</param>\n    /// <param name=\"skipHasAttachment\">When set, skip messages that contain attachments.</param>\n    /// <param name=\"skipAttachmentExtension\">Attachment extensions that, if present, will cause skipping.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static async IAsyncEnumerable<ImapEmailMessage> GetImapJunkAsync(\n        ImapClient client,\n        string? folder = null,\n        IEnumerable<string>? skipFrom = null,\n        IEnumerable<string>? skipTo = null,\n        IEnumerable<string>? skipSubjectContains = null,\n        IEnumerable<string>? skipMessageId = null,\n        IEnumerable<uint>? skipUid = null,\n        bool skipHasAttachment = false,\n        IEnumerable<string>? skipAttachmentExtension = null,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n        var junk = client.GetCachedFolder(folder ?? \"Junk\", FolderAccess.ReadOnly);\n        var uids = await junk.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);\n        var criteria = CreateSkipCriteria(skipFrom, skipTo, skipSubjectContains, skipMessageId, skipUid, skipAttachmentExtension);\n        foreach (var uid in uids) {\n            if (criteria.Uids != null && criteria.Uids.Contains(uid.Id)) continue;\n            var message = await junk.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n            if (ShouldSkipMessage(message, skipHasAttachment, criteria)) {\n                continue;\n            }\n            yield return new ImapEmailMessage(uid, message);\n        }\n    }\n\n    private static JunkSkipCriteria CreateSkipCriteria(\n        IEnumerable<string>? skipFrom,\n        IEnumerable<string>? skipTo,\n        IEnumerable<string>? skipSubjectContains,\n        IEnumerable<string>? skipMessageId,\n        IEnumerable<uint>? skipUid,\n        IEnumerable<string>? skipAttachmentExtension) =>\n        new() {\n            MessageIds = skipMessageId != null ? new HashSet<string>(skipMessageId, StringComparer.OrdinalIgnoreCase) : null,\n            Uids = skipUid != null ? new HashSet<uint>(skipUid) : null,\n            From = skipFrom != null ? new HashSet<string>(skipFrom, StringComparer.OrdinalIgnoreCase) : null,\n            To = skipTo != null ? new HashSet<string>(skipTo, StringComparer.OrdinalIgnoreCase) : null,\n            SubjectTokens = skipSubjectContains?\n                .Where(value => !string.IsNullOrWhiteSpace(value))\n                .ToArray(),\n            AttachmentExtensions = NormalizeAttachmentExtensions(skipAttachmentExtension)\n        };\n\n    private static HashSet<string>? NormalizeAttachmentExtensions(IEnumerable<string>? skipAttachmentExtension) {\n        if (skipAttachmentExtension == null) {\n            return null;\n        }\n\n        var normalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var extension in skipAttachmentExtension) {\n            if (string.IsNullOrWhiteSpace(extension)) {\n                continue;\n            }\n\n            var value = extension.Trim().TrimStart('.');\n            if (value.Length > 0) {\n                normalized.Add(value);\n            }\n        }\n\n        return normalized;\n    }\n\n    private static bool ShouldSkipMessage(MimeMessage message, bool skipHasAttachment, JunkSkipCriteria criteria) {\n        if (criteria.MessageIds != null && message.MessageId != null && criteria.MessageIds.Contains(message.MessageId)) {\n            return true;\n        }\n\n        if (criteria.From != null && message.From.Mailboxes.Any(mailbox => criteria.From.Contains(mailbox.Address))) {\n            return true;\n        }\n\n        if (criteria.To != null && message.To.Mailboxes.Any(mailbox => criteria.To.Contains(mailbox.Address))) {\n            return true;\n        }\n\n        if (criteria.SubjectTokens != null && message.Subject != null &&\n            criteria.SubjectTokens.Any(token => message.Subject.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)) {\n            return true;\n        }\n\n        if (skipHasAttachment && message.Attachments.Any()) {\n            return true;\n        }\n\n        if (criteria.AttachmentExtensions != null && message.Attachments.OfType<MimePart>().Any(att =>\n                criteria.AttachmentExtensions.Contains(System.IO.Path.GetExtension(att.FileName ?? string.Empty).TrimStart('.')))) {\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/MailboxSearcher.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing MailKit.Search;\nusing MimeKit;\nusing System.Text;\nusing System.Globalization;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Mailozaurr.NonDeliveryReports;\nusing Mailozaurr.DmarcReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides mailbox search helpers for IMAP and POP3.\n/// </summary>\npublic static class MailboxSearcher {\n    /// <summary>\n    /// Searches an IMAP mailbox and returns matching messages.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Optional folder to search. Defaults to the inbox.</param>\n    /// <param name=\"subject\">Optional \"subject contains\" filter.</param>\n    /// <param name=\"fromContains\">Optional \"from contains\" filter.</param>\n    /// <param name=\"toContains\">Optional \"to contains\" filter.</param>\n    /// <param name=\"bodyContains\">Optional \"body contains\" filter.</param>\n    /// <param name=\"priority\">Optional message priority to match.</param>\n    /// <param name=\"since\">Optional UTC lower bound for message delivery dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message delivery dates.</param>\n    /// <param name=\"hasAttachment\">If true, only messages with attachments are returned.</param>\n    /// <param name=\"additionalQueries\">Additional IMAP search queries to combine.</param>\n    /// <param name=\"maxResults\">Maximum number of results to return. Use 0 for unlimited.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    /// <param name=\"queryString\">Optional free-form query string parsed into filters.</param>\n    public static async Task<IList<ImapEmailMessage>> SearchImapAsync(\n        ImapClient client,\n        string? folder = null,\n        string? subject = null,\n        string? fromContains = null,\n        string? toContains = null,\n        string? bodyContains = null,\n        MessagePriority? priority = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        bool hasAttachment = false,\n        IEnumerable<SearchQuery>? additionalQueries = null,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default,\n        string? queryString = null) {\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadOnly);\n        SearchQuery search = SearchQuery.All;\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (!string.IsNullOrWhiteSpace(subject)) search = search.And(SearchQuery.SubjectContains(subject!));\n        if (!string.IsNullOrWhiteSpace(fromContains)) search = search.And(SearchQuery.FromContains(fromContains!));\n        if (!string.IsNullOrWhiteSpace(toContains)) search = search.And(SearchQuery.ToContains(toContains!));\n        if (!string.IsNullOrWhiteSpace(bodyContains)) search = search.And(SearchQuery.BodyContains(bodyContains!));\n        if (sinceUtc.HasValue) search = search.And(SearchQuery.DeliveredAfter(sinceUtc.Value));\n        if (beforeUtc.HasValue) search = search.And(SearchQuery.DeliveredBefore(beforeUtc.Value));\n        if (additionalQueries != null) {\n            foreach (var q in additionalQueries) {\n                if (q != null) search = search.And(q);\n            }\n        }\n          if (!string.IsNullOrWhiteSpace(queryString)) {\n              try {\n                  var parsed = ParseQuery(queryString);\n                  if (!string.IsNullOrWhiteSpace(parsed.Subject)) search = search.And(SearchQuery.SubjectContains(parsed.Subject!));\n                  if (!string.IsNullOrWhiteSpace(parsed.FromContains)) search = search.And(SearchQuery.FromContains(parsed.FromContains!));\n                  if (!string.IsNullOrWhiteSpace(parsed.ToContains)) search = search.And(SearchQuery.ToContains(parsed.ToContains!));\n                  if (!string.IsNullOrWhiteSpace(parsed.BodyContains)) search = search.And(SearchQuery.BodyContains(parsed.BodyContains!));\n                  var parsedSince = NormalizeToUtc(parsed.Since);\n                  var parsedBefore = NormalizeToUtc(parsed.Before);\n                  if (parsedSince.HasValue) search = search.And(SearchQuery.DeliveredAfter(parsedSince.Value));\n                  if (parsedBefore.HasValue) search = search.And(SearchQuery.DeliveredBefore(parsedBefore.Value));\n                  foreach (var q in parsed.AdditionalQueries) search = search.And(q);\n                  hasAttachment |= parsed.HasAttachment;\n                  if (!priority.HasValue) priority = parsed.Priority;\n              } catch (Exception ex) {\n                  LoggingMessages.Logger.WriteWarning(\"Failed to parse IMAP query string: {0}\", ex.Message);\n              }\n        }\n        var uids = await mailFolder.SearchAsync(search, cancellationToken).ConfigureAwait(false);\n        var result = new List<ImapEmailMessage>(uids.Count);\n        foreach (var uid in uids) {\n            var message = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n            if (hasAttachment && !message.Attachments.Any()) continue;\n            if (priority.HasValue && message.Priority != ConvertPriority(priority.Value)) continue;\n            result.Add(new ImapEmailMessage(uid, message));\n            if (maxResults > 0 && result.Count >= maxResults) break;\n        }\n        return result;\n    }\n\n    /// <summary>\n    /// Searches a POP3 mailbox and returns matching messages.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"subject\">Optional \"subject contains\" filter.</param>\n    /// <param name=\"fromContains\">Optional \"from contains\" filter.</param>\n    /// <param name=\"toContains\">Optional \"to contains\" filter.</param>\n    /// <param name=\"bodyContains\">Optional \"body contains\" filter.</param>\n    /// <param name=\"priority\">Optional message priority to match.</param>\n    /// <param name=\"since\">Optional UTC lower bound for message delivery dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message delivery dates.</param>\n    /// <param name=\"hasAttachment\">If true, only messages with attachments are returned.</param>\n    /// <param name=\"maxResults\">Maximum number of results to return. Use 0 for unlimited.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    /// <param name=\"queryString\">Optional free-form query string parsed into filters.</param>\n    public static async Task<IList<Pop3EmailMessage>> SearchPop3Async(\n        Pop3Client client,\n        string? subject = null,\n        string? fromContains = null,\n        string? toContains = null,\n        string? bodyContains = null,\n        MessagePriority? priority = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        bool hasAttachment = false,\n        int maxResults = 0,\n        CancellationToken cancellationToken = default,\n        string? queryString = null) {\n        if (!string.IsNullOrWhiteSpace(queryString)) {\n              try {\n                  var parsed = ParseQuery(queryString);\n                  if (string.IsNullOrWhiteSpace(subject)) subject = parsed.Subject;\n                  if (string.IsNullOrWhiteSpace(fromContains)) fromContains = parsed.FromContains;\n                  if (string.IsNullOrWhiteSpace(toContains)) toContains = parsed.ToContains;\n                  if (string.IsNullOrWhiteSpace(bodyContains)) bodyContains = parsed.BodyContains;\n                  if (!since.HasValue) since = parsed.Since;\n                  if (!before.HasValue) before = parsed.Before;\n                  if (!priority.HasValue) priority = parsed.Priority;\n                  hasAttachment |= parsed.HasAttachment;\n              } catch (Exception ex) {\n                  LoggingMessages.Logger.WriteWarning(\"Failed to parse POP3 query string: {0}\", ex.Message);\n              }\n        }\n        var results = new List<Pop3EmailMessage>();\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        for (int i = 0; i < client.Count; i++) {\n            var message = await client.GetMessageAsync(i, cancellationToken).ConfigureAwait(false);\n            if (!string.IsNullOrWhiteSpace(subject) && (message.Subject == null || message.Subject.IndexOf(subject, StringComparison.OrdinalIgnoreCase) < 0)) continue;\n              if (!string.IsNullOrWhiteSpace(fromContains) && !AddressMatches(message.From, fromContains!)) continue;\n              if (!string.IsNullOrWhiteSpace(toContains) && !AddressMatches(message.To, toContains!)) continue;\n            if (!string.IsNullOrWhiteSpace(bodyContains)) {\n                var textBody = message.TextBody ?? string.Empty;\n                var htmlBody = message.HtmlBody ?? string.Empty;\n                if (textBody.IndexOf(bodyContains, StringComparison.OrdinalIgnoreCase) < 0 &&\n                    htmlBody.IndexOf(bodyContains, StringComparison.OrdinalIgnoreCase) < 0) continue;\n            }\n            var msgDate = message.Date.UtcDateTime;\n            if (sinceUtc.HasValue && msgDate < sinceUtc.Value) continue;\n            if (beforeUtc.HasValue && msgDate > beforeUtc.Value) continue;\n            if (priority.HasValue && message.Priority != ConvertPriority(priority.Value)) continue;\n            if (hasAttachment && !message.Attachments.Any()) continue;\n            results.Add(new Pop3EmailMessage(i, message));\n            if (maxResults > 0 && results.Count >= maxResults) break;\n        }\n        return results;\n    }\n\n    /// <summary>\n    /// Searches for Non-Delivery Reports in an IMAP mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Optional folder to search. Defaults to the inbox.</param>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<NonDeliveryReport>> SearchNonDeliveryReportsAsync(\n        ImapClient client,\n        string? folder = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        CancellationToken cancellationToken = default) {\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadOnly);\n        var search = BuildNonDeliveryReportSearchQuery(since, before);\n        var uids = await mailFolder.SearchAsync(search, cancellationToken).ConfigureAwait(false);\n        var results = new List<NonDeliveryReport>();\n        if (parallelDownloadLimit <= 1) {\n            foreach (var uid in uids) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var msg = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n                var reports = FilterNonDeliveryReports(new[] { msg }, since, before, recipientContains, messageId);\n                if (reports.Count > 0) {\n                    foreach (var r in reports) {\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                        results.Add(r);\n                    }\n                    if (maxResults > 0 && results.Count >= maxResults) break;\n                }\n            }\n        } else {\n            var uidArray = uids.ToArray();\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, uidArray.Length)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        int current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= uidArray.Length || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = next++;\n                        }\n                        MimeMessage msg;\n                        try {\n                            msg = await mailFolder.GetMessageAsync(uidArray[current], cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterNonDeliveryReports(new[] { msg }, since, before, recipientContains, messageId);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    /// <summary>\n    /// Searches for Non-Delivery Reports in a POP3 mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<NonDeliveryReport>> SearchNonDeliveryReportsAsync(\n        Pop3Client client,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        CancellationToken cancellationToken = default) {\n        var results = new List<NonDeliveryReport>();\n        if (parallelDownloadLimit <= 1) {\n            for (int idx = 0; idx < client.Count; idx++) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var msg = await client.GetMessageAsync(idx, cancellationToken).ConfigureAwait(false);\n                var reports = FilterNonDeliveryReports(new[] { msg }, since, before, recipientContains, messageId);\n                if (reports.Count > 0) {\n                    foreach (var r in reports) {\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                        results.Add(r);\n                    }\n                    if (maxResults > 0 && results.Count >= maxResults) break;\n                }\n            }\n        } else {\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, client.Count)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        int current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= client.Count || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = next++;\n                        }\n                        MimeMessage msg;\n                        try {\n                            msg = await client.GetMessageAsync(current, cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterNonDeliveryReports(new[] { msg }, since, before, recipientContains, messageId);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    /// <summary>\n    /// Searches for Non-Delivery Reports using Microsoft Graph.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential used to access the mailbox.</param>\n    /// <param name=\"userPrincipalName\">UPN of the mailbox to search.</param>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<NonDeliveryReport>> SearchNonDeliveryReportsAsync(\n        GraphCredential credential,\n        string userPrincipalName,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        CancellationToken cancellationToken = default) {\n        var filters = new List<string>();\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        var subjectFilters = new List<string>();\n        foreach (var pattern in NonDeliveryReportSubjectPatterns.Values) {\n            subjectFilters.Add($\"contains(subject,'{pattern.Replace(\"'\", \"''\")}')\");\n        }\n        if (subjectFilters.Count > 0) filters.Add($\"({string.Join(\" or \", subjectFilters)})\");\n        if (sinceUtc.HasValue) filters.Add($\"receivedDateTime ge {sinceUtc.Value:o}\");\n        if (beforeUtc.HasValue) filters.Add($\"receivedDateTime le {beforeUtc.Value:o}\");\n        var filter = filters.Count > 0 ? string.Join(\" and \", filters) : null;\n        var msgs = await MicrosoftGraphUtils.GetMailMessagesAsync(\n            credential,\n            userPrincipalName,\n            new[] { \"id\" },\n            filter,\n            maxResults > 0 ? maxResults : (int?)null,\n            cancellationToken).ConfigureAwait(false);\n        var results = new List<NonDeliveryReport>();\n        if (parallelDownloadLimit <= 1) {\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (maxResults > 0 && results.Count >= maxResults) break;\n                if (m.TryGetValue(\"id\", out var idObj) && idObj is string id) {\n                    var mime = await MicrosoftGraphUtils.GetMailMessageMimeAsync(credential, userPrincipalName, id, cancellationToken).ConfigureAwait(false);\n                    var reports = FilterNonDeliveryReports(new[] { mime }, since, before, recipientContains, messageId);\n                    if (reports.Count > 0) {\n                        foreach (var r in reports) {\n                            if (maxResults > 0 && results.Count >= maxResults) break;\n                            results.Add(r);\n                        }\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                    }\n                }\n            }\n        } else {\n            var msgArray = msgs.ToArray();\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, msgArray.Length)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        Dictionary<string, object>? current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= msgArray.Length || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = msgArray[next++];\n                        }\n                        if (!current.TryGetValue(\"id\", out var idObj) || idObj is not string id) continue;\n                        MimeMessage mime;\n                        try {\n                            cts.Token.ThrowIfCancellationRequested();\n                            mime = await MicrosoftGraphUtils.GetMailMessageMimeAsync(credential, userPrincipalName, id, cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterNonDeliveryReports(new[] { mime }, since, before, recipientContains, messageId);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    /// <summary>\n    /// Searches for Non-Delivery Reports using the Gmail API.\n    /// </summary>\n    /// <param name=\"client\">Initialized Gmail API client.</param>\n    /// <param name=\"userId\">User mailbox identifier (e.g. \"me\").</param>\n    /// <param name=\"since\">Optional UTC lower bound for report timestamps.</param>\n    /// <param name=\"before\">Optional UTC upper bound for report timestamps.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<NonDeliveryReport>> SearchNonDeliveryReportsAsync(\n        GmailApiClient client,\n        string userId,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? recipientContains = null,\n        string? messageId = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        CancellationToken cancellationToken = default) {\n        string query = BuildGmailNonDeliveryReportQuery(since, before);\n        var msgs = await client.ListAsync(userId, query, maxResults > 0 ? maxResults : (int?)null, cancellationToken).ConfigureAwait(false);\n        var results = new List<NonDeliveryReport>();\n        if (parallelDownloadLimit <= 1) {\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (maxResults > 0 && results.Count >= maxResults) break;\n                if (!string.IsNullOrEmpty(m.Id)) {\n                    var mime = await client.GetMimeMessageAsync(userId, m.Id!, cancellationToken).ConfigureAwait(false);\n                    var reports = FilterNonDeliveryReports(new[] { mime }, since, before, recipientContains, messageId);\n                    if (reports.Count > 0) {\n                        foreach (var r in reports) {\n                            if (maxResults > 0 && results.Count >= maxResults) break;\n                            results.Add(r);\n                        }\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                    }\n                }\n            }\n        } else {\n            var msgArray = msgs.ToArray();\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, msgArray.Length)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        GmailMessage? current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= msgArray.Length || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = msgArray[next++];\n                        }\n                        if (string.IsNullOrEmpty(current.Id)) continue;\n                        MimeMessage mime;\n                        try {\n                            mime = await client.GetMimeMessageAsync(userId, current.Id!, cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterNonDeliveryReports(new[] { mime }, since, before, recipientContains, messageId);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    // DMARC report helpers\n\n    /// <summary>\n    /// Searches for DMARC aggregate reports in an IMAP mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"folder\">Optional folder to search. Defaults to the inbox.</param>\n    /// <param name=\"since\">Optional UTC lower bound for message dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message dates.</param>\n    /// <param name=\"domain\">Optional domain that report subjects should contain.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"maxUncompressedSize\">Maximum uncompressed attachment size to inspect, in bytes.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<DmarcReport>> SearchDmarcReportsAsync(\n        ImapClient client,\n        string? folder = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        long maxUncompressedSize = 10 * 1024 * 1024,\n        CancellationToken cancellationToken = default) {\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadOnly);\n        var search = BuildDmarcReportSearchQuery(since, before, domain);\n        var uids = await mailFolder.SearchAsync(search, cancellationToken).ConfigureAwait(false);\n        var results = new List<DmarcReport>();\n        if (parallelDownloadLimit <= 1) {\n            foreach (var uid in uids) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var msg = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n                var reports = FilterDmarcReports(new[] { msg }, since, before, domain, maxUncompressedSize);\n                if (reports.Count > 0) {\n                    foreach (var r in reports) {\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                        results.Add(r);\n                    }\n                    if (maxResults > 0 && results.Count >= maxResults) break;\n                }\n            }\n        } else {\n            var uidArray = uids.ToArray();\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, uidArray.Length)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        int current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= uidArray.Length || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = next++;\n                        }\n                        MimeMessage msg;\n                        try {\n                            msg = await mailFolder.GetMessageAsync(uidArray[current], cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterDmarcReports(new[] { msg }, since, before, domain, maxUncompressedSize);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    /// <summary>\n    /// Searches for DMARC aggregate reports in a POP3 mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"since\">Optional UTC lower bound for message dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message dates.</param>\n    /// <param name=\"domain\">Optional domain that report subjects should contain.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"maxUncompressedSize\">Maximum uncompressed attachment size to inspect, in bytes.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<DmarcReport>> SearchDmarcReportsAsync(\n        Pop3Client client,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        long maxUncompressedSize = 10 * 1024 * 1024,\n        CancellationToken cancellationToken = default) {\n        var results = new List<DmarcReport>();\n        if (parallelDownloadLimit <= 1) {\n            for (int idx = 0; idx < client.Count; idx++) {\n                cancellationToken.ThrowIfCancellationRequested();\n                var msg = await client.GetMessageAsync(idx, cancellationToken).ConfigureAwait(false);\n                var reports = FilterDmarcReports(new[] { msg }, since, before, domain, maxUncompressedSize);\n                if (reports.Count > 0) {\n                    foreach (var r in reports) {\n                        if (maxResults > 0 && results.Count >= maxResults) break;\n                        results.Add(r);\n                    }\n                    if (maxResults > 0 && results.Count >= maxResults) break;\n                }\n            }\n        } else {\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            var workers = new Task[Math.Min(parallelDownloadLimit, client.Count)];\n            int next = 0;\n            int resultCount = 0;\n            var gate = new object();\n            for (int i = 0; i < workers.Length; i++) {\n                workers[i] = Task.Run(async () => {\n                    while (true) {\n                        int current;\n                        lock (gate) {\n                            if (cts.IsCancellationRequested || next >= client.Count || (maxResults > 0 && resultCount >= maxResults)) return;\n                            current = next++;\n                        }\n                        MimeMessage msg;\n                        try {\n                            msg = await client.GetMessageAsync(current, cts.Token).ConfigureAwait(false);\n                        } catch (OperationCanceledException) {\n                            return;\n                        }\n                        var reports = FilterDmarcReports(new[] { msg }, since, before, domain, maxUncompressedSize);\n                        if (reports.Count == 0) continue;\n                        lock (gate) {\n                            foreach (var r in reports) {\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                                results.Add(r);\n                                resultCount++;\n                                if (maxResults > 0 && resultCount >= maxResults) {\n                                    cts.Cancel();\n                                    break;\n                                }\n                            }\n                        }\n                    }\n                }, CancellationToken.None);\n            }\n            try {\n                await Task.WhenAll(workers).ConfigureAwait(false);\n            } catch (OperationCanceledException) {\n            }\n        }\n\n        return maxResults > 0 && results.Count > maxResults ? results.GetRange(0, maxResults) : results;\n    }\n\n    /// <summary>\n    /// Searches for DMARC aggregate reports using Microsoft Graph.\n    /// </summary>\n    /// <param name=\"credential\">Graph credential used to access the mailbox.</param>\n    /// <param name=\"userPrincipalName\">UPN of the mailbox to search.</param>\n    /// <param name=\"since\">Optional UTC lower bound for message dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message dates.</param>\n    /// <param name=\"domain\">Optional domain that report subjects should contain.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"maxUncompressedSize\">Maximum uncompressed attachment size to inspect, in bytes.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<DmarcReport>> SearchDmarcReportsAsync(\n        GraphCredential credential,\n        string userPrincipalName,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        long maxUncompressedSize = 10 * 1024 * 1024,\n        CancellationToken cancellationToken = default) {\n        var filters = new List<string> { \"hasAttachments eq true\", \"contains(subject,'report domain')\" };\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (sinceUtc.HasValue) filters.Add($\"receivedDateTime ge {sinceUtc.Value:o}\");\n        if (beforeUtc.HasValue) filters.Add($\"receivedDateTime le {beforeUtc.Value:o}\");\n        if (!string.IsNullOrWhiteSpace(domain)) filters.Add($\"contains(subject,'{domain!.Replace(\"'\", \"''\")}')\");\n        var filter = string.Join(\" and \", filters);\n        var msgs = await MicrosoftGraphUtils.GetMailMessagesAsync(\n            credential,\n            userPrincipalName,\n            new[] { \"id\" },\n            filter,\n            maxResults > 0 ? maxResults : (int?)null,\n            cancellationToken).ConfigureAwait(false);\n        var mimeMessages = new List<MimeMessage>(msgs.Count);\n        if (parallelDownloadLimit <= 1) {\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (m.TryGetValue(\"id\", out var idObj) && idObj is string id) {\n                    var mime = await MicrosoftGraphUtils.GetMailMessageMimeAsync(credential, userPrincipalName, id, cancellationToken).ConfigureAwait(false);\n                    mimeMessages.Add(mime);\n                }\n            }\n        } else {\n            using var semaphore = new SemaphoreSlim(parallelDownloadLimit);\n            var tasks = new List<Task>();\n\n            async Task DownloadMessageAsync(string id) {\n                await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);\n                try {\n                    cancellationToken.ThrowIfCancellationRequested();\n                    var mime = await MicrosoftGraphUtils.GetMailMessageMimeAsync(credential, userPrincipalName, id, cancellationToken).ConfigureAwait(false);\n                    lock (mimeMessages) mimeMessages.Add(mime);\n                } finally {\n                    semaphore.Release();\n                }\n            }\n\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (m.TryGetValue(\"id\", out var idObj) && idObj is string id) {\n                    tasks.Add(DownloadMessageAsync(id));\n                }\n            }\n\n            await Task.WhenAll(tasks).ConfigureAwait(false);\n        }\n\n        return FilterDmarcReports(mimeMessages, since, before, domain, maxUncompressedSize);\n    }\n\n    /// <summary>\n    /// Searches for DMARC aggregate reports using the Gmail API.\n    /// </summary>\n    /// <param name=\"client\">Initialized Gmail API client.</param>\n    /// <param name=\"userId\">User mailbox identifier (e.g. \"me\").</param>\n    /// <param name=\"since\">Optional UTC lower bound for message dates.</param>\n    /// <param name=\"before\">Optional UTC upper bound for message dates.</param>\n    /// <param name=\"domain\">Optional domain that report subjects should contain.</param>\n    /// <param name=\"maxResults\">Optional limit for the number of reports returned. Use 0 for unlimited.</param>\n    /// <param name=\"parallelDownloadLimit\">Maximum number of concurrent message downloads.</param>\n    /// <param name=\"maxUncompressedSize\">Maximum uncompressed attachment size to inspect, in bytes.</param>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public static async Task<IList<DmarcReport>> SearchDmarcReportsAsync(\n        GmailApiClient client,\n        string userId,\n        DateTime? since = null,\n        DateTime? before = null,\n        string? domain = null,\n        int maxResults = 0,\n        int parallelDownloadLimit = 4,\n        long maxUncompressedSize = 10 * 1024 * 1024,\n        CancellationToken cancellationToken = default) {\n        string query = BuildGmailDmarcReportQuery(since, before, domain);\n        var msgs = await client.ListAsync(userId, query, maxResults > 0 ? maxResults : (int?)null, cancellationToken).ConfigureAwait(false);\n        var mimeMessages = new List<MimeMessage>(msgs.Count);\n        if (parallelDownloadLimit <= 1) {\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (!string.IsNullOrEmpty(m.Id)) {\n                    var mime = await client.GetMimeMessageAsync(userId, m.Id!, cancellationToken).ConfigureAwait(false);\n                    mimeMessages.Add(mime);\n                    if (maxResults > 0 && mimeMessages.Count >= maxResults) break;\n                }\n            }\n        } else {\n            using var semaphore = new SemaphoreSlim(parallelDownloadLimit);\n            var tasks = new List<Task>();\n\n            async Task DownloadMessageAsync(string id) {\n                await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);\n                try {\n                    cancellationToken.ThrowIfCancellationRequested();\n                    var mime = await client.GetMimeMessageAsync(userId, id, cancellationToken).ConfigureAwait(false);\n                    lock (mimeMessages) mimeMessages.Add(mime);\n                } finally {\n                    semaphore.Release();\n                }\n            }\n\n            foreach (var m in msgs) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (!string.IsNullOrEmpty(m.Id)) {\n                    tasks.Add(DownloadMessageAsync(m.Id!));\n                }\n                if (maxResults > 0 && tasks.Count >= maxResults) break;\n            }\n\n            await Task.WhenAll(tasks).ConfigureAwait(false);\n        }\n\n        return FilterDmarcReports(mimeMessages, since, before, domain, maxUncompressedSize);\n    }\n\n    /// <summary>\n    /// Filters DMARC reports by date and domain.\n    /// </summary>\n    /// <param name=\"messages\">Collection of MIME messages to inspect.</param>\n    /// <param name=\"since\">Optional UTC lower bound for the message date.</param>\n    /// <param name=\"before\">Optional UTC upper bound for the message date.</param>\n    /// <param name=\"domain\">Optional domain that report attachments should match.</param>\n    /// <param name=\"maxUncompressedSize\">Maximum uncompressed attachment size to inspect, in bytes.</param>\n    internal static IList<DmarcReport> FilterDmarcReports(\n        IEnumerable<MimeMessage> messages,\n        DateTime? since,\n        DateTime? before,\n        string? domain,\n        long maxUncompressedSize = 10 * 1024 * 1024) {\n        var results = new List<DmarcReport>();\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        foreach (var message in messages) {\n            var msgDate = message.Date.UtcDateTime;\n            if (sinceUtc.HasValue && msgDate < sinceUtc.Value) continue;\n            if (beforeUtc.HasValue && msgDate > beforeUtc.Value) continue;\n            var report = new DmarcReport {\n                From = message.From.Mailboxes.FirstOrDefault()?.Address,\n                Subject = message.Subject,\n                Date = message.Date\n            };\n            var domainMatched = string.IsNullOrWhiteSpace(domain);\n            foreach (var att in message.Attachments) {\n                if (IsDmarcAttachment(att) && att is MimePart part) {\n                    if (!string.IsNullOrWhiteSpace(domain) && !AttachmentMatchesDomain(part, domain!, maxUncompressedSize)) continue;\n                    if (part.Content == null) {\n                        continue;\n                    }\n\n                    var stream = part.Content.Open();\n                    report.Attachments.Add(new DmarcReportAttachment(part.FileName ?? \"report.zip\", stream));\n                    if (!domainMatched) domainMatched = true;\n                }\n            }\n            if (report.Attachments.Count > 0 && domainMatched) results.Add(report);\n        }\n        return results;\n    }\n\n    private static bool AttachmentMatchesDomain(MimePart part, string domain, long maxUncompressedSize) {\n        if (part.FileName?.IndexOf(domain, StringComparison.OrdinalIgnoreCase) >= 0) return true;\n        if (part.Content == null) {\n            return false;\n        }\n\n        try {\n            using var stream = part.Content.Open();\n            var name = part.FileName ?? string.Empty;\n            if (name.EndsWith(\".zip\", StringComparison.OrdinalIgnoreCase)) {\n                using var zip = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false);\n                foreach (var entry in zip.Entries) {\n                    if (entry.Length > maxUncompressedSize) {\n                        LoggingMessages.Logger.WriteError(\"Zip entry {0} exceeds max size {1}\", entry.FullName, maxUncompressedSize);\n                        continue;\n                    }\n                    using var entryStream = entry.Open();\n                    if (XmlStreamContainsDomain(entryStream, domain, maxUncompressedSize)) return true;\n                }\n            } else if (name.EndsWith(\".gz\", StringComparison.OrdinalIgnoreCase)) {\n                using var gz = new GZipStream(stream, CompressionMode.Decompress);\n                if (XmlStreamContainsDomain(gz, domain, maxUncompressedSize)) return true;\n            } else {\n                if (XmlStreamContainsDomain(stream, domain, maxUncompressedSize)) return true;\n            }\n        } catch (Exception ex) {\n            LoggingMessages.Logger.WriteError(\"Failed to process attachment {0}: {1}\", part.FileName ?? string.Empty, ex.Message);\n        }\n        return false;\n    }\n\n    private static bool XmlStreamContainsDomain(Stream stream, string domain, long maxUncompressedSize) {\n        var settings = new System.Xml.XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true, CloseInput = true };\n        using var reader = System.Xml.XmlReader.Create(new LimitedStream(stream, maxUncompressedSize), settings);\n        while (reader.Read()) {\n            if (reader.NodeType == System.Xml.XmlNodeType.Element && reader.LocalName.Equals(\"domain\", StringComparison.OrdinalIgnoreCase)) {\n                var value = reader.ReadElementContentAsString();\n                if (value.Equals(domain, StringComparison.OrdinalIgnoreCase)) return true;\n            }\n        }\n        return false;\n    }\n\n    internal static SearchQuery BuildDmarcReportSearchQuery(DateTime? since, DateTime? before, string? domain) {\n        SearchQuery search = SearchQuery.SubjectContains(\"report domain\");\n        var hasAtt = typeof(SearchQuery).GetProperty(\"HasAttachment\", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)?.GetValue(null) as SearchQuery;\n        search = search.And(hasAtt ?? SearchQuery.HeaderContains(\"Content-Disposition\", \"attachment\"));\n        if (!string.IsNullOrWhiteSpace(domain)) search = search.And(SearchQuery.SubjectContains(domain!));\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (sinceUtc.HasValue) search = search.And(SearchQuery.DeliveredAfter(sinceUtc.Value));\n        if (beforeUtc.HasValue) search = search.And(SearchQuery.DeliveredBefore(beforeUtc.Value));\n        return search;\n    }\n\n    private static string EscapeGmailQueryValue(string value) {\n        var sb = new StringBuilder(value.Length);\n        foreach (var c in value) {\n            if (c == '\\\\' || c == '\"' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') sb.Append('\\\\');\n            sb.Append(c);\n        }\n        return sb.ToString();\n    }\n\n    internal static string BuildGmailDmarcReportQuery(DateTime? since, DateTime? before, string? domain) {\n        var sb = new StringBuilder(\"subject:\\\"report domain\\\" has:attachment\");\n        if (!string.IsNullOrWhiteSpace(domain)) sb.Append(' ').Append(\"subject:\\\"\").Append(EscapeGmailQueryValue(domain!)).Append(\"\\\"\");\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (sinceUtc.HasValue) sb.Append(' ').Append(\"after:\").Append(sinceUtc.Value.ToString(\"yyyy'/'MM'/'dd\", CultureInfo.InvariantCulture));\n        if (beforeUtc.HasValue) sb.Append(' ').Append(\"before:\").Append(beforeUtc.Value.ToString(\"yyyy'/'MM'/'dd\", CultureInfo.InvariantCulture));\n        return sb.ToString().Trim();\n    }\n\n    private static bool IsDmarcAttachment(MimeEntity entity) {\n        if (entity is MimePart part) {\n            var name = part.FileName;\n            if (!string.IsNullOrEmpty(name) && (name!.EndsWith(\".zip\", StringComparison.OrdinalIgnoreCase) || name.EndsWith(\".gz\", StringComparison.OrdinalIgnoreCase) || name.EndsWith(\".xml\", StringComparison.OrdinalIgnoreCase))) return true;\n            var ct = part.ContentType;\n            if (ct != null) {\n                if (ct.MediaType.Equals(\"application\", StringComparison.OrdinalIgnoreCase)) {\n                    if (ct.MediaSubtype.Equals(\"zip\", StringComparison.OrdinalIgnoreCase) || ct.MediaSubtype.Equals(\"gzip\", StringComparison.OrdinalIgnoreCase) || ct.MediaSubtype.Equals(\"xml\", StringComparison.OrdinalIgnoreCase)) return true;\n                } else if (ct.MediaType.Equals(\"text\", StringComparison.OrdinalIgnoreCase)) {\n                    if (ct.MediaSubtype.Equals(\"xml\", StringComparison.OrdinalIgnoreCase)) return true;\n                }\n            }\n        }\n        return false;\n    }\n\n\n    /// <summary>\n    /// Filters Non-Delivery Reports by date, recipient and message id.\n    /// </summary>\n    /// <param name=\"messages\">Collection of MIME messages to inspect.</param>\n    /// <param name=\"since\">Optional UTC lower bound for the report timestamp.</param>\n    /// <param name=\"before\">Optional UTC upper bound for the report timestamp.</param>\n    /// <param name=\"recipientContains\">Optional string that the recipient should contain.</param>\n    /// <param name=\"messageId\">Optional original message id to match.</param>\n    internal static IList<NonDeliveryReport> FilterNonDeliveryReports(\n        IEnumerable<MimeMessage> messages,\n        DateTime? since,\n        DateTime? before,\n        string? recipientContains,\n        string? messageId) {\n        messageId = NonDeliveryReport.NormalizeMessageId(messageId);\n        var results = new List<NonDeliveryReport>();\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        foreach (var message in messages) {\n            foreach (var report in MimeKitUtils.GetNonDeliveryReports(message)) {\n                var reportDate = (report.LastAttemptDate ?? report.Timestamp).UtcDateTime;\n                if (sinceUtc.HasValue && reportDate < sinceUtc.Value) continue;\n                if (beforeUtc.HasValue && reportDate > beforeUtc.Value) continue;\n                if (!string.IsNullOrWhiteSpace(recipientContains) && !RecipientMatches(report, recipientContains!)) continue;\n                if (!string.IsNullOrWhiteSpace(messageId) && !string.Equals(report.OriginalMessageId, messageId, StringComparison.OrdinalIgnoreCase)) continue;\n                results.Add(report);\n            }\n        }\n        return results;\n    }\n\n    internal static string BuildGmailNonDeliveryReportQuery(DateTime? since, DateTime? before) {\n        var sb = new StringBuilder();\n        bool first = true;\n        foreach (var pattern in NonDeliveryReportSubjectPatterns.Values) {\n            if (!first) sb.Append(\" OR \");\n            sb.Append(\"subject:\\\"\").Append(EscapeGmailQueryValue(pattern)).Append(\"\\\"\");\n            first = false;\n        }\n        if (sb.Length > 0) {\n            sb.Insert(0, \"(\");\n            sb.Append(')');\n        }\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (sinceUtc.HasValue) sb.Append(' ').Append(\"after:\").Append(sinceUtc.Value.ToString(\"yyyy'/'MM'/'dd\", CultureInfo.InvariantCulture));\n        if (beforeUtc.HasValue) sb.Append(' ').Append(\"before:\").Append(beforeUtc.Value.ToString(\"yyyy'/'MM'/'dd\", CultureInfo.InvariantCulture));\n        return sb.ToString().Trim();\n    }\n\n      private static bool RecipientMatches(NonDeliveryReport report, string filter) {\n          var final = report.FinalRecipientAddress;\n          if (final != null &&\n              final.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true;\n          var original = report.OriginalRecipientAddress;\n          if (original != null &&\n              original.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true;\n          return false;\n      }\n\n    private static MimeKit.MessagePriority ConvertPriority(MessagePriority priority)\n        => priority switch {\n            MessagePriority.High => MimeKit.MessagePriority.Urgent,\n            MessagePriority.Low => MimeKit.MessagePriority.NonUrgent,\n            _ => MimeKit.MessagePriority.Normal,\n        };\n\n    private static bool AddressMatches(InternetAddressList list, string filter) {\n        foreach (var addr in list.Mailboxes) {\n            if (!string.IsNullOrWhiteSpace(addr.Address) &&\n                addr.Address.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true;\n            var displayName = addr.Name;\n            if (!string.IsNullOrWhiteSpace(displayName) && displayName!.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) return true;\n        }\n        return false;\n    }\n\n    internal static SearchQuery BuildNonDeliveryReportSearchQuery(DateTime? since, DateTime? before) {\n        SearchQuery search = SearchQuery.HeaderContains(\"Content-Type\", \"delivery-status\");\n        SearchQuery? subjectQuery = null;\n        foreach (var pattern in NonDeliveryReportSubjectPatterns.Values) {\n            var q = SearchQuery.SubjectContains(pattern);\n            subjectQuery = subjectQuery == null ? q : subjectQuery.Or(q);\n        }\n        if (subjectQuery != null) search = search.Or(subjectQuery);\n        var sinceUtc = NormalizeToUtc(since);\n        var beforeUtc = NormalizeToUtc(before);\n        if (sinceUtc.HasValue) search = search.And(SearchQuery.DeliveredAfter(sinceUtc.Value));\n        if (beforeUtc.HasValue) search = search.And(SearchQuery.DeliveredBefore(beforeUtc.Value));\n        return search;\n    }\n\n    internal sealed class ParsedQuery {\n        public string? Subject { get; set; }\n        public string? FromContains { get; set; }\n        public string? ToContains { get; set; }\n        public string? BodyContains { get; set; }\n        public MessagePriority? Priority { get; set; }\n        public DateTime? Since { get; set; }\n        public DateTime? Before { get; set; }\n        public bool HasAttachment { get; set; }\n        public List<SearchQuery> AdditionalQueries { get; } = new();\n    }\n\n      internal static ParsedQuery ParseQuery(string? query) {\n          var result = new ParsedQuery();\n          if (string.IsNullOrWhiteSpace(query)) return result;\n          foreach (var token in SplitTokens(query!)) {\n            var parts = token.Split(new[] { ':' }, 2);\n            if (parts.Length == 2) {\n                var key = parts[0];\n                var value = Unquote(parts[1]);\n                if (string.Equals(key, \"from\", StringComparison.OrdinalIgnoreCase)) {\n                    result.FromContains = value;\n                } else if (string.Equals(key, \"to\", StringComparison.OrdinalIgnoreCase)) {\n                    result.ToContains = value;\n                } else if (string.Equals(key, \"subject\", StringComparison.OrdinalIgnoreCase)) {\n                    result.Subject = value;\n                } else if (string.Equals(key, \"since\", StringComparison.OrdinalIgnoreCase)) {\n                    if (DateTime.TryParse(value, out var sd)) result.Since = sd;\n                } else if (string.Equals(key, \"before\", StringComparison.OrdinalIgnoreCase)) {\n                    if (DateTime.TryParse(value, out var bd)) result.Before = bd;\n                } else if (string.Equals(key, \"priority\", StringComparison.OrdinalIgnoreCase)) {\n                    if (Enum.TryParse(value, true, out MessagePriority pr)) result.Priority = pr;\n                } else if (string.Equals(key, \"has\", StringComparison.OrdinalIgnoreCase)) {\n                    if (value.Equals(\"attachment\", StringComparison.OrdinalIgnoreCase) || value.Equals(\"attachments\", StringComparison.OrdinalIgnoreCase)) result.HasAttachment = true;\n                } else if (string.Equals(key, \"body\", StringComparison.OrdinalIgnoreCase)) {\n                    result.BodyContains = value;\n                } else {\n                    result.AdditionalQueries.Add(SearchQuery.MessageContains(token));\n                }\n            } else {\n                var text = Unquote(token);\n                result.AdditionalQueries.Add(SearchQuery.MessageContains(text));\n            }\n        }\n        return result;\n    }\n\n    private static DateTime? NormalizeToUtc(DateTime? value) {\n        if (!value.HasValue) {\n            return null;\n        }\n\n        var dt = value.Value;\n        if (dt.Kind == DateTimeKind.Unspecified) {\n            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);\n        }\n\n        return dt.ToUniversalTime();\n    }\n\n    private static IEnumerable<string> SplitTokens(string input) {\n        var list = new List<string>();\n        var current = new System.Text.StringBuilder();\n        bool inQuotes = false;\n        foreach (var ch in input) {\n            if (ch == '\"') {\n                inQuotes = !inQuotes;\n            } else if (char.IsWhiteSpace(ch) && !inQuotes) {\n                if (current.Length > 0) {\n                    list.Add(current.ToString());\n                    current.Clear();\n                }\n            } else {\n                current.Append(ch);\n            }\n        }\n        if (current.Length > 0) list.Add(current.ToString());\n        return list;\n    }\n\n    private static string Unquote(string value) {\n        if (value.Length > 1 &&\n            value.StartsWith(\"\\\"\", StringComparison.Ordinal) &&\n            value.EndsWith(\"\\\"\", StringComparison.Ordinal))\n            return value.Substring(1, value.Length - 2);\n        return value;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/MessageFetcher.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing MailKit.Search;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for fetching messages from IMAP or POP3 clients.\n/// </summary>\npublic static class MessageFetcher {\n    /// <summary>\n    /// Fetches messages from an IMAP client using optional filters.\n    /// </summary>\n    /// <param name=\"client\">The connected IMAP client.</param>\n    /// <param name=\"folder\">Optional folder to search, defaults to Inbox.</param>\n    /// <param name=\"subject\">Subject filter.</param>\n    /// <param name=\"fromContains\">Sender address filter.</param>\n    /// <param name=\"toContains\">Recipient address filter.</param>\n    /// <param name=\"priority\">Message priority filter.</param>\n    /// <param name=\"since\">Earliest delivery date.</param>\n    /// <param name=\"before\">Latest delivery date.</param>\n    /// <param name=\"all\">If set, ignores other filters.</param>\n    /// <param name=\"delete\">If set, messages are deleted after fetching.</param>\n    /// <param name=\"hasAttachment\">When set, only messages with attachments are returned.</param>\n    /// <param name=\"additionalQueries\">Additional <see cref=\"SearchQuery\"/> filters.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Collection of matching messages.</returns>\n    public static IAsyncEnumerable<ImapEmailMessage> Fetch(\n        ImapClient client,\n        string? folder = null,\n        string? subject = null,\n        string? fromContains = null,\n        string? toContains = null,\n        MessagePriority? priority = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        bool all = false,\n        bool delete = false,\n        bool hasAttachment = false,\n        IEnumerable<SearchQuery>? additionalQueries = null,\n        CancellationToken cancellationToken = default) =>\n        Fetch(\n            client,\n            folder,\n            subject,\n            fromContains,\n            toContains,\n            priority,\n            since,\n            before,\n            all,\n            delete,\n            hasAttachment,\n            additionalQueries,\n            dryRun: false,\n            cancellationToken);\n\n    /// <summary>\n    /// Fetches messages from an IMAP client using optional filters, optionally simulating deletes.\n    /// </summary>\n    public static async IAsyncEnumerable<ImapEmailMessage> Fetch(\n        ImapClient client,\n        string? folder,\n        string? subject,\n        string? fromContains,\n        string? toContains,\n        MessagePriority? priority,\n        DateTime? since,\n        DateTime? before,\n        bool all,\n        bool delete,\n        bool hasAttachment,\n        IEnumerable<SearchQuery>? additionalQueries,\n        bool dryRun,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n        var performDelete = delete && !dryRun;\n        IMailFolder mailFolder;\n        try {\n            mailFolder = client.GetCachedFolder(folder, performDelete ? FolderAccess.ReadWrite : FolderAccess.ReadOnly);\n        } catch (FolderNotFoundException ex) {\n            LoggingMessages.Logger.WriteError($\"Failed to get folder '{folder}': {ex.Message}\");\n            throw;\n        }\n\n        SearchQuery query = SearchQuery.All;\n        if (!all) {\n            if (!string.IsNullOrWhiteSpace(subject)) {\n                query = query.And(SearchQuery.SubjectContains(subject!));\n            }\n            if (!string.IsNullOrWhiteSpace(fromContains)) {\n                query = query.And(SearchQuery.FromContains(fromContains!));\n            }\n            if (!string.IsNullOrWhiteSpace(toContains)) {\n                query = query.And(SearchQuery.ToContains(toContains!));\n            }\n            if (since.HasValue) {\n                query = query.And(SearchQuery.DeliveredAfter(since.Value));\n            }\n            if (before.HasValue) {\n                query = query.And(SearchQuery.DeliveredBefore(before.Value));\n            }\n        }\n\n        if (additionalQueries != null) {\n            foreach (var q in additionalQueries) {\n                if (q != null) {\n                    query = query.And(q);\n                }\n            }\n        }\n\n        var uids = await mailFolder.SearchAsync(query, cancellationToken).ConfigureAwait(false);\n        foreach (var uid in uids) {\n            MimeMessage msg;\n            try {\n                msg = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n            } catch (MessageNotFoundException ex) {\n                LoggingMessages.Logger.WriteError($\"Failed to get message UID {uid}: {ex.Message}\");\n                throw;\n            }\n            if (hasAttachment && !msg.Attachments.Any()) {\n                continue;\n            }\n            if (priority.HasValue && msg.Priority != ConvertPriority(priority.Value)) {\n                continue;\n            }\n            var reports = MimeKitUtils.GetNonDeliveryReports(msg);\n            yield return new ImapEmailMessage(uid, msg, reports);\n            if (performDelete) {\n                await mailFolder.AddFlagsAsync(uid, MessageFlags.Deleted, true, cancellationToken).ConfigureAwait(false);\n            }\n        }\n        if (performDelete && uids.Count > 0) {\n            await mailFolder.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// Fetches messages from a POP3 client using optional filters.\n    /// </summary>\n    /// <param name=\"client\">The connected POP3 client.</param>\n    /// <param name=\"subject\">Subject filter.</param>\n    /// <param name=\"fromContains\">Sender address filter.</param>\n    /// <param name=\"toContains\">Recipient address filter.</param>\n    /// <param name=\"priority\">Message priority filter.</param>\n    /// <param name=\"since\">Earliest delivery date.</param>\n    /// <param name=\"before\">Latest delivery date.</param>\n    /// <param name=\"all\">If set, ignores other filters.</param>\n    /// <param name=\"delete\">If set, messages are deleted after fetching.</param>\n    /// <param name=\"hasAttachment\">When set, only messages with attachments are returned.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>Collection of matching messages.</returns>\n    public static IAsyncEnumerable<Pop3EmailMessage> Fetch(\n        Pop3Client client,\n        string? subject = null,\n        string? fromContains = null,\n        string? toContains = null,\n        MessagePriority? priority = null,\n        DateTime? since = null,\n        DateTime? before = null,\n        bool all = false,\n        bool delete = false,\n        bool hasAttachment = false,\n        CancellationToken cancellationToken = default) =>\n        Fetch(\n            client,\n            subject,\n            fromContains,\n            toContains,\n            priority,\n            since,\n            before,\n            all,\n            delete,\n            hasAttachment,\n            dryRun: false,\n            cancellationToken);\n\n    /// <summary>\n    /// Fetches messages from a POP3 client using optional filters, optionally simulating deletes.\n    /// </summary>\n    public static async IAsyncEnumerable<Pop3EmailMessage> Fetch(\n        Pop3Client client,\n        string? subject,\n        string? fromContains,\n        string? toContains,\n        MessagePriority? priority,\n        DateTime? since,\n        DateTime? before,\n        bool all,\n        bool delete,\n        bool hasAttachment,\n        bool dryRun,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n        var performDelete = delete && !dryRun;\n        for (int i = 0; i < client.Count; i++) {\n            MimeMessage message;\n            try {\n                message = await client.GetMessageAsync(i, cancellationToken).ConfigureAwait(false);\n            } catch (Pop3CommandException ex) {\n                LoggingMessages.Logger.WriteError($\"Failed to get message index {i}: {ex.Message}\");\n                throw;\n            }\n\n            if (!all) {\n                if (!string.IsNullOrWhiteSpace(subject) && (message.Subject == null || message.Subject.IndexOf(subject, StringComparison.OrdinalIgnoreCase) < 0)) {\n                    continue;\n                }\n                if (!string.IsNullOrWhiteSpace(fromContains) && !AddressMatches(message.From, fromContains!)) {\n                    continue;\n                }\n                if (!string.IsNullOrWhiteSpace(toContains) && !AddressMatches(message.To, toContains!)) {\n                    continue;\n                }\n                var msgDate = message.Date.DateTime;\n                if (since.HasValue && msgDate < since.Value) {\n                    continue;\n                }\n                if (before.HasValue && msgDate > before.Value) {\n                    continue;\n                }\n                if (priority.HasValue && message.Priority != ConvertPriority(priority.Value)) {\n                    continue;\n                }\n            }\n\n            if (hasAttachment && !message.Attachments.Any()) {\n                continue;\n            }\n\n            var reports = MimeKitUtils.GetNonDeliveryReports(message);\n            yield return new Pop3EmailMessage(i, message, reports);\n            if (performDelete) {\n                await client.DeleteMessageAsync(i, cancellationToken).ConfigureAwait(false);\n            }\n        }\n    }\n\n    private static MimeKit.MessagePriority ConvertPriority(MessagePriority priority) {\n        return priority switch {\n            MessagePriority.High => MimeKit.MessagePriority.Urgent,\n            MessagePriority.Low => MimeKit.MessagePriority.NonUrgent,\n            _ => MimeKit.MessagePriority.Normal,\n        };\n    }\n\n    private static bool AddressMatches(InternetAddressList list, string filter) {\n        foreach (var addr in list.Mailboxes) {\n            if (!string.IsNullOrWhiteSpace(addr.Address) &&\n                addr.Address.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) {\n                return true;\n            }\n            var displayName = addr.Name;\n            if (!string.IsNullOrWhiteSpace(displayName) &&\n                displayName!.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/MessageFlagSetter.cs",
    "content": "using System.Runtime.CompilerServices;\nusing System.Collections.Concurrent;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for setting message flags on IMAP and POP3 servers.\n/// </summary>\npublic static class MessageFlagSetter {\n    /// <summary>\n    /// Abstraction for flag operations on a folder.\n    /// </summary>\n    public interface IImapFolder {\n        /// <summary>Adds flags to the specified message.</summary>\n        /// <param name=\"uid\">Message unique identifier.</param>\n        /// <param name=\"flags\">Flags to add.</param>\n        /// <param name=\"silent\">Whether to perform the operation silently.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n        /// <summary>Removes flags from the specified message.</summary>\n        /// <param name=\"uid\">Message unique identifier.</param>\n        /// <param name=\"flags\">Flags to remove.</param>\n        /// <param name=\"silent\">Whether to perform the operation silently.</param>\n        /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n        Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default);\n    }\n\n    private class FolderWrapper(IMailFolder folder) : IImapFolder {\n        /// <inheritdoc />\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            folder.AddFlagsAsync(uid, flags, silent, cancellationToken);\n        /// <inheritdoc />\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) =>\n            folder.RemoveFlagsAsync(uid, flags, silent, cancellationToken);\n    }\n\n    /// <summary>\n    /// Sets or clears message flags in an IMAP folder.\n    /// </summary>\n    public static Task SetFlagsAsync(ImapClient client, UniqueId uid, MessageFlags flags, bool add, string? folder = null, CancellationToken cancellationToken = default) =>\n        SetFlagsAsync(client, uid, flags, add, dryRun: false, folder, cancellationToken);\n\n    /// <summary>\n    /// Sets or clears message flags in an IMAP folder, optionally simulating the change.\n    /// </summary>\n    public static Task SetFlagsAsync(ImapClient client, UniqueId uid, MessageFlags flags, bool add, bool dryRun, string? folder = null, CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return Task.CompletedTask;\n        }\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n        return SetFlagsAsync(new FolderWrapper(mailFolder), uid, flags, add, cancellationToken);\n    }\n\n    /// <summary>\n    /// Sets or clears message flags using an abstract folder.\n    /// </summary>\n    public static Task SetFlagsAsync(IImapFolder folder, UniqueId uid, MessageFlags flags, bool add, CancellationToken cancellationToken = default) =>\n        SetFlagsAsync(folder, uid, flags, add, dryRun: false, cancellationToken);\n\n    /// <summary>\n    /// Sets or clears message flags using an abstract folder, optionally simulating the change.\n    /// </summary>\n    public static Task SetFlagsAsync(IImapFolder folder, UniqueId uid, MessageFlags flags, bool add, bool dryRun, CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return Task.CompletedTask;\n        }\n        return add ? folder.AddFlagsAsync(uid, flags, true, cancellationToken)\n            : folder.RemoveFlagsAsync(uid, flags, true, cancellationToken);\n    }\n\n    private static readonly ConditionalWeakTable<Pop3Client, ConcurrentDictionary<int, bool>> Pop3Flags = new();\n\n    /// <summary>\n    /// Sets or clears the local read flag for a POP3 message.\n    /// </summary>\n    public static Task SetReadAsync(Pop3Client client, int index, bool read, CancellationToken cancellationToken = default) =>\n        SetReadAsync(client, index, read, dryRun: false, cancellationToken);\n\n    /// <summary>\n    /// Sets or clears the local read flag for a POP3 message, optionally simulating the change.\n    /// </summary>\n    public static Task SetReadAsync(Pop3Client client, int index, bool read, bool dryRun, CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return Task.CompletedTask;\n        }\n        var state = Pop3Flags.GetOrCreateValue(client);\n        state[index] = read;\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Tries to get the local read flag for a POP3 message.\n    /// </summary>\n    /// <param name=\"client\">POP3 client.</param>\n    /// <param name=\"index\">Message index.</param>\n    /// <param name=\"read\">Returns the current read state.</param>\n    /// <returns><c>true</c> when a state was found.</returns>\n    public static bool TryGetPop3Read(Pop3Client client, int index, out bool read) {\n        var state = Pop3Flags.GetOrCreateValue(client);\n        return state.TryGetValue(index, out read);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/MessageRemover.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for removing messages from IMAP or POP3 clients.\n/// </summary>\npublic static class MessageRemover {\n    /// <summary>\n    /// Deletes a single message from an IMAP folder.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"uid\">Unique identifier of the message.</param>\n    /// <param name=\"folder\">Optional folder, defaults to Inbox.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static Task DeleteAsync(\n        ImapClient client,\n        UniqueId uid,\n        string? folder = null,\n        CancellationToken cancellationToken = default) =>\n        DeleteAsync(client, uid, dryRun: false, folder, cancellationToken);\n\n    /// <summary>\n    /// Deletes a single message from an IMAP folder, optionally simulating the change.\n    /// </summary>\n    public static async Task DeleteAsync(\n        ImapClient client,\n        UniqueId uid,\n        bool dryRun,\n        string? folder = null,\n        CancellationToken cancellationToken = default) {\n        await DeleteAsync(client, uid, dryRun, folder, expunge: true, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes a single message from an IMAP folder, optionally simulating the change and controlling expunge behavior.\n    /// </summary>\n    public static async Task DeleteAsync(\n        ImapClient client,\n        UniqueId uid,\n        bool dryRun,\n        string? folder,\n        bool expunge,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n        await mailFolder.AddFlagsAsync(uid, MessageFlags.Deleted, true, cancellationToken).ConfigureAwait(false);\n        if (expunge) {\n            await mailFolder.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// Deletes multiple messages from an IMAP folder.\n    /// </summary>\n    /// <param name=\"client\">Connected IMAP client.</param>\n    /// <param name=\"uids\">Collection of message identifiers.</param>\n    /// <param name=\"folder\">Optional folder, defaults to Inbox.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static Task DeleteAsync(\n        ImapClient client,\n        IEnumerable<UniqueId> uids,\n        string? folder = null,\n        CancellationToken cancellationToken = default) =>\n        DeleteAsync(client, uids, dryRun: false, folder, cancellationToken);\n\n    /// <summary>\n    /// Deletes multiple messages from an IMAP folder, optionally simulating the change.\n    /// </summary>\n    public static async Task DeleteAsync(\n        ImapClient client,\n        IEnumerable<UniqueId> uids,\n        bool dryRun,\n        string? folder = null,\n        CancellationToken cancellationToken = default) {\n        await DeleteAsync(client, uids, dryRun, folder, expunge: true, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes multiple messages from an IMAP folder, optionally simulating the change and controlling expunge behavior.\n    /// </summary>\n    public static async Task DeleteAsync(\n        ImapClient client,\n        IEnumerable<UniqueId> uids,\n        bool dryRun,\n        string? folder,\n        bool expunge,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        var list = new List<UniqueId>(uids);\n        if (list.Count == 0) {\n            return;\n        }\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadWrite);\n        await mailFolder.AddFlagsAsync(list, MessageFlags.Deleted, true, cancellationToken).ConfigureAwait(false);\n        if (expunge) {\n            await mailFolder.ExpungeAsync(cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// Deletes a single message from a POP3 mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"index\">Index of the message to delete.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static Task DeleteAsync(\n        Pop3Client client,\n        int index,\n        CancellationToken cancellationToken = default) =>\n        DeleteAsync(client, index, dryRun: false, cancellationToken);\n\n    /// <summary>\n    /// Deletes a single message from a POP3 mailbox, optionally simulating the change.\n    /// </summary>\n    public static async Task DeleteAsync(\n        Pop3Client client,\n        int index,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        await client.DeleteMessageAsync(index, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Deletes multiple messages from a POP3 mailbox.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"indexes\">Indexes of messages to delete.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    public static Task DeleteAsync(\n        Pop3Client client,\n        IEnumerable<int> indexes,\n        CancellationToken cancellationToken = default) =>\n        DeleteAsync(client, indexes, dryRun: false, cancellationToken);\n\n    /// <summary>\n    /// Deletes multiple messages from a POP3 mailbox, optionally simulating the change.\n    /// </summary>\n    public static async Task DeleteAsync(\n        Pop3Client client,\n        IEnumerable<int> indexes,\n        bool dryRun,\n        CancellationToken cancellationToken = default) {\n        if (dryRun) {\n            return;\n        }\n        foreach (var i in indexes) {\n            await client.DeleteMessageAsync(i, cancellationToken).ConfigureAwait(false);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/Pop3AttachmentPayloadBuilder.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Status of POP3 attachment payload build operation.\n/// </summary>\npublic enum Pop3AttachmentBuildStatus {\n    /// <summary>Attachment payload was built.</summary>\n    Success,\n    /// <summary>Attachment index was not found.</summary>\n    NotFound,\n    /// <summary>Attachment exceeded maximum allowed payload size.</summary>\n    SizeLimitExceeded\n}\n\n/// <summary>\n/// Materialized POP3 attachment payload.\n/// </summary>\npublic sealed record Pop3AttachmentPayload(\n    string FileName,\n    string ContentType,\n    byte[] Bytes);\n\n/// <summary>\n/// Result of POP3 attachment payload build operation.\n/// </summary>\npublic sealed record Pop3AttachmentBuildResult(\n    Pop3AttachmentBuildStatus Status,\n    Pop3AttachmentPayload? Payload,\n    string? Error) {\n    /// <summary>True when payload was produced.</summary>\n    public bool Success => Payload != null;\n}\n\n/// <summary>\n/// Builds binary attachment payload from MIME messages retrieved over POP3.\n/// </summary>\npublic static class Pop3AttachmentPayloadBuilder {\n    /// <summary>\n    /// Builds attachment payload by index.\n    /// </summary>\n    public static Pop3AttachmentBuildResult Build(MimeMessage message, int attachmentIndex, long maxBytes) {\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (maxBytes < 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxBytes));\n        }\n\n        var attachments = new List<MimeEntity>();\n        foreach (var attachment in message.Attachments) {\n            attachments.Add(attachment);\n        }\n\n        if (attachmentIndex < 0 || attachmentIndex >= attachments.Count) {\n            return new Pop3AttachmentBuildResult(\n                Pop3AttachmentBuildStatus.NotFound,\n                null,\n                \"Attachment not found.\");\n        }\n\n        var entity = attachments[attachmentIndex];\n        string fileName;\n        string contentType;\n        if (entity is MimePart part) {\n            fileName = part.FileName ?? part.ContentDisposition?.FileName ?? part.ContentType?.Name ?? string.Empty;\n            contentType = part.ContentType?.MimeType ?? \"application/octet-stream\";\n        } else if (entity is MessagePart messagePart) {\n            fileName = messagePart.ContentDisposition?.FileName ?? messagePart.ContentType?.Name ?? \"message.eml\";\n            contentType = \"message/rfc822\";\n        } else {\n            fileName = \"attachment\";\n            contentType = \"application/octet-stream\";\n        }\n\n        using var ms = new MemoryStream();\n        try {\n            using var limited = new SizeLimitedWriteStream(ms, maxBytes);\n            if (entity is MimePart mimePart) {\n                if (mimePart.Content != null) {\n                    mimePart.Content.DecodeTo(limited);\n                } else {\n                    mimePart.WriteTo(limited);\n                }\n            } else if (entity is MessagePart nestedMessagePart) {\n                if (nestedMessagePart.Message != null) {\n                    nestedMessagePart.Message.WriteTo(limited);\n                } else {\n                    nestedMessagePart.WriteTo(limited);\n                }\n            } else {\n                entity.WriteTo(limited);\n            }\n        } catch (InvalidDataException ex) {\n            return new Pop3AttachmentBuildResult(\n                Pop3AttachmentBuildStatus.SizeLimitExceeded,\n                null,\n                ex.Message);\n        }\n\n        fileName = string.IsNullOrWhiteSpace(fileName) ? $\"attachment-{attachmentIndex}\" : fileName.Trim();\n        return new Pop3AttachmentBuildResult(\n            Pop3AttachmentBuildStatus.Success,\n            new Pop3AttachmentPayload(fileName, contentType, ms.ToArray()),\n            null);\n    }\n\n    private sealed class SizeLimitedWriteStream : Stream {\n        private readonly Stream _inner;\n        private readonly long _maxBytes;\n        private long _writtenBytes;\n\n        internal SizeLimitedWriteStream(Stream inner, long maxBytes) {\n            _inner = inner;\n            _maxBytes = maxBytes;\n        }\n\n        public override bool CanRead => false;\n        public override bool CanSeek => false;\n        public override bool CanWrite => true;\n        public override long Length => _inner.Length;\n        public override long Position {\n            get => _inner.Position;\n            set => throw new NotSupportedException();\n        }\n\n        public override void Flush() => _inner.Flush();\n        public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();\n        public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();\n        public override void SetLength(long value) => throw new NotSupportedException();\n\n        public override void Write(byte[] buffer, int offset, int count) {\n            if (count < 0) {\n                throw new ArgumentOutOfRangeException(nameof(count));\n            }\n\n            if (_maxBytes >= 0 && _writtenBytes + count > _maxBytes) {\n                throw new InvalidDataException($\"Attachment exceeds max allowed size of {_maxBytes} bytes.\");\n            }\n\n            _inner.Write(buffer, offset, count);\n            _writtenBytes += count;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/Pop3MailboxBrowser.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Pop3;\nusing MimeKit;\nusing MimeKit.Utils;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Lightweight POP3 mailbox browsing helpers that avoid downloading full messages when possible.\n/// </summary>\npublic static class Pop3MailboxBrowser {\n    /// <summary>\n    /// Represents the outcome of resolving a POP3 message request.\n    /// </summary>\n    public enum Pop3MessageResolveStatus {\n        /// <summary>Message was resolved successfully.</summary>\n        Success,\n        /// <summary>Neither index nor uid was provided.</summary>\n        MissingIdentifier,\n        /// <summary>Provided index is invalid (negative).</summary>\n        InvalidIndex,\n        /// <summary>UID lookup is not supported by the POP3 server.</summary>\n        UidLookupUnsupported,\n        /// <summary>Message was not found.</summary>\n        NotFound\n    }\n\n    /// <summary>Summary of a POP3 message derived from message headers.</summary>\n    public sealed record Pop3MessageHeaderSnapshot(\n        int Index,\n        string? Uid,\n        long? MessageSize,\n        string? MessageId,\n        string From,\n        string To,\n        string? Subject,\n        DateTime DateUtc,\n        bool HasAttachmentsHint);\n\n    /// <summary>Response envelope for POP3 header listing.</summary>\n    public sealed record Pop3ListSnapshot(\n        int TotalCount,\n        IReadOnlyList<Pop3MessageHeaderSnapshot> Messages);\n\n    /// <summary>Resolved message snapshot for POP3.</summary>\n    public sealed record Pop3ResolvedMessageSnapshot(\n        int Index,\n        string? Uid,\n        long? MessageSize,\n        MimeMessage Message);\n\n    /// <summary>Result of resolving a POP3 message request.</summary>\n    public sealed record Pop3MessageResolveResult(\n        Pop3MessageResolveStatus Status,\n        Pop3ResolvedMessageSnapshot? Snapshot);\n\n    /// <summary>Result of deleting a POP3 message by index/UIDL identifier.</summary>\n    public sealed record Pop3MessageDeleteResult(\n        Pop3MessageResolveStatus Status,\n        int? DeletedIndex,\n        string? DeletedUid);\n\n    /// <summary>\n    /// Lists messages as header-only snapshots, starting from the newest message.\n    /// </summary>\n    /// <remarks>\n    /// POP3 servers expose messages using zero-based indices. This method iterates from newest to oldest, applying an offset\n    /// (from the newest message) and returning up to <paramref name=\"limit\"/> messages.\n    /// </remarks>\n    public static Task<Pop3ListSnapshot> ListMessageHeadersAsync(\n        Pop3Client client,\n        int limit,\n        int offset,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        return ListMessageHeadersCoreAsync(new MailKitPop3MailboxClient(client), limit, offset, cancellationToken);\n    }\n\n    /// <summary>\n    /// Resolves and downloads a single message by index or UIDL value.\n    /// </summary>\n    public static Task<Pop3MessageResolveResult> ResolveMessageAsync(\n        Pop3Client client,\n        int? requestedIndex,\n        string? requestedUid,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        return ResolveMessageCoreAsync(new MailKitPop3MailboxClient(client), requestedIndex, requestedUid, cancellationToken);\n    }\n\n    /// <summary>\n    /// Resolves a POP3 message by index or UIDL and marks it for deletion.\n    /// </summary>\n    public static Task<Pop3MessageDeleteResult> DeleteMessageAsync(\n        Pop3Client client,\n        int? requestedIndex,\n        string? requestedUid,\n        CancellationToken cancellationToken = default) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        return DeleteMessageCoreAsync(new MailKitPop3MailboxClient(client), requestedIndex, requestedUid, cancellationToken);\n    }\n\n    internal interface IPop3MailboxClient {\n        int Count { get; }\n        Task<HeaderList> GetMessageHeadersAsync(int index, CancellationToken cancellationToken);\n        Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken);\n        Task<string> GetMessageUidAsync(int index, CancellationToken cancellationToken);\n        Task<IList<string>> GetMessageUidsAsync(CancellationToken cancellationToken);\n        Task DeleteMessageAsync(int index, CancellationToken cancellationToken);\n        long GetMessageSize(int index, CancellationToken cancellationToken);\n    }\n\n    internal sealed class MailKitPop3MailboxClient : IPop3MailboxClient {\n        private readonly Pop3Client _client;\n\n        internal MailKitPop3MailboxClient(Pop3Client client) {\n            _client = client ?? throw new ArgumentNullException(nameof(client));\n        }\n\n        public int Count => _client.Count;\n\n        public Task<HeaderList> GetMessageHeadersAsync(int index, CancellationToken cancellationToken) =>\n            _client.GetMessageHeadersAsync(index, cancellationToken);\n\n        public Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken) =>\n            _client.GetMessageAsync(index, cancellationToken);\n\n        public Task<string> GetMessageUidAsync(int index, CancellationToken cancellationToken) =>\n            _client.GetMessageUidAsync(index, cancellationToken);\n\n        public Task<IList<string>> GetMessageUidsAsync(CancellationToken cancellationToken) =>\n            _client.GetMessageUidsAsync(cancellationToken);\n\n        public Task DeleteMessageAsync(int index, CancellationToken cancellationToken) =>\n            _client.DeleteMessageAsync(index, cancellationToken);\n\n        public long GetMessageSize(int index, CancellationToken cancellationToken) =>\n            _client.GetMessageSize(index, cancellationToken);\n    }\n\n    internal static async Task<Pop3ListSnapshot> ListMessageHeadersCoreAsync(\n        IPop3MailboxClient client,\n        int limit,\n        int offset,\n        CancellationToken cancellationToken) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (limit < 1) {\n            throw new ArgumentOutOfRangeException(nameof(limit));\n        }\n        if (offset < 0) {\n            throw new ArgumentOutOfRangeException(nameof(offset));\n        }\n\n        var totalCount = client.Count;\n        var messages = new List<Pop3MessageHeaderSnapshot>();\n        var start = totalCount - 1 - offset;\n        for (var index = start; index >= 0 && messages.Count < limit; index--) {\n            var headers = await client.GetMessageHeadersAsync(index, cancellationToken).ConfigureAwait(false);\n            var uid = await TryGetMessageUidAsync(client, index, cancellationToken).ConfigureAwait(false);\n            var messageSize = TryGetMessageSize(client, index, cancellationToken);\n\n            messages.Add(new Pop3MessageHeaderSnapshot(\n                Index: index,\n                Uid: uid,\n                MessageSize: messageSize,\n                MessageId: NormalizeMessageIdValue(headers[HeaderId.MessageId]),\n                From: headers[HeaderId.From] ?? string.Empty,\n                To: headers[HeaderId.To] ?? string.Empty,\n                Subject: string.IsNullOrWhiteSpace(headers[HeaderId.Subject]) ? null : headers[HeaderId.Subject],\n                DateUtc: ParseHeaderDateUtc(headers[HeaderId.Date]),\n                HasAttachmentsHint: HasAttachmentHint(headers)));\n        }\n\n        return new Pop3ListSnapshot(totalCount, messages);\n    }\n\n    internal static async Task<Pop3MessageResolveResult> ResolveMessageCoreAsync(\n        IPop3MailboxClient client,\n        int? requestedIndex,\n        string? requestedUid,\n        CancellationToken cancellationToken) {\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n\n        var uid = NormalizeOptional(requestedUid);\n        if (requestedIndex is null && string.IsNullOrWhiteSpace(uid)) {\n            return new Pop3MessageResolveResult(Pop3MessageResolveStatus.MissingIdentifier, null);\n        }\n\n        if (requestedIndex is not null && requestedIndex.Value < 0) {\n            return new Pop3MessageResolveResult(Pop3MessageResolveStatus.InvalidIndex, null);\n        }\n\n        var resolvedIndex = requestedIndex ?? -1;\n        if (!string.IsNullOrWhiteSpace(uid)) {\n            var lookup = await TryResolveMessageIndexByUidAsync(client, uid!, cancellationToken).ConfigureAwait(false);\n            if (!lookup.Supported) {\n                return new Pop3MessageResolveResult(Pop3MessageResolveStatus.UidLookupUnsupported, null);\n            }\n            if (lookup.Index is null) {\n                return new Pop3MessageResolveResult(Pop3MessageResolveStatus.NotFound, null);\n            }\n            resolvedIndex = lookup.Index.Value;\n        }\n\n        var totalCount = client.Count;\n        if (resolvedIndex < 0 || resolvedIndex >= totalCount) {\n            return new Pop3MessageResolveResult(Pop3MessageResolveStatus.NotFound, null);\n        }\n\n        var resolvedUid = string.IsNullOrWhiteSpace(uid)\n            ? await TryGetMessageUidAsync(client, resolvedIndex, cancellationToken).ConfigureAwait(false)\n            : uid;\n        var messageSize = TryGetMessageSize(client, resolvedIndex, cancellationToken);\n        var message = await client.GetMessageAsync(resolvedIndex, cancellationToken).ConfigureAwait(false);\n\n        return new Pop3MessageResolveResult(\n            Pop3MessageResolveStatus.Success,\n            new Pop3ResolvedMessageSnapshot(resolvedIndex, resolvedUid, messageSize, message));\n    }\n\n    internal static async Task<Pop3MessageDeleteResult> DeleteMessageCoreAsync(\n        IPop3MailboxClient client,\n        int? requestedIndex,\n        string? requestedUid,\n        CancellationToken cancellationToken) {\n        var resolved = await ResolveMessageCoreAsync(client, requestedIndex, requestedUid, cancellationToken).ConfigureAwait(false);\n        if (resolved.Status != Pop3MessageResolveStatus.Success || resolved.Snapshot is null) {\n            return new Pop3MessageDeleteResult(resolved.Status, null, null);\n        }\n\n        await client.DeleteMessageAsync(resolved.Snapshot.Index, cancellationToken).ConfigureAwait(false);\n        return new Pop3MessageDeleteResult(Pop3MessageResolveStatus.Success, resolved.Snapshot.Index, resolved.Snapshot.Uid);\n    }\n\n    internal static string? NormalizeOptional(string? raw) =>\n        string.IsNullOrWhiteSpace(raw) ? null : raw!.Trim();\n\n    internal static string? NormalizeMessageIdValue(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n        var trimmed = value!.Trim();\n        if (trimmed.Length > 1 && trimmed[0] == '<' && trimmed[trimmed.Length - 1] == '>') {\n            trimmed = trimmed.Substring(1, trimmed.Length - 2);\n        }\n        return trimmed;\n    }\n\n    private static DateTime ParseHeaderDateUtc(string? raw) {\n        if (string.IsNullOrWhiteSpace(raw)) {\n            return DateTime.UtcNow;\n        }\n\n        // Prefer RFC822/RFC2822 parsing.\n        if (DateUtils.TryParse(raw, out var parsed)) {\n            return parsed.UtcDateTime;\n        }\n\n        // As a fallback (non-standard servers), attempt a broad parse.\n        if (DateTimeOffset.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var dt)) {\n            return dt.UtcDateTime;\n        }\n\n        return DateTime.UtcNow;\n    }\n\n    private static bool HasAttachmentHint(HeaderList headers) {\n        var contentType = headers[HeaderId.ContentType] ?? string.Empty;\n        var contentDisposition = headers[HeaderId.ContentDisposition] ?? string.Empty;\n        if (contentDisposition.Contains(\"attachment\", StringComparison.OrdinalIgnoreCase)) {\n            return true;\n        }\n        if (contentType.Contains(\"multipart/mixed\", StringComparison.OrdinalIgnoreCase) ||\n            contentType.Contains(\"multipart/related\", StringComparison.OrdinalIgnoreCase) ||\n            contentType.Contains(\"name=\", StringComparison.OrdinalIgnoreCase)) {\n            return true;\n        }\n        return false;\n    }\n\n    private static async Task<Pop3UidLookupResult> TryResolveMessageIndexByUidAsync(\n        IPop3MailboxClient client,\n        string uid,\n        CancellationToken cancellationToken) {\n        try {\n            var uids = await client.GetMessageUidsAsync(cancellationToken).ConfigureAwait(false);\n            for (var i = 0; i < uids.Count; i++) {\n                if (string.Equals(uids[i], uid, StringComparison.Ordinal)) {\n                    return new Pop3UidLookupResult(true, i);\n                }\n            }\n\n            return new Pop3UidLookupResult(true, null);\n        } catch (NotSupportedException) {\n            return new Pop3UidLookupResult(false, null);\n        }\n    }\n\n    private static async Task<string?> TryGetMessageUidAsync(IPop3MailboxClient client, int index, CancellationToken cancellationToken) {\n        try {\n            return await client.GetMessageUidAsync(index, cancellationToken).ConfigureAwait(false);\n        } catch {\n            return null;\n        }\n    }\n\n    private static long? TryGetMessageSize(IPop3MailboxClient client, int index, CancellationToken cancellationToken) {\n        try {\n            return client.GetMessageSize(index, cancellationToken);\n        } catch {\n            return null;\n        }\n    }\n\n    private sealed record Pop3UidLookupResult(bool Supported, int? Index);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Receive/Pop3PollListener.cs",
    "content": "using MailKit.Net.Pop3;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Polls a POP3 mailbox for new messages and raises events when they arrive.\n/// </summary>\n/// <remarks>\n/// Uses basic polling rather than the IMAP IDLE approach and is\n/// therefore best suited for servers without IDLE support.\n/// </remarks>\npublic class Pop3PollListener : IDisposable, IAsyncDisposable {\n    private readonly Pop3Client _client;\n    private readonly HashSet<string> _knownUids = new HashSet<string>(StringComparer.Ordinal);\n    private CancellationTokenSource? _cancel;\n    private Task? _pollingTask;\n    private readonly TimeSpan _interval;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Pop3PollListener\"/> class.\n    /// </summary>\n    /// <param name=\"client\">Connected POP3 client.</param>\n    /// <param name=\"interval\">Polling interval.</param>\n    public Pop3PollListener(Pop3Client client, TimeSpan? interval = null) {\n        _client = client ?? throw new ArgumentNullException(nameof(client));\n        _interval = interval ?? TimeSpan.FromMinutes(1);\n    }\n\n    /// <summary>\n    /// Occurs when a new message arrives.\n    /// </summary>\n    public event EventHandler<Pop3EmailMessage>? MessageArrived;\n\n    /// <summary>\n    /// Occurs when an error is encountered while polling.\n    /// </summary>\n    public event EventHandler<Exception>? PollError;\n\n    /// <summary>\n    /// Starts listening for new messages.\n    /// </summary>\n    public async Task StartAsync(CancellationToken cancellationToken = default) {\n        if (_cancel != null) {\n            throw new InvalidOperationException(\"Listener already started.\");\n        }\n\n        _cancel = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        try {\n            _knownUids.Clear();\n            var count = GetMessageCount();\n            for (var i = 0; i < count; i++) {\n                var uid = await GetMessageUidAsync(i, _cancel.Token).ConfigureAwait(false);\n                if (!string.IsNullOrEmpty(uid)) {\n                    _knownUids.Add(uid);\n                }\n            }\n\n            _pollingTask = PollLoopAsync();\n        } catch {\n            _knownUids.Clear();\n            _cancel.Dispose();\n            _cancel = null;\n            _pollingTask = null;\n            throw;\n        }\n    }\n\n    /// <summary>\n    /// Stops listening for new messages.\n    /// </summary>\n    public void Stop() => StopAsync().GetAwaiter().GetResult();\n\n    /// <summary>\n    /// Stops listening for new messages and waits for the polling loop to finish.\n    /// </summary>\n    public async Task StopAsync() {\n        var cancel = _cancel;\n        var pollingTask = _pollingTask;\n\n        if (cancel == null) {\n            if (pollingTask != null) {\n                try {\n                    await pollingTask.ConfigureAwait(false);\n                } finally {\n                    _pollingTask = null;\n                }\n            }\n\n            return;\n        }\n\n        try {\n            cancel.Cancel();\n            if (pollingTask != null) {\n                try {\n                    await pollingTask.ConfigureAwait(false);\n                } catch (OperationCanceledException) when (cancel.IsCancellationRequested) {\n                }\n            }\n        } finally {\n            cancel.Dispose();\n            _cancel = null;\n            _pollingTask = null;\n        }\n    }\n\n    private async Task PollLoopAsync() {\n        while (!_cancel!.IsCancellationRequested) {\n            try {\n                await DelayAsync(_interval, _cancel.Token).ConfigureAwait(false);\n                await NoOpAsync(_cancel.Token).ConfigureAwait(false);\n                var count = GetMessageCount();\n                var currentUids = new HashSet<string>(StringComparer.Ordinal);\n                for (var i = 0; i < count; i++) {\n                    var uid = await GetMessageUidAsync(i, _cancel.Token).ConfigureAwait(false);\n                    if (string.IsNullOrEmpty(uid)) {\n                        continue;\n                    }\n\n                    currentUids.Add(uid);\n\n                    if (_knownUids.Contains(uid)) {\n                        continue;\n                    }\n\n                    var message = await GetMessageAsync(i, _cancel.Token).ConfigureAwait(false);\n                    MessageArrived?.Invoke(this, new Pop3EmailMessage(i, message));\n                    _knownUids.Add(uid);\n                }\n\n                _knownUids.IntersectWith(currentUids);\n            } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                break;\n            } catch (Exception ex) {\n                PollError?.Invoke(this, ex);\n                try {\n                    await DelayAsync(TimeSpan.FromSeconds(5), _cancel.Token).ConfigureAwait(false);\n                } catch (OperationCanceledException) when (_cancel.IsCancellationRequested) {\n                    break;\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// Retrieves the total number of messages in the current mailbox.\n    /// </summary>\n    /// <returns>The number of messages available.</returns>\n    protected virtual int GetMessageCount() => _client.Count;\n\n    /// <summary>\n    /// Retrieves a message UID by index.\n    /// </summary>\n    /// <param name=\"index\">Message index.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>The message UID.</returns>\n    protected virtual Task<string> GetMessageUidAsync(int index, CancellationToken cancellationToken) =>\n        _client.GetMessageUidAsync(index, cancellationToken);\n\n    /// <summary>\n    /// Retrieves a message by index.\n    /// </summary>\n    /// <param name=\"index\">Message index.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>The message.</returns>\n    protected virtual Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken) =>\n        _client.GetMessageAsync(index, cancellationToken);\n\n    /// <summary>\n    /// Sends a NOOP command to keep the POP3 connection alive.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected virtual Task NoOpAsync(CancellationToken cancellationToken) =>\n        _client.NoOpAsync(cancellationToken);\n\n    /// <summary>\n    /// Delays the polling loop execution.\n    /// </summary>\n    /// <param name=\"interval\">Delay interval.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected virtual Task DelayAsync(TimeSpan interval, CancellationToken cancellationToken) =>\n        Task.Delay(interval, cancellationToken);\n\n    /// <inheritdoc />\n    public void Dispose() {\n        Stop();\n        GC.SuppressFinalize(this);\n    }\n\n    /// <inheritdoc />\n    public async ValueTask DisposeAsync() {\n        await StopAsync().ConfigureAwait(false);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Resources/allowlist.conf",
    "content": "# placeholder allowed disposable domain list\nallowed.example.com\n"
  },
  {
    "path": "Sources/Mailozaurr/Resources/disposable_email_blocklist.conf",
    "content": "# placeholder disposable domain list\nexample.com\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridAttachment.cs",
    "content": "using MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents an attachment in a SendGrid message.\n/// </summary>\n/// <remarks>\n/// The file content is stored as a Base64 string as required by the\n/// SendGrid API.\n/// </remarks>\npublic class SendGridAttachment {\n    /// <summary>Gets or sets the filename of the attachment.</summary>\n    public string Filename { get; set; }\n\n    /// <summary>Gets or sets the content of the attachment, encoded in Base64.</summary>\n    public string Content { get; set; }\n\n    /// <summary>Gets or sets the MIME type of the attachment.</summary>\n    public string Type { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the disposition of the attachment.</summary>\n    public string Disposition { get; set; } = \"attachment\";\n\n    /// <summary>Gets or sets the optional content identifier of the attachment.</summary>\n    public string? ContentId { get; set; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SendGridAttachment\"/> class using raw content.\n    /// </summary>\n    /// <param name=\"fileName\">File name to associate with the attachment.</param>\n    /// <param name=\"content\">Attachment content as a byte array.</param>\n    /// <param name=\"contentType\">Optional MIME type of the attachment.</param>\n    /// <param name=\"disposition\">Disposition applied to the attachment.</param>\n    /// <param name=\"contentId\">Optional content identifier for inline attachments.</param>\n    public SendGridAttachment(string fileName, byte[] content, string? contentType = null, string? disposition = \"attachment\", string? contentId = null) {\n        if (string.IsNullOrWhiteSpace(fileName)) {\n            throw new ArgumentException(\"File name must be provided.\", nameof(fileName));\n        }\n\n        content ??= Array.Empty<byte>();\n\n        Filename = fileName;\n        Content = Convert.ToBase64String(content);\n        Type = !string.IsNullOrWhiteSpace(contentType) ? contentType! : (MimeTypes.GetMimeType(fileName) ?? \"application/octet-stream\");\n        Disposition = string.IsNullOrWhiteSpace(disposition) ? \"attachment\" : disposition!;\n        ContentId = contentId;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SendGridAttachment\"/> class from a file path.\n    /// </summary>\n    /// <param name=\"filePath\">The path of the file to attach.</param>\n    public SendGridAttachment(string filePath) {\n        if (string.IsNullOrWhiteSpace(filePath)) {\n            throw new ArgumentException(\"File path must be provided.\", nameof(filePath));\n        }\n\n        var bytes = File.ReadAllBytes(filePath);\n        var fileName = Path.GetFileName(filePath);\n\n        Filename = fileName;\n        Content = Convert.ToBase64String(bytes);\n        Type = MimeTypes.GetMimeType(filePath);\n        Disposition = \"attachment\";\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridClient.cs",
    "content": "﻿using System.Threading;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// A client for sending emails using the SendGrid API.\n/// </summary>\n/// <remarks>\n/// Only key functionality required by the module is implemented;\n/// it is not intended as a full wrapper of the SendGrid SDK.\n/// </remarks>\npublic sealed class SendGridClient : IDisposable {\n    /// <summary>\n    /// Gets the JSON representation of the message to be sent.\n    /// </summary>\n    private string MessageJson { get; set; } = string.Empty;\n\n    /// <summary>\n    /// The HttpClient used to send HTTP requests.\n    /// </summary>\n    private readonly HttpClient _client;\n    private bool _disposed;\n\n    /// <summary>\n    /// Stopwatch to measure the time taken to send an email.\n    /// </summary>\n    public readonly Stopwatch Stopwatch;\n\n    /// <summary>\n    /// When set, sending is simulated and no SendGrid request is issued.\n    /// </summary>\n    public bool DryRun { get; set; }\n\n    /// <summary>\n    /// Gets or sets the sender of the email.\n    /// </summary>\n    public object? From { get; set; }\n\n    /// <summary>\n    /// Gets or sets the list of primary recipients of the email.\n    /// </summary>\n    public List<object>? To { get; set; }\n\n    /// <summary>\n    /// Gets or sets the list of carbon copy (CC) recipients of the email.\n    /// </summary>\n    public List<object>? Cc { get; set; }\n\n    /// <summary>\n    /// Gets or sets the list of blind carbon copy (BCC) recipients of the email.\n    /// </summary>\n    public List<object>? Bcc { get; set; }\n\n    /// <summary>\n    /// Gets or sets the reply-to address for the email.\n    /// </summary>\n    public object? ReplyTo { get; set; }\n\n    /// <summary>\n    /// Gets or sets the attachments to include with the email.\n    /// </summary>\n    public List<AttachmentDescriptor>? Attachments { get; set; }\n\n    /// <summary>Custom headers to include with the message.</summary>\n    public Dictionary<string, string>? Headers { get; set; }\n\n    /// <summary>\n    /// Gets or sets the subject of the email.\n    /// </summary>\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Gets or sets the plain text content of the email.\n    /// </summary>\n    public string Text { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the HTML content of the email.\n    /// </summary>\n    public string Html { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the priority of the email.\n    /// </summary>\n    public MessagePriority Priority { get; set; }\n\n    /// <summary>\n    /// Gets or sets a value indicating whether to send separate emails to each recipient.\n    /// </summary>\n    public bool SeparateTo { get; set; }\n\n    /// <summary>\n    /// Gets or sets the credentials used for authentication with the SendGrid API.\n    /// </summary>\n    public ICredentials? Credentials { get; set; }\n\n    /// <summary>\n    /// Gets or sets the action to take when an error occurs.\n    /// </summary>\n    public ActionPreference? ErrorAction { get; set; }\n\n    /// <summary>\n    /// Repository used to persist messages that require retrying.\n    /// </summary>\n    public IPendingMessageRepository? PendingMessageRepository { get; set; }\n\n    /// <summary>\n    /// Number of times to retry sending the message when an error occurs.\n    /// </summary>\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay in milliseconds between retry attempts.\n    /// </summary>\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Factor used to increase the delay for each subsequent retry. A value of\n    /// 1 disables backoff.\n    /// </summary>\n    public double RetryDelayBackoff { get; set; } = 1.0;\n    /// <summary>Maximum delay in milliseconds between retries. 0 disables capping.</summary>\n    public int MaxDelayMilliseconds { get; set; } = 0;\n    /// <summary>Jitter window in milliseconds added to retry delay. 0 disables jitter.</summary>\n    public int JitterMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// If set to <c>true</c>, retries will occur even on non-transient\n    /// failures. Otherwise only transient errors trigger retries.\n    /// </summary>\n    public bool RetryAlways { get; set; } = false;\n\n    /// <summary>Webhook invoked after sending.</summary>\n    public string? WebhookUrl { get; set; }\n\n    /// <summary>\n    /// Gets a string containing the email addresses of all recipients of the email.\n    /// </summary>\n    public string SentTo {\n        get {\n            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            var addresses = new List<string>();\n            if (To != null) {\n                addresses.AddRange(To.Select(ConvertToEmailObject).Where(x => x != null && seen.Add(x.Email)).Select(x => x!.Email));\n            }\n            if (Cc != null) {\n                addresses.AddRange(Cc.Select(ConvertToEmailObject).Where(x => x != null && seen.Add(x.Email)).Select(x => x!.Email));\n            }\n            if (Bcc != null) {\n                addresses.AddRange(Bcc.Select(ConvertToEmailObject).Where(x => x != null && seen.Add(x.Email)).Select(x => x!.Email));\n            }\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>\n    /// Gets the email address of the sender of the email.\n    /// </summary>\n    public string SentFrom => From != null ? Helpers.GetEmailAddress(From) : string.Empty;\n\n    /// <summary>\n    /// Gets or sets the log collector for this client.\n    /// </summary>\n    public LogCollector LogCollector { get; set; } = new();\n\n    /// <summary>\n    /// Initializes a new instance of the SendGridClient class.\n    /// </summary>\n    public SendGridClient() {\n        Stopwatch = Stopwatch.StartNew();\n        _client = new HttpClient {\n            Timeout = TimeSpan.FromSeconds(30)\n        };\n    }\n\n    /// <summary>\n    /// Converts the provided object to a SendGridEmailAddress object.\n    /// </summary>\n    /// <param name=\"emailAddress\">The object to convert.</param>\n    /// <returns>A SendGridEmailAddress object, or null if the provided object is null or an empty string.</returns>\n    private SendGridEmailAddress? ConvertToEmailObject(object? emailAddress) {\n        if (emailAddress == null) {\n            return null;\n        }\n\n        if (emailAddress is SendGridEmailAddress sendGridEmail) {\n            return sendGridEmail;\n        }\n\n        var emailAsString = Convert.ToString(emailAddress);\n        if (string.IsNullOrWhiteSpace(emailAsString)) {\n            return null;\n        }\n\n        if (emailAddress is string emailString) {\n            return new SendGridEmailAddress { Email = emailString };\n        }\n\n        if (emailAddress is IDictionary<string, object> emailDict) {\n            if (!emailDict.ContainsKey(\"Email\")) {\n                throw new ArgumentException(\"Dictionary is missing required key 'Email'.\", nameof(emailAddress));\n            }\n\n            var emailValue = Convert.ToString(emailDict[\"Email\"]);\n            if (string.IsNullOrWhiteSpace(emailValue)) {\n                return null;\n            }\n\n            var nameValue = emailDict.ContainsKey(\"Name\") ? Convert.ToString(emailDict[\"Name\"]) : null;\n            return new SendGridEmailAddress { Email = emailValue, Name = nameValue };\n        }\n\n        throw new ArgumentException(\n            $\"Unsupported email address type {emailAddress.GetType().Name}. Expected string, SendGridEmailAddress, or IDictionary<string, object>.\",\n            nameof(emailAddress));\n    }\n\n    /// <summary>\n    /// Converts a collection of attachment descriptors to <see cref=\"SendGridAttachment\"/> objects.\n    /// </summary>\n    /// <param name=\"attachments\">Attachments to convert.</param>\n    /// <param name=\"logger\">Logger used to emit warnings.</param>\n    /// <returns>List of converted attachments.</returns>\n    private static List<SendGridAttachment> ConvertAttachments(IEnumerable<AttachmentDescriptor>? attachments, LogCollector logger) {\n        var result = new List<SendGridAttachment>();\n        if (attachments == null) {\n            return result;\n        }\n\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        foreach (var descriptor in attachments) {\n            if (descriptor == null) {\n                continue;\n            }\n\n            var path = descriptor.SourcePath;\n            if (!string.IsNullOrWhiteSpace(path)) {\n                if (!seen.Add(path!)) {\n                    continue;\n                }\n\n                if (descriptor is FileAttachmentDescriptor fileDescriptor && !File.Exists(fileDescriptor.FilePath)) {\n                    logger.LogWarning($\"Send-EmailMessage - File not found: {fileDescriptor.FilePath}. Skipping attachment.\");\n                    continue;\n                }\n            }\n\n            result.Add(CreateSendGridAttachment(descriptor));\n        }\n\n        return result;\n    }\n\n    private static SendGridAttachment CreateSendGridAttachment(AttachmentDescriptor descriptor) {\n        if (descriptor is MimeEntityAttachmentDescriptor) {\n            throw new ArgumentException(\"SendGrid attachments do not support MimeEntity descriptors.\", nameof(descriptor));\n        }\n\n        var fileName = descriptor.FileName;\n        if (string.IsNullOrWhiteSpace(fileName) && descriptor.SourcePath is string sourcePath) {\n            fileName = Path.GetFileName(sourcePath);\n        }\n\n        fileName ??= \"attachment\";\n\n        var contentType = descriptor.ContentType;\n        if (string.IsNullOrWhiteSpace(contentType)) {\n            contentType = MimeTypes.GetMimeType(fileName);\n        }\n\n        var disposition = descriptor.ContentDisposition?.Disposition ?? \"attachment\";\n        var bytes = descriptor.GetContentBytes();\n        return new SendGridAttachment(fileName, bytes, contentType, disposition, descriptor.ContentId);\n    }\n\n    /// <summary>\n    /// Creates a SendGridMessage object from the properties of this SendGridClient.\n    /// </summary>\n    public void CreateMessage() {\n        ThrowIfDisposed();\n\n        var attachments = ConvertAttachments(Attachments, LogCollector);\n\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        var personalizations = new List<SendGridPersonalization>\n            {\n                new SendGridPersonalization\n                {\n                    To = To?.Where(t => t != null)\n                        .Select(ConvertToEmailObject)\n                        .Where(x => x != null && seen.Add(x.Email))\n                        .Select(x => x!)\n                        .ToList(),\n                    Cc = Cc?.Where(c => c != null)\n                        .Select(ConvertToEmailObject)\n                        .Where(x => x != null && seen.Add(x.Email))\n                        .Select(x => x!)\n                        .ToList(),\n                    Bcc = Bcc?.Where(b => b != null)\n                        .Select(ConvertToEmailObject)\n                        .Where(x => x != null && seen.Add(x.Email))\n                        .Select(x => x!)\n                        .ToList()\n                }\n            }\n            .Where(p => p.To != null || p.Cc != null || p.Bcc != null)\n            .ToList();\n\n        var content = new List<SendGridContent> {\n                new SendGridContent { Type = \"text/plain\", Value = Text },\n                new SendGridContent { Type = \"text/html\", Value = Html }\n            }.Where(c => !string.IsNullOrEmpty(c.Value)).ToList();\n\n\n        var fromAddress = ConvertToEmailObject(From) ?? throw new InvalidOperationException(\"From address is required.\");\n\n        var message = new SendGridMessage {\n            Personalizations = personalizations,\n            From = fromAddress,\n            Subject = Subject,\n            Content = content,\n            ReplyTo = ConvertToEmailObject(ReplyTo),\n            Attachments = attachments,\n            Headers = Headers\n        };\n\n        MessageJson = JsonSerializer.Serialize(message, MailozaurrJsonContext.Default.SendGridMessage);\n        //Console.WriteLine(MessageJson);\n    }\n\n    /// <summary>\n    /// Sends an email asynchronously using the SendGrid API.\n    /// </summary>\n    /// <returns>A Task that represents the asynchronous operation. The task result contains the result of the email sending operation.</returns>\n    public Task<SmtpResult> SendEmailAsync() => SendEmailAsync(CancellationToken.None);\n\n    /// <summary>\n    /// Sends an email asynchronously using the SendGrid API.\n    /// </summary>\n    /// <returns>A Task that represents the asynchronous operation. The task result contains the result of the email sending operation.</returns>\n    public async Task<SmtpResult> SendEmailAsync(CancellationToken cancellationToken) {\n        ThrowIfDisposed();\n        if (DryRun) {\n            LogCollector.LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping SendGrid send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\");\n        }\n        string apiKey;\n        if (Credentials is NetworkCredential networkCredential) {\n            apiKey = networkCredential.Password;\n        } else {\n            const string message = \"Credentials must be NetworkCredential\";\n            LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using SendGrid: {message}\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidCastException(message);\n            }\n            var credFail = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, string.Empty, message);\n            await Helpers.PostWebhookAsync(WebhookUrl, credFail, cancellationToken).ConfigureAwait(false);\n            return credFail;\n        }\n\n        int attempts = 0;\n        Exception? lastException = null;\n        string? lastContent = null;\n        do {\n            try {\n                using var request = new HttpRequestMessage(HttpMethod.Post, \"https://api.sendgrid.com/v3/mail/send\") {\n                    Content = new StringContent(MessageJson, Encoding.UTF8, \"application/json\")\n                };\n                request.Headers.Add(\"Authorization\", $\"Bearer {apiKey}\");\n\n                using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n                lastContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n                lastContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                LogCollector.LogVerbose($\"Send-EmailMessage - Sent email to {SentTo} using SendGrid\");\n\n                if (response.IsSuccessStatusCode) {\n                    var okResult = new SmtpResult(true, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, response.StatusCode.ToString());\n                    await Helpers.PostWebhookAsync(WebhookUrl, okResult, cancellationToken).ConfigureAwait(false);\n                    return okResult;\n                }\n\n                var message = $\"Status code {response.StatusCode}: {lastContent}\";\n                lastException = new HttpRequestException(message);\n                LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using SendGrid: {message}\");\n            } catch (HttpRequestException ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - HTTP error during sending using SendGrid: {ex.Message}\");\n                if ((!Helpers.IsTransient(ex) && !RetryAlways) || attempts >= RetryCount) {\n                    await QueuePendingMessageAsync(apiKey, cancellationToken).ConfigureAwait(false);\n                    if (ErrorAction == ActionPreference.Stop && lastException != null) {\n                        throw lastException;\n                    }\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, lastContent, lastException?.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken).ConfigureAwait(false);\n                    return failResult;\n                }\n\n                var delayMilliseconds = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempts));\n                if (MaxDelayMilliseconds > 0 && delayMilliseconds > MaxDelayMilliseconds) {\n                    delayMilliseconds = MaxDelayMilliseconds;\n                }\n                if (JitterMilliseconds > 0 && delayMilliseconds > 0) {\n                    delayMilliseconds += GraphRetryHelperRandom.NextInt(JitterMilliseconds + 1);\n                }\n                if (delayMilliseconds > 0) {\n                    await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds), cancellationToken).ConfigureAwait(false);\n                }\n            } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                throw;\n            } catch (TaskCanceledException ex) {\n                lastException = ex;\n                LogCollector.LogWarning($\"Send-EmailMessage - Request canceled: {ex.Message}\");\n                if ((!Helpers.IsTransient(ex) && !RetryAlways) || attempts >= RetryCount) {\n                    await QueuePendingMessageAsync(apiKey, cancellationToken).ConfigureAwait(false);\n                    if (ErrorAction == ActionPreference.Stop && lastException != null) {\n                        throw lastException;\n                    }\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, lastContent, lastException?.Message);\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken).ConfigureAwait(false);\n                    return failResult;\n                }\n                var delayMs = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempts));\n                if (MaxDelayMilliseconds > 0 && delayMs > MaxDelayMilliseconds) {\n                    delayMs = MaxDelayMilliseconds;\n                }\n                if (JitterMilliseconds > 0 && delayMs > 0) {\n                    delayMs += GraphRetryHelperRandom.NextInt(JitterMilliseconds + 1);\n                }\n                if (delayMs > 0) {\n                    await Task.Delay(TimeSpan.FromMilliseconds(delayMs), cancellationToken).ConfigureAwait(false);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n\n        await QueuePendingMessageAsync(apiKey, cancellationToken).ConfigureAwait(false);\n        var finalResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, \"SendGridApi\", 0, Stopwatch.Elapsed, lastContent, lastException?.Message);\n        await Helpers.PostWebhookAsync(WebhookUrl, finalResult, cancellationToken).ConfigureAwait(false);\n        return finalResult;\n    }\n\n    private async Task QueuePendingMessageAsync(string apiKey, CancellationToken cancellationToken) {\n        if (PendingMessageRepository == null) {\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(MessageJson)) {\n            return;\n        }\n\n        if (string.IsNullOrEmpty(apiKey)) {\n            return;\n        }\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord {\n            MessageId = Guid.NewGuid().ToString(\"N\"),\n            Timestamp = now,\n            NextAttemptAt = now,\n            Provider = EmailProvider.SendGrid\n        };\n        record.ProviderData[SendGridPendingMessageSender.MessageJsonKey] = MessageJson;\n        var protector = CredentialProtection.Default;\n        record.ProviderData[SendGridPendingMessageSender.ApiKeyProtectedKey] = protector.Protect(apiKey);\n        record.ProviderData.Remove(SendGridPendingMessageSender.ApiKeyKey);\n        record.ProviderData.Remove(SendGridPendingMessageSender.ApiKeyBase64Key);\n\n        try {\n            await PendingMessageRepository.SaveAsync(record, cancellationToken).ConfigureAwait(false);\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (Exception ex) {\n            LogCollector.LogWarning($\"Send-EmailMessage - Failed to persist SendGrid pending message: {ex.Message}\");\n        }\n    }\n\n    private void ThrowIfDisposed() {\n        if (_disposed) {\n            throw new ObjectDisposedException(nameof(SendGridClient));\n        }\n    }\n\n    /// <summary>\n    /// Releases resources used by the <see cref=\"SendGridClient\"/>.\n    /// </summary>\n    public void Dispose() {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    private void Dispose(bool disposing) {\n        if (_disposed) {\n            return;\n        }\n\n        if (disposing) {\n            _client.Dispose();\n        }\n\n        _disposed = true;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridContent.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents the content of a SendGrid message.\n/// </summary>\n/// <remarks>\n/// SendGrid supports multiple content parts; typically one plain\n/// text and one HTML part.\n/// </remarks>\npublic class SendGridContent {\n    /// <summary>Gets or sets the type of the content.</summary>\n    public string? Type { get; set; }\n\n    /// <summary>Gets or sets the value of the content.</summary>\n    public string? Value { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridEmailAddress.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents an email address in a SendGrid message.\n/// </summary>\n/// <remarks>\n/// SendGrid expects addresses in this structured form when\n/// constructing the JSON payload.\n/// </remarks>\npublic class SendGridEmailAddress {\n    /// <summary>Gets or sets the email address.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string Email { get; set; } = string.Empty;\n\n    /// <summary>Gets or sets the name associated with the email address.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Name { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridMessage.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Represents a message to be sent using SendGrid.\n/// </summary>\n/// <remarks>\n/// This is a simplified model tailored for the module and does not\n/// expose every SendGrid feature.\n/// </remarks>\npublic class SendGridMessage {\n    /// <summary>\n    /// Gets or sets the list of personalizations for the message.\n    /// </summary>\n    public List<SendGridPersonalization> Personalizations { get; set; } = [];\n\n    /// <summary>\n    /// Gets or sets the sender of the message.\n    /// </summary>\n    public SendGridEmailAddress From { get; set; } = null!;\n\n    /// <summary>\n    /// Gets or sets the subject of the message.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Gets or sets the content of the message.\n    /// </summary>\n    public List<SendGridContent> Content { get; set; } = [];\n\n    /// <summary>\n    /// Gets or sets the reply-to address for the message.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public SendGridEmailAddress? ReplyTo { get; set; }\n\n    /// <summary>Attachments to include with the message.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<SendGridAttachment>? Attachments { get; set; }\n\n    /// <summary>Custom headers to include with the message.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public Dictionary<string, string>? Headers { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SendGrid/SendGridPersonalization.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a personalization in a SendGrid message.\n/// </summary>\n/// <remarks>\n/// SendGrid supports multiple personalizations per message to send\n/// customized content to several recipients.\n/// </remarks>\npublic class SendGridPersonalization {\n    /// <summary>Gets or sets the list of recipients for the message.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<SendGridEmailAddress>? To { get; set; }\n\n    /// <summary>Gets or sets the list of CC recipients for the message.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<SendGridEmailAddress>? Cc { get; set; }\n\n    /// <summary>Gets or sets the list of BCC recipients for the message.</summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public List<SendGridEmailAddress>? Bcc { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/FilePendingMessageRepository.cs",
    "content": "using System.Runtime.CompilerServices;\nusing System.Text.Json;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Stores pending message records in a single newline-delimited JSON file.\n/// </summary>\npublic sealed class FilePendingMessageRepository : IPendingMessageRepository {\n    private const string UpsertEntryType = \"upsert\";\n    private const string TombstoneEntryType = \"tombstone\";\n    private const int DefaultCompactionThreshold = 64;\n\n    private readonly string filePath;\n    private readonly SemaphoreSlim gate = new(1, 1);\n    private readonly Dictionary<string, long> index = new(StringComparer.OrdinalIgnoreCase);\n    private static readonly byte[] NewlineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);\n    private int dirtyEntryCount;\n\n    /// <summary>Creates a new repository using the specified options.</summary>\n    /// <param name=\"options\">Configuration for directory and file naming.</param>\n    public FilePendingMessageRepository(PendingMessageRepositoryOptions? options = null)\n        : this(GetFilePath(options)) { }\n\n    /// <summary>\n    /// Creates a new repository using the specified file path.\n    /// </summary>\n    /// <param name=\"filePath\">Path to the file that stores pending messages.</param>\n    public FilePendingMessageRepository(string filePath) {\n        this.filePath = filePath;\n        if (File.Exists(filePath)) {\n            BuildIndex();\n        }\n    }\n\n    private static string GetFilePath(PendingMessageRepositoryOptions? options) {\n        options ??= new PendingMessageRepositoryOptions();\n        var directory = string.IsNullOrWhiteSpace(options.DirectoryPath) ? Path.GetTempPath() : options.DirectoryPath;\n        string name;\n        try {\n            name = options.FileNamingScheme?.Invoke() ?? \"pending.log\";\n        } catch (Exception ex) {\n            throw new InvalidOperationException(\"FileNamingScheme failed to provide a file name\", ex);\n        }\n        return Path.Combine(directory, name);\n    }\n\n    private void BuildIndex() {\n        index.Clear();\n        dirtyEntryCount = 0;\n\n        if (!File.Exists(filePath)) {\n            return;\n        }\n\n        try {\n            foreach (var line in LogFileLineReader.ReadLinesWithOffsets(filePath)) {\n                if (!TryParseLogEntry(line.Line, out var entry)) {\n                    continue;\n                }\n\n                switch (entry.Kind) {\n                    case LogEntryKind.Upsert:\n                        if (index.ContainsKey(entry.MessageId)) {\n                            dirtyEntryCount++;\n                        }\n                        index[entry.MessageId] = line.Offset;\n                        break;\n                    case LogEntryKind.Tombstone:\n                        index.Remove(entry.MessageId);\n                        dirtyEntryCount++;\n                        break;\n                }\n            }\n        } catch (FileNotFoundException) {\n            index.Clear();\n            dirtyEntryCount = 0;\n        } catch (DirectoryNotFoundException) {\n            index.Clear();\n            dirtyEntryCount = 0;\n        }\n    }\n\n    private enum LogEntryKind {\n        Upsert,\n        Tombstone\n    }\n\n    private readonly struct LogEntry {\n        public LogEntry(LogEntryKind kind, string messageId, PendingMessageRecord? record) {\n            Kind = kind;\n            MessageId = messageId;\n            Record = record;\n        }\n\n        public LogEntryKind Kind { get; }\n\n        public string MessageId { get; }\n\n        public PendingMessageRecord? Record { get; }\n    }\n\n    private static PendingMessageLogEnvelope CreateUpsertEnvelope(PendingMessageRecord record) => new() {\n        EntryType = UpsertEntryType,\n        MessageId = record.MessageId,\n        Record = record\n    };\n\n    private static PendingMessageLogEnvelope CreateTombstoneEnvelope(string messageId) => new() {\n        EntryType = TombstoneEntryType,\n        MessageId = messageId\n    };\n\n    private static byte[] SerializeEnvelope(PendingMessageLogEnvelope envelope) => JsonSerializer.SerializeToUtf8Bytes(envelope, MailozaurrJsonContext.Default.PendingMessageLogEnvelope);\n\n    private async Task<long> AppendEnvelopeAsync(PendingMessageLogEnvelope envelope, CancellationToken cancellationToken) {\n        var payload = SerializeEnvelope(envelope);\n\n        using var write = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read);\n        var offset = write.Position;\n\n        await write.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false);\n        await write.WriteAsync(NewlineBytes, 0, NewlineBytes.Length, cancellationToken).ConfigureAwait(false);\n        await write.FlushAsync(cancellationToken).ConfigureAwait(false);\n\n        return offset;\n    }\n\n    private async Task CompactIfNeededAsync(CancellationToken cancellationToken) {\n        if (dirtyEntryCount < DefaultCompactionThreshold) {\n            return;\n        }\n\n        if (!File.Exists(filePath)) {\n            return;\n        }\n\n        await CompactAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task CompactAsync(CancellationToken cancellationToken) {\n        var temp = filePath + \".compact\";\n\n        if (File.Exists(temp)) {\n            File.Delete(temp);\n        }\n\n        var records = new Dictionary<string, PendingMessageRecord>(StringComparer.OrdinalIgnoreCase);\n        var orderedIds = new List<string>();\n\n        using (var read = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None))\n        using (var reader = new StreamReader(read, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) {\n            string? line;\n            while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {\n                cancellationToken.ThrowIfCancellationRequested();\n\n                if (!TryParseLogEntry(line, out var entry)) {\n                    continue;\n                }\n\n                switch (entry.Kind) {\n                    case LogEntryKind.Upsert when entry.Record != null:\n                        if (!records.ContainsKey(entry.MessageId)) {\n                            orderedIds.Add(entry.MessageId);\n                        }\n                        records[entry.MessageId] = entry.Record;\n                        break;\n                    case LogEntryKind.Tombstone:\n                        records.Remove(entry.MessageId);\n                        orderedIds.RemoveAll(id => string.Equals(id, entry.MessageId, StringComparison.OrdinalIgnoreCase));\n                        break;\n                }\n            }\n        }\n\n        var newIndex = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);\n\n        try {\n            using (var write = new FileStream(temp, FileMode.Create, FileAccess.Write, FileShare.None)) {\n                long position = 0;\n\n                foreach (var id in orderedIds) {\n                    if (!records.TryGetValue(id, out var record)) {\n                        continue;\n                    }\n\n                    var envelope = CreateUpsertEnvelope(record);\n                    var payload = SerializeEnvelope(envelope);\n\n                    await write.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false);\n                    await write.WriteAsync(NewlineBytes, 0, NewlineBytes.Length, cancellationToken).ConfigureAwait(false);\n\n                    newIndex[id] = position;\n                    position += payload.Length + NewlineBytes.Length;\n                }\n\n                await write.FlushAsync(cancellationToken).ConfigureAwait(false);\n            }\n\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n\n            File.Move(temp, filePath);\n\n            index.Clear();\n            foreach (var pair in newIndex) {\n                index[pair.Key] = pair.Value;\n            }\n\n            dirtyEntryCount = 0;\n        } finally {\n            if (File.Exists(temp)) {\n                File.Delete(temp);\n            }\n        }\n    }\n\n    private static bool TryParseLogEntry(string json, out LogEntry entry) {\n        entry = default;\n\n        if (string.IsNullOrWhiteSpace(json)) {\n            return false;\n        }\n\n        try {\n            using var document = JsonDocument.Parse(json);\n            var root = document.RootElement;\n\n            if (TryGetPropertyCaseInsensitive(root, \"entryType\", out var entryTypeElement) && entryTypeElement.ValueKind == JsonValueKind.String) {\n                var entryType = entryTypeElement.GetString();\n\n                if (string.Equals(entryType, TombstoneEntryType, StringComparison.OrdinalIgnoreCase)) {\n                    if (TryReadMessageId(root, out var tombstoneMessageId)) {\n                        entry = new LogEntry(LogEntryKind.Tombstone, tombstoneMessageId, null);\n                        return true;\n                    }\n\n                    return false;\n                }\n\n                if (string.Equals(entryType, UpsertEntryType, StringComparison.OrdinalIgnoreCase)) {\n                    PendingMessageRecord? record = null;\n                    if (TryGetPropertyCaseInsensitive(root, \"record\", out var recordElement) && recordElement.ValueKind == JsonValueKind.Object) {\n                        record = recordElement.Deserialize(MailozaurrJsonContext.Default.PendingMessageRecord);\n                        if (record != null) {\n                            _ = record.ProviderData;\n                        }\n                    }\n\n                    string? messageId = record?.MessageId;\n\n                    if (string.IsNullOrWhiteSpace(messageId) && TryReadMessageId(root, out var entryMessageId)) {\n                        messageId = entryMessageId;\n                        if (record != null) {\n                            record.MessageId = messageId;\n                        }\n                    }\n\n                    if (!string.IsNullOrWhiteSpace(messageId) && record != null) {\n                        entry = new LogEntry(LogEntryKind.Upsert, messageId!, record);\n                        return true;\n                    }\n\n                    return false;\n                }\n\n                return false;\n            }\n\n            var legacyRecord = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.PendingMessageRecord);\n            if (legacyRecord != null && !string.IsNullOrWhiteSpace(legacyRecord.MessageId)) {\n                _ = legacyRecord.ProviderData;\n                entry = new LogEntry(LogEntryKind.Upsert, legacyRecord.MessageId, legacyRecord);\n                return true;\n            }\n        } catch (JsonException) {\n            return false;\n        }\n\n        return false;\n    }\n\n    private static bool TryReadMessageId(JsonElement element, out string messageId) {\n        if (TryGetPropertyCaseInsensitive(element, \"messageId\", out var messageIdElement) && messageIdElement.ValueKind == JsonValueKind.String) {\n            messageId = messageIdElement.GetString() ?? string.Empty;\n            return !string.IsNullOrWhiteSpace(messageId);\n        }\n\n        messageId = string.Empty;\n        return false;\n    }\n\n    private static bool TryGetPropertyCaseInsensitive(JsonElement element, string propertyName, out JsonElement value) {\n        foreach (var property in element.EnumerateObject()) {\n            if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) {\n                value = property.Value;\n                return true;\n            }\n        }\n\n        value = default;\n        return false;\n    }\n\n    /// <summary>Saves a pending message to the repository.</summary>\n    public async Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var directory = Path.GetDirectoryName(filePath);\n            if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n                Directory.CreateDirectory(directory);\n            }\n\n            if (record.NextAttemptAt == default) {\n                record.NextAttemptAt = DateTimeOffset.UtcNow;\n            }\n\n            _ = record.ProviderData;\n            var hasExistingRecord = index.ContainsKey(record.MessageId);\n            var envelope = CreateUpsertEnvelope(record);\n            var offset = await AppendEnvelopeAsync(envelope, cancellationToken).ConfigureAwait(false);\n\n            if (hasExistingRecord) {\n                dirtyEntryCount++;\n            }\n\n            index[record.MessageId] = offset;\n\n            await CompactIfNeededAsync(cancellationToken).ConfigureAwait(false);\n        } finally {\n            gate.Release();\n        }\n    }\n\n    /// <summary>Attempts to lease a due pending message for processing.</summary>\n    public async Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n        string messageId,\n        DateTimeOffset dueBeforeOrAt,\n        DateTimeOffset leaseUntil,\n        CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var current = await GetByMessageIdCoreAsync(messageId, cancellationToken).ConfigureAwait(false);\n            if (current == null || current.NextAttemptAt > dueBeforeOrAt) {\n                return null;\n            }\n\n            _ = current.ProviderData;\n            current.NextAttemptAt = leaseUntil;\n            var offset = await AppendEnvelopeAsync(CreateUpsertEnvelope(current), cancellationToken).ConfigureAwait(false);\n            dirtyEntryCount++;\n            index[current.MessageId] = offset;\n\n            await CompactIfNeededAsync(cancellationToken).ConfigureAwait(false);\n            return current;\n        } catch (FileNotFoundException) {\n            return null;\n        } catch (DirectoryNotFoundException) {\n            return null;\n        } finally {\n            gate.Release();\n        }\n    }\n\n    /// <summary>Retrieves a pending message by its ID.</summary>\n    public async Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n        if (!File.Exists(filePath)) {\n            return null;\n        }\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            return await GetByMessageIdCoreAsync(messageId, cancellationToken).ConfigureAwait(false);\n        } catch (FileNotFoundException) {\n            return null;\n        } catch (DirectoryNotFoundException) {\n            return null;\n        } finally {\n            gate.Release();\n        }\n    }\n\n    private async Task<PendingMessageRecord?> GetByMessageIdCoreAsync(string messageId, CancellationToken cancellationToken) {\n        if (!index.TryGetValue(messageId, out var offset)) {\n            return null;\n        }\n\n        using var read = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        read.Seek(offset, SeekOrigin.Begin);\n        using var reader = new StreamReader(read, Encoding.UTF8, false, 1024, leaveOpen: true);\n        cancellationToken.ThrowIfCancellationRequested();\n        string? line = await reader.ReadLineAsync().ConfigureAwait(false);\n        if (line == null) {\n            return null;\n        }\n\n        if (!TryParseLogEntry(line, out var entry) || entry.Kind != LogEntryKind.Upsert || entry.Record == null) {\n            return null;\n        }\n\n        if (!string.Equals(entry.MessageId, messageId, StringComparison.OrdinalIgnoreCase)) {\n            return null;\n        }\n\n        return entry.Record;\n    }\n\n    /// <summary>Enumerates all pending messages.</summary>\n    public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n        if (!File.Exists(filePath)) {\n            yield break;\n        }\n        var snapshot = new List<PendingMessageRecord>();\n\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var orderedIds = new List<string>();\n            var recordsById = new Dictionary<string, PendingMessageRecord>(StringComparer.OrdinalIgnoreCase);\n            using var read = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n            using var reader = new StreamReader(read, Encoding.UTF8, false, 1024, leaveOpen: true);\n            string? line;\n            while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (!TryParseLogEntry(line, out var entry)) {\n                    continue;\n                }\n\n                switch (entry.Kind) {\n                    case LogEntryKind.Upsert when entry.Record != null:\n                        if (!recordsById.ContainsKey(entry.MessageId)) {\n                            orderedIds.Add(entry.MessageId);\n                        }\n\n                        recordsById[entry.MessageId] = entry.Record;\n                        break;\n                    case LogEntryKind.Tombstone:\n                        recordsById.Remove(entry.MessageId);\n                        orderedIds.RemoveAll(id => string.Equals(id, entry.MessageId, StringComparison.OrdinalIgnoreCase));\n                        break;\n                }\n            }\n\n            foreach (var id in orderedIds) {\n                if (recordsById.TryGetValue(id, out var record)) {\n                    snapshot.Add(record);\n                }\n            }\n        } catch (FileNotFoundException) {\n            snapshot.Clear();\n        } catch (DirectoryNotFoundException) {\n            snapshot.Clear();\n        } finally {\n            gate.Release();\n        }\n\n        foreach (var record in snapshot) {\n            cancellationToken.ThrowIfCancellationRequested();\n            yield return record;\n        }\n    }\n\n    /// <summary>Removes a pending message by its ID.</summary>\n    public async Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            if (!index.ContainsKey(messageId)) {\n                return;\n            }\n\n            if (!File.Exists(filePath)) {\n                index.Remove(messageId);\n                return;\n            }\n            var envelope = CreateTombstoneEnvelope(messageId);\n            await AppendEnvelopeAsync(envelope, cancellationToken).ConfigureAwait(false);\n\n            index.Remove(messageId);\n            dirtyEntryCount++;\n\n            await CompactIfNeededAsync(cancellationToken).ConfigureAwait(false);\n        } finally {\n            gate.Release();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/FileSentMessageRepository.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Stores sent message records in a single newline-delimited JSON file.\n/// </summary>\npublic sealed class FileSentMessageRepository : ISentMessageRepository {\n    private readonly string filePath;\n    private readonly SemaphoreSlim gate = new(1, 1);\n    private readonly Dictionary<string, long> index = new(StringComparer.OrdinalIgnoreCase);\n    private static readonly byte[] NewlineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);\n\n    /// <summary>\n    /// Creates a new repository using the specified file path.\n    /// </summary>\n    /// <param name=\"filePath\">Path to the log file that stores sent message records.</param>\n    public FileSentMessageRepository(string filePath) {\n        this.filePath = filePath;\n        if (File.Exists(filePath)) {\n            BuildIndex();\n        }\n    }\n\n    private void BuildIndex() {\n        index.Clear();\n        try {\n            foreach (var entry in LogFileLineReader.ReadLinesWithOffsets(filePath)) {\n                var line = entry.Line;\n                if (string.IsNullOrWhiteSpace(line)) {\n                    continue;\n                }\n\n                if (TryDeserializeRecord(line, out var record) && !string.IsNullOrEmpty(record!.MessageId)) {\n                    index[record.MessageId] = entry.Offset;\n                }\n            }\n        } catch (FileNotFoundException) {\n            index.Clear();\n        } catch (DirectoryNotFoundException) {\n            index.Clear();\n        }\n    }\n\n    private static bool TryDeserializeRecord(string json, out SentMessageRecord? record) {\n        record = null;\n        if (string.IsNullOrWhiteSpace(json)) {\n            return false;\n        }\n\n        try {\n            record = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.SentMessageRecord);\n            return record != null;\n        } catch (JsonException) {\n            return false;\n        }\n    }\n\n    /// <summary>Saves a record to the log file.</summary>\n    public async Task SaveAsync(SentMessageRecord record, CancellationToken cancellationToken = default) {\n        await gate.WaitAsync(cancellationToken);\n        try {\n            var directory = Path.GetDirectoryName(filePath);\n            if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n                Directory.CreateDirectory(directory);\n            }\n            using var write = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read);\n            var offset = write.Position;\n            await JsonSerializer.SerializeAsync(write, record, MailozaurrJsonContext.Default.SentMessageRecord, cancellationToken);\n            await write.WriteAsync(NewlineBytes, 0, NewlineBytes.Length, cancellationToken);\n            index[record.MessageId] = offset;\n        } finally {\n            gate.Release();\n        }\n    }\n\n    /// <summary>Retrieves a record by message ID from the log file.</summary>\n    public async Task<SentMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n        if (!File.Exists(filePath)) {\n            return null;\n        }\n        await gate.WaitAsync(cancellationToken);\n        try {\n            if (!index.TryGetValue(messageId, out var offset)) {\n                return null;\n            }\n            using var read = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n            read.Seek(offset, SeekOrigin.Begin);\n            using var reader = new StreamReader(read, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true);\n            string? line = await reader.ReadLineAsync();\n            if (string.IsNullOrWhiteSpace(line)) {\n                return null;\n            }\n            if (TryDeserializeRecord(line, out var record) && string.Equals(record!.MessageId, messageId, StringComparison.OrdinalIgnoreCase)) {\n                return record;\n            }\n            return null;\n        } catch (FileNotFoundException) {\n            return null;\n        } catch (DirectoryNotFoundException) {\n            return null;\n        } finally {\n            gate.Release();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/IPendingMessageProcessorObserver.cs",
    "content": "using System;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides telemetry hooks for <see cref=\"PendingMessageProcessor\"/> operations.\n/// </summary>\npublic interface IPendingMessageProcessorObserver {\n    /// <summary>Called when a pending message is skipped before attempting delivery.</summary>\n    void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason);\n\n    /// <summary>Called immediately before a delivery attempt is made.</summary>\n    void MessageAttemptStarted(PendingMessageRecord record, int attempt);\n\n    /// <summary>Called when a message is successfully sent.</summary>\n    void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration);\n\n    /// <summary>\n    /// Called when a delivery attempt fails.\n    /// </summary>\n    /// <param name=\"record\">The record that was processed.</param>\n    /// <param name=\"attempt\">The attempt number that failed.</param>\n    /// <param name=\"exception\">The exception describing the failure.</param>\n    /// <param name=\"duration\">How long the attempt took.</param>\n    /// <param name=\"willRetry\">Indicates whether another attempt will be scheduled.</param>\n    /// <param name=\"retryDelay\">Delay until the next attempt if <paramref name=\"willRetry\"/> is <see langword=\"true\"/>.</param>\n    void MessageFailed(\n        PendingMessageRecord record,\n        int attempt,\n        Exception exception,\n        TimeSpan duration,\n        bool willRetry,\n        TimeSpan? retryDelay);\n\n    /// <summary>Called when a message is removed from the pending queue without being sent.</summary>\n    void MessageDropped(\n        PendingMessageRecord record,\n        int attempt,\n        PendingMessageDropReason reason,\n        Exception? exception);\n}\n\n/// <summary>\n/// Indicates why a pending message was skipped.\n/// </summary>\npublic enum PendingMessageSkipReason {\n    /// <summary>The record does not specify a valid message identifier.</summary>\n    MissingMessageId,\n\n    /// <summary>The record is scheduled for a future attempt.</summary>\n    NotDue,\n\n    /// <summary>The record was already leased by another processor.</summary>\n    LeaseNotAcquired\n}\n\n/// <summary>\n/// Indicates why a pending message was removed from the queue.\n/// </summary>\npublic enum PendingMessageDropReason {\n    /// <summary>The message exceeded the configured retry limit.</summary>\n    RetryLimitReached,\n\n    /// <summary>The sender reported a permanent failure.</summary>\n    PermanentFailure\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/IPendingMessageRepository.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Abstraction for persisting and retrieving queued email messages.\n/// </summary>\npublic interface IPendingMessageRepository {\n    /// <summary>Saves a pending message record.</summary>\n    /// <param name=\"record\">The record to save.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Atomically acquires a processing lease for a due message.\n    /// </summary>\n    /// <param name=\"messageId\">The message id to lease.</param>\n    /// <param name=\"dueBeforeOrAt\">Latest due time that still qualifies the message for processing.</param>\n    /// <param name=\"leaseUntil\">Timestamp written to <see cref=\"PendingMessageRecord.NextAttemptAt\"/> when the lease is acquired.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>\n    /// The current leased record when acquisition succeeds; otherwise <c>null</c>.\n    /// </returns>\n    Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n        string messageId,\n        DateTimeOffset dueBeforeOrAt,\n        DateTimeOffset leaseUntil,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a pending message by its unique message id.</summary>\n    /// <param name=\"messageId\">The message id to search for.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>The pending message record or <c>null</c> if not found.</returns>\n    Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default);\n\n    /// <summary>Enumerates all pending message records.</summary>\n    /// <param name=\"cancellationToken\">Token used to cancel the enumeration.</param>\n    /// <returns>Asynchronous sequence of pending message records.</returns>\n    IAsyncEnumerable<PendingMessageRecord> GetAllAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a message from the repository by id.</summary>\n    /// <param name=\"messageId\">The message id to remove.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    Task RemoveAsync(string messageId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/IPendingMessageSender.cs",
    "content": "using System.Threading;\n\nusing System.Threading.Tasks;\n\n\n\nnamespace Mailozaurr;\n\n\n\n/// <summary>Provides a mechanism to send pending messages using provider-specific transports.</summary>\n\npublic interface IPendingMessageSender {\n\n    /// <summary>Sends the pending message.</summary>\n    /// <param name=\"record\">The pending message metadata.</param>\n    /// <param name=\"ct\">Token used to observe cancellation requests.</param>\n\n    Task SendAsync(PendingMessageRecord record, CancellationToken ct);\n\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/ISentMessageRepository.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Abstraction for persisting and retrieving messages that were successfully sent.\n/// </summary>\npublic interface ISentMessageRepository {\n    /// <summary>Saves a sent message record.</summary>\n    /// <param name=\"record\">The record to save.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    Task SaveAsync(SentMessageRecord record, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a sent message by its unique message id.</summary>\n    /// <param name=\"messageId\">The message id to search for.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>The sent message record or <c>null</c> if not found.</returns>\n    Task<SentMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/LogFileLineReader.cs",
    "content": "using System.Text;\n\nnamespace Mailozaurr;\n\ninternal static class LogFileLineReader {\n    internal readonly struct LineRecord {\n        public LineRecord(long offset, string line) {\n            Offset = offset;\n            Line = line;\n        }\n\n        public long Offset { get; }\n\n        public string Line { get; }\n    }\n\n    public static IEnumerable<LineRecord> ReadLinesWithOffsets(string filePath) {\n        using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        foreach (var line in ReadLinesWithOffsets(stream)) {\n            yield return line;\n        }\n    }\n\n    private static IEnumerable<LineRecord> ReadLinesWithOffsets(FileStream stream) {\n        var buffer = new List<byte>();\n\n        while (true) {\n            var offset = stream.Position;\n            var endOfFile = false;\n\n            while (true) {\n                var value = stream.ReadByte();\n                if (value < 0) {\n                    endOfFile = true;\n                    break;\n                }\n\n                if (value == '\\n') {\n                    if (buffer.Count > 0 && buffer[buffer.Count - 1] == '\\r') {\n                        buffer.RemoveAt(buffer.Count - 1);\n                    }\n\n                    break;\n                }\n\n                buffer.Add((byte) value);\n            }\n\n            if (endOfFile && buffer.Count == 0) {\n                yield break;\n            }\n\n            yield return new LineRecord(offset, buffer.Count == 0 ? string.Empty : Encoding.UTF8.GetString(buffer.ToArray()));\n            buffer.Clear();\n\n            if (endOfFile) {\n                yield break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/NoopPendingMessageSender.cs",
    "content": "using System.Threading;\n\nusing System.Threading.Tasks;\n\n\n\nnamespace Mailozaurr;\n\n\n\n/// <summary>Provides a no-operation implementation for queued messages without a provider.</summary>\n\ninternal sealed class NoopPendingMessageSender : IPendingMessageSender {\n\n    internal static NoopPendingMessageSender Instance { get; } = new();\n\n\n\n    private NoopPendingMessageSender() {\n\n    }\n\n\n\n    public Task SendAsync(PendingMessageRecord record, CancellationToken ct) => Task.CompletedTask;\n\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/PendingMessageProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Processes pending messages by dispatching them through provider specific senders.\n/// </summary>\npublic sealed class PendingMessageProcessor {\n    private static readonly TimeSpan MinimumLeaseDuration = TimeSpan.FromSeconds(30);\n    private readonly IPendingMessageRepository repository;\n    private readonly PendingMessageSenderFactory senderFactory;\n    private readonly Func<int, TimeSpan> retryDelaySelector;\n    private readonly Func<DateTimeOffset> clock;\n    private readonly int maxRetryAttempts;\n    private readonly InternalLogger? logger;\n    private readonly IPendingMessageProcessorObserver observer;\n    private readonly Func<Exception, bool> permanentFailureDetector;\n    private readonly TimeSpan processingLeaseDuration;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"PendingMessageProcessor\"/> class.\n    /// </summary>\n    /// <param name=\"repository\">Repository used to load and persist pending messages.</param>\n    /// <param name=\"senderFactory\">Factory used to resolve the correct sender for each record.</param>\n    /// <param name=\"retryDelaySelector\">Provides the delay applied before the next retry attempt.</param>\n    /// <param name=\"clock\">Supplies the current time used for scheduling retries.</param>\n    /// <param name=\"maxRetryAttempts\">Maximum number of delivery attempts performed before giving up on a message.</param>\n    /// <param name=\"logger\">Optional logger used to record processing diagnostics.</param>\n    /// <param name=\"observer\">Optional observer used to emit telemetry about processing outcomes.</param>\n    /// <param name=\"permanentFailureDetector\">Optional delegate that classifies whether a failure should skip retries.</param>\n    /// <param name=\"processingLeaseDuration\">\n    /// Optional duration used to lease records while they are being processed to avoid concurrent handling.\n    /// <see cref=\"TimeSpan.Zero\"/> uses a minimum safety lease of 30 seconds.\n    /// </param>\n    public PendingMessageProcessor(\n        IPendingMessageRepository repository,\n        PendingMessageSenderFactory? senderFactory = null,\n        Func<int, TimeSpan>? retryDelaySelector = null,\n        Func<DateTimeOffset>? clock = null,\n        int maxRetryAttempts = 5,\n        InternalLogger? logger = null,\n        IPendingMessageProcessorObserver? observer = null,\n        Func<Exception, bool>? permanentFailureDetector = null,\n        TimeSpan? processingLeaseDuration = null) {\n        this.repository = repository ?? throw new ArgumentNullException(nameof(repository));\n        this.senderFactory = senderFactory ?? new PendingMessageSenderFactory();\n        this.retryDelaySelector = retryDelaySelector ?? DefaultRetryDelaySelector;\n        this.clock = clock ?? (() => DateTimeOffset.UtcNow);\n        if (maxRetryAttempts <= 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxRetryAttempts), \"Maximum retry attempts must be greater than zero.\");\n        }\n\n        this.maxRetryAttempts = maxRetryAttempts;\n        this.logger = logger;\n        this.observer = observer ?? NullPendingMessageProcessorObserver.Instance;\n        this.permanentFailureDetector = permanentFailureDetector ?? DefaultPermanentFailureDetector;\n        processingLeaseDuration ??= TimeSpan.FromMinutes(1);\n        if (processingLeaseDuration < TimeSpan.Zero) {\n            throw new ArgumentOutOfRangeException(nameof(processingLeaseDuration), \"Processing lease duration cannot be negative.\");\n        }\n\n        this.processingLeaseDuration = processingLeaseDuration.Value;\n    }\n\n    /// <summary>\n    /// Attempts to send all pending messages that are due for processing.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Token used to observe cancellation requests.</param>\n    public async Task ProcessAsync(CancellationToken cancellationToken = default) {\n        await using var enumerator = repository.GetAllAsync(cancellationToken).GetAsyncEnumerator(cancellationToken);\n        while (await enumerator.MoveNextAsync().ConfigureAwait(false)) {\n            cancellationToken.ThrowIfCancellationRequested();\n            var record = enumerator.Current;\n            if (record == null) {\n                continue;\n            }\n\n            if (string.IsNullOrWhiteSpace(record.MessageId)) {\n                observer.MessageSkipped(record, PendingMessageSkipReason.MissingMessageId);\n                continue;\n            }\n\n            var now = clock();\n            if (record.NextAttemptAt > now) {\n                observer.MessageSkipped(record, PendingMessageSkipReason.NotDue);\n                continue;\n            }\n\n            if (record.AttemptCount >= maxRetryAttempts) {\n                logger?.WriteWarning($\"Removing message {record.MessageId} after reaching the retry limit ({record.AttemptCount}).\");\n                observer.MessageDropped(record, record.AttemptCount, PendingMessageDropReason.RetryLimitReached, null);\n                await repository.RemoveAsync(record.MessageId, cancellationToken).ConfigureAwait(false);\n                continue;\n            }\n\n            var leasedRecord = await AcquireProcessingLeaseAsync(record, now, cancellationToken).ConfigureAwait(false);\n            if (leasedRecord == null) {\n                logger?.WriteVerbose($\"Skipping message {record.MessageId} because another processor already acquired the processing lease.\");\n                observer.MessageSkipped(record, PendingMessageSkipReason.LeaseNotAcquired);\n                continue;\n            }\n\n            var originalAttemptCount = leasedRecord.AttemptCount;\n            var attempt = leasedRecord.IncrementAttemptCount();\n            observer.MessageAttemptStarted(leasedRecord, attempt);\n            var stopwatch = Stopwatch.StartNew();\n\n            try {\n                var sender = senderFactory.GetSender(leasedRecord);\n                await sender.SendAsync(leasedRecord, cancellationToken).ConfigureAwait(false);\n                stopwatch.Stop();\n                observer.MessageSent(leasedRecord, attempt, stopwatch.Elapsed);\n                await repository.RemoveAsync(leasedRecord.MessageId, cancellationToken).ConfigureAwait(false);\n            } catch (OperationCanceledException ex) {\n                stopwatch.Stop();\n                leasedRecord.ExchangeAttemptCount(originalAttemptCount);\n                leasedRecord.NextAttemptAt = ApplyDelay(clock(), TimeSpan.Zero);\n                observer.MessageFailed(leasedRecord, attempt, ex, stopwatch.Elapsed, willRetry: false, retryDelay: null);\n                try {\n                    await repository.SaveAsync(leasedRecord, CancellationToken.None).ConfigureAwait(false);\n                } catch (Exception saveEx) {\n                    logger?.WriteWarning($\"Failed to release processing lease for message {leasedRecord.MessageId} after cancellation: {saveEx.Message}\");\n                }\n                throw;\n            } catch (Exception ex) {\n                stopwatch.Stop();\n                var permanentFailure = permanentFailureDetector(ex);\n                var willRetry = !permanentFailure && attempt < maxRetryAttempts;\n                TimeSpan? delay = null;\n                if (willRetry) {\n                    delay = NormalizeDelay(retryDelaySelector(attempt));\n                    var failureTime = clock();\n                    leasedRecord.NextAttemptAt = ApplyDelay(failureTime, delay.Value);\n                }\n\n                observer.MessageFailed(leasedRecord, attempt, ex, stopwatch.Elapsed, willRetry, delay);\n\n                if (!willRetry) {\n                    logger?.WriteWarning($\"Dropping message {leasedRecord.MessageId} due to {(permanentFailure ? \"permanent failure\" : \"exceeding retry attempts\")}: {ex.Message}\");\n                    observer.MessageDropped(leasedRecord, attempt, permanentFailure ? PendingMessageDropReason.PermanentFailure : PendingMessageDropReason.RetryLimitReached, ex);\n                    await repository.RemoveAsync(leasedRecord.MessageId, cancellationToken).ConfigureAwait(false);\n                    continue;\n                }\n\n                logger?.WriteWarning($\"Failed to send message {leasedRecord.MessageId}. Scheduling retry #{attempt + 1} at {leasedRecord.NextAttemptAt:O}. Error: {ex.Message}\");\n                await repository.SaveAsync(leasedRecord, cancellationToken).ConfigureAwait(false);\n            }\n        }\n    }\n\n    private async Task<PendingMessageRecord?> AcquireProcessingLeaseAsync(\n        PendingMessageRecord record,\n        DateTimeOffset now,\n        CancellationToken cancellationToken) {\n        var leaseDuration = processingLeaseDuration > TimeSpan.Zero ? processingLeaseDuration : MinimumLeaseDuration;\n        var leaseUntil = ApplyDelay(now, leaseDuration);\n        return await repository.TryAcquireLeaseAsync(record.MessageId, now, leaseUntil, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static TimeSpan DefaultRetryDelaySelector(int attempt) {\n        var exponent = Math.Max(0, attempt - 1);\n        var minutes = Math.Pow(2, exponent);\n        var capped = Math.Min(60, minutes);\n        return TimeSpan.FromMinutes(capped);\n    }\n\n    private static TimeSpan NormalizeDelay(TimeSpan delay) {\n        if (delay <= TimeSpan.Zero || delay == TimeSpan.MinValue) {\n            return TimeSpan.Zero;\n        }\n\n        return delay;\n    }\n\n    private static bool DefaultPermanentFailureDetector(Exception exception) =>\n        exception is InvalidOperationException or ArgumentException;\n\n    private static DateTimeOffset ApplyDelay(DateTimeOffset reference, TimeSpan delay) {\n        if (delay <= TimeSpan.Zero) {\n            return reference;\n        }\n\n        var maxIncrement = DateTimeOffset.MaxValue - reference;\n        if (delay > maxIncrement) {\n            return DateTimeOffset.MaxValue;\n        }\n\n        return reference + delay;\n    }\n\n    private sealed class NullPendingMessageProcessorObserver : IPendingMessageProcessorObserver {\n        internal static NullPendingMessageProcessorObserver Instance { get; } = new();\n\n        private NullPendingMessageProcessorObserver() {\n        }\n\n        public void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason) {\n        }\n\n        public void MessageAttemptStarted(PendingMessageRecord record, int attempt) {\n        }\n\n        public void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration) {\n        }\n\n        public void MessageFailed(\n            PendingMessageRecord record,\n            int attempt,\n            Exception exception,\n            TimeSpan duration,\n            bool willRetry,\n            TimeSpan? retryDelay) {\n        }\n\n        public void MessageDropped(\n            PendingMessageRecord record,\n            int attempt,\n            PendingMessageDropReason reason,\n            Exception? exception) {\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/PendingMessageRecord.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents a message pending to be sent.\n/// </summary>\npublic sealed class PendingMessageRecord {\n    private int attemptCount;\n\n    /// <summary>Identifier of the message.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Time at which the message was queued.</summary>\n    public DateTimeOffset Timestamp { get; set; }\n\n    /// <summary>Time when the next send attempt should occur.</summary>\n    public DateTimeOffset NextAttemptAt { get; set; }\n\n    /// <summary>Number of times delivery has been attempted.</summary>\n    public int AttemptCount {\n        get => Volatile.Read(ref attemptCount);\n        set => Volatile.Write(ref attemptCount, value);\n    }\n\n    /// <summary>Base64-encoded MIME message.</summary>\n    public string MimeMessage { get; set; } = string.Empty;\n\n    /// <summary>SMTP server used when the message was queued.</summary>\n    public string? Server { get; set; }\n\n    /// <summary>Port of the SMTP server.</summary>\n    public int? Port { get; set; }\n\n    /// <summary>User name for authentication.</summary>\n    public string? UserName { get; set; }\n\n    /// <summary>\n    /// Protected password encoded as Base64.\n    /// </summary>\n    public string? Password { get; set; }\n\n    /// <summary>\n    /// Provider that should handle the queued message.\n    /// </summary>\n    public EmailProvider Provider { get; set; } = EmailProvider.None;\n\n    private Dictionary<string, string>? providerData;\n\n    /// <summary>\n    /// Arbitrary provider-specific fields required to resume delivery.\n    /// </summary>\n    public Dictionary<string, string> ProviderData {\n        get => providerData ??= new Dictionary<string, string>();\n        set => providerData = value ?? new Dictionary<string, string>();\n    }\n\n    /// <summary>\n    /// Atomically increments <see cref=\"AttemptCount\"/> and returns the updated value.\n    /// </summary>\n    public int IncrementAttemptCount() => Interlocked.Increment(ref attemptCount);\n\n    /// <summary>\n    /// Atomically sets <see cref=\"AttemptCount\"/> to the specified value.\n    /// </summary>\n    /// <param name=\"value\">Value assigned to the attempt counter.</param>\n    /// <returns>The previous value stored in the attempt counter.</returns>\n    public int ExchangeAttemptCount(int value) => Interlocked.Exchange(ref attemptCount, value);\n\n    /// <summary>\n    /// Creates a detached copy of this record.\n    /// </summary>\n    public PendingMessageRecord Clone() => new() {\n        MessageId = MessageId,\n        Timestamp = Timestamp,\n        NextAttemptAt = NextAttemptAt,\n        AttemptCount = AttemptCount,\n        MimeMessage = MimeMessage,\n        Server = Server,\n        Port = Port,\n        UserName = UserName,\n        Password = Password,\n        Provider = Provider,\n        ProviderData = new Dictionary<string, string>(ProviderData)\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/PendingMessageRepositoryOptions.cs",
    "content": "using System;\nusing System.IO;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Options for configuring <see cref=\"FilePendingMessageRepository\"/>.\n/// </summary>\npublic sealed class PendingMessageRepositoryOptions {\n    private string directoryPath = Path.GetTempPath();\n    private Func<string> fileNamingScheme = () => \"pending.log\";\n\n    /// <summary>Directory where pending message data is stored.</summary>\n    public string DirectoryPath {\n        get => directoryPath;\n        set {\n            if (string.IsNullOrWhiteSpace(value)) {\n                throw new ArgumentException(\"DirectoryPath cannot be null or empty\", nameof(value));\n            }\n            directoryPath = value;\n        }\n    }\n\n    /// <summary>Provides the file name used for storing pending messages.</summary>\n    public Func<string> FileNamingScheme {\n        get => fileNamingScheme;\n        set => fileNamingScheme = value ?? throw new ArgumentNullException(nameof(value));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/PendingMessageSenderFactory.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Creates provider specific <see cref=\"IPendingMessageSender\"/> instances for queued messages.\n/// </summary>\npublic sealed class PendingMessageSenderFactory {\n    private readonly IReadOnlyDictionary<EmailProvider, IPendingMessageSender> senders;\n    private readonly IPendingMessageSender fallbackSender;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"PendingMessageSenderFactory\"/> class.\n    /// </summary>\n    /// <param name=\"senders\">Known provider specific senders.</param>\n    /// <param name=\"fallbackSender\">The sender used when no provider specific sender is registered.</param>\n    public PendingMessageSenderFactory(\n        IEnumerable<KeyValuePair<EmailProvider, IPendingMessageSender>>? senders = null,\n        IPendingMessageSender? fallbackSender = null) {\n        this.senders = CreateMap(senders);\n        this.fallbackSender = fallbackSender ?? NoopPendingMessageSender.Instance;\n    }\n\n    /// <summary>\n    /// Gets the sender appropriate for the supplied <paramref name=\"record\"/>.\n    /// </summary>\n    /// <param name=\"record\">Pending message metadata used to select the sender.</param>\n    /// <returns>The sender registered for the provider or a fallback instance.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when <paramref name=\"record\"/> is <c>null</c>.</exception>\n    public IPendingMessageSender GetSender(PendingMessageRecord record) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n\n        return Resolve(record.Provider);\n    }\n\n    /// <summary>\n    /// Resolves the sender registered for <paramref name=\"provider\"/>.\n    /// </summary>\n    /// <param name=\"provider\">The provider whose sender should be returned.</param>\n    /// <returns>The sender registered for the provider or a fallback instance.</returns>\n    public IPendingMessageSender Resolve(EmailProvider provider) {\n        if (senders.TryGetValue(provider, out var sender)) {\n            return sender;\n        }\n\n        return fallbackSender;\n    }\n\n    private static IReadOnlyDictionary<EmailProvider, IPendingMessageSender> CreateMap(\n        IEnumerable<KeyValuePair<EmailProvider, IPendingMessageSender>>? entries) {\n        var map = new Dictionary<EmailProvider, IPendingMessageSender>();\n\n        foreach (var defaultEntry in CreateDefaultSenders()) {\n            map[defaultEntry.Key] = defaultEntry.Value;\n        }\n\n        if (entries != null) {\n            foreach (var entry in entries) {\n                if (entry.Value == null) {\n                    throw new ArgumentException(\"Sender value cannot be null.\", nameof(entries));\n                }\n\n                map[entry.Key] = entry.Value;\n            }\n        }\n\n        return map;\n    }\n\n    private static IEnumerable<KeyValuePair<EmailProvider, IPendingMessageSender>> CreateDefaultSenders() {\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.None, new SmtpPendingMessageSender());\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.SendGrid, new SendGridPendingMessageSender());\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.Mailgun, new MailgunPendingMessageSender());\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.SES, new SesPendingMessageSender());\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.Gmail, new GmailPendingMessageSender());\n        yield return new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.Graph, new GraphPendingMessageSender());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/SendLogResolver.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Resolves log entries for previously sent messages based on non-delivery reports.\n/// </summary>\npublic sealed class SendLogResolver {\n    private readonly ISentMessageRepository repository;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SendLogResolver\"/> class.\n    /// </summary>\n    /// <param name=\"repository\">Repository used to look up sent messages.</param>\n    public SendLogResolver(ISentMessageRepository repository) =>\n        this.repository = repository ?? throw new ArgumentNullException(nameof(repository));\n\n    /// <summary>\n    /// Attempts to resolve a sent message record from a non-delivery report.\n    /// </summary>\n    /// <param name=\"report\">Non-delivery report to analyze.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    /// <returns>The matching <see cref=\"SentMessageRecord\"/>, or <c>null</c> if not found.</returns>\n    public async Task<SentMessageRecord?> ResolveAsync(NonDeliveryReport report, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(report.OriginalMessageId)) {\n            return null;\n        }\n        var messageId = report.OriginalMessageId!;\n        var record = await repository.GetByMessageIdAsync(messageId, cancellationToken);\n        if (record != null) {\n            var recipient = SentMessageRecipients.NormalizeAddress(report.FinalRecipientAddress ?? report.OriginalRecipientAddress);\n            if (!string.IsNullOrWhiteSpace(recipient)) {\n                var recipients = SentMessageRecipients.Parse(record.Recipients);\n                if (!recipients.Any(r => string.Equals(r, recipient, StringComparison.OrdinalIgnoreCase))) {\n                    return null;\n                }\n            }\n        }\n        return record;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/GmailPendingMessageSender.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing Org.BouncyCastle.Asn1.Pkcs;\nusing Org.BouncyCastle.Crypto;\nusing Org.BouncyCastle.Crypto.Parameters;\nusing Org.BouncyCastle.OpenSsl;\nusing Org.BouncyCastle.Pkcs;\nusing Org.BouncyCastle.Security;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued Gmail messages using <see cref=\"GmailApiClient\"/>.\n/// </summary>\npublic sealed class GmailPendingMessageSender : IPendingMessageSender {\n    internal const string UserIdKey = \"UserId\";\n    internal const string AccessTokenKey = \"AccessToken\";\n    internal const string AccessTokenBase64Key = \"AccessTokenBase64\";\n    internal const string AccessTokenProtectedKey = \"AccessTokenProtected\";\n    internal const string UserNameKey = \"UserName\";\n    internal const string ExpiresOnKey = \"ExpiresOn\";\n    internal const string RefreshTokenKey = \"RefreshToken\";\n    internal const string RefreshTokenBase64Key = \"RefreshTokenBase64\";\n    internal const string RefreshTokenProtectedKey = \"RefreshTokenProtected\";\n    internal const string ClientIdKey = \"ClientId\";\n    internal const string ClientSecretProtectedKey = \"ClientSecretProtected\";\n    internal const string ServiceAccountJsonProtectedKey = \"ServiceAccountJsonProtected\";\n    internal const string ServiceAccountSubjectKey = \"ServiceAccountSubject\";\n\n    private const string TokenEndpoint = \"https://oauth2.googleapis.com/token\";\n    private const string GmailSendScope = \"https://www.googleapis.com/auth/gmail.send\";\n\n    private readonly Func<OAuthCredential, Func<CancellationToken, Task<string>>?, GmailApiClient> clientFactory;\n    private readonly ICredentialProtector credentialProtector;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"clientFactory\">Factory used to create <see cref=\"GmailApiClient\"/> instances.</param>\n    /// <param name=\"credentialProtector\">Protector used to decrypt stored secrets.</param>\n    public GmailPendingMessageSender(\n        Func<OAuthCredential, Func<CancellationToken, Task<string>>?, GmailApiClient>? clientFactory = null,\n        ICredentialProtector? credentialProtector = null) {\n        this.clientFactory = clientFactory ?? ((credential, refresher) => new GmailApiClient(credential, refresher));\n        this.credentialProtector = credentialProtector ?? CredentialProtection.Default;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"GmailPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"clientFactory\">Factory used to create <see cref=\"GmailApiClient\"/> instances.</param>\n    [Obsolete(\"Use the overload accepting a refresh-aware factory.\")]\n    public GmailPendingMessageSender(Func<OAuthCredential, GmailApiClient> clientFactory)\n        : this(\n            (credential, refresher) => refresher != null\n                ? new GmailApiClient(credential, refresher)\n                : (clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)))(credential)) {\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (!record.ProviderData.TryGetValue(UserIdKey, out var userId) || string.IsNullOrWhiteSpace(userId)) {\n            throw new InvalidOperationException(\"Pending Gmail message is missing the user identifier.\");\n        }\n\n        var message = await LoadMessageAsync(record, ct).ConfigureAwait(false);\n        var accessToken = ResolveAccessToken(record.ProviderData);\n        var credential = BuildCredential(record.ProviderData, accessToken, userId);\n\n        var refreshContext = BuildRefreshContext(record.ProviderData, credential);\n        Func<CancellationToken, Task<string>>? refresher = null;\n        if (refreshContext != null) {\n            refresher = cancellationToken => RefreshAccessTokenAsync(credential, refreshContext, cancellationToken);\n            if (ShouldRefreshToken(credential.ExpiresOn)) {\n                await refresher(ct).ConfigureAwait(false);\n            }\n        } else if (ShouldRefreshToken(credential.ExpiresOn)) {\n            throw new InvalidOperationException(\n                \"Pending Gmail message cannot be sent because the access token has expired and refresh data is unavailable.\");\n        }\n\n        using var client = clientFactory(credential, refresher);\n        var retryAttempted = false;\n        while (true) {\n            try {\n                _ = await client.SendAsync(userId, message, ct).ConfigureAwait(false);\n                break;\n            } catch (GmailAuthenticationException ex) when (refresher == null) {\n                throw new InvalidOperationException(\n                    \"Pending Gmail message could not be authenticated and no refresh data is available.\", ex);\n            } catch (GmailAuthenticationException) when (!retryAttempted && refresher != null) {\n                retryAttempted = true;\n                continue;\n            }\n        }\n    }\n\n    private static async Task<MimeMessage> LoadMessageAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (string.IsNullOrWhiteSpace(record.MimeMessage)) {\n            throw new InvalidOperationException(\"Pending Gmail message does not contain MIME content.\");\n        }\n        var bytes = Convert.FromBase64String(record.MimeMessage);\n        using var stream = new MemoryStream(bytes);\n        return await MimeMessage.LoadAsync(stream, ct).ConfigureAwait(false);\n    }\n\n    private static OAuthCredential BuildCredential(Dictionary<string, string> providerData, string accessToken, string userId) {\n        var credential = new OAuthCredential {\n            AccessToken = accessToken,\n            UserName = providerData.TryGetValue(UserNameKey, out var userName) && !string.IsNullOrWhiteSpace(userName)\n                ? userName\n                : userId,\n            ExpiresOn = ResolveExpiration(providerData),\n        };\n\n        var refreshToken = ResolveRefreshToken(providerData);\n        if (!string.IsNullOrEmpty(refreshToken)) {\n            credential.RefreshToken = refreshToken;\n        }\n\n        var clientId = ResolveClientId(providerData);\n        if (!string.IsNullOrEmpty(clientId)) {\n            credential.ClientId = clientId;\n        }\n\n        var clientSecret = ResolveProtectedString(providerData, ClientSecretProtectedKey);\n        if (!string.IsNullOrEmpty(clientSecret)) {\n            credential.ClientSecret = clientSecret;\n        }\n\n        var serviceAccountJson = ResolveProtectedString(providerData, ServiceAccountJsonProtectedKey);\n        if (!string.IsNullOrEmpty(serviceAccountJson)) {\n            credential.ServiceAccountJson = serviceAccountJson;\n        }\n\n        var subject = ResolveServiceAccountSubject(providerData);\n        if (!string.IsNullOrEmpty(subject)) {\n            credential.ServiceAccountSubject = subject;\n        }\n\n        return credential;\n    }\n\n    private static string ResolveAccessToken(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(AccessTokenProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(AccessTokenBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n\n        if (providerData.TryGetValue(AccessTokenKey, out var token) && !string.IsNullOrWhiteSpace(token)) {\n            return token;\n        }\n\n        throw new InvalidOperationException(\"Pending Gmail message is missing an access token.\");\n    }\n\n    private static string? ResolveRefreshToken(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(RefreshTokenProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(RefreshTokenBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n\n        if (providerData.TryGetValue(RefreshTokenKey, out var token) && !string.IsNullOrWhiteSpace(token)) {\n            return token;\n        }\n\n        return null;\n    }\n\n    private static DateTimeOffset ResolveExpiration(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ExpiresOnKey, out var value) && !string.IsNullOrWhiteSpace(value) &&\n            DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)) {\n            return parsed;\n        }\n        return DateTimeOffset.MaxValue;\n    }\n\n    private GmailRefreshContext? BuildRefreshContext(Dictionary<string, string> providerData, OAuthCredential credential) {\n        credential.ClientId ??= ResolveClientId(providerData);\n        credential.ClientSecret ??= ResolveProtectedString(providerData, ClientSecretProtectedKey);\n        credential.ServiceAccountJson ??= ResolveProtectedString(providerData, ServiceAccountJsonProtectedKey);\n        credential.ServiceAccountSubject ??= ResolveServiceAccountSubject(providerData);\n\n        var refreshToken = credential.RefreshToken;\n        var hasRefreshToken = !string.IsNullOrEmpty(refreshToken);\n        var hasServiceAccount = !string.IsNullOrEmpty(credential.ServiceAccountJson);\n\n        if (!hasRefreshToken && !hasServiceAccount) {\n            return null;\n        }\n\n        if (!hasServiceAccount && string.IsNullOrEmpty(credential.ClientId)) {\n            return null;\n        }\n\n        return new GmailRefreshContext(\n            providerData,\n            refreshToken,\n            credential.ClientId,\n            credential.ClientSecret,\n            credential.ServiceAccountJson,\n            credential.ServiceAccountSubject,\n            credentialProtector);\n    }\n\n    private static string? ResolveClientId(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ClientIdKey, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value;\n        }\n        return null;\n    }\n\n    private static string? ResolveProtectedString(Dictionary<string, string> providerData, string key) {\n        if (providerData.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(value);\n            return string.IsNullOrEmpty(decrypted) ? null : decrypted;\n        }\n        return null;\n    }\n\n    private static string? ResolveServiceAccountSubject(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ServiceAccountSubjectKey, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value;\n        }\n        return null;\n    }\n\n    private static bool ShouldRefreshToken(DateTimeOffset expiresOn) => expiresOn <= DateTimeOffset.UtcNow.AddMinutes(1);\n\n    private async Task<string> RefreshAccessTokenAsync(OAuthCredential credential, GmailRefreshContext context, CancellationToken cancellationToken) {\n        if (context.HasClientSecret && context.HasRefreshToken) {\n            var refreshToken = context.RefreshToken!;\n            var refreshed = await ExchangeRefreshTokenAsync(context.ClientId!, context.ClientSecret!, refreshToken, cancellationToken)\n                .ConfigureAwait(false);\n            credential.AccessToken = refreshed.AccessToken;\n            if (!string.IsNullOrEmpty(refreshed.RefreshToken)) {\n                credential.RefreshToken = refreshed.RefreshToken;\n            }\n\n            if (refreshed.ExpiresOn.HasValue) {\n                credential.ExpiresOn = refreshed.ExpiresOn.Value;\n            } else if (credential.ExpiresOn <= DateTimeOffset.UtcNow) {\n                credential.ExpiresOn = DateTimeOffset.UtcNow.AddHours(1);\n            }\n\n            context.UpdateStoredCredential(credential);\n            return credential.AccessToken;\n        }\n\n        if (context.HasServiceAccount) {\n            var refreshed = await ExchangeServiceAccountTokenAsync(context, cancellationToken).ConfigureAwait(false);\n            credential.AccessToken = refreshed.AccessToken;\n            credential.RefreshToken = null;\n            credential.ExpiresOn = refreshed.ExpiresOn ?? DateTimeOffset.UtcNow.AddHours(1);\n            context.UpdateStoredCredential(credential);\n            return credential.AccessToken;\n        }\n\n        throw new InvalidOperationException(\"Pending Gmail message is missing OAuth client context required to refresh the access token.\");\n    }\n\n    private static async Task<(string AccessToken, string? RefreshToken, DateTimeOffset? ExpiresOn)> ExchangeRefreshTokenAsync(\n        string clientId,\n        string clientSecret,\n        string refreshToken,\n        CancellationToken cancellationToken) {\n        using var content = new FormUrlEncodedContent(new Dictionary<string, string> {\n            { \"client_id\", clientId },\n            { \"client_secret\", clientSecret },\n            { \"refresh_token\", refreshToken },\n            { \"grant_type\", \"refresh_token\" }\n        });\n        using var request = new HttpRequestMessage(HttpMethod.Post, TokenEndpoint) { Content = content };\n        using var response = await Helpers.SharedHttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new InvalidOperationException($\"Failed to refresh Gmail access token: {payload}\");\n        }\n\n        using var document = JsonDocument.Parse(payload);\n        if (!document.RootElement.TryGetProperty(\"access_token\", out var tokenProperty)) {\n            throw new InvalidOperationException(\"Gmail token refresh response did not include an access token.\");\n        }\n\n        var accessToken = tokenProperty.GetString();\n        if (string.IsNullOrEmpty(accessToken)) {\n            throw new InvalidOperationException(\"Gmail token refresh response contained an empty access token.\");\n        }\n\n        string? newRefresh = null;\n        if (document.RootElement.TryGetProperty(\"refresh_token\", out var refreshProperty)) {\n            newRefresh = refreshProperty.GetString();\n        }\n\n        DateTimeOffset? expiresOn = null;\n        if (document.RootElement.TryGetProperty(\"expires_in\", out var expiresProperty) &&\n            expiresProperty.TryGetInt64(out var seconds) && seconds > 0) {\n            expiresOn = DateTimeOffset.UtcNow.AddSeconds(seconds);\n        }\n\n        return (accessToken!, newRefresh, expiresOn);\n    }\n\n    private async Task<(string AccessToken, DateTimeOffset? ExpiresOn)> ExchangeServiceAccountTokenAsync(\n        GmailRefreshContext context,\n        CancellationToken cancellationToken) {\n        var serviceAccountJson = context.ServiceAccountJson;\n        if (string.IsNullOrEmpty(serviceAccountJson)) {\n            throw new InvalidOperationException(\"Pending Gmail message is missing service account credentials required to mint a new access token.\");\n        }\n\n        using var document = JsonDocument.Parse(serviceAccountJson!);\n        var root = document.RootElement;\n        if (!root.TryGetProperty(\"client_email\", out var emailProperty)) {\n            throw new InvalidOperationException(\"Service account JSON does not contain the client_email field.\");\n        }\n\n        var clientEmail = emailProperty.GetString();\n        if (string.IsNullOrWhiteSpace(clientEmail)) {\n            throw new InvalidOperationException(\"Service account client_email value is empty.\");\n        }\n        var clientEmailValue = clientEmail!;\n\n        if (!root.TryGetProperty(\"private_key\", out var keyProperty)) {\n            throw new InvalidOperationException(\"Service account JSON does not contain the private_key field.\");\n        }\n\n        var privateKey = keyProperty.GetString();\n        if (string.IsNullOrWhiteSpace(privateKey)) {\n            throw new InvalidOperationException(\"Service account private key is empty.\");\n        }\n\n        privateKey = privateKey!.Replace(\"\\\\n\", \"\\n\").Trim();\n        var assertion = CreateServiceAccountAssertion(clientEmailValue, privateKey, context.ServiceAccountSubject);\n\n        using var content = new FormUrlEncodedContent(new Dictionary<string, string> {\n            { \"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\" },\n            { \"assertion\", assertion }\n        });\n\n        using var request = new HttpRequestMessage(HttpMethod.Post, TokenEndpoint) { Content = content };\n        using var response = await Helpers.SharedHttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new InvalidOperationException($\"Failed to mint Gmail service account access token: {payload}\");\n        }\n\n        using var tokenDocument = JsonDocument.Parse(payload);\n        if (!tokenDocument.RootElement.TryGetProperty(\"access_token\", out var tokenProperty)) {\n            throw new InvalidOperationException(\"Gmail service account response did not include an access token.\");\n        }\n\n        var accessToken = tokenProperty.GetString();\n        if (string.IsNullOrEmpty(accessToken)) {\n            throw new InvalidOperationException(\"Gmail service account response contained an empty access token.\");\n        }\n        var accessTokenValue = accessToken!;\n\n        DateTimeOffset? expiresOn = null;\n        if (tokenDocument.RootElement.TryGetProperty(\"expires_in\", out var expiresProperty) &&\n            expiresProperty.TryGetInt64(out var seconds) && seconds > 0) {\n            expiresOn = DateTimeOffset.UtcNow.AddSeconds(seconds);\n        }\n\n        return (accessTokenValue, expiresOn);\n    }\n\n    private static string CreateServiceAccountAssertion(string clientEmail, string privateKeyPem, string? subject) {\n        var now = DateTimeOffset.UtcNow;\n        var headerJson = JsonSerializer.Serialize(new Dictionary<string, object> {\n            { \"alg\", \"RS256\" },\n            { \"typ\", \"JWT\" }\n        }, MailozaurrJsonContext.Default.DictionaryStringObject);\n\n        var payload = new Dictionary<string, object> {\n            { \"iss\", clientEmail },\n            { \"scope\", GmailSendScope },\n            { \"aud\", TokenEndpoint },\n            { \"iat\", now.ToUnixTimeSeconds() },\n            { \"exp\", now.AddHours(1).ToUnixTimeSeconds() }\n        };\n\n        if (!string.IsNullOrEmpty(subject)) {\n            payload[\"sub\"] = subject!;\n        }\n\n        var payloadJson = JsonSerializer.Serialize(payload, MailozaurrJsonContext.Default.DictionaryStringObject);\n        var header = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson));\n        var body = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));\n        var unsignedToken = string.Concat(header, '.', body);\n\n        using var rsa = CreateRsaFromPrivateKey(privateKeyPem);\n        var signature = rsa.SignData(Encoding.UTF8.GetBytes(unsignedToken), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        var signatureSegment = Base64UrlEncode(signature);\n        return string.Concat(unsignedToken, '.', signatureSegment);\n    }\n\n    private static RSA CreateRsaFromPrivateKey(string privateKeyPem) {\n        using var reader = new StringReader(privateKeyPem);\n        var pemReader = new PemReader(reader);\n        object? keyObject = pemReader.ReadObject();\n        if (keyObject == null) {\n            throw new InvalidOperationException(\"Service account private key is invalid.\");\n        }\n\n        RsaPrivateCrtKeyParameters? rsaParameters = null;\n        switch (keyObject) {\n            case AsymmetricCipherKeyPair pair:\n                rsaParameters = pair.Private as RsaPrivateCrtKeyParameters;\n                break;\n            case RsaPrivateCrtKeyParameters parameters:\n                rsaParameters = parameters;\n                break;\n            case PrivateKeyInfo privateKeyInfo:\n                rsaParameters = PrivateKeyFactory.CreateKey(privateKeyInfo) as RsaPrivateCrtKeyParameters;\n                break;\n            case AsymmetricKeyParameter keyParameter when keyParameter.IsPrivate:\n                rsaParameters = keyParameter as RsaPrivateCrtKeyParameters;\n                break;\n        }\n\n        if (rsaParameters == null) {\n            throw new InvalidOperationException(\"Service account private key format is not supported.\");\n        }\n\n        var rsa = RSA.Create();\n        rsa.ImportParameters(DotNetUtilities.ToRSAParameters(rsaParameters));\n        return rsa;\n    }\n\n    private static string Base64UrlEncode(byte[] data) {\n        var base64 = Convert.ToBase64String(data);\n        return base64.TrimEnd('=').Replace('+', '-').Replace('/', '_');\n    }\n\n    private sealed class GmailRefreshContext {\n        private readonly Dictionary<string, string> providerData;\n        private readonly ICredentialProtector protector;\n\n        internal GmailRefreshContext(\n            Dictionary<string, string> providerData,\n            string? refreshToken,\n            string? clientId,\n            string? clientSecret,\n            string? serviceAccountJson,\n            string? serviceAccountSubject,\n            ICredentialProtector protector) {\n            this.providerData = providerData ?? throw new ArgumentNullException(nameof(providerData));\n            RefreshToken = refreshToken;\n            ClientId = clientId;\n            ClientSecret = clientSecret;\n            ServiceAccountJson = serviceAccountJson;\n            ServiceAccountSubject = serviceAccountSubject;\n            this.protector = protector ?? throw new ArgumentNullException(nameof(protector));\n        }\n\n        internal string? RefreshToken { get; private set; }\n        internal string? ClientId { get; }\n        internal string? ClientSecret { get; }\n        internal string? ServiceAccountJson { get; }\n        internal string? ServiceAccountSubject { get; }\n\n        internal bool HasClientSecret => !string.IsNullOrEmpty(ClientId) && !string.IsNullOrEmpty(ClientSecret);\n        internal bool HasServiceAccount => !string.IsNullOrEmpty(ServiceAccountJson);\n        internal bool HasRefreshToken => !string.IsNullOrEmpty(RefreshToken);\n\n        internal void UpdateStoredCredential(OAuthCredential credential) {\n            providerData[GmailPendingMessageSender.AccessTokenProtectedKey] = protector.Protect(credential.AccessToken);\n            providerData.Remove(GmailPendingMessageSender.AccessTokenBase64Key);\n            providerData.Remove(GmailPendingMessageSender.AccessTokenKey);\n\n            if (!string.IsNullOrEmpty(credential.RefreshToken)) {\n                providerData[GmailPendingMessageSender.RefreshTokenProtectedKey] = protector.Protect(credential.RefreshToken!);\n                providerData.Remove(GmailPendingMessageSender.RefreshTokenBase64Key);\n                providerData.Remove(GmailPendingMessageSender.RefreshTokenKey);\n                RefreshToken = credential.RefreshToken!;\n            } else {\n                providerData.Remove(GmailPendingMessageSender.RefreshTokenProtectedKey);\n                providerData.Remove(GmailPendingMessageSender.RefreshTokenBase64Key);\n                providerData.Remove(GmailPendingMessageSender.RefreshTokenKey);\n                RefreshToken = null;\n            }\n\n            providerData[GmailPendingMessageSender.ExpiresOnKey] = credential.ExpiresOn.ToString(\"o\", CultureInfo.InvariantCulture);\n\n            if (!string.IsNullOrEmpty(credential.ClientId)) {\n                providerData[GmailPendingMessageSender.ClientIdKey] = credential.ClientId!;\n            }\n\n            if (!string.IsNullOrEmpty(credential.ClientSecret)) {\n                providerData[GmailPendingMessageSender.ClientSecretProtectedKey] = protector.Protect(credential.ClientSecret!);\n            }\n\n            if (!string.IsNullOrEmpty(credential.ServiceAccountJson)) {\n                providerData[GmailPendingMessageSender.ServiceAccountJsonProtectedKey] = protector.Protect(credential.ServiceAccountJson!);\n            }\n\n            if (!string.IsNullOrEmpty(credential.ServiceAccountSubject)) {\n                providerData[GmailPendingMessageSender.ServiceAccountSubjectKey] = credential.ServiceAccountSubject!;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/GraphPendingMessageSender.cs",
    "content": "using System.Globalization;\nusing System.IO;\nusing System.Text;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued Microsoft Graph messages using <see cref=\"GraphApiClient\"/>.\n/// </summary>\npublic sealed class GraphPendingMessageSender : IPendingMessageSender {\n    /// <summary>Provider-data key storing the Graph mailbox user id.</summary>\n    public const string UserIdKey = \"UserId\";\n\n    /// <summary>Provider-data key storing the Graph mailbox user name.</summary>\n    public const string UserNameKey = \"UserName\";\n\n    /// <summary>Provider-data key storing the access-token expiry timestamp.</summary>\n    public const string ExpiresOnKey = \"ExpiresOn\";\n\n    /// <summary>Provider-data key storing the raw access token.</summary>\n    public const string AccessTokenKey = \"AccessToken\";\n\n    /// <summary>Provider-data key storing the base64-encoded access token.</summary>\n    public const string AccessTokenBase64Key = \"AccessTokenBase64\";\n\n    /// <summary>Provider-data key storing the protected access token.</summary>\n    public const string AccessTokenProtectedKey = \"AccessTokenProtected\";\n\n    /// <summary>Provider-data key storing the Graph client id.</summary>\n    public const string ClientIdKey = \"ClientId\";\n\n    /// <summary>Provider-data key storing the Graph tenant id.</summary>\n    public const string TenantIdKey = \"TenantId\";\n\n    /// <summary>Provider-data key storing the protected Graph client secret.</summary>\n    public const string ClientSecretProtectedKey = \"ClientSecretProtected\";\n\n    /// <summary>Provider-data key storing the Graph certificate path.</summary>\n    public const string CertificatePathKey = \"CertificatePath\";\n\n    /// <summary>Provider-data key storing the protected Graph certificate password.</summary>\n    public const string CertificatePasswordProtectedKey = \"CertificatePasswordProtected\";\n\n    private readonly Func<OAuthCredential, GraphApiClient> clientFactory;\n    private readonly Func<GraphCredential, CancellationToken, Task<string>> acquireAccessTokenAsync;\n\n    /// <summary>\n    /// Creates a new pending-message sender for Graph.\n    /// </summary>\n    public GraphPendingMessageSender(\n        Func<OAuthCredential, GraphApiClient>? clientFactory = null,\n        Func<GraphCredential, CancellationToken, Task<string>>? acquireAccessTokenAsync = null) {\n        this.clientFactory = clientFactory ?? (credential => new GraphApiClient(credential));\n        this.acquireAccessTokenAsync = acquireAccessTokenAsync ?? DefaultAcquireAccessTokenAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (!record.ProviderData.TryGetValue(UserIdKey, out var userId) || string.IsNullOrWhiteSpace(userId)) {\n            throw new InvalidOperationException(\"Pending Graph message is missing the user identifier.\");\n        }\n\n        var message = await LoadMessageAsync(record, ct).ConfigureAwait(false);\n        var credential = await ResolveCredentialAsync(record.ProviderData, userId, ct).ConfigureAwait(false);\n\n        using var client = clientFactory(credential);\n        await GraphMimeMessageSender.SendAsync(client, userId, message, ct).ConfigureAwait(false);\n    }\n\n    private async Task<OAuthCredential> ResolveCredentialAsync(\n        Dictionary<string, string> providerData,\n        string userId,\n        CancellationToken cancellationToken) {\n        var accessToken = ResolveAccessToken(providerData);\n        var expiresOn = ResolveExpiration(providerData);\n        var graphCredential = BuildGraphCredential(providerData);\n\n        if ((string.IsNullOrEmpty(accessToken) || ShouldRefreshToken(expiresOn)) && graphCredential != null) {\n            accessToken = await acquireAccessTokenAsync(graphCredential, cancellationToken).ConfigureAwait(false);\n            expiresOn = DateTimeOffset.UtcNow.AddMinutes(55);\n            providerData[AccessTokenProtectedKey] = CredentialProtection.Default.Protect(accessToken);\n            providerData.Remove(AccessTokenKey);\n            providerData.Remove(AccessTokenBase64Key);\n            providerData[ExpiresOnKey] = expiresOn.ToString(\"o\", CultureInfo.InvariantCulture);\n        }\n\n        if (string.IsNullOrEmpty(accessToken)) {\n            throw new InvalidOperationException(\"Pending Graph message is missing an access token and cannot mint a new one.\");\n        }\n\n        return new OAuthCredential {\n            UserName = providerData.TryGetValue(UserNameKey, out var userName) && !string.IsNullOrWhiteSpace(userName)\n                ? userName\n                : userId,\n            AccessToken = accessToken!,\n            ExpiresOn = expiresOn,\n            ClientId = providerData.TryGetValue(ClientIdKey, out var clientId) && !string.IsNullOrWhiteSpace(clientId)\n                ? clientId\n                : null,\n            ClientSecret = ResolveProtectedString(providerData, ClientSecretProtectedKey)\n        };\n    }\n\n    private static async Task<MimeMessage> LoadMessageAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (string.IsNullOrWhiteSpace(record.MimeMessage)) {\n            throw new InvalidOperationException(\"Pending Graph message does not contain MIME content.\");\n        }\n\n        var bytes = Convert.FromBase64String(record.MimeMessage);\n        using var stream = new MemoryStream(bytes);\n        return await MimeMessage.LoadAsync(stream, ct).ConfigureAwait(false);\n    }\n\n    private static string? ResolveAccessToken(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(AccessTokenProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(AccessTokenBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n\n        if (providerData.TryGetValue(AccessTokenKey, out var token) && !string.IsNullOrWhiteSpace(token)) {\n            return token;\n        }\n\n        return null;\n    }\n\n    private static DateTimeOffset ResolveExpiration(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ExpiresOnKey, out var value) && !string.IsNullOrWhiteSpace(value) &&\n            DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)) {\n            return parsed;\n        }\n\n        return DateTimeOffset.MaxValue;\n    }\n\n    private static GraphCredential? BuildGraphCredential(Dictionary<string, string> providerData) {\n        if (!providerData.TryGetValue(ClientIdKey, out var clientId) || string.IsNullOrWhiteSpace(clientId) ||\n            !providerData.TryGetValue(TenantIdKey, out var tenantId) || string.IsNullOrWhiteSpace(tenantId)) {\n            return null;\n        }\n\n        var clientSecret = ResolveProtectedString(providerData, ClientSecretProtectedKey);\n        providerData.TryGetValue(CertificatePathKey, out var certificatePath);\n        var certificatePassword = ResolveProtectedString(providerData, CertificatePasswordProtectedKey);\n\n        if (string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(certificatePath)) {\n            return null;\n        }\n\n        return new GraphCredential {\n            ClientId = clientId.Trim(),\n            DirectoryId = tenantId.Trim(),\n            ClientSecret = string.IsNullOrWhiteSpace(clientSecret) ? null : clientSecret,\n            CertificatePath = string.IsNullOrWhiteSpace(certificatePath) ? null : certificatePath.Trim(),\n            CertificatePassword = string.IsNullOrWhiteSpace(certificatePassword) ? null : certificatePassword\n        };\n    }\n\n    private static string? ResolveProtectedString(Dictionary<string, string> providerData, string key) {\n        if (providerData.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(value);\n            return string.IsNullOrEmpty(decrypted) ? null : decrypted;\n        }\n\n        return null;\n    }\n\n    private static bool ShouldRefreshToken(DateTimeOffset expiresOn) => expiresOn <= DateTimeOffset.UtcNow.AddMinutes(1);\n\n    private static async Task<string> DefaultAcquireAccessTokenAsync(GraphCredential credential, CancellationToken cancellationToken) {\n        var authorization = await MicrosoftGraphUtils.ConnectO365GraphAsync(\n            credential,\n            credential.DirectoryId,\n            \"https://graph.microsoft.com\",\n            cancellationToken).ConfigureAwait(false);\n        return NormalizeAccessToken(authorization);\n    }\n\n    private static string NormalizeAccessToken(string authorizationHeaderValue) {\n        if (string.IsNullOrWhiteSpace(authorizationHeaderValue)) {\n            throw new InvalidOperationException(\"Graph authorization did not return a token.\");\n        }\n\n        var trimmed = authorizationHeaderValue.Trim();\n        const string bearerPrefix = \"Bearer \";\n        return trimmed.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)\n            ? trimmed.Substring(bearerPrefix.Length).Trim()\n            : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/MailgunPendingMessageSender.cs",
    "content": "using System.Net.Http.Headers;\nusing System.Text;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued Mailgun messages using the REST API.\n/// </summary>\npublic sealed class MailgunPendingMessageSender : IPendingMessageSender {\n    internal const string DomainKey = \"Domain\";\n    internal const string ApiKeyKey = \"ApiKey\";\n    internal const string ApiKeyBase64Key = \"ApiKeyBase64\";\n    internal const string ApiKeyProtectedKey = \"ApiKeyProtected\";\n    internal const string EndpointKey = \"Endpoint\";\n\n    private readonly HttpClient httpClient;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"MailgunPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"httpClient\">HTTP client used to send requests.</param>\n    public MailgunPendingMessageSender(HttpClient? httpClient = null) {\n        this.httpClient = httpClient ?? Helpers.SharedHttpClient;\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (string.IsNullOrWhiteSpace(record.MimeMessage)) {\n            throw new InvalidOperationException(\"Pending Mailgun message does not contain MIME content.\");\n        }\n        if (!record.ProviderData.TryGetValue(DomainKey, out var domain) || string.IsNullOrWhiteSpace(domain)) {\n            throw new InvalidOperationException(\"Pending Mailgun message is missing the Mailgun domain.\");\n        }\n        var apiKey = ResolveApiKey(record.ProviderData);\n        var endpoint = ResolveEndpoint(record.ProviderData, domain);\n        var bytes = Convert.FromBase64String(record.MimeMessage);\n\n        using var content = new MultipartFormDataContent();\n        using var messageContent = new ByteArrayContent(bytes);\n        messageContent.Headers.ContentType = new MediaTypeHeaderValue(\"message/rfc822\");\n        var fileName = string.IsNullOrWhiteSpace(record.MessageId) ? \"message.eml\" : $\"{record.MessageId}.eml\";\n        content.Add(messageContent, \"message\", fileName);\n\n        using var request = new HttpRequestMessage(HttpMethod.Post, endpoint) {\n            Content = content\n        };\n        var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($\"api:{apiKey}\"));\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Basic\", credentials);\n\n        using var response = await httpClient.SendAsync(request, ct).ConfigureAwait(false);\n        if (!response.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n            var error = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);\n#else\n            var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new HttpRequestException($\"Mailgun returned {(int)response.StatusCode} ({response.StatusCode}): {error}\");\n        }\n    }\n\n    private static Uri ResolveEndpoint(Dictionary<string, string> providerData, string domain) {\n        if (providerData.TryGetValue(EndpointKey, out var value) && !string.IsNullOrWhiteSpace(value) && Uri.TryCreate(value, UriKind.Absolute, out var uri)) {\n            return uri;\n        }\n        return new Uri($\"https://api.mailgun.net/v3/{domain}/messages.mime\");\n    }\n\n    private static string ResolveApiKey(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ApiKeyProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(ApiKeyBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n        if (providerData.TryGetValue(ApiKeyKey, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value;\n        }\n        throw new InvalidOperationException(\"Pending Mailgun message is missing API key information.\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/SendGridPendingMessageSender.cs",
    "content": "using System.Net.Http.Headers;\nusing System.Text;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued SendGrid messages using the REST API.\n/// </summary>\npublic sealed class SendGridPendingMessageSender : IPendingMessageSender {\n    internal const string MessageJsonKey = \"MessageJson\";\n    internal const string ApiKeyKey = \"ApiKey\";\n    internal const string ApiKeyBase64Key = \"ApiKeyBase64\";\n    internal const string ApiKeyProtectedKey = \"ApiKeyProtected\";\n    internal const string EndpointKey = \"Endpoint\";\n\n    private static readonly Uri DefaultEndpoint = new(\"https://api.sendgrid.com/v3/mail/send\");\n\n    private readonly HttpClient httpClient;\n    private readonly Uri endpoint;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SendGridPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"httpClient\">HTTP client used to send requests.</param>\n    /// <param name=\"endpoint\">Optional override for the SendGrid endpoint.</param>\n    public SendGridPendingMessageSender(HttpClient? httpClient = null, Uri? endpoint = null) {\n        this.httpClient = httpClient ?? Helpers.SharedHttpClient;\n        this.endpoint = endpoint ?? DefaultEndpoint;\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (!record.ProviderData.TryGetValue(MessageJsonKey, out var json) || string.IsNullOrWhiteSpace(json)) {\n            throw new InvalidOperationException(\"Pending SendGrid message is missing serialized payload.\");\n        }\n        var apiKey = ResolveApiKey(record.ProviderData);\n        using var request = new HttpRequestMessage(HttpMethod.Post, ResolveEndpoint(record.ProviderData));\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n        request.Content = new StringContent(json, Encoding.UTF8, \"application/json\");\n        using var response = await httpClient.SendAsync(request, ct).ConfigureAwait(false);\n        if (!response.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n            var error = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);\n#else\n            var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n            throw new HttpRequestException($\"SendGrid returned {(int)response.StatusCode} ({response.StatusCode}): {error}\");\n        }\n    }\n\n    private static Uri ResolveEndpoint(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(EndpointKey, out var value) && !string.IsNullOrWhiteSpace(value) && Uri.TryCreate(value, UriKind.Absolute, out var uri)) {\n            return uri;\n        }\n        return DefaultEndpoint;\n    }\n\n    private static string ResolveApiKey(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(ApiKeyProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(ApiKeyBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n        if (providerData.TryGetValue(ApiKeyKey, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value;\n        }\n        throw new InvalidOperationException(\"Pending SendGrid message is missing API key information.\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/SesPendingMessageSender.cs",
    "content": "using System.Globalization;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued Amazon SES messages using the REST API.\n/// </summary>\npublic sealed class SesPendingMessageSender : IPendingMessageSender {\n    internal const string AccessKeyIdKey = \"AccessKeyId\";\n    internal const string SecretAccessKeyKey = \"SecretAccessKey\";\n    internal const string AccessKeyIdBase64Key = \"AccessKeyIdBase64\";\n    internal const string SecretAccessKeyBase64Key = \"SecretAccessKeyBase64\";\n    internal const string AccessKeyIdProtectedKey = \"AccessKeyIdProtected\";\n    internal const string SecretAccessKeyProtectedKey = \"SecretAccessKeyProtected\";\n    internal const string RegionKey = \"Region\";\n\n    private readonly HttpClient httpClient;\n    private readonly Func<DateTime> utcNow;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SesPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"httpClient\">HTTP client used to send requests.</param>\n    /// <param name=\"utcNow\">Clock used for signing requests.</param>\n    public SesPendingMessageSender(HttpClient? httpClient = null, Func<DateTime>? utcNow = null) {\n        this.httpClient = httpClient ?? Helpers.SharedHttpClient;\n        this.utcNow = utcNow ?? (() => DateTime.UtcNow);\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (string.IsNullOrWhiteSpace(record.MimeMessage)) {\n            throw new InvalidOperationException(\"Pending SES message does not contain MIME content.\");\n        }\n        var accessKey = ResolveAccessKey(record.ProviderData);\n        var secretKey = ResolveSecretKey(record.ProviderData);\n        var region = record.ProviderData.TryGetValue(RegionKey, out var regionValue) && !string.IsNullOrWhiteSpace(regionValue)\n            ? regionValue\n            : \"us-east-1\";\n\n        var body = BuildRequestBody(record.MimeMessage);\n        var request = CreateRequest(accessKey, secretKey, region, body, utcNow());\n        request.Content = new StringContent(body, Encoding.UTF8, \"application/x-www-form-urlencoded\");\n\n        using (request)\n        using (var response = await httpClient.SendAsync(request, ct).ConfigureAwait(false)) {\n            if (!response.IsSuccessStatusCode) {\n#if NET5_0_OR_GREATER\n                var error = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);\n#else\n                var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n                throw new HttpRequestException($\"SES returned {(int)response.StatusCode} ({response.StatusCode}): {error}\");\n            }\n        }\n    }\n\n    private static string BuildRequestBody(string mimeMessageBase64) {\n        return $\"Action=SendRawEmail&RawMessage.Data={Uri.EscapeDataString(mimeMessageBase64)}&Version=2010-12-01\";\n    }\n\n    private static string ResolveAccessKey(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(AccessKeyIdProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(AccessKeyIdBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n\n        if (providerData.TryGetValue(AccessKeyIdKey, out var accessKey) && !string.IsNullOrWhiteSpace(accessKey)) {\n            return accessKey;\n        }\n\n        throw new InvalidOperationException(\"Pending SES message is missing the AWS access key identifier.\");\n    }\n\n    private static string ResolveSecretKey(Dictionary<string, string> providerData) {\n        if (providerData.TryGetValue(SecretAccessKeyProtectedKey, out var protectedValue) && !string.IsNullOrWhiteSpace(protectedValue)) {\n            var decrypted = CredentialProtection.UnprotectWithFallback(protectedValue);\n            if (!string.IsNullOrEmpty(decrypted)) {\n                return decrypted;\n            }\n        }\n\n        if (providerData.TryGetValue(SecretAccessKeyBase64Key, out var encoded) && !string.IsNullOrWhiteSpace(encoded)) {\n            var bytes = Convert.FromBase64String(encoded);\n            return Encoding.UTF8.GetString(bytes);\n        }\n\n        if (providerData.TryGetValue(SecretAccessKeyKey, out var secretKey) && !string.IsNullOrWhiteSpace(secretKey)) {\n            return secretKey;\n        }\n\n        throw new InvalidOperationException(\"Pending SES message is missing the AWS secret access key.\");\n    }\n\n    private static HttpRequestMessage CreateRequest(string accessKey, string secretKey, string region, string content, DateTime nowUtc) {\n        var request = new HttpRequestMessage(HttpMethod.Post, new Uri($\"https://email.{region}.amazonaws.com/\"));\n        var amzDate = nowUtc.ToString(\"yyyyMMdd'T'HHmmss'Z'\", CultureInfo.InvariantCulture);\n        var dateStamp = nowUtc.ToString(\"yyyyMMdd\", CultureInfo.InvariantCulture);\n        var canonicalHeaders = $\"content-type:application/x-www-form-urlencoded\\nhost:email.{region}.amazonaws.com\\nx-amz-date:{amzDate}\\n\";\n        const string signedHeaders = \"content-type;host;x-amz-date\";\n        var payloadHash = Sha256Hex(content);\n        var canonicalRequest = $\"POST\\n/\\n\\n{canonicalHeaders}\\n{signedHeaders}\\n{payloadHash}\";\n        var credentialScope = $\"{dateStamp}/{region}/ses/aws4_request\";\n        var stringToSign = $\"AWS4-HMAC-SHA256\\n{amzDate}\\n{credentialScope}\\n{Sha256Hex(canonicalRequest)}\";\n        var signingKey = GetSignatureKey(secretKey, dateStamp, region, \"ses\");\n        var signature = ToHex(HmacSha256(signingKey, stringToSign));\n        var authorization = $\"AWS4-HMAC-SHA256 Credential={accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}\";\n\n        request.Headers.TryAddWithoutValidation(\"x-amz-date\", amzDate);\n        request.Headers.TryAddWithoutValidation(\"Authorization\", authorization);\n        return request;\n    }\n\n    private static byte[] GetSignatureKey(string key, string dateStamp, string regionName, string serviceName) {\n        var kDate = HmacSha256(Encoding.UTF8.GetBytes(\"AWS4\" + key), dateStamp);\n        var kRegion = HmacSha256(kDate, regionName);\n        var kService = HmacSha256(kRegion, serviceName);\n        return HmacSha256(kService, \"aws4_request\");\n    }\n\n    private static byte[] HmacSha256(byte[] key, string data) {\n        using var hmac = new HMACSHA256(key);\n        return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));\n    }\n\n    private static string Sha256Hex(string data) {\n        using var sha = SHA256.Create();\n        var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(data));\n        return ToHex(hash);\n    }\n\n    private static string ToHex(byte[] bytes) {\n        var builder = new StringBuilder(bytes.Length * 2);\n        for (var i = 0; i < bytes.Length; i++) {\n            builder.Append(bytes[i].ToString(\"x2\", CultureInfo.InvariantCulture));\n        }\n        return builder.ToString();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/Senders/SmtpPendingMessageSender.cs",
    "content": "using System.Globalization;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Sends queued SMTP messages using <see cref=\"ClientSmtp\"/>.\n/// </summary>\npublic sealed class SmtpPendingMessageSender : IPendingMessageSender {\n    private const string SecureSocketOptionsKey = \"SecureSocketOptions\";\n    private const string UseSslKey = \"UseSsl\";\n    private const string SkipCertificateValidationKey = \"SkipCertificateValidation\";\n    private const string CheckCertificateRevocationKey = \"CheckCertificateRevocation\";\n    private const string TimeoutKey = \"TimeoutMilliseconds\";\n\n    private readonly Func<ClientSmtp> clientFactory;\n    private readonly SecureSocketOptions defaultSecureSocketOptions;\n    private readonly bool defaultUseSsl;\n    private readonly bool defaultSkipCertificateValidation;\n    private readonly bool defaultCheckCertificateRevocation;\n    private readonly int? defaultTimeout;\n    private readonly ICredentialProtector credentialProtector;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SmtpPendingMessageSender\"/> class.\n    /// </summary>\n    /// <param name=\"clientFactory\">Factory used to create <see cref=\"ClientSmtp\"/> instances.</param>\n    /// <param name=\"secureSocketOptions\">Default secure socket options.</param>\n    /// <param name=\"useSsl\">Whether SSL should be forced when options are auto.</param>\n    /// <param name=\"skipCertificateValidation\">Whether certificate validation should be skipped.</param>\n    /// <param name=\"checkCertificateRevocation\">Whether certificate revocation should be checked.</param>\n    /// <param name=\"timeout\">Optional operation timeout in milliseconds.</param>\n    /// <param name=\"credentialProtector\">Optional credential protector used to decrypt queued passwords.</param>\n    public SmtpPendingMessageSender(\n        Func<ClientSmtp>? clientFactory = null,\n        SecureSocketOptions secureSocketOptions = SecureSocketOptions.Auto,\n        bool useSsl = false,\n        bool skipCertificateValidation = false,\n        bool checkCertificateRevocation = true,\n        int? timeout = null,\n        ICredentialProtector? credentialProtector = null) {\n        this.clientFactory = clientFactory ?? (() => Smtp.ClientFactory(null));\n        defaultSecureSocketOptions = secureSocketOptions;\n        defaultUseSsl = useSsl;\n        defaultSkipCertificateValidation = skipCertificateValidation;\n        defaultCheckCertificateRevocation = checkCertificateRevocation;\n        defaultTimeout = timeout;\n        this.credentialProtector = credentialProtector ?? CredentialProtection.Default;\n    }\n\n    /// <inheritdoc />\n    public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        if (record == null) {\n            throw new ArgumentNullException(nameof(record));\n        }\n        if (string.IsNullOrWhiteSpace(record.MimeMessage)) {\n            throw new InvalidOperationException(\"Pending message does not contain a MIME payload.\");\n        }\n        if (string.IsNullOrWhiteSpace(record.Server)) {\n            throw new InvalidOperationException(\"Pending message is missing SMTP server information.\");\n        }\n\n        var message = await LoadMessageAsync(record, ct).ConfigureAwait(false);\n        var secureSocketOptions = ResolveSecureSocketOptions(record.ProviderData);\n        var useSsl = ResolveBool(record.ProviderData, UseSslKey, defaultUseSsl);\n        if (useSsl && secureSocketOptions == SecureSocketOptions.Auto) {\n            secureSocketOptions = SecureSocketOptions.StartTls;\n        }\n        var skipValidation = ResolveBool(record.ProviderData, SkipCertificateValidationKey, defaultSkipCertificateValidation);\n        var checkRevocation = ResolveBool(record.ProviderData, CheckCertificateRevocationKey, defaultCheckCertificateRevocation);\n        var timeout = ResolveInt(record.ProviderData, TimeoutKey, defaultTimeout);\n\n        var port = record.Port ?? 25;\n        using var client = clientFactory();\n        if (timeout.HasValue) {\n            client.Timeout = timeout.Value;\n        }\n        if (skipValidation) {\n            client.ServerCertificateValidationCallback = (_, _, _, _) => true;\n        }\n        client.CheckCertificateRevocation = checkRevocation;\n\n        try {\n            await client.ConnectAsync(record.Server!, port, secureSocketOptions, ct).ConfigureAwait(false);\n            if (!string.IsNullOrEmpty(record.UserName)) {\n                var password = DecodePassword(record.Password);\n                await client.AuthenticateAsync(record.UserName!, password, ct).ConfigureAwait(false);\n            }\n            await client.SendAsync(message, ct).ConfigureAwait(false);\n        } finally {\n            try {\n                if (client.IsConnected) {\n                    await client.DisconnectAsync(true, ct).ConfigureAwait(false);\n                }\n            } catch {\n                // Ignored - disconnect best effort.\n            }\n        }\n    }\n\n    private static async Task<MimeMessage> LoadMessageAsync(PendingMessageRecord record, CancellationToken ct) {\n        var bytes = Convert.FromBase64String(record.MimeMessage);\n        using var stream = new MemoryStream(bytes);\n        return await MimeMessage.LoadAsync(stream, ct).ConfigureAwait(false);\n    }\n\n    private SecureSocketOptions ResolveSecureSocketOptions(Dictionary<string, string> providerData) {\n        if (providerData == null) {\n            return defaultSecureSocketOptions;\n        }\n        if (providerData.TryGetValue(SecureSocketOptionsKey, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            if (Enum.TryParse(value, ignoreCase: true, out SecureSocketOptions parsed)) {\n                return parsed;\n            }\n            if (int.TryParse(value, out var numeric) && Enum.IsDefined(typeof(SecureSocketOptions), numeric)) {\n                return (SecureSocketOptions)numeric;\n            }\n        }\n        return defaultSecureSocketOptions;\n    }\n\n    private static bool ResolveBool(Dictionary<string, string> providerData, string key, bool defaultValue) {\n        if (providerData != null && providerData.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            if (bool.TryParse(value, out var parsed)) {\n                return parsed;\n            }\n            if (int.TryParse(value, out var numeric)) {\n                return numeric != 0;\n            }\n        }\n        return defaultValue;\n    }\n\n    private static int? ResolveInt(Dictionary<string, string> providerData, string key, int? defaultValue) {\n        if (providerData != null && providerData.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) {\n                return parsed;\n            }\n        }\n        return defaultValue;\n    }\n\n    internal string DecodePassword(string? password) => CredentialProtection.UnprotectWithFallback(credentialProtector, password);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/SentMessageRecipients.cs",
    "content": "using MimeKit;\n\nnamespace Mailozaurr;\n\ninternal static class SentMessageRecipients {\n    public static string Serialize(InternetAddressList? recipients) {\n        if (recipients == null || recipients.Count == 0) {\n            return string.Empty;\n        }\n\n        return string.Join(\",\",\n            recipients.Mailboxes\n                .Select(mailbox => NormalizeAddress(mailbox.Address))\n                .Where(address => !string.IsNullOrWhiteSpace(address))\n                .Distinct(StringComparer.OrdinalIgnoreCase)!);\n    }\n\n    public static IReadOnlyList<string> Parse(string? recipients) {\n        if (string.IsNullOrWhiteSpace(recipients)) {\n            return Array.Empty<string>();\n        }\n\n        var output = new List<string>();\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        foreach (var candidate in recipients!.Split(',')) {\n            var normalized = NormalizeAddress(candidate);\n            if (string.IsNullOrWhiteSpace(normalized)) {\n                continue;\n            }\n\n            var address = normalized!;\n            if (!seen.Add(address)) {\n                continue;\n            }\n\n            output.Add(address);\n        }\n\n        return output;\n    }\n\n    public static string? NormalizeAddress(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var normalized = value!.Trim();\n        var semicolonIndex = normalized.IndexOf(';');\n        if (semicolonIndex >= 0 && semicolonIndex + 1 < normalized.Length) {\n            normalized = normalized.Substring(semicolonIndex + 1).Trim();\n        }\n\n        var start = normalized.LastIndexOf('<');\n        var end = normalized.LastIndexOf('>');\n        if (start >= 0 && end > start) {\n            normalized = normalized.Substring(start + 1, end - start - 1).Trim();\n        }\n\n        if (normalized.StartsWith(\"\\\"\", StringComparison.Ordinal) && normalized.EndsWith(\"\\\"\", StringComparison.Ordinal) && normalized.Length > 1) {\n            normalized = normalized.Substring(1, normalized.Length - 2).Trim();\n        }\n\n        return normalized.Length == 0 ? null : normalized;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/SentMessages/SentMessageRecord.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Represents a record of a message that has been sent.\n/// </summary>\npublic sealed class SentMessageRecord {\n    /// <summary>Identifier of the message.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Comma-separated list of recipients.</summary>\n    public string Recipients { get; set; } = string.Empty;\n\n    /// <summary>Subject line of the message.</summary>\n    public string Subject { get; set; } = string.Empty;\n\n    /// <summary>Time at which the message was sent.</summary>\n    public DateTimeOffset Timestamp { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Serialization/JsonDtos.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>Simple payload used to send raw MIME data to Gmail.</summary>\npublic sealed class GmailRawRequest {\n    /// <summary>Initializes the request with base64-url encoded MIME content.</summary>\n    public GmailRawRequest(string raw) => Raw = raw;\n\n    /// <summary>Base64-url encoded MIME message.</summary>\n    [JsonPropertyName(\"raw\")]\n    public string Raw { get; }\n}\n\n/// <summary>Batch envelope for Microsoft Graph batch API.</summary>\npublic sealed class GraphBatchPayload {\n    /// <summary>Individual batch requests.</summary>\n    [JsonPropertyName(\"requests\")]\n    public List<GraphBatchRequestPayload> Requests { get; set; } = new();\n}\n\n/// <summary>Single request entry inside a Graph batch.</summary>\npublic sealed class GraphBatchRequestPayload {\n    /// <summary>Client-supplied identifier for correlating responses.</summary>\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>HTTP method name.</summary>\n    [JsonPropertyName(\"method\")]\n    public string Method { get; set; } = string.Empty;\n\n    /// <summary>Relative URL within Graph.</summary>\n    [JsonPropertyName(\"url\")]\n    public string Url { get; set; } = string.Empty;\n\n    /// <summary>Optional per-request headers.</summary>\n    [JsonPropertyName(\"headers\")]\n    public IDictionary<string, string>? Headers { get; set; }\n\n    /// <summary>Optional JSON payload.</summary>\n    [JsonPropertyName(\"body\")]\n    public JsonElement? Body { get; set; }\n}\n\n/// <summary>Batch search request payload for Graph search API.</summary>\npublic sealed class GraphSearchPayload {\n    /// <summary>Search requests to execute.</summary>\n    [JsonPropertyName(\"requests\")]\n    public List<GraphSearchRequest> Requests { get; set; } = new();\n}\n\n/// <summary>Represents a single Graph search request.</summary>\npublic sealed class GraphSearchRequest {\n    /// <summary>Entity types to search (e.g., message).</summary>\n    [JsonPropertyName(\"entityTypes\")]\n    public string[] EntityTypes { get; set; } = Array.Empty<string>();\n\n    /// <summary>Zero-based result offset.</summary>\n    [JsonPropertyName(\"from\")]\n    public int From { get; set; }\n\n    /// <summary>Maximum number of hits to return.</summary>\n    [JsonPropertyName(\"size\")]\n    public int Size { get; set; }\n\n    /// <summary>Query text definition.</summary>\n    [JsonPropertyName(\"query\")]\n    public GraphSearchQuery Query { get; set; } = new();\n\n    /// <summary>User principals to scope the search to.</summary>\n    [JsonPropertyName(\"userScopes\")]\n    public string[] UserScopes { get; set; } = Array.Empty<string>();\n}\n\n/// <summary>Holds the query text for Graph search.</summary>\npublic sealed class GraphSearchQuery {\n    /// <summary>Raw KQL-like query string.</summary>\n    [JsonPropertyName(\"queryString\")]\n    public string QueryString { get; set; } = string.Empty;\n}\n\n/// <summary>Payload for move/copy operations in Graph.</summary>\npublic sealed class GraphDestinationRequest {\n    /// <summary>Target folder id.</summary>\n    [JsonPropertyName(\"destinationId\")]\n    public string? DestinationId { get; set; }\n}\n\n/// <summary>Payload to mark messages read/unread.</summary>\npublic sealed class GraphMarkReadRequest {\n    /// <summary>True to mark read; false to mark unread.</summary>\n    [JsonPropertyName(\"isRead\")]\n    public bool IsRead { get; set; }\n}\n\n/// <summary>Payload to flag/unflag messages.</summary>\npublic sealed class GraphSetFlagRequest {\n    /// <summary>Flag object.</summary>\n    [JsonPropertyName(\"flag\")]\n    public GraphSetFlagRequestFlag Flag { get; set; } = new();\n}\n\n/// <summary>Flag object for <see cref=\"GraphSetFlagRequest\"/>.</summary>\npublic sealed class GraphSetFlagRequestFlag {\n    /// <summary>Flag status (for example: flagged, notFlagged).</summary>\n    [JsonPropertyName(\"flagStatus\")]\n    public string? FlagStatus { get; set; }\n}\n\n/// <summary>Payload to rename a Graph mail folder.</summary>\npublic sealed class GraphFolderRenameRequest {\n    /// <summary>New display name.</summary>\n    [JsonPropertyName(\"displayName\")]\n    public string? DisplayName { get; set; }\n}\n\n/// <summary>Envelope stored in the pending message log.</summary>\npublic sealed class PendingMessageLogEnvelope {\n    /// <summary>Entry type (upsert or tombstone).</summary>\n    public string EntryType { get; set; } = string.Empty;\n\n    /// <summary>Message id affected by this entry.</summary>\n    public string? MessageId { get; set; }\n\n    /// <summary>Full pending message record, when applicable.</summary>\n    public PendingMessageRecord? Record { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Serialization/MailozaurrJsonContext.cs",
    "content": "using System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\n/// <summary>Source-generated metadata used by Mailozaurr for AOT-safe JSON serialization.</summary>\n[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]\n[JsonSerializable(typeof(SmtpResult))]\n[JsonSerializable(typeof(PendingMessageRecord))]\n[JsonSerializable(typeof(PendingMessageLogEnvelope))]\n[JsonSerializable(typeof(SentMessageRecord))]\n[JsonSerializable(typeof(OAuthCredential))]\n[JsonSerializable(typeof(Dictionary<string, OAuthCredential>))]\n[JsonSerializable(typeof(OAuthCredentialCacheEntry), TypeInfoPropertyName = \"OAuthCredentialCacheEntry\")]\n[JsonSerializable(typeof(Dictionary<string, OAuthCredentialCacheEntry>), TypeInfoPropertyName = \"DictionaryStringOAuthCredentialCacheEntry\")]\n[JsonSerializable(typeof(Dictionary<string, string>))]\n[JsonSerializable(typeof(Dictionary<string, bool>), TypeInfoPropertyName = \"DictionaryStringBool\")]\n[JsonSerializable(typeof(Dictionary<string, int>), TypeInfoPropertyName = \"DictionaryStringInt\")]\n[JsonSerializable(typeof(Dictionary<string, double>), TypeInfoPropertyName = \"DictionaryStringDouble\")]\n[JsonSerializable(typeof(Dictionary<string, long>), TypeInfoPropertyName = \"DictionaryStringLong\")]\n[JsonSerializable(typeof(Dictionary<string, object>))]\n[JsonSerializable(typeof(Dictionary<string, JsonElement>))]\n[JsonSerializable(typeof(Dictionary<string, object?>), TypeInfoPropertyName = \"DictionaryStringObjectNullable\")]\n[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = \"DictionaryStringObject\")]\n[JsonSerializable(typeof(Dictionary<string, JsonElement?>))]\n[JsonSerializable(typeof(object), TypeInfoPropertyName = \"Object\")]\n[JsonSerializable(typeof(JsonElement))]\n[JsonSerializable(typeof(GraphAuthorization))]\n[JsonSerializable(typeof(GraphApiError))]\n[JsonSerializable(typeof(GraphApiErrorDetail))]\n[JsonSerializable(typeof(GraphApiInnerError))]\n[JsonSerializable(typeof(GraphApiDiagnostic))]\n[JsonSerializable(typeof(GraphApiErrorHeaders))]\n[JsonSerializable(typeof(GraphApiErrorResponse))]\n[JsonSerializable(typeof(GraphApiServerInfo))]\n[JsonSerializable(typeof(GraphUploadSessionResult))]\n[JsonSerializable(typeof(GraphMessage))]\n[JsonSerializable(typeof(GraphMessageContainer))]\n[JsonSerializable(typeof(GraphContent))]\n[JsonSerializable(typeof(GraphEmailAddress))]\n[JsonSerializable(typeof(GraphEmail))]\n[JsonSerializable(typeof(GraphAttachment))]\n[JsonSerializable(typeof(GraphAttachmentItem))]\n[JsonSerializable(typeof(GraphAttachmentItemWrapper))]\n[JsonSerializable(typeof(GraphInboxRule))]\n[JsonSerializable(typeof(GraphInboxRuleActions))]\n[JsonSerializable(typeof(GraphInboxRulePredicates))]\n[JsonSerializable(typeof(GraphMailboxPermission))]\n[JsonSerializable(typeof(GraphMailboxGrantee))]\n[JsonSerializable(typeof(GraphEvent))]\n[JsonSerializable(typeof(GraphEventAttendee))]\n[JsonSerializable(typeof(GraphEventTime))]\n[JsonSerializable(typeof(GraphBatchPayload))]\n[JsonSerializable(typeof(GraphBatchRequestPayload))]\n[JsonSerializable(typeof(GraphSearchPayload))]\n[JsonSerializable(typeof(GraphSearchRequest))]\n[JsonSerializable(typeof(GraphSearchQuery))]\n[JsonSerializable(typeof(GraphDestinationRequest))]\n[JsonSerializable(typeof(GraphMarkReadRequest))]\n[JsonSerializable(typeof(GraphSetFlagRequest))]\n[JsonSerializable(typeof(GraphSetFlagRequestFlag))]\n[JsonSerializable(typeof(GraphFolderRenameRequest))]\n[JsonSerializable(typeof(GraphApiClient.GraphCreateSubscriptionRequest))]\n[JsonSerializable(typeof(GraphApiClient.GraphRenewSubscriptionRequest))]\n[JsonSerializable(typeof(GraphApiClient.GraphSubscription))]\n[JsonSerializable(typeof(GraphApiClient.GraphSubscriptionListResponse))]\n[JsonSerializable(typeof(GmailMessage))]\n[JsonSerializable(typeof(GmailMessagePayload))]\n[JsonSerializable(typeof(GmailMessageHeader))]\n[JsonSerializable(typeof(GmailMessageBody))]\n[JsonSerializable(typeof(GmailThread))]\n[JsonSerializable(typeof(GmailThreadInfo))]\n[JsonSerializable(typeof(GmailAttachmentInfo))]\n[JsonSerializable(typeof(GmailLabel))]\n[JsonSerializable(typeof(GmailRawRequest))]\n[JsonSerializable(typeof(GmailApiClient.AttachmentResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailListResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailThreadListResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailLabelListResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailModifyLabelsRequest))]\n[JsonSerializable(typeof(GmailApiClient.GmailBatchModifyRequest))]\n[JsonSerializable(typeof(GmailApiClient.GmailBatchDeleteRequest))]\n[JsonSerializable(typeof(GmailApiClient.GmailImportMessageRequest))]\n[JsonSerializable(typeof(GmailApiClient.GmailWatchRequest))]\n[JsonSerializable(typeof(GmailApiClient.GmailWatchResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailProfile))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryListResponse))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryRecord))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryMessageAdded))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryMessageDeleted))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryLabelAdded))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryLabelRemoved))]\n[JsonSerializable(typeof(GmailApiClient.GmailHistoryMessageRef))]\n[JsonSerializable(typeof(Attachment))]\n[JsonSerializable(typeof(SendGridMessage))]\n[JsonSerializable(typeof(SendGridPersonalization))]\n[JsonSerializable(typeof(SendGridEmailAddress))]\n[JsonSerializable(typeof(SendGridContent))]\n[JsonSerializable(typeof(SendGridAttachment))]\n[JsonSerializable(typeof(PendingMessageRepositoryOptions))]\n[JsonSerializable(typeof(Dictionary<string, string[]>))]\n[JsonSerializable(typeof(Dictionary<string, IList<string>>))]\npublic partial class MailozaurrJsonContext : JsonSerializerContext;\n"
  },
  {
    "path": "Sources/Mailozaurr/Serialization/UnixTimeSecondsDateTimeOffsetConverter.cs",
    "content": "using System;\nusing System.Globalization;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr;\n\ninternal sealed class UnixTimeSecondsDateTimeOffsetConverter : JsonConverter<DateTimeOffset> {\n    public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {\n        if (reader.TokenType == JsonTokenType.Null) {\n            return DateTimeOffset.MinValue;\n        }\n\n        if (reader.TokenType == JsonTokenType.Number) {\n            if (reader.TryGetInt64(out var numeric)) {\n                return FromUnix(numeric);\n            }\n\n            if (reader.TryGetDouble(out var numericDouble)) {\n                return FromUnix((long)numericDouble);\n            }\n        }\n\n        if (reader.TokenType == JsonTokenType.String) {\n            var value = reader.GetString();\n            if (string.IsNullOrWhiteSpace(value)) {\n                return DateTimeOffset.MinValue;\n            }\n\n            if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) {\n                return FromUnix(numeric);\n            }\n\n            if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var numericDouble)) {\n                return FromUnix((long)numericDouble);\n            }\n\n            if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)) {\n                return parsed;\n            }\n\n            if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out parsed)) {\n                return parsed;\n            }\n        }\n\n        return DateTimeOffset.MinValue;\n    }\n\n    public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) {\n        writer.WriteStringValue(value.ToString(\"O\", CultureInfo.InvariantCulture));\n    }\n\n    private static DateTimeOffset FromUnix(long value) {\n        // Threshold: ~316 years from epoch (year 2286); values beyond are assumed to be milliseconds.\n        return value > 9_999_999_999L\n            ? DateTimeOffset.FromUnixTimeMilliseconds(value)\n            : DateTimeOffset.FromUnixTimeSeconds(value);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/ClientSmtp.Disposal.cs",
    "content": "namespace Mailozaurr;\n\npublic partial class ClientSmtp\n{\n    /// <summary>\n    /// Finalizer to ensure the client is properly disposed.\n    /// </summary>\n    ~ClientSmtp()\n    {\n        Dispose(false);\n    }\n\n    /// <summary>\n    /// Releases the unmanaged resources used by the client and optionally disposes of the managed resources.\n    /// </summary>\n    /// <param name=\"disposing\">If set to <c>true</c> the method has been invoked directly or indirectly by a user's code.</param>\n    protected override void Dispose(bool disposing)\n    {\n        if (IsConnected)\n        {\n            try\n            {\n                Disconnect(true);\n            }\n            catch\n            {\n                // ignore errors while disposing\n            }\n        }\n        base.Dispose(disposing);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/ClientSmtp.cs",
    "content": "using System.Runtime.ExceptionServices;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Mailozaurr.Definitions;\nusing MimeKit.Utils;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Extension of <see cref=\"SmtpClient\"/> used to build and send messages.\n/// </summary>\npublic partial class ClientSmtp : SmtpClient {\n    /// <summary>Subject of the message.</summary>\n    public string Subject { get; set; } = string.Empty;\n    /// <summary>HTML body of the message.</summary>\n    public string HtmlBody { get; set; } = string.Empty;\n    /// <summary>Plain text body of the message.</summary>\n    public string TextBody { get; set; } = string.Empty;\n    /// <summary>Attachments to include with the message.</summary>\n    public List<AttachmentDescriptor>? Attachments { get; set; } = new List<AttachmentDescriptor>();\n    /// <summary>Inline attachments to embed in the message.</summary>\n    public List<AttachmentDescriptor>? InlineAttachments { get; set; } = new List<AttachmentDescriptor>();\n    /// <summary>The sender address.</summary>\n    public object? From { get; set; }\n    /// <summary>Primary recipients.</summary>\n    public IEnumerable<object>? To { get; set; } = new List<object>();\n    /// <summary>Carbon copy recipients.</summary>\n    public IEnumerable<object>? Cc { get; set; } = new List<object>();\n    /// <summary>Blind carbon copy recipients.</summary>\n    public IEnumerable<object>? Bcc { get; set; } = new List<object>();\n    /// <summary>Reply-to address.</summary>\n    public object? ReplyTo { get; set; }\n    /// <summary>The underlying MIME message.</summary>\n    public MimeMessage Message { get; set; } = new MimeMessage();\n    /// <summary>Priority of the message.</summary>\n    public MessagePriority Priority { get; set; }\n    /// <summary>Delivery notification options.</summary>\n    public DeliveryNotification[]? DeliveryNotificationOption { get; set; }\n    /// <summary>Custom headers to add to the message.</summary>\n    public IDictionary<string, string>? Headers { get; set; }\n    /// <summary>Download remote images referenced in HtmlBody and embed them.</summary>\n    public bool AutoEmbedRemoteImages { get; set; } = false;\n    /// <summary>Comma separated list of all recipients.</summary>\n    public string SentTo {\n        get {\n            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            var addresses = new List<string>();\n            if (To != null) {\n                addresses.AddRange(ConvertToMailboxAddressesUnique(To, seen).Select(x => x.Address));\n            }\n            if (Cc != null) {\n                addresses.AddRange(ConvertToMailboxAddressesUnique(Cc, seen).Select(x => x.Address));\n            }\n            if (Bcc != null) {\n                addresses.AddRange(ConvertToMailboxAddressesUnique(Bcc, seen).Select(x => x.Address));\n            }\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>The address(es) the message is sent from.</summary>\n    public string SentFrom {\n        get {\n            var addresses = From != null\n                ? ConvertToMailboxAddress(From).Select(x => x.Address)\n                : Enumerable.Empty<string>();\n            return string.Join(\",\", addresses);\n        }\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"ClientSmtp\"/> class.</summary>\n    public ClientSmtp() {\n        Priority = MessagePriority.Normal;\n    }\n\n    /// <summary>Initializes a new instance of the <see cref=\"ClientSmtp\"/> class using the specified logger.</summary>\n    /// <param name=\"protocolLogger\">The protocol logger.</param>\n    public ClientSmtp(ProtocolLogger protocolLogger) : base(protocolLogger) {\n        Priority = MessagePriority.Normal;\n    }\n\n    /// <summary>\n    /// Returns the currently negotiated SMTP capabilities.\n    /// Overridable for tests that need to fake server features.\n    /// </summary>\n    public virtual SmtpCapabilities GetCapabilitiesSnapshot() => Capabilities;\n\n    /// <summary>\n    /// Determines which delivery status notifications should be requested for the specified recipient.\n    /// </summary>\n    /// <param name=\"message\">The message being sent.</param>\n    /// <param name=\"mailbox\">The recipient mailbox address.</param>\n    /// <returns>The delivery status notification flags to use, or <c>null</c>.</returns>\n    protected override DeliveryStatusNotification? GetDeliveryStatusNotifications(MimeMessage message, MailboxAddress mailbox) {\n        DeliveryStatusNotification combinedOption = 0;\n        if (DeliveryNotificationOption != null) {\n            foreach (var option in DeliveryNotificationOption) {\n                switch (option) {\n                    case DeliveryNotification.None:\n                        break;\n                    case DeliveryNotification.Delay:\n                        combinedOption |= DeliveryStatusNotification.Delay;\n                        break;\n                    case DeliveryNotification.Never:\n                        combinedOption |= DeliveryStatusNotification.Never;\n                        break;\n                    case DeliveryNotification.OnFailure:\n                        combinedOption |= DeliveryStatusNotification.Failure;\n                        break;\n                    case DeliveryNotification.OnSuccess:\n                        combinedOption |= DeliveryStatusNotification.Success;\n                        break;\n                }\n            }\n        }\n        return combinedOption;\n    }\n\n    /// <summary>\n    /// Builds the <see cref=\"MimeMessage\"/> based on the configured properties.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public void CreateMessage(CancellationToken cancellationToken = default) {\n        var task = Task.Run(\n            async () => await CreateMessageAsync(cancellationToken).ConfigureAwait(false),\n            cancellationToken);\n        try {\n            task.Wait(cancellationToken);\n        } catch (AggregateException ex) when (ex.InnerExceptions.Count == 1) {\n            ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();\n        }\n    }\n\n    /// <summary>\n    /// Asynchronously builds the <see cref=\"MimeMessage\"/> based on the configured properties.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public async Task CreateMessageAsync(CancellationToken cancellationToken = default) {\n        cancellationToken.ThrowIfCancellationRequested();\n        InlineAttachments ??= new List<AttachmentDescriptor>();\n        var message = new MimeMessage();\n        AddAddressesToMessage(message);\n        SetMessagePriority(message);\n        await BuildMessageBodyAsync(message, cancellationToken).ConfigureAwait(false);\n        message.Subject = Subject;\n        AddHeaders(message);\n        Message = message;\n    }\n\n    private void AddAddressesToMessage(MimeMessage message) {\n        var fromAddresses = From != null ? ConvertToMailboxAddress(From).ToList() : new List<MailboxAddress>();\n        if (fromAddresses.Any()) {\n            LoggingMessages.Logger.WriteVerbose(\"Adding from address to message: {0}\", fromAddresses.First());\n            message.From.Add(fromAddresses.First());\n        }\n\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        if (To != null && To.Any()) {\n            message.To.AddRange(ConvertToMailboxAddressesUnique(To, seen));\n        }\n\n        if (Cc != null && Cc.Any()) {\n            message.Cc.AddRange(ConvertToMailboxAddressesUnique(Cc, seen));\n        }\n\n        if (Bcc != null && Bcc.Any()) {\n            message.Bcc.AddRange(ConvertToMailboxAddressesUnique(Bcc, seen));\n        }\n\n        if (ReplyTo != null) {\n            var replyToAddresses = ConvertToMailboxAddress(ReplyTo).ToList();\n            if (replyToAddresses.Any()) {\n                message.ReplyTo.Add(replyToAddresses.First());\n            }\n        }\n    }\n\n    private void SetMessagePriority(MimeMessage message) {\n        LoggingMessages.Logger.WriteVerbose(\"Setting message priority to {0}\", Priority);\n        switch (Priority) {\n            case MessagePriority.High:\n                message.Priority = MimeKit.MessagePriority.Urgent;\n                break;\n            case MessagePriority.Low:\n                message.Priority = MimeKit.MessagePriority.NonUrgent;\n                break;\n            default:\n                message.Priority = MimeKit.MessagePriority.Normal;\n                break;\n        }\n    }\n\n    private async Task BuildMessageBodyAsync(MimeMessage message, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        var bodyBuilder = new BodyBuilder();\n        if (!string.IsNullOrWhiteSpace(HtmlBody)) {\n            bodyBuilder.HtmlBody = HtmlBody;\n        }\n        if (!string.IsNullOrWhiteSpace(TextBody)) {\n            bodyBuilder.TextBody = TextBody;\n        }\n        if (Attachments != null) {\n            var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            foreach (var descriptor in Attachments) {\n                if (descriptor == null) {\n                    continue;\n                }\n\n                var path = descriptor.SourcePath;\n                if (!string.IsNullOrWhiteSpace(path) && !seenPaths.Add(path!)) {\n                    continue;\n                }\n\n                if (descriptor is FileAttachmentDescriptor fileDescriptor && !File.Exists(fileDescriptor.FilePath)) {\n                    LoggingMessages.Logger.WriteWarning(\n                        $\"Send-EmailMessage - File not found: {fileDescriptor.FilePath}. Skipping attachment.\");\n                    continue;\n                }\n\n                bodyBuilder.Attachments.Add(descriptor.CreateMimeEntity(inline: false));\n            }\n        }\n        if (InlineAttachments != null) {\n            var seenInline = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            foreach (var descriptor in InlineAttachments) {\n                if (descriptor == null) {\n                    continue;\n                }\n\n                var path = descriptor.SourcePath;\n                if (!string.IsNullOrWhiteSpace(path) && !seenInline.Add(path!)) {\n                    continue;\n                }\n\n                if (descriptor is FileAttachmentDescriptor fileDescriptor && !File.Exists(fileDescriptor.FilePath)) {\n                    LoggingMessages.Logger.WriteWarning(\n                        $\"Send-EmailMessage - File not found: {fileDescriptor.FilePath}. Skipping inline attachment.\");\n                    continue;\n                }\n\n                var entity = descriptor.CreateMimeEntity(inline: true);\n                bodyBuilder.LinkedResources.Add(entity);\n\n                if (entity is MimePart inlinePart && string.IsNullOrWhiteSpace(inlinePart.ContentId)) {\n                    inlinePart.ContentId = MimeUtils.GenerateMessageId();\n                }\n            }\n        }\n        if (AutoEmbedRemoteImages && bodyBuilder.HtmlBody is { } htmlBody && !string.IsNullOrWhiteSpace(htmlBody)) {\n            var (html, images) = await HtmlUtils.DownloadRemoteImagesAsync(htmlBody, cancellationToken).ConfigureAwait(false);\n            cancellationToken.ThrowIfCancellationRequested();\n            bodyBuilder.HtmlBody = html;\n            HtmlBody = html;\n            foreach (var img in images) {\n                using var ms = new MemoryStream(img.Data);\n                var part = new MimePart(img.MediaType) {\n                    Content = new MimeContent(ms),\n                    FileName = img.ContentId,\n                    ContentId = img.ContentId,\n                    ContentDisposition = new ContentDisposition(ContentDisposition.Inline)\n                };\n                bodyBuilder.LinkedResources.Add(part);\n            }\n        }\n        cancellationToken.ThrowIfCancellationRequested();\n        message.Body = bodyBuilder.ToMessageBody();\n    }\n\n    private void AddHeaders(MimeMessage message) {\n        if (Headers == null) return;\n        foreach (var kvp in Headers) {\n            message.Headers.Add(kvp.Key, kvp.Value);\n        }\n    }\n\n    /// <summary>\n    /// Saves the constructed message to the specified file path.\n    /// </summary>\n    /// <param name=\"path\">Destination file path.</param>\n    public void SaveMessage(string path) {\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n        Message.WriteTo(path);\n    }\n\n    /// <summary>\n    /// Asynchronously saves the constructed message to the specified file path.\n    /// </summary>\n    /// <param name=\"path\">Destination file path.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public async Task SaveMessageAsync(string path, CancellationToken cancellationToken = default) {\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true);\n        await Message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    private IEnumerable<MailboxAddress> ConvertToMailboxAddressesUnique(IEnumerable<object>? inputs, HashSet<string> seen) {\n        if (inputs == null) yield break;\n        foreach (var input in inputs) {\n            foreach (var address in ConvertToMailboxAddress(input)) {\n                var lowered = address.Address.ToLowerInvariant();\n                if (seen.Add(lowered)) {\n                    yield return address;\n                }\n            }\n        }\n    }\n\n    private IEnumerable<MailboxAddress> ConvertToMailboxAddress(object input) {\n        switch (input) {\n            case string str:\n                foreach (var address in ConvertStringToMailboxAddresses(str)) {\n                    yield return address;\n                }\n                break;\n            case IDictionary dict:\n                foreach (var address in ConvertDictionaryToMailboxAddresses(dict)) {\n                    yield return address;\n                }\n                break;\n            case MailboxAddress mailbox:\n                yield return mailbox;\n                break;\n            case IEnumerable<object> list:\n                foreach (var address in ConvertListToMailboxAddresses(list)) {\n                    yield return address;\n                }\n                break;\n            default:\n                throw new ArgumentException($\"Invalid input type for ConvertToMailboxAddress: {input}\");\n        }\n    }\n\n    private IEnumerable<MailboxAddress> ConvertStringToMailboxAddresses(string value) {\n        MailboxAddress mailbox;\n        try {\n            mailbox = MailboxAddress.Parse(value);\n        } catch (Exception ex) {\n            LoggingMessages.Logger.WriteWarning($\"Failed to parse address '{value}': {ex.Message}\");\n            yield break;\n        }\n\n        if (value.Contains('<') || value.Contains('>')) {\n            yield return mailbox;\n        } else {\n            yield return new MailboxAddress(string.Empty, mailbox.Address);\n        }\n    }\n\n    private IEnumerable<MailboxAddress> ConvertDictionaryToMailboxAddresses(IDictionary dict) {\n        if (dict.Contains(\"Name\") && dict.Contains(\"Email\")) {\n            var name = Convert.ToString(dict[\"Name\"]) ?? string.Empty;\n            var email = Convert.ToString(dict[\"Email\"]);\n            if (!string.IsNullOrWhiteSpace(email)) {\n                yield return new MailboxAddress(name, email);\n            }\n        }\n    }\n\n    private IEnumerable<MailboxAddress> ConvertListToMailboxAddresses(IEnumerable<object> list) {\n        foreach (var item in list) {\n            foreach (var address in ConvertToMailboxAddress(item)) {\n                yield return address;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/NativeSentMailboxOperations.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Shared native-provider helpers for Sent-folder append and duplicate detection.\n/// </summary>\npublic static class NativeSentMailboxOperations {\n    /// <summary>\n    /// Default Graph folder used for Sent copy operations.\n    /// </summary>\n    public const string DefaultGraphSentFolder = \"Sent Items\";\n\n    /// <summary>\n    /// Default Gmail system Sent label id.\n    /// </summary>\n    public const string DefaultGmailSentLabelId = \"SENT\";\n\n    /// <summary>\n    /// Result of native Sent append operation.\n    /// </summary>\n    public sealed class NativeSentAppendResult {\n        /// <summary>True when append succeeded.</summary>\n        public bool Appended { get; set; }\n\n        /// <summary>Resolved folder/label display name.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Resolved provider message id when available.</summary>\n        public string? MessageId { get; set; }\n    }\n\n    /// <summary>\n    /// Result of native Sent duplicate probe.\n    /// </summary>\n    public sealed class NativeSentDuplicateProbeResult {\n        /// <summary>True when duplicate message was found.</summary>\n        public bool IsMatch { get; set; }\n\n        /// <summary>Resolved folder/label display name.</summary>\n        public string? Folder { get; set; }\n\n        /// <summary>Matched message-id token.</summary>\n        public string? MessageId { get; set; }\n\n        /// <summary>Non-match helper value.</summary>\n        public static NativeSentDuplicateProbeResult None { get; } = new();\n    }\n\n    /// <summary>\n    /// Resolves Graph Sent folder name using request override, account override, and fallback.\n    /// </summary>\n    public static string ResolveGraphSentFolderName(\n        string? requestedSentFolder,\n        string? configuredSentFolder,\n        string fallbackFolder = DefaultGraphSentFolder) {\n        return ResolveFolderName(requestedSentFolder, configuredSentFolder, fallbackFolder);\n    }\n\n    /// <summary>\n    /// Resolves Graph Sent folder selector value accepted by Graph APIs.\n    /// </summary>\n    public static string ResolveGraphSentFolderSelector(\n        string? requestedSentFolder,\n        string? configuredSentFolder,\n        string fallbackFolder = DefaultGraphSentFolder) {\n        var folderName = ResolveGraphSentFolderName(requestedSentFolder, configuredSentFolder, fallbackFolder);\n        return GraphMailboxBrowser.ResolveFolderSelector(folderName);\n    }\n\n    /// <summary>\n    /// Resolves Gmail Sent label id for native Sent copy operations.\n    /// </summary>\n    public static string ResolveGmailSentLabelId(\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null) {\n        _ = requestedSentFolder;\n        _ = configuredSentFolder;\n        return DefaultGmailSentLabelId;\n    }\n\n    /// <summary>\n    /// Resolves Gmail Sent folder display value used for caller metadata.\n    /// </summary>\n    public static string ResolveGmailSentFolderName(\n        string? requestedSentFolder,\n        string? configuredSentFolder,\n        string fallbackFolder = DefaultGmailSentLabelId) {\n        return ResolveFolderName(requestedSentFolder, configuredSentFolder, fallbackFolder);\n    }\n\n    /// <summary>\n    /// Imports a MIME message into Graph Sent folder.\n    /// </summary>\n    public static async Task<NativeSentAppendResult> AppendToGraphSentAsync(\n        GraphMailboxBrowser browser,\n        MimeMessage message,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        int maxInlineAttachmentBytes = GraphMimePreparation.DefaultMaxInlineAttachmentBytes,\n        string? idempotencyHeaderName = null,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n        if (maxInlineAttachmentBytes < 0) {\n            throw new ArgumentOutOfRangeException(nameof(maxInlineAttachmentBytes), \"maxInlineAttachmentBytes must be zero or greater.\");\n        }\n\n        var folder = ResolveGraphSentFolderName(requestedSentFolder, configuredSentFolder);\n        var imported = await browser.ImportMessageAsync(\n            message,\n            folder: folder,\n            maxInlineAttachmentBytes: maxInlineAttachmentBytes,\n            idempotencyHeaderName: idempotencyHeaderName,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new NativeSentAppendResult {\n            Appended = true,\n            Folder = folder,\n            MessageId = NormalizeOptional(imported.MessageId)\n        };\n    }\n\n    /// <summary>\n    /// Imports a MIME message into Gmail Sent label.\n    /// </summary>\n    public static async Task<NativeSentAppendResult> AppendToGmailSentAsync(\n        GmailMailboxBrowser browser,\n        MimeMessage message,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n        if (message == null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var labelId = ResolveGmailSentLabelId(requestedSentFolder, configuredSentFolder);\n        _ = await browser.ImportMessageAsync(\n            message,\n            labelId: labelId,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new NativeSentAppendResult {\n            Appended = true,\n            Folder = ResolveGmailSentFolderName(requestedSentFolder, configuredSentFolder),\n            MessageId = NormalizeMessageIdToken(message.MessageId)\n        };\n    }\n\n    /// <summary>\n    /// Probes Graph Sent folder for duplicate RFC822 message-id.\n    /// </summary>\n    public static async Task<NativeSentDuplicateProbeResult> FindGraphSentDuplicateAsync(\n        GraphMailboxBrowser browser,\n        string messageIdToken,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n\n        var normalizedToken = NormalizeMessageIdToken(messageIdToken);\n        if (normalizedToken == null) {\n            throw new ArgumentException(\"messageIdToken is required.\", nameof(messageIdToken));\n        }\n\n        var folder = ResolveGraphSentFolderName(requestedSentFolder, configuredSentFolder);\n        var probe = await browser.FindMessageByInternetMessageIdAsync(\n            normalizedToken,\n            folder: folder,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        if (!probe.IsMatch) {\n            return NativeSentDuplicateProbeResult.None;\n        }\n\n        return new NativeSentDuplicateProbeResult {\n            IsMatch = true,\n            Folder = folder,\n            MessageId = NormalizeMessageIdToken(probe.MessageId) ?? normalizedToken\n        };\n    }\n\n    /// <summary>\n    /// Probes Gmail Sent label for duplicate RFC822 message-id.\n    /// </summary>\n    public static async Task<NativeSentDuplicateProbeResult> FindGmailSentDuplicateAsync(\n        GmailMailboxBrowser browser,\n        string messageIdToken,\n        string? requestedSentFolder = null,\n        string? configuredSentFolder = null,\n        CancellationToken cancellationToken = default) {\n        if (browser == null) {\n            throw new ArgumentNullException(nameof(browser));\n        }\n\n        var normalizedToken = NormalizeMessageIdToken(messageIdToken);\n        if (normalizedToken == null) {\n            throw new ArgumentException(\"messageIdToken is required.\", nameof(messageIdToken));\n        }\n\n        var labelId = ResolveGmailSentLabelId(requestedSentFolder, configuredSentFolder);\n        var probe = await browser.FindSentMessageByRfc822MessageIdAsync(\n            normalizedToken,\n            sentLabelId: labelId,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        if (!probe.IsMatch) {\n            return NativeSentDuplicateProbeResult.None;\n        }\n\n        return new NativeSentDuplicateProbeResult {\n            IsMatch = true,\n            Folder = ResolveGmailSentFolderName(requestedSentFolder, configuredSentFolder),\n            MessageId = NormalizeMessageIdToken(probe.MessageId) ?? normalizedToken\n        };\n    }\n\n    private static string ResolveFolderName(string? requestedFolder, string? configuredFolder, string fallbackFolder) {\n        var requested = NormalizeOptional(requestedFolder);\n        if (requested != null) {\n            return requested;\n        }\n\n        var configured = NormalizeOptional(configuredFolder);\n        if (configured != null) {\n            return configured;\n        }\n\n        var fallback = NormalizeOptional(fallbackFolder);\n        return fallback ?? DefaultGraphSentFolder;\n    }\n\n    private static string? NormalizeOptional(string? value) {\n        var trimmed = (value ?? string.Empty).Trim();\n        return trimmed.Length == 0 ? null : trimmed;\n    }\n\n    private static string? NormalizeMessageIdToken(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var token = value!.Trim();\n        if (token.StartsWith(\"<\", StringComparison.Ordinal)) {\n            token = token.Substring(1);\n        }\n        if (token.EndsWith(\">\", StringComparison.Ordinal)) {\n            token = token.Substring(0, token.Length - 1);\n        }\n\n        token = token.Trim();\n        return token.Length == 0 ? null : token;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/Smtp.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Globalization;\nusing System.Net;\nusing System.Net.Security;\nusing System.Runtime.InteropServices;\nusing System.Security;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing Org.BouncyCastle.Bcpg.OpenPgp;\nusing System.Threading.Tasks;\nusing System.Threading;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// High level wrapper around <see cref=\"ClientSmtp\"/> that exposes convenient methods and retry logic.\n/// </summary>\n/// <remarks>\n/// Provides connection pooling and supports authentication using\n/// multiple mechanisms depending on server capabilities.\n/// <para>Instances are not thread-safe for concurrent operations. Calls to\n/// <see cref=\"Send\"/> or <see cref=\"SendAsync(System.Threading.CancellationToken)\"/>\n/// are serialized so that only one send executes at a time per instance.</para>\n/// </remarks>\npublic class Smtp {\n    private static ClientSmtp CreateDefaultClient(ProtocolLogger? logger) => logger == null ? new ClientSmtp() : new ClientSmtp(logger);\n\n    /// <summary>Factory used to create <see cref=\"ClientSmtp\"/> instances.</summary>\n    public static Func<ProtocolLogger?, ClientSmtp> ClientFactory { get; set; } = CreateDefaultClient;\n\n    /// <summary>Restores the default SMTP client factory.</summary>\n    public static void ResetClientFactory() => ClientFactory = CreateDefaultClient;\n    /// <summary>Configuration used for protocol logging.</summary>\n    public LoggingConfigurator? Logging;\n\n    /// <summary>LogCollector for capturing logs from async operations.</summary>\n    public LogCollector? LogCollector { get; set; }\n\n    /// <summary>\n    /// When set, sending is simulated and no network or repository side-effects are performed.\n    /// </summary>\n    public bool DryRun { get; set; }\n\n    /// <summary>Repository used to persist sent message metadata.</summary>\n    public ISentMessageRepository? SentMessageRepository { get; set; }\n    /// <summary>Repository used to persist pending messages for later retry.</summary>\n    public IPendingMessageRepository? PendingMessageRepository { get; set; }\n\n    private const string ProviderDataSecureSocketOptionsKey = \"SecureSocketOptions\";\n    private const string ProviderDataUseSslKey = \"UseSsl\";\n    private const string ProviderDataSkipCertificateValidationKey = \"SkipCertificateValidation\";\n    private const string ProviderDataCheckCertificateRevocationKey = \"CheckCertificateRevocation\";\n    private const string ProviderDataTimeoutKey = \"TimeoutMilliseconds\";\n\n    private string? _pendingMessagesPath;\n    /// <summary>Directory path for storing pending messages.</summary>\n    public string? PendingMessagesPath {\n        get => _pendingMessagesPath;\n        set {\n            if (string.IsNullOrWhiteSpace(value)) {\n                _pendingMessagesPath = value;\n                return;\n            }\n            if (!string.Equals(_pendingMessagesPath, value, StringComparison.OrdinalIgnoreCase)) {\n                var options = new PendingMessageRepositoryOptions { DirectoryPath = value! };\n                PendingMessageRepository = new FilePendingMessageRepository(options);\n            }\n            _pendingMessagesPath = value;\n        }\n    }\n\n    /// <summary>Underlying SMTP client used to send messages.</summary>\n    public ClientSmtp Client { get; private set; }\n\n    private SecureSocketOptions _activeSecureSocketOptions = SecureSocketOptions.Auto;\n    private bool _activeUseSsl;\n\n    /// <summary>Credentials used during authentication.</summary>\n    public NetworkCredential? Credential { get; private set; }\n\n    /// <summary>\n    /// Effective secure socket options used by the active or most recent connection attempt.\n    /// </summary>\n    public SecureSocketOptions ActiveSecureSocketOptions => _activeSecureSocketOptions;\n\n    /// <summary>\n    /// Optional identity hint used to isolate SMTP connection pooling by credentials.\n    /// </summary>\n    public string? ConnectionPoolIdentity { get; set; }\n\n    /// <summary>\n    /// Optional per-instance override controlling whether this SMTP session should\n    /// use the shared connection pool.\n    /// </summary>\n    public bool? UseConnectionPool { get; set; }\n\n    /// <summary>Subject of the message.</summary>\n    public string Subject {\n        get => Client.Subject;\n        set => Client.Subject = value;\n    }\n\n    /// <summary>HTML body of the message.</summary>\n    public string HtmlBody {\n        get => Client.HtmlBody;\n        set => Client.HtmlBody = value;\n    }\n\n    /// <summary>Plain text body of the message.</summary>\n    public string TextBody {\n        get => Client.TextBody;\n        set => Client.TextBody = value;\n    }\n\n    /// <summary>Attachments to include with the message.</summary>\n    public List<AttachmentDescriptor>? Attachments {\n        get => Client.Attachments;\n        set => Client.Attachments = value;\n    }\n\n    /// <summary>Inline attachments to embed in the message.</summary>\n    public List<AttachmentDescriptor>? InlineAttachments {\n        get => Client.InlineAttachments;\n        set => Client.InlineAttachments = value;\n    }\n\n    /// <summary>Custom headers to add to the message.</summary>\n    public IDictionary<string, string>? Headers {\n        get => Client.Headers;\n        set => Client.Headers = value;\n    }\n\n    /// <summary>The sender address.</summary>\n    public object? From {\n        get => Client.From;\n        set => Client.From = value;\n    }\n\n    /// <summary>Primary recipients.</summary>\n    public IEnumerable<object>? To {\n        get => Client.To;\n        set => Client.To = value;\n    }\n\n    /// <summary>Carbon copy recipients.</summary>\n    public IEnumerable<object>? Cc {\n        get => Client.Cc;\n        set => Client.Cc = value;\n    }\n\n    /// <summary>Blind carbon copy recipients.</summary>\n    public IEnumerable<object>? Bcc {\n        get => Client.Bcc;\n        set => Client.Bcc = value;\n    }\n\n    /// <summary>Reply-to address.</summary>\n    public object? ReplyTo {\n        get => Client.ReplyTo;\n        set => Client.ReplyTo = value;\n    }\n\n    /// <summary>The underlying MIME message.</summary>\n    public MimeMessage Message {\n        get => Client.Message;\n        set => Client.Message = value;\n    }\n\n    /// <summary>Priority of the message.</summary>\n    public MessagePriority Priority {\n        get => Client.Priority;\n        set => Client.Priority = value;\n    }\n\n    /// <summary>Delivery notification options.</summary>\n    public DeliveryNotification[]? DeliveryNotificationOption {\n        get => Client.DeliveryNotificationOption;\n        set {\n            if (value != null) {\n                Client.DeliveryNotificationOption = value;\n            }\n        }\n    }\n\n    /// <summary>Timeout for SMTP operations in milliseconds.</summary>\n    public int Timeout {\n        get => Client.Timeout;\n        set => Client.Timeout = value;\n    }\n\n    /// <summary>Number of retry attempts on failure.</summary>\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>Base delay in milliseconds between retries.</summary>\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>Exponential backoff multiplier for retries.</summary>\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>Maximum delay in milliseconds between retries. 0 disables capping.</summary>\n    public int MaxDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>Jitter window in milliseconds added to retry delay. 0 disables jitter.</summary>\n    public int JitterMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// When set to <see langword=\"true\"/>, replaces local image references in\n    /// <see cref=\"HtmlBody\"/> with inline attachments.\n    /// </summary>\n    public bool AutoEmbedImages { get; set; } = false;\n\n    /// <summary>\n    /// When set to <see langword=\"true\"/>, automatically builds the MIME message\n    /// from the configured properties before sending when the message is empty.\n    /// </summary>\n    public bool AutoCreateMessage { get; set; } = false;\n\n    /// <summary>\n    /// When set to <see langword=\"true\"/>, downloads remote images referenced in\n    /// <see cref=\"HtmlBody\"/> and embeds them as inline attachments.\n    /// </summary>\n    public bool AutoEmbedRemoteImages {\n        get => Client.AutoEmbedRemoteImages;\n        set => Client.AutoEmbedRemoteImages = value;\n    }\n\n    /// <summary>\n    /// Forces retries even when the encountered error is not considered\n    /// transient. By default retries occur only for transient failures.\n    /// </summary>\n    public bool RetryAlways { get; set; } = false;\n\n    /// <summary>Webhook invoked after sending.</summary>\n    public string? WebhookUrl { get; set; }\n\n    /// <summary>Validate the server certificate against revocation lists.</summary>\n    public bool CheckCertificateRevocation {\n        get => Client.CheckCertificateRevocation;\n        set => Client.CheckCertificateRevocation = value;\n    }\n\n    private bool _skipCertificateValidation;\n    private readonly SemaphoreSlim _sendLock = new(1, 1);\n    private string? _poolIdentity;\n\n    private bool IsConnectionPoolingEnabled => UseConnectionPool ?? SmtpConnectionPool.PoolingEnabled;\n    /// <summary>Skip server certificate validation.</summary>\n    public bool SkipCertificateValidation {\n        get => _skipCertificateValidation;\n        set {\n            _skipCertificateValidation = value;\n            if (value) {\n                Client.ServerCertificateValidationCallback = (s, c, h, e) => true;\n            } else {\n                Client.ServerCertificateValidationCallback = null;\n            }\n        }\n    }\n\n    /// <summary>Domain name to use in the SMTP HELO.</summary>\n    public string LocalDomain {\n        get => Client.LocalDomain ?? string.Empty;\n        set {\n            if (value != \"\") Client.LocalDomain = value;\n        }\n    }\n\n    /// <summary>Delivery status notification type to request.</summary>\n    public DeliveryStatusNotificationType? DeliveryStatusNotificationType {\n        get => Client.DeliveryStatusNotificationType;\n        set {\n            if (value != null) {\n                Client.DeliveryStatusNotificationType = value.Value;\n            }\n        }\n    }\n\n    /// <summary>Custom certificate validation callback.</summary>\n    public RemoteCertificateValidationCallback? ServerCertificateValidationCallback {\n        get => Client.ServerCertificateValidationCallback;\n        set => Client.ServerCertificateValidationCallback = value;\n    }\n\n    /// <summary>Port used to connect to the SMTP server.</summary>\n    public int Port { get; private set; } = 25;\n\n    /// <summary>SMTP server host name.</summary>\n    public string Server { get; private set; } = String.Empty;\n\n    /// <summary>Action to take when an error occurs.</summary>\n    public ActionPreference? ErrorAction { get; set; }\n\n    /// <summary>Comma separated list of all recipients.</summary>\n    public string SentTo => Client.SentTo;\n    /// <summary>Normalized address the message is sent from.</summary>\n    public string SentFrom => Helpers.GetEmailAddress(From ?? string.Empty);\n\n    /// <summary>Stopwatch measuring the time of operations.</summary>\n    public readonly Stopwatch Stopwatch;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Smtp\"/> class, with optional logging configuration.\n    /// </summary>\n    /// <param name=\"logging\">The logging.</param>\n    public Smtp(LoggingConfigurator? logging = null) {\n        Stopwatch = Stopwatch.StartNew();\n        Logging = logging;\n        Client = ClientFactory(logging?.ProtocolLogger);\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Smtp\"/> class with logging configuration.\n    /// </summary>\n    /// <param name=\"logPath\">The log path.</param>\n    /// <param name=\"logConsole\">if set to <c>true</c> [log console].</param>\n    /// <param name=\"logObject\">if set to <c>true</c> [log object].</param>\n    /// <param name=\"logTimestamps\">if set to <c>true</c> [log timestamps].</param>\n    /// <param name=\"logSecrets\">if set to <c>true</c> [log secrets].</param>\n    /// <param name=\"logTimestampsFormat\">The log timestamps format.</param>\n    /// <param name=\"logServerPrefix\">The log server prefix.</param>\n    /// <param name=\"logClientPrefix\">The log client prefix.</param>\n    /// <param name=\"logOverwrite\">if set to <c>true</c> [log overwrite].</param>\n    public Smtp(string logPath, bool logConsole, bool logObject, bool logTimestamps, bool logSecrets,\n        string? logTimestampsFormat = null, string? logServerPrefix = null, string? logClientPrefix = null,\n        bool logOverwrite = false) {\n\n        LogVerbose($\"Send-EmailMessage - Logging configuration: Path: {logPath}, Console: {logConsole}, Object: {logObject}, Timestamps: {logTimestamps}, Secrets: {logSecrets}, TimestampsFormat: {logTimestampsFormat}, ServerPrefix: {logServerPrefix}, ClientPrefix: {logClientPrefix}, Overwrite: {logOverwrite}\");\n        Logging = new LoggingConfigurator();\n        Logging.ConfigureLogging(logPath, logConsole, logObject, logTimestamps, logSecrets, logTimestampsFormat, logServerPrefix, logClientPrefix, logOverwrite);\n        Client = ClientFactory(Logging.ProtocolLogger);\n        Stopwatch = Stopwatch.StartNew();\n    }\n\n\n    /// <summary>\n    /// Connects to the specified SMTP server and returns detailed information about the connection.\n    /// </summary>\n    /// <param name=\"server\">SMTP server name.</param>\n    /// <param name=\"port\">Port number.</param>\n    /// <param name=\"secureSocketOptions\">Controls SSL/TLS usage.</param>\n    /// <param name=\"useSsl\">Compatibility flag overriding <paramref name=\"secureSocketOptions\"/> when set.</param>\n    public static SmtpConnectionInfo TestConnection(string server, int port, SecureSocketOptions secureSocketOptions = SecureSocketOptions.Auto, bool useSsl = false)\n    {\n        var logging = new LoggingConfigurator();\n        logging.ConfigureLogging(null, false, true, false, false);\n\n        var smtp = new Smtp(logging);\n        _ = smtp.Connect(server, port, secureSocketOptions, useSsl);\n\n        string? banner = null;\n        string? software = null;\n\n        if (logging.LogStream != null)\n        {\n            logging.LogStream.Position = 0;\n            using var reader = new StreamReader(logging.LogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);\n            var line = reader.ReadLine();\n            if (!string.IsNullOrWhiteSpace(line))\n            {\n                var trimmed = line.Trim();\n                if (trimmed.StartsWith(\"<--\", StringComparison.Ordinal))\n                {\n                    trimmed = trimmed.Substring(3).Trim();\n                }\n                banner = trimmed;\n                var parts = trimmed.Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);\n                if (parts.Length >= 3)\n                {\n                    software = parts[2];\n                }\n            }\n        }\n\n        var persistent = false;\n        try\n        {\n            smtp.Client.NoOp();\n            persistent = smtp.Client.IsConnected;\n        }\n        catch\n        {\n            persistent = false;\n        }\n\n        var info = new SmtpConnectionInfo(server, port, banner, software, smtp.Client.GetCapabilitiesSnapshot(), persistent);\n        smtp.Disconnect();\n        smtp.Dispose();\n        return info;\n    }\n\n    /// <summary>\n    /// Creates the MIME message using the current property values.\n    /// </summary>\n    public void CreateMessage(CancellationToken cancellationToken = default) {\n        cancellationToken.ThrowIfCancellationRequested();\n        PrepareInlineAttachments();\n        Client.CreateMessage(cancellationToken);\n    }\n\n    /// <summary>\n    /// Asynchronously creates the MIME message using the current property values.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public async Task CreateMessageAsync(CancellationToken cancellationToken = default) {\n        cancellationToken.ThrowIfCancellationRequested();\n        PrepareInlineAttachments();\n        await Client.CreateMessageAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <summary>\n    /// Saves the constructed message to the specified path.\n    /// </summary>\n    /// <param name=\"path\">Destination file path.</param>\n    public void SaveMessage(string path) {\n        if (!string.IsNullOrWhiteSpace(path)) {\n            Client.SaveMessage(path);\n        }\n    }\n\n    /// <summary>\n    /// Asynchronously saves the constructed message to the specified path.\n    /// </summary>\n    /// <param name=\"path\">Destination file path.</param>\n    /// <param name=\"cancellationToken\">Token used to cancel the operation.</param>\n    public Task SaveMessageAsync(string path, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            return Task.CompletedTask;\n        }\n\n        return Client.SaveMessageAsync(path, cancellationToken);\n    }\n\n    private void PrepareInlineAttachments() {\n        if (!AutoEmbedImages) {\n            return;\n        }\n\n        var (html, paths) = HtmlUtils.ExtractLocalImagePaths(HtmlBody);\n        HtmlBody = html;\n        if (paths.Count <= 0) {\n            return;\n        }\n\n        InlineAttachments ??= new List<AttachmentDescriptor>();\n        foreach (var path in paths) {\n            if (InlineAttachments.Any(d => string.Equals(d.SourcePath, path, StringComparison.OrdinalIgnoreCase))) {\n                continue;\n            }\n\n            InlineAttachments.Add(new FileAttachmentDescriptor(path));\n        }\n    }\n\n    /// <summary>\n    /// Connect to the SMTP server using the provided server and port.\n    /// </summary>\n    /// <param name=\"server\"></param>\n    /// <param name=\"port\"></param>\n    /// <param name=\"secureSocketOptions\">Options controlling SSL/TLS usage. If left\n    /// as <see cref=\"SecureSocketOptions.Auto\"/> and <paramref name=\"useSsl\"/> is\n    /// <c>true</c>, <see cref=\"SecureSocketOptions.StartTls\"/> will be used.</param>\n    /// <param name=\"useSsl\">Compatibility switch. Overrides\n    /// <paramref name=\"secureSocketOptions\"/> only when set to <c>true</c> and the\n    /// option is left as <see cref=\"SecureSocketOptions.Auto\"/>.</param>\n    /// <returns></returns>\n    public SmtpResult Connect(string server, int port, SecureSocketOptions secureSocketOptions = SecureSocketOptions.Auto, bool useSsl = false) {\n        var oldServer = Server;\n        var oldPort = Port;\n        var oldPoolIdentity = _poolIdentity ?? GetConnectionPoolIdentity();\n        Server = server;\n        Port = port;\n        if (!SmtpValidation.TryValidateServer(server, port, out var validationError))\n        {\n            string message = validationError ?? \"Invalid SMTP server settings.\";\n            LogWarning($\"Send-EmailMessage - {message}\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidOperationException(message);\n            }\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, server ?? string.Empty, port, Stopwatch.Elapsed, \"\", message);\n        }\n        var effectiveOptions = secureSocketOptions;\n        if (useSsl && effectiveOptions == SecureSocketOptions.Auto) {\n            // Maintain backwards compatibility with Send-MailMessage by\n            // defaulting to StartTls when the UseSsl flag is supplied and\n            // no explicit option was provided.\n            effectiveOptions = SecureSocketOptions.StartTls;\n        }\n        _activeUseSsl = useSsl;\n        _activeSecureSocketOptions = effectiveOptions;\n        if (DryRun) {\n            LogVerbose($\"Send-EmailMessage - DryRun enabled, skipping connect to {server} on port {port} using SSL: {effectiveOptions}\");\n            return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"Connection skipped (WhatIf)\");\n        }\n        if (Client.IsConnected)\n        {\n            if (IsConnectionPoolingEnabled)\n            {\n                SmtpConnectionPool.ReturnClient(oldServer, oldPort, Client, oldPoolIdentity, IsConnectionPoolingEnabled);\n            }\n            else\n            {\n                Client.Disconnect(true);\n            }\n            Client = ClientFactory(Logging?.ProtocolLogger);\n        }\n\n        var poolIdentity = GetConnectionPoolIdentity();\n        var pooled = SmtpConnectionPool.TryRentClient(server, port, poolIdentity, IsConnectionPoolingEnabled);\n        if (pooled != null)\n        {\n            Client = pooled;\n        }\n        try {\n            if (!Client.IsConnected)\n            {\n                Client.Connect(server, port, effectiveOptions);\n            }\n            _poolIdentity = poolIdentity;\n            LogVerbose($\"Connected to {server} on {port} port using SSL: {effectiveOptions}\");\n            return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"\");\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error during connect: {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: Port? ({port} was used), Using SSL? ({effectiveOptions}, was used). You can also try 'SkipCertificateValidation' or 'SkipCertificateRevocation'.\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Asynchronously connect to the SMTP server using the provided server and port.\n    /// </summary>\n    /// <param name=\"server\"></param>\n    /// <param name=\"port\"></param>\n    /// <param name=\"secureSocketOptions\">Options controlling SSL/TLS usage. If left\n    /// as <see cref=\"SecureSocketOptions.Auto\"/> and <paramref name=\"useSsl\"/> is\n    /// <c>true</c>, <see cref=\"SecureSocketOptions.StartTls\"/> will be used.</param>\n    /// <param name=\"useSsl\">Compatibility switch. Overrides\n    /// <paramref name=\"secureSocketOptions\"/> only when set to <c>true</c> and the\n    /// option is left as <see cref=\"SecureSocketOptions.Auto\"/>.</param>\n    /// <returns></returns>\n    public Task<SmtpResult> ConnectAsync(\n        string server,\n        int port,\n        SecureSocketOptions secureSocketOptions = SecureSocketOptions.Auto,\n        bool useSsl = false) {\n        return ConnectAsync(server, port, secureSocketOptions, useSsl, CancellationToken.None);\n    }\n\n    /// <summary>\n    /// Asynchronously connect to the SMTP server using the provided server and port.\n    /// </summary>\n    /// <param name=\"server\"></param>\n    /// <param name=\"port\"></param>\n    /// <param name=\"secureSocketOptions\">Options controlling SSL/TLS usage. If left\n    /// as <see cref=\"SecureSocketOptions.Auto\"/> and <paramref name=\"useSsl\"/> is\n    /// <c>true</c>, <see cref=\"SecureSocketOptions.StartTls\"/> will be used.</param>\n    /// <param name=\"useSsl\">Compatibility switch. Overrides\n    /// <paramref name=\"secureSocketOptions\"/> only when set to <c>true</c> and the\n    /// option is left as <see cref=\"SecureSocketOptions.Auto\"/>.</param>\n    /// <param name=\"cancellationToken\">Cancellation token for the connect operation.</param>\n    /// <returns></returns>\n    public async Task<SmtpResult> ConnectAsync(\n        string server,\n        int port,\n        SecureSocketOptions secureSocketOptions,\n        bool useSsl,\n        CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        var oldServer = Server;\n        var oldPort = Port;\n        var oldPoolIdentity = _poolIdentity ?? GetConnectionPoolIdentity();\n        Server = server;\n        Port = port;\n        if (!SmtpValidation.TryValidateServer(server, port, out var validationError))\n        {\n            string message = validationError ?? \"Invalid SMTP server settings.\";\n            LogWarning($\"Send-EmailMessage - {message}\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidOperationException(message);\n            }\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, server ?? string.Empty, port, Stopwatch.Elapsed, \"\", message);\n        }\n        var effectiveOptions = secureSocketOptions;\n        if (useSsl && effectiveOptions == SecureSocketOptions.Auto) {\n            // Maintain backwards compatibility with Send-MailMessage by\n            // defaulting to StartTls when the UseSsl flag is supplied and\n            // no explicit option was provided.\n            effectiveOptions = SecureSocketOptions.StartTls;\n        }\n        _activeUseSsl = useSsl;\n        _activeSecureSocketOptions = effectiveOptions;\n        if (DryRun) {\n            LogVerbose($\"Send-EmailMessage - DryRun enabled, skipping connect to {server} on port {port} using SSL: {effectiveOptions}\");\n            return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"Connection skipped (WhatIf)\");\n        }\n        if (Client.IsConnected)\n        {\n            if (IsConnectionPoolingEnabled)\n            {\n                SmtpConnectionPool.ReturnClient(oldServer, oldPort, Client, oldPoolIdentity, IsConnectionPoolingEnabled);\n            }\n            else\n            {\n                Client.Disconnect(true);\n            }\n            Client = ClientFactory(Logging?.ProtocolLogger);\n        }\n\n        var poolIdentity = GetConnectionPoolIdentity();\n        var pooled = SmtpConnectionPool.TryRentClient(server, port, poolIdentity, IsConnectionPoolingEnabled);\n        if (pooled != null)\n        {\n            Client = pooled;\n        }\n        try {\n            if (!Client.IsConnected)\n            {\n                await Client.ConnectAsync(server, port, effectiveOptions, cancellationToken).ConfigureAwait(false);\n            }\n            _poolIdentity = poolIdentity;\n            LogVerbose($\"Connected to {server} on {port} port using SSL: {effectiveOptions}\");\n            return new SmtpResult(true, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"\");\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error during connect: {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: Port? ({port} was used), Using SSL? ({effectiveOptions}, was used). You can also try 'SkipCertificateValidation' or 'SkipCertificateRevocation'.\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Connect, SentTo, SentFrom, server, port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Connects and authenticates using the provided user name/secret in one step.\n    /// </summary>\n    /// <param name=\"server\">SMTP server hostname.</param>\n    /// <param name=\"port\">SMTP server port.</param>\n    /// <param name=\"userName\">SMTP user name.</param>\n    /// <param name=\"secret\">SMTP password or OAuth token.</param>\n    /// <param name=\"secureSocketOptions\">TLS/SSL options.</param>\n    /// <param name=\"useSsl\">Compatibility SSL switch.</param>\n    /// <param name=\"authMode\">Authentication mode.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Combined connect/auth outcome.</returns>\n    public async Task<SmtpConnectAuthenticateResult> ConnectAndAuthenticateAsync(\n        string server,\n        int port,\n        string userName,\n        string secret,\n        SecureSocketOptions secureSocketOptions = SecureSocketOptions.Auto,\n        bool useSsl = false,\n        ProtocolAuthMode authMode = ProtocolAuthMode.Basic,\n        CancellationToken cancellationToken = default) {\n        cancellationToken.ThrowIfCancellationRequested();\n\n        var normalizedUserName = userName?.Trim() ?? string.Empty;\n        var previousConnectionPoolIdentity = ConnectionPoolIdentity;\n        var shouldOverrideConnectionPoolIdentity =\n            string.IsNullOrWhiteSpace(previousConnectionPoolIdentity) &&\n            !string.IsNullOrWhiteSpace(normalizedUserName);\n\n        if (shouldOverrideConnectionPoolIdentity) {\n            ConnectionPoolIdentity = normalizedUserName;\n        }\n\n        SmtpResult connectResult;\n        try {\n            connectResult = await ConnectAsync(server, port, secureSocketOptions, useSsl, cancellationToken).ConfigureAwait(false);\n        } finally {\n            if (shouldOverrideConnectionPoolIdentity) {\n                ConnectionPoolIdentity = previousConnectionPoolIdentity;\n            }\n        }\n\n        if (!connectResult.Status) {\n            return new SmtpConnectAuthenticateResult {\n                IsSuccess = false,\n                SecureSocketOptions = ActiveSecureSocketOptions,\n                ErrorCode = \"connect_failed\",\n                Error = connectResult.Error ?? \"Connect failed.\",\n                IsTransient = SmtpValidation.TryValidateServer(server, port, out _)\n            };\n        }\n\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping authentication.\");\n            Credential = new NetworkCredential(normalizedUserName, secret ?? string.Empty);\n            return new SmtpConnectAuthenticateResult {\n                IsSuccess = true,\n                SecureSocketOptions = ActiveSecureSocketOptions\n            };\n        }\n\n        try {\n            await ProtocolAuth.AuthenticateSmtpAsync(Client, normalizedUserName, secret, authMode, cancellationToken).ConfigureAwait(false);\n            Credential = new NetworkCredential(normalizedUserName, secret ?? string.Empty);\n            return new SmtpConnectAuthenticateResult {\n                IsSuccess = true,\n                SecureSocketOptions = ActiveSecureSocketOptions\n            };\n        } catch (OperationCanceledException) {\n            throw;\n        } catch (Exception ex) {\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpConnectAuthenticateResult {\n                IsSuccess = false,\n                SecureSocketOptions = ActiveSecureSocketOptions,\n                ErrorCode = \"auth_failed\",\n                Error = ex.Message,\n                IsTransient = false\n            };\n        }\n    }\n\n    /// <summary>\n    /// Authenticate using the provided credentials.\n    /// </summary>\n    /// <param name=\"Credentials\"></param>\n    /// <param name=\"isOAuth\"></param>\n    /// <returns></returns>\n    public SmtpResult Authenticate(ICredentials Credentials, bool isOAuth = false) {\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping authentication.\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"Authentication skipped (WhatIf)\");\n        }\n        if (Credentials is NetworkCredential networkCredential)\n        {\n            if (!SmtpValidation.TryValidateCredentials(networkCredential.UserName, networkCredential.Password, out var validationError))\n            {\n                string message = validationError ?? \"Invalid SMTP credentials.\";\n                LogWarning($\"Send-EmailMessage - {message}\");\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw new InvalidOperationException(message);\n                }\n                return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", message);\n            }\n        }\n        try {\n            if (isOAuth) {\n                var oauthCredential = Credentials as NetworkCredential;\n                if (oauthCredential != null) {\n                    Credential = oauthCredential;\n                    var (userName, token) = Helpers.ConvertFromOAuth2Credential(oauthCredential);\n                    var oauth2 = new SaslMechanismOAuth2(userName, token);\n                    Client.Authenticate(oauth2);\n                }\n                LogVerbose($\"Send-EmailMessage - Authenticated using oAuth\");\n            } else {\n                Credential = Credentials as NetworkCredential;\n                Client.Authenticate(Credentials);\n            }\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error during authentication (oAuth): {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: OAuth? ({isOAuth} was used), ICredentials? ({Credentials}, was used).\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Asynchronously authenticate using the provided credentials.\n    /// </summary>\n    /// <param name=\"Credentials\"></param>\n    /// <param name=\"isOAuth\"></param>\n    /// <returns></returns>\n    public async Task<SmtpResult> AuthenticateAsync(ICredentials Credentials, bool isOAuth = false) {\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping authentication.\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"Authentication skipped (WhatIf)\");\n        }\n        try {\n            if (isOAuth) {\n                var networkCredential = Credentials as NetworkCredential;\n                if (networkCredential != null) {\n                    Credential = networkCredential;\n                    var (userName, token) = Helpers.ConvertFromOAuth2Credential(networkCredential);\n                    var oauth2 = new SaslMechanismOAuth2(userName, token);\n                    await Client.AuthenticateAsync(oauth2);\n                }\n                LogVerbose($\"Send-EmailMessage - Authenticated using oAuth\");\n            } else {\n                Credential = Credentials as NetworkCredential;\n                await Client.AuthenticateAsync(Credentials);\n            }\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error during authentication (oAuth): {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: OAuth? ({isOAuth} was used), ICredentials? ({Credentials}, was used).\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Authenticate using the default credentials of the current user (NTLM)\n    /// </summary>\n    /// <returns></returns>\n    public SmtpResult AuthenticateDefaultCredentials() {\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping authentication.\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"Authentication skipped (WhatIf)\");\n        }\n        try {\n            var mechanism = new SaslMechanismNtlmIntegrated();\n            Client.Authenticate(mechanism);\n            LogVerbose($\"Send-EmailMessage - Authenticated using default credentials\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Could not authenticate using default credentials. Error: {ex.Message}\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// Returns the plain text password, decrypting it when <paramref name=\"isSecureString\"/> is true.\n    /// </summary>\n    /// <param name=\"password\">Password value.</param>\n    /// <param name=\"isSecureString\">Indicates if the password is protected.</param>\n    /// <returns>The plain text password.</returns>\n    public string ConvertSecureStringToPlainString(string password, bool isSecureString) {\n        if (isSecureString) {\n            // Convert the encrypted string back to a SecureString\n            SecureString securePassword = SecureStringHelper.Unprotect(password);\n\n            // Convert the SecureString to a plain string\n            IntPtr unmanagedString = IntPtr.Zero;\n            try {\n                unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassword);\n                return Marshal.PtrToStringUni(unmanagedString) ?? string.Empty;\n            } finally {\n                Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);\n            }\n        }\n        return password;\n    }\n\n    /// <summary>\n    /// Authenticate using the specified user name and password. After the\n    /// authentication attempt, the plain text value is either overwritten or\n    /// protected again to avoid leaving sensitive data in memory.\n    /// </summary>\n    /// <param name=\"username\">The user name.</param>\n    /// <param name=\"password\">\n    /// Password value. When <paramref name=\"isSecureString\"/> is <c>true</c>, the\n    /// string is re-secured using <see cref=\"SecureStringHelper.Protect\"/> after\n    /// authentication completes.\n    /// </param>\n    /// <param name=\"isSecureString\">Indicates whether the password was\n    /// previously protected.</param>\n    /// <param name=\"mechanism\">Authentication mechanism to use.</param>\n    /// <returns>An <see cref=\"SmtpResult\"/> representing the outcome.</returns>\n    public SmtpResult Authenticate(string username, string password, bool isSecureString, AuthenticationMechanism mechanism = AuthenticationMechanism.Plain) {\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping authentication.\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"Authentication skipped (WhatIf)\");\n        }\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        Credential = new NetworkCredential(username, password);\n        try {\n            if (!SmtpValidation.TryValidateCredentials(username, password, out var validationError))\n            {\n                string message = validationError ?? \"Invalid SMTP credentials.\";\n                LogWarning($\"Send-EmailMessage - {message}\");\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw new InvalidOperationException(message);\n                }\n                return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", message);\n            }\n            switch (mechanism) {\n                case AuthenticationMechanism.CramMd5:\n                    Client.Authenticate(new SaslMechanismCramMd5(username, password));\n                    break;\n                case AuthenticationMechanism.Login:\n                    Client.Authenticate(new SaslMechanismLogin(username, password));\n                    break;\n                default:\n                    Client.Authenticate(new SaslMechanismPlain(username, password));\n                    break;\n            }\n            LogVerbose($\"Send-EmailMessage - Authenticated as {username}\");\n            return new SmtpResult(true, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error during authentication: {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: Username? ({username} was used), Password?.\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.Authenticate, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Send the email message.\n    /// </summary>\n    /// <remarks>\n    /// Concurrent calls are serialized so that only one send executes at a time for a\n    /// given instance.\n    /// </remarks>\n    /// <returns></returns>\n    public SmtpResult Send() {\n        _sendLock.Wait();\n        try {\n            return SendCoreAsync().GetAwaiter().GetResult();\n        } finally {\n            _sendLock.Release();\n        }\n    }\n\n    /// <summary>\n    /// Send the email message asynchronously.\n    /// </summary>\n    /// <remarks>\n    /// Concurrent calls are serialized so that only one send executes at a time for a\n    /// given instance.\n    /// </remarks>\n    /// <returns></returns>\n    public async Task<SmtpResult> SendAsync(CancellationToken cancellationToken = default) {\n        await _sendLock.WaitAsync(cancellationToken);\n        try {\n            return await SendCoreAsync(cancellationToken);\n        } finally {\n            _sendLock.Release();\n        }\n    }\n\n    /// <summary>\n    /// Attempts to send all messages stored in <see cref=\"PendingMessageRepository\"/>.\n    /// </summary>\n    /// <remarks>\n    /// Messages are removed from the repository only when sending succeeds. On\n    /// success the message is also logged via <see cref=\"SentMessageRepository\"/>,\n    /// if configured.\n    /// </remarks>\n    public async Task ProcessPendingMessagesAsync(CancellationToken cancellationToken = default) {\n        if (PendingMessageRepository == null) {\n            return;\n        }\n\n        await foreach (var record in PendingMessageRepository.GetAllAsync(cancellationToken)) {\n            cancellationToken.ThrowIfCancellationRequested();\n            if (DryRun) {\n                LogVerbose($\"ProcessPendingMessages - DryRun enabled, skipping {record.MessageId}\");\n                continue;\n            }\n            if (string.IsNullOrWhiteSpace(record.MimeMessage) || string.IsNullOrEmpty(record.MessageId)) {\n                continue;\n            }\n            if (record.NextAttemptAt > DateTimeOffset.UtcNow) {\n                continue;\n            }\n\n            if (record.Provider != EmailProvider.None) {\n                continue;\n            }\n\n            MimeMessage message;\n            try {\n                var bytes = Convert.FromBase64String(record.MimeMessage);\n                using var ms = new MemoryStream(bytes);\n                message = await MimeMessage.LoadAsync(ms, cancellationToken);\n            } catch (Exception ex) {\n                LogWarning($\"ProcessPendingMessages - Failed to parse {record.MessageId}: {ex.Message}\");\n                continue;\n            }\n\n            var originalSkipValidation = SkipCertificateValidation;\n            var originalCheckRevocation = CheckCertificateRevocation;\n            var originalTimeout = Timeout;\n            var originalSecureOptions = _activeSecureSocketOptions;\n            var originalUseSsl = _activeUseSsl;\n            var originalPoolIdentity = ConnectionPoolIdentity;\n            var secureSocketOptions = _activeSecureSocketOptions;\n            var useSsl = _activeUseSsl;\n            if (record.ProviderData != null && record.ProviderData.Count > 0) {\n                if (record.ProviderData.TryGetValue(ProviderDataSecureSocketOptionsKey, out var secureValue)\n                    && Enum.TryParse(secureValue, out SecureSocketOptions parsedSecure)) {\n                    secureSocketOptions = parsedSecure;\n                }\n                if (record.ProviderData.TryGetValue(ProviderDataUseSslKey, out var useSslValue)\n                    && bool.TryParse(useSslValue, out var parsedUseSsl)) {\n                    useSsl = parsedUseSsl;\n                }\n                if (record.ProviderData.TryGetValue(ProviderDataSkipCertificateValidationKey, out var skipValue)\n                    && bool.TryParse(skipValue, out var parsedSkip)) {\n                    SkipCertificateValidation = parsedSkip;\n                }\n                if (record.ProviderData.TryGetValue(ProviderDataCheckCertificateRevocationKey, out var revocationValue)\n                    && bool.TryParse(revocationValue, out var parsedRevocation)) {\n                    CheckCertificateRevocation = parsedRevocation;\n                }\n                if (record.ProviderData.TryGetValue(ProviderDataTimeoutKey, out var timeoutValue)\n                    && int.TryParse(timeoutValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timeout)) {\n                    Timeout = timeout;\n                }\n            }\n            if (!string.IsNullOrWhiteSpace(record.UserName)) {\n                ConnectionPoolIdentity = record.UserName;\n            }\n\n            try {\n                var server = record.Server ?? Server;\n                var port = record.Port ?? Port;\n                if (!string.IsNullOrWhiteSpace(server)) {\n                    Connect(server, port, secureSocketOptions, useSsl);\n                    if (!string.IsNullOrEmpty(record.UserName)) {\n                        var pwd = CredentialProtection.UnprotectWithFallback(record.Password);\n                        var cred = Helpers.ConvertFromPlainText(record.UserName!, pwd);\n                        Authenticate(cred);\n                    }\n                }\n\n                await Client.SendAsync(message, cancellationToken);\n                LogVerbose($\"Send-EmailMessage - Sent email to {message.To}\");\n                if (SentMessageRepository != null) {\n                    var sentRecord = new SentMessageRecord {\n                        MessageId = message.MessageId ?? record.MessageId,\n                        Recipients = SentMessageRecipients.Serialize(message.To),\n                        Subject = message.Subject ?? string.Empty,\n                        Timestamp = DateTimeOffset.UtcNow\n                    };\n                    await SentMessageRepository.SaveAsync(sentRecord, cancellationToken);\n                }\n                await PendingMessageRepository.RemoveAsync(record.MessageId!, cancellationToken);\n            } catch (Exception ex) {\n                LogWarning($\"ProcessPendingMessages - Error sending {record.MessageId}: {ex.Message}\");\n                var attempt = record.IncrementAttemptCount();\n                var delay = CalculateRetryDelay(attempt - 1);\n                record.NextAttemptAt = delay > TimeSpan.Zero\n                    ? DateTimeOffset.UtcNow.Add(delay)\n                    : DateTimeOffset.UtcNow;\n                await PendingMessageRepository.SaveAsync(record, cancellationToken);\n            } finally {\n                Disconnect();\n                SkipCertificateValidation = originalSkipValidation;\n                CheckCertificateRevocation = originalCheckRevocation;\n                Timeout = originalTimeout;\n                _activeSecureSocketOptions = originalSecureOptions;\n                _activeUseSsl = originalUseSsl;\n                ConnectionPoolIdentity = originalPoolIdentity;\n            }\n        }\n    }\n\n    /// <summary>\n    /// Logs a verbose message using LogCollector if available, otherwise uses LoggingMessages.Logger.\n    /// </summary>\n    private void LogVerbose(string message) {\n        if (LogCollector != null) {\n            LogCollector.LogVerbose(message);\n        } else {\n            LoggingMessages.Logger.WriteVerbose(message);\n        }\n    }\n\n    /// <summary>\n    /// Logs a warning message using LogCollector if available, otherwise uses LoggingMessages.Logger.\n    /// </summary>\n    private void LogWarning(string message) {\n        if (LogCollector != null) {\n            LogCollector.LogWarning(message);\n        } else {\n            LoggingMessages.Logger.WriteWarning(message);\n        }\n    }\n\n    private Dictionary<string, string> CreateProviderDataSnapshot() {\n        var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n            [ProviderDataSecureSocketOptionsKey] = _activeSecureSocketOptions.ToString(),\n            [ProviderDataUseSslKey] = _activeUseSsl.ToString(CultureInfo.InvariantCulture),\n            [ProviderDataSkipCertificateValidationKey] = _skipCertificateValidation.ToString(CultureInfo.InvariantCulture),\n            [ProviderDataCheckCertificateRevocationKey] = Client.CheckCertificateRevocation.ToString(CultureInfo.InvariantCulture),\n            [ProviderDataTimeoutKey] = Client.Timeout.ToString(CultureInfo.InvariantCulture)\n        };\n\n        return data;\n    }\n\n    private string GetConnectionPoolIdentity() {\n        var userName = ConnectionPoolIdentity;\n        var domain = string.Empty;\n        if (string.IsNullOrWhiteSpace(userName)) {\n            userName = Credential?.UserName;\n            domain = Credential?.Domain ?? string.Empty;\n        }\n        if (!string.IsNullOrWhiteSpace(domain)) {\n            userName = string.IsNullOrWhiteSpace(userName) ? domain : $\"{domain}\\\\{userName}\";\n        }\n        if (string.IsNullOrWhiteSpace(userName)) {\n            userName = \"anonymous\";\n        }\n        return $\"{userName}|{_activeSecureSocketOptions}|{_activeUseSsl}\";\n    }\n\n    private TimeSpan CalculateRetryDelay(int attempt) {\n        if (attempt < 0) attempt = 0;\n        var delayMilliseconds = (int)Math.Round(RetryDelayMilliseconds * Math.Pow(RetryDelayBackoff, attempt));\n        if (MaxDelayMilliseconds > 0 && delayMilliseconds > MaxDelayMilliseconds) {\n            delayMilliseconds = MaxDelayMilliseconds;\n        }\n        if (JitterMilliseconds > 0 && delayMilliseconds > 0) {\n            delayMilliseconds += GraphRetryHelperRandom.NextInt(JitterMilliseconds + 1);\n        }\n        return delayMilliseconds > 0\n            ? TimeSpan.FromMilliseconds(delayMilliseconds)\n            : TimeSpan.Zero;\n    }\n\n    private string EnsureMessageId() {\n        var id = Message.MessageId;\n        if (string.IsNullOrEmpty(id)) {\n            Message.MessageId = id = MimeKit.Utils.MimeUtils.GenerateMessageId();\n        }\n        return id!;\n    }\n\n    private async Task SaveSentMessageAsync(string messageId, CancellationToken cancellationToken) {\n        if (SentMessageRepository == null) {\n            return;\n        }\n\n        var record = new SentMessageRecord {\n            MessageId = messageId,\n            Recipients = SentMessageRecipients.Serialize(Message?.To),\n            Subject = Subject,\n            Timestamp = DateTimeOffset.UtcNow\n        };\n        await SentMessageRepository.SaveAsync(record, cancellationToken);\n    }\n\n    private async Task RemovePendingMessageAsync(string? messageId, CancellationToken cancellationToken) {\n        if (PendingMessageRepository == null || string.IsNullOrEmpty(messageId)) {\n            return;\n        }\n\n        var safeMessageId = messageId!;\n        await PendingMessageRepository.RemoveAsync(safeMessageId, cancellationToken);\n    }\n\n    private async Task EnqueuePendingMessageAsync(string messageId, ICredentialProtector credentialProtector, CancellationToken cancellationToken) {\n        if (PendingMessageRepository == null) {\n            return;\n        }\n\n        using var ms = new MemoryStream();\n        await Message.WriteToAsync(ms, cancellationToken);\n        var record = new PendingMessageRecord {\n            MessageId = messageId,\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            Timestamp = DateTimeOffset.UtcNow,\n            NextAttemptAt = DateTimeOffset.UtcNow,\n            Provider = EmailProvider.None,\n            Server = Server,\n            Port = Port,\n            UserName = Credential?.UserName,\n            Password = string.IsNullOrEmpty(Credential?.Password)\n                ? null\n                : credentialProtector.Protect(Credential!.Password),\n            ProviderData = CreateProviderDataSnapshot()\n        };\n        await PendingMessageRepository.SaveAsync(record, cancellationToken);\n    }\n\n    private async Task<SmtpResult?> EnsureMessageReadyAsync(CancellationToken cancellationToken)\n    {\n        var message = Message;\n        bool messageHasContent = message != null && MessageHasContent(message);\n        bool hasPayload = HasPropertyPayload();\n        bool hasHeaderPayload = (message != null && message.Headers != null && message.Headers.Count > 0) ||\n                                (Headers != null && Headers.Count > 0);\n\n        bool shouldAutoCreate = AutoCreateMessage && !messageHasContent && hasPayload;\n        if (shouldAutoCreate)\n        {\n            PreserveCustomHeaders(message);\n            await CreateMessageAsync(cancellationToken).ConfigureAwait(false);\n            message = Message;\n            messageHasContent = message != null && MessageHasContent(message);\n        }\n\n        bool hasSender = message != null && MessageHasSender(message);\n        bool hasMaterial = hasPayload || messageHasContent || hasHeaderPayload;\n        if (!hasSender && hasMaterial)\n        {\n            string messageText = \"SMTP message has no sender. Call CreateMessage/CreateMessageAsync after setting From/To/Subject, or enable AutoCreateMessage.\";\n            LogWarning($\"Send-EmailMessage - {messageText}\");\n            if (ErrorAction == ActionPreference.Stop)\n            {\n                throw new InvalidOperationException(messageText);\n            }\n\n            var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText)\n            {\n                MessageId = Message?.MessageId\n            };\n            await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken).ConfigureAwait(false);\n            return failResult;\n        }\n\n        return null;\n    }\n\n    private bool HasPropertyPayload()\n    {\n        if (HasAddressValue(From) || HasAddressValue(ReplyTo))\n        {\n            return true;\n        }\n\n        if (HasRecipientValues(To) || HasRecipientValues(Cc) || HasRecipientValues(Bcc))\n        {\n            return true;\n        }\n\n        if (!string.IsNullOrWhiteSpace(Subject) ||\n            !string.IsNullOrWhiteSpace(HtmlBody) ||\n            !string.IsNullOrWhiteSpace(TextBody))\n        {\n            return true;\n        }\n\n        if (Attachments != null && Attachments.Count > 0)\n        {\n            return true;\n        }\n\n        if (InlineAttachments != null && InlineAttachments.Count > 0)\n        {\n            return true;\n        }\n\n        if (Headers != null && Headers.Count > 0)\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    private static bool MessageHasSender(MimeMessage message)\n    {\n        if (message == null)\n        {\n            return false;\n        }\n\n        if (message.From.Count > 0)\n        {\n            return true;\n        }\n\n        return message.Sender != null;\n    }\n\n    private static bool MessageHasContent(MimeMessage message)\n    {\n        if (message == null)\n        {\n            return false;\n        }\n\n        if (message.From.Count > 0 || message.To.Count > 0 || message.Cc.Count > 0 || message.Bcc.Count > 0 ||\n            message.ReplyTo.Count > 0 || message.Sender != null)\n        {\n            return true;\n        }\n\n        if (!string.IsNullOrWhiteSpace(message.Subject))\n        {\n            return true;\n        }\n\n        return message.Body != null;\n    }\n\n    private void PreserveCustomHeaders(MimeMessage? message)\n    {\n        if (message == null || message.Headers == null || message.Headers.Count == 0)\n        {\n            return;\n        }\n\n        Dictionary<string, string>? merged = null;\n        if (Headers is Dictionary<string, string> headerDict)\n        {\n            merged = headerDict;\n        }\n        else if (Headers != null && Headers.Count > 0)\n        {\n            merged = new Dictionary<string, string>(Headers, StringComparer.OrdinalIgnoreCase);\n        }\n\n        foreach (var header in message.Headers)\n        {\n            if (header.Id != MimeKit.HeaderId.Unknown)\n            {\n                continue;\n            }\n\n            if (string.IsNullOrWhiteSpace(header.Field))\n            {\n                continue;\n            }\n\n            merged ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n            if (!merged.ContainsKey(header.Field))\n            {\n                merged[header.Field] = header.Value ?? string.Empty;\n            }\n        }\n\n        if (merged != null && !ReferenceEquals(merged, Headers))\n        {\n            Headers = merged;\n        }\n    }\n\n    private static bool HasAddressValue(object? value)\n    {\n        if (value == null)\n        {\n            return false;\n        }\n\n        if (value is string text)\n        {\n            return !string.IsNullOrWhiteSpace(text);\n        }\n\n        return true;\n    }\n\n    private static bool HasRecipientValues(IEnumerable<object>? recipients)\n    {\n        if (recipients == null)\n        {\n            return false;\n        }\n\n        foreach (var recipient in recipients)\n        {\n            if (recipient == null)\n            {\n                continue;\n            }\n\n            if (recipient is string text)\n            {\n                if (!string.IsNullOrWhiteSpace(text))\n                {\n                    return true;\n                }\n\n                continue;\n            }\n\n            return true;\n        }\n\n        return false;\n    }\n\n    private async Task<SmtpResult> SendCoreAsync(CancellationToken cancellationToken = default) {\n        if (DryRun) {\n            LogVerbose(\"Send-EmailMessage - DryRun enabled, skipping send.\");\n            return new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\") {\n                MessageId = Message?.MessageId\n            };\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        var credentialProtector = CredentialProtection.Default;\n        var readinessResult = await EnsureMessageReadyAsync(cancellationToken).ConfigureAwait(false);\n        if (readinessResult != null)\n        {\n            return readinessResult;\n        }\n\n        do {\n            try {\n                await Client.SendAsync(Message, cancellationToken);\n                LogVerbose($\"Send-EmailMessage - Sent email to {SentTo}\");\n                await SaveSentMessageAsync(Message.MessageId ?? string.Empty, cancellationToken);\n                await RemovePendingMessageAsync(Message.MessageId, cancellationToken);\n                var result = new SmtpResult(true, EmailAction.Send, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging) {\n                    MessageId = Message.MessageId\n                };\n                await Helpers.PostWebhookAsync(WebhookUrl, result, cancellationToken);\n                return result;\n            } catch (Exception ex) {\n                lastException = ex;\n                LogWarning($\"Send-EmailMessage - Error during sending: {ex.Message}\");\n                if ((!Helpers.IsTransient(ex) && !RetryAlways) || attempts >= RetryCount) {\n                    if (ErrorAction == ActionPreference.Stop) {\n                        throw;\n                    }\n                    var id = EnsureMessageId();\n                    await EnqueuePendingMessageAsync(id, credentialProtector, cancellationToken);\n                    var failResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message) {\n                        MessageId = id\n                    };\n                    await Helpers.PostWebhookAsync(WebhookUrl, failResult, cancellationToken);\n                    return failResult;\n                }\n\n                var delay = CalculateRetryDelay(attempts);\n                if (delay > TimeSpan.Zero) {\n                    await Task.Delay(delay, cancellationToken);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n\n        var finalId = EnsureMessageId();\n        await EnqueuePendingMessageAsync(finalId, credentialProtector, cancellationToken);\n        var finalResult = new SmtpResult(false, EmailAction.Send, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", lastException?.Message) {\n            MessageId = finalId\n        };\n        await Helpers.PostWebhookAsync(WebhookUrl, finalResult, cancellationToken);\n        return finalResult;\n    }\n\n\n    /// <summary>\n    /// Disconnects from the SMTP server.\n    /// </summary>\n    public void Disconnect() {\n        if (Client.IsConnected) {\n            if (IsConnectionPoolingEnabled) {\n                var identity = _poolIdentity ?? GetConnectionPoolIdentity();\n                SmtpConnectionPool.ReturnClient(Server, Port, Client, identity, IsConnectionPoolingEnabled);\n                Client = ClientFactory(Logging?.ProtocolLogger);\n            } else {\n                Client.Disconnect(true);\n            }\n        }\n        Stopwatch.Stop();\n    }\n\n    /// <summary>\n    /// Releases the SMTP connection and associated resources.\n    /// </summary>\n    public void Dispose() {\n        var clientToDispose = Client;\n        if (Client.IsConnected) {\n            if (IsConnectionPoolingEnabled) {\n                var identity = _poolIdentity ?? GetConnectionPoolIdentity();\n                SmtpConnectionPool.ReturnClient(Server, Port, Client, identity, IsConnectionPoolingEnabled);\n                Client = ClientFactory(Logging?.ProtocolLogger);\n                clientToDispose = Client;\n            } else {\n                Client.Disconnect(true);\n            }\n        }\n        clientToDispose.Dispose();\n        Stopwatch.Stop();\n    }\n\n    /// <summary>\n    /// S/MIME encrypt the message using a PFX certificate file.\n    /// </summary>\n    /// <param name=\"pfxFilePath\">Path to the PFX file.</param>\n    /// <param name=\"password\">Certificate password.</param>\n    /// <param name=\"isSecureString\">Indicates if the password is protected.</param>\n    /// <returns></returns>\n    public SmtpResult Encrypt(string pfxFilePath, string password, bool isSecureString) {\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        try {\n            using var certificate = new X509Certificate2(pfxFilePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);\n            return Encrypt(certificate);\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// S/MIME encrypt the message using a certificate from the store.\n    /// </summary>\n    /// <param name=\"certificateThumbprint\">Certificate thumbprint.</param>\n    /// <returns></returns>\n    public SmtpResult Encrypt(string certificateThumbprint) {\n        // Load the certificate from the Windows Certificate Store\n        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);\n        store.Open(OpenFlags.ReadOnly);\n\n        X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, false);\n\n        if (certificates.Count > 0) {\n            // Use the certificate directly from the store to encrypt the email\n            return Encrypt(certificates[0]);\n        } else {\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new Exception(\"Certificate not found in the store.\");\n            }\n            return new SmtpResult(false, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", \"Certificate not found in the store.\");\n        }\n    }\n\n    /// <summary>\n    /// S/MIME encrypt the message using the specified certificate instance.\n    /// </summary>\n    /// <param name=\"certificate\">Certificate to encrypt with.</param>\n    /// <returns></returns>\n    public SmtpResult Encrypt(X509Certificate2 certificate) {\n        MimeMessage message = Message;\n        var body = message.Body;\n        if (body is null) {\n            const string messageText = \"Message body is empty.\";\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidOperationException(messageText);\n            }\n            return new SmtpResult(false, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n        }\n        // encrypt our message body using a temporary S/MIME context to avoid SQLite dependency\n        using (var ctx = new TemporarySecureMimeContext()) {\n            try {\n                // Create a CmsRecipientCollection and add the CmsRecipient to it\n                var recipients = new CmsRecipientCollection();\n                recipients.Add(new CmsRecipient(certificate));\n\n                // Encrypt the message body with the certificate\n                message.Body = ApplicationPkcs7Mime.Encrypt(ctx, recipients, body!);\n            } catch (Exception ex) {\n                LogWarning($\"Send-EmailMessage - Error during encryption: {ex.Message}\");\n                LogWarning($\"Send-EmailMessage - Possible issue: Certificate? ({certificate.Thumbprint} was used).\");\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw;\n                }\n                return new SmtpResult(false, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n            }\n        }\n\n        Message = message;\n        return new SmtpResult(true, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n    }\n\n    /// <summary>\n    /// S/MIME sign the message using the specified certificate.\n    /// </summary>\n    /// <param name=\"certificate\">Certificate used for signing.</param>\n    /// <returns></returns>\n    public SmtpResult Sign(X509Certificate2 certificate) {\n        MimeMessage message = Message;\n        var body = message.Body;\n        if (body is null) {\n            const string messageText = \"Message body is empty.\";\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidOperationException(messageText);\n            }\n            return new SmtpResult(false, EmailAction.SMimeSignature, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n        }\n        // digitally sign our message body using a temporary S/MIME context\n        // TemporarySecureMimeContext avoids the SQLite dependency of DefaultSecureMimeContext\n        using (var ctx = new TemporarySecureMimeContext()) {\n            try {\n                var signer = new CmsSigner(certificate) {\n                    DigestAlgorithm = DigestAlgorithm.Sha1\n                };\n                message.Body = MultipartSigned.Create(ctx, signer, body!);\n            } catch (Exception ex) {\n                LogWarning($\"Send-EmailMessage - Error during signing: {ex.Message}\");\n                LogWarning($\"Send-EmailMessage - Possible issue: Certificate? ({certificate.Thumbprint} was used).\");\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw;\n                }\n                return new SmtpResult(false, EmailAction.SMimeSignature, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n            }\n        }\n        Message = message;\n        return new SmtpResult(true, EmailAction.SMimeSignature, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n    }\n\n    /// <summary>\n    /// S/MIME sign the message using a PFX certificate file.\n    /// </summary>\n    /// <param name=\"pfxFilePath\">Path to the PFX file.</param>\n    /// <param name=\"password\">Certificate password.</param>\n    /// <param name=\"isSecureString\">Indicates if the password is protected.</param>\n    /// <returns></returns>\n    public SmtpResult Sign(string pfxFilePath, string password, bool isSecureString) {\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        try {\n            using var certificate = new X509Certificate2(pfxFilePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);\n            return Sign(certificate);\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// S/MIME sign the message using a certificate from the store.\n    /// </summary>\n    /// <param name=\"certificateThumbprint\">Certificate thumbprint.</param>\n    /// <returns></returns>\n    public SmtpResult Sign(string certificateThumbprint) {\n        // Load the certificate from the Windows Certificate Store\n        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);\n        store.Open(OpenFlags.ReadOnly);\n\n        X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, false);\n\n        if (certificates.Count > 0) {\n            // Use the certificate directly from the store to sign the email\n            return Sign(certificates[0]);\n        }\n\n        var messageText = \"Certificate not found in the store.\";\n        LogWarning($\"Send-EmailMessage - {messageText}\");\n        LogWarning($\"Send-EmailMessage - Possible issue: Thumbprint '{certificateThumbprint}' is invalid or the certificate is missing.\");\n\n        if (ErrorAction == ActionPreference.Stop) {\n            throw new Exception(messageText);\n        }\n\n        return new SmtpResult(false, EmailAction.SMimeSignature, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n    }\n\n    /// <summary>\n    /// PKCS#7 sign the message using a PFX certificate file.\n    /// </summary>\n    /// <param name=\"pfxFilePath\">Path to the PFX file.</param>\n    /// <param name=\"password\">Certificate password.</param>\n    /// <param name=\"isSecureString\">Indicates if the password is protected.</param>\n    /// <returns></returns>\n    public SmtpResult Pkcs7Sign(string pfxFilePath, string password, bool isSecureString) {\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        try {\n            using var certificate = new X509Certificate2(pfxFilePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);\n            return Pkcs7Sign(certificate);\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// PKCS#7 sign the message using a certificate from the store.\n    /// </summary>\n    /// <param name=\"certificateThumbprint\">Certificate thumbprint.</param>\n    /// <returns></returns>\n    public SmtpResult Pkcs7Sign(string certificateThumbprint) {\n        // Load the certificate from the Windows Certificate Store\n        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);\n        store.Open(OpenFlags.ReadOnly);\n\n        X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbprint, false);\n\n        if (certificates.Count > 0) {\n            // Use the certificate directly from the store to sign the email\n            return Pkcs7Sign(certificates[0]);\n        } else {\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new Exception(\"Certificate not found in the store.\");\n            }\n            return new SmtpResult(false, EmailAction.SMimeSignaturePKCS7, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", \"Certificate not found in the store.\");\n        }\n    }\n\n    /// <summary>\n    /// PKCS#7 sign the message using the specified certificate.\n    /// </summary>\n    /// <param name=\"certificate\">Certificate used for signing.</param>\n    /// <returns></returns>\n    public SmtpResult Pkcs7Sign(X509Certificate2 certificate) {\n        try {\n            MimeMessage message = Message;\n            var body = message.Body;\n            if (body is null) {\n                const string messageText = \"Message body is empty.\";\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw new InvalidOperationException(messageText);\n                }\n                return new SmtpResult(false, EmailAction.SMimeSignaturePKCS7, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n            }\n            // digitally sign our message body using a temporary S/MIME context to avoid SQLite dependency\n            using (var ctx = new TemporarySecureMimeContext()) {\n                // Create a signer with the certificate\n                var signer = new CmsSigner(certificate) {\n                    DigestAlgorithm = DigestAlgorithm.Sha256\n                };\n\n                // Sign the message body with the signer\n                message.Body = ApplicationPkcs7Mime.Sign(ctx, signer, body!);\n            }\n\n            Message = message;\n            return new SmtpResult(true, EmailAction.SMimeSignaturePKCS7, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        } catch (Exception ex) {\n            LogWarning($\"Send-EmailMessage - Error: {ex.Message}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: Certificate? ({certificate.Thumbprint} was used).\");\n            if (ErrorAction == ActionPreference.Stop) {\n                throw;\n            }\n            return new SmtpResult(false, EmailAction.SMimeSignaturePKCS7, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n        }\n    }\n\n    /// <summary>\n    /// S/MIME Sign and encrypt the email using the provided certificate thumbprint.\n    /// </summary>\n    /// <param name=\"certificateThumbprint\"></param>\n    /// <returns></returns>\n    public SmtpResult SignAndEncrypt(string certificateThumbprint) {\n        // Sign the email\n        SmtpResult signResult = Sign(certificateThumbprint);\n        if (!signResult.Status) {\n            return signResult;\n        }\n\n        // Encrypt the signed email\n        SmtpResult encryptResult = Encrypt(certificateThumbprint);\n        if (!encryptResult.Status) {\n            return encryptResult;\n        }\n\n        return new SmtpResult(true, EmailAction.SMimeSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n    }\n\n    /// <summary>\n    /// S/MIME Sign and encrypt the email using the provided PFX file and password.\n    /// </summary>\n    /// <param name=\"pfxFilePath\"></param>\n    /// <param name=\"password\"></param>\n    /// <param name=\"isSecureString\"></param>\n    /// <returns></returns>\n    public SmtpResult SignAndEncrypt(string pfxFilePath, string password, bool isSecureString) {\n        // Sign the email\n        SmtpResult signResult = Sign(pfxFilePath, password, isSecureString);\n        if (!signResult.Status) {\n            return signResult;\n        }\n\n        // Encrypt the signed email\n        SmtpResult encryptResult = Encrypt(pfxFilePath, password, isSecureString);\n        if (!encryptResult.Status) {\n            return encryptResult;\n        }\n\n        return new SmtpResult(true, EmailAction.SMimeSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n    }\n\n    /// <summary>\n    /// Performs the specified S/MIME action using the provided certificate.\n    /// </summary>\n    /// <param name=\"emailActionEncryption\">The operation to perform.</param>\n    /// <param name=\"certificate\">Certificate instance.</param>\n    public SmtpResult Encrypt(EmailActionEncryption emailActionEncryption, X509Certificate2 certificate) {\n        return emailActionEncryption switch {\n            EmailActionEncryption.SMIMESign => Sign(certificate),\n            EmailActionEncryption.SMIMESignPkcs7 => Pkcs7Sign(certificate),\n            EmailActionEncryption.SMIMEEncrypt => Encrypt(certificate),\n            EmailActionEncryption.SMIMESignAndEncrypt => SignAndEncrypt(certificate),\n            _ => new SmtpResult(true, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", \"EmailActionEncryption None\")\n        };\n    }\n\n    /// <summary>\n    /// S/MIME Sign and encrypt the email using the provided certificate.\n    /// </summary>\n    /// <param name=\"certificate\">Certificate to use.</param>\n    public SmtpResult SignAndEncrypt(X509Certificate2 certificate) {\n        SmtpResult signResult = Sign(certificate);\n        if (!signResult.Status) {\n            return signResult;\n        }\n\n        SmtpResult encryptResult = Encrypt(certificate);\n        if (!encryptResult.Status) {\n            return encryptResult;\n        }\n\n        return new SmtpResult(true, EmailAction.SMimeSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n    }\n\n    /// <summary>\n    /// Encrypts the current message using the specified OpenPGP public key.\n    /// </summary>\n    /// <param name=\"publicKeyPath\">Path to the recipient public key.</param>\n    /// <returns>The result of the encryption operation.</returns>\n    public SmtpResult PgpEncrypt(string publicKeyPath) {\n        if (!File.Exists(publicKeyPath)) {\n            string messageText = $\"Public key file not found: {publicKeyPath}\";\n            LogWarning($\"Send-EmailMessage - {messageText}\");\n            LogWarning($\"Send-EmailMessage - Possible issue: Path '{publicKeyPath}' is invalid. Verify the file exists and the path is correct.\");\n            return new SmtpResult(false, EmailAction.PgpEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n        }\n\n        MimeMessage message = Message;\n        var body = message.Body;\n        if (body is null) {\n            const string messageText = \"Message body is empty.\";\n            if (ErrorAction == ActionPreference.Stop) {\n                throw new InvalidOperationException(messageText);\n            }\n            return new SmtpResult(false, EmailAction.PgpEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n        }\n        using (var ctx = new EphemeralOpenPgpContext()) {\n            using (var pub = File.OpenRead(publicKeyPath))\n                ctx.Import(pub);\n            var recipients = message.To.Mailboxes.Concat(message.Cc.Mailboxes).Concat(message.Bcc.Mailboxes).ToList();\n            try {\n                var keys = ctx.GetPublicKeys(recipients);\n                message.Body = MultipartEncrypted.Encrypt(ctx, keys, body!);\n            } catch (Exception ex) {\n                if (ErrorAction == ActionPreference.Stop) throw;\n                return new SmtpResult(false, EmailAction.PgpEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n            }\n            Message = message;\n            return new SmtpResult(true, EmailAction.PgpEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n        }\n    }\n\n    /// <summary>\n    /// Signs the current message using OpenPGP keys.\n    /// </summary>\n    /// <param name=\"publicKeyPath\">Path to the public key.</param>\n    /// <param name=\"privateKeyPath\">Path to the private key.</param>\n    /// <param name=\"password\">Password protecting the private key.</param>\n    /// <param name=\"isSecureString\">Whether the password is protected.</param>\n    /// <returns>The result of the signing operation.</returns>\n    public SmtpResult PgpSign(string publicKeyPath, string privateKeyPath, string password, bool isSecureString) {\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        try {\n            MimeMessage message = Message;\n            var body = message.Body;\n            if (body is null) {\n                const string messageText = \"Message body is empty.\";\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw new InvalidOperationException(messageText);\n                }\n                return new SmtpResult(false, EmailAction.PgpSign, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n            }\n            using (var ctx = new EphemeralOpenPgpContext(password)) {\n                if (!File.Exists(publicKeyPath)) {\n                    string messageText = $\"Public key file not found: {publicKeyPath}\";\n                    LogWarning($\"Send-EmailMessage - {messageText}\");\n                    LogWarning($\"Send-EmailMessage - Possible issue: Path '{publicKeyPath}' is invalid. Verify the file exists and the path is correct.\");\n                    return new SmtpResult(false, EmailAction.PgpSign, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n                }\n                if (!File.Exists(privateKeyPath)) {\n                    string messageText = $\"Private key file not found: {privateKeyPath}\";\n                    LogWarning($\"Send-EmailMessage - {messageText}\");\n                    LogWarning($\"Send-EmailMessage - Possible issue: Path '{privateKeyPath}' is invalid. Verify the file exists and the path is correct.\");\n                    return new SmtpResult(false, EmailAction.PgpSign, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n                }\n\n                using (var pub = File.OpenRead(publicKeyPath))\n                    ctx.Import(pub);\n                using (var sec = File.OpenRead(privateKeyPath))\n                    ctx.Import(new PgpSecretKeyRingBundle(new Org.BouncyCastle.Bcpg.ArmoredInputStream(sec)));\n                try {\n                    var signer = message.From.Mailboxes.First();\n                    var signingKey = ctx.GetSigningKey(signer);\n                    message.Body = MultipartSigned.Create(ctx, signingKey, DigestAlgorithm.Sha256, body!);\n                    var signed = (MultipartSigned)message.Body;\n                    var sigs = signed.Verify(ctx);\n                    foreach (var sig in sigs)\n                        sig.Verify();\n                } catch (Exception ex) {\n                    if (ErrorAction == ActionPreference.Stop) throw;\n                    return new SmtpResult(false, EmailAction.PgpSign, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n                }\n                Message = message;\n                return new SmtpResult(true, EmailAction.PgpSign, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n            }\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Signs and encrypts the current message using OpenPGP keys.\n    /// </summary>\n    /// <param name=\"publicKeyPath\">Path to the public key.</param>\n    /// <param name=\"privateKeyPath\">Path to the private key.</param>\n    /// <param name=\"password\">Password protecting the private key.</param>\n    /// <param name=\"isSecureString\">Whether the password is protected.</param>\n    /// <returns>The result of the sign and encrypt operation.</returns>\n    public SmtpResult PgpSignAndEncrypt(string publicKeyPath, string privateKeyPath, string password, bool isSecureString) {\n        password = ConvertSecureStringToPlainString(password, isSecureString);\n        try {\n            MimeMessage message = Message;\n            var body = message.Body;\n            if (body is null) {\n                const string messageText = \"Message body is empty.\";\n                if (ErrorAction == ActionPreference.Stop) {\n                    throw new InvalidOperationException(messageText);\n                }\n                return new SmtpResult(false, EmailAction.PgpSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n            }\n            using (var ctx = new EphemeralOpenPgpContext(password)) {\n                if (!File.Exists(publicKeyPath)) {\n                    string messageText = $\"Public key file not found: {publicKeyPath}\";\n                    LogWarning($\"Send-EmailMessage - {messageText}\");\n                    LogWarning($\"Send-EmailMessage - Possible issue: Path '{publicKeyPath}' is invalid. Verify the file exists and the path is correct.\");\n                    return new SmtpResult(false, EmailAction.PgpSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n                }\n                if (!File.Exists(privateKeyPath)) {\n                    string messageText = $\"Private key file not found: {privateKeyPath}\";\n                    LogWarning($\"Send-EmailMessage - {messageText}\");\n                    LogWarning($\"Send-EmailMessage - Possible issue: Path '{privateKeyPath}' is invalid. Verify the file exists and the path is correct.\");\n                    return new SmtpResult(false, EmailAction.PgpSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", messageText);\n                }\n\n                using (var pub = File.OpenRead(publicKeyPath))\n                    ctx.Import(pub);\n                using (var sec = File.OpenRead(privateKeyPath))\n                    ctx.Import(new PgpSecretKeyRingBundle(new Org.BouncyCastle.Bcpg.ArmoredInputStream(sec)));\n                var recipients = message.To.Mailboxes.Concat(message.Cc.Mailboxes).Concat(message.Bcc.Mailboxes).ToList();\n                try {\n                    var signingKey = ctx.GetSigningKey(message.From.Mailboxes.First());\n                    var encKeys = ctx.GetPublicKeys(recipients);\n                    message.Body = MultipartEncrypted.SignAndEncrypt(ctx, signingKey, DigestAlgorithm.Sha256, EncryptionAlgorithm.Cast5, encKeys, body!);\n                } catch (Exception ex) {\n                    if (ErrorAction == ActionPreference.Stop) throw;\n                    return new SmtpResult(false, EmailAction.PgpSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", ex.Message);\n                }\n                Message = message;\n                return new SmtpResult(true, EmailAction.PgpSignAndEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, Logging);\n            }\n        } finally {\n            if (isSecureString) {\n                using var securePwd = SecureStringHelper.FromPlainTextString(password);\n                password = SecureStringHelper.Protect(securePwd);\n            } else {\n                password = new string('\\0', password.Length);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Performs the specified S/MIME action using a PFX certificate file.\n    /// </summary>\n    /// <param name=\"emailActionEncryption\">The operation to perform.</param>\n    /// <param name=\"pfxFilePath\">Path to the PFX file.</param>\n    /// <param name=\"password\">Certificate password.</param>\n    /// <param name=\"isSecureString\">Indicates if the password is protected.</param>\n    /// <returns></returns>\n    public SmtpResult Encrypt(EmailActionEncryption emailActionEncryption, string pfxFilePath, string password, bool isSecureString) {\n        switch (emailActionEncryption) {\n            case EmailActionEncryption.SMIMESign:\n                return Sign(pfxFilePath, password, isSecureString);\n            case EmailActionEncryption.SMIMESignPkcs7:\n                return Pkcs7Sign(pfxFilePath, password, isSecureString);\n            case EmailActionEncryption.SMIMEEncrypt:\n                return Encrypt(pfxFilePath, password, isSecureString);\n            case EmailActionEncryption.SMIMESignAndEncrypt:\n                return SignAndEncrypt(pfxFilePath, password, isSecureString);\n            default:\n                // user did not specify an encryption type, we skip things\n                return new SmtpResult(true, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", \"EmailActionEncryption None\");\n        }\n    }\n    /// <summary>\n    /// Performs the specified S/MIME action using a certificate from the store.\n    /// </summary>\n    /// <param name=\"emailActionEncryption\">The operation to perform.</param>\n    /// <param name=\"certificateThumbprint\">Certificate thumbprint.</param>\n    /// <returns></returns>\n    public SmtpResult Encrypt(EmailActionEncryption emailActionEncryption, string certificateThumbprint) {\n        switch (emailActionEncryption) {\n            case EmailActionEncryption.SMIMESign:\n                return Sign(certificateThumbprint);\n            case EmailActionEncryption.SMIMESignPkcs7:\n                return Pkcs7Sign(certificateThumbprint);\n            case EmailActionEncryption.SMIMEEncrypt:\n                return Encrypt(certificateThumbprint);\n            case EmailActionEncryption.SMIMESignAndEncrypt:\n                return SignAndEncrypt(certificateThumbprint);\n            default:\n                // user did not specify an encryption type, we skip things\n                return new SmtpResult(true, EmailAction.SMimeEncrypt, SentTo, SentFrom, Server, Port, Stopwatch.Elapsed, \"\", \"EmailActionEncryption None\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpAppendExecutionResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Describes optional append-to-sent outcome for SMTP pipeline orchestration.\n/// </summary>\npublic sealed class SmtpAppendExecutionResult {\n    /// <summary>\n    /// Gets a value indicating whether append operation succeeded.\n    /// </summary>\n    public bool Appended { get; set; }\n\n    /// <summary>\n    /// Gets appended folder name when append succeeded.\n    /// </summary>\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// Gets append error when append failed.\n    /// </summary>\n    public string? Error { get; set; }\n\n    /// <summary>\n    /// Gets empty append outcome.\n    /// </summary>\n    public static SmtpAppendExecutionResult None { get; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpAppendPipeline.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides append-to-sent execution primitives for SMTP send pipelines.\n/// </summary>\npublic static class SmtpAppendPipeline {\n    /// <summary>\n    /// Appends a message to a sent folder and returns normalized append outcome.\n    /// </summary>\n    /// <param name=\"sentFolder\">Target sent folder.</param>\n    /// <param name=\"message\">Message to append.</param>\n    /// <param name=\"flags\">Message flags used for appended copy.</param>\n    /// <param name=\"appendAsync\">Optional append callback override (for testing/customization).</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Append execution result.</returns>\n    public static async Task<SmtpAppendExecutionResult> TryAppendToSentAsync(\n        IMailFolder sentFolder,\n        MimeMessage message,\n        MessageFlags flags = MessageFlags.Seen,\n        Func<IMailFolder, MimeMessage, MessageFlags, CancellationToken, Task>? appendAsync = null,\n        CancellationToken cancellationToken = default) {\n        if (sentFolder is null) {\n            throw new ArgumentNullException(nameof(sentFolder));\n        }\n        if (message is null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        try {\n            if (sentFolder.IsOpen && sentFolder.Access != FolderAccess.ReadWrite) {\n                await sentFolder.CloseAsync(false, cancellationToken).ConfigureAwait(false);\n            }\n            if (!sentFolder.IsOpen || sentFolder.Access != FolderAccess.ReadWrite) {\n                await sentFolder.OpenAsync(FolderAccess.ReadWrite, cancellationToken).ConfigureAwait(false);\n            }\n\n            if (appendAsync is null) {\n                await sentFolder.AppendAsync(message, flags, cancellationToken).ConfigureAwait(false);\n            } else {\n                await appendAsync(sentFolder, message, flags, cancellationToken).ConfigureAwait(false);\n            }\n            return new SmtpAppendExecutionResult {\n                Appended = true,\n                Folder = sentFolder.FullName\n            };\n        } catch (Exception ex) {\n            return new SmtpAppendExecutionResult {\n                Appended = false,\n                Folder = sentFolder.FullName,\n                Error = ex.Message\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpConnectAuthenticateResult.cs",
    "content": "using MailKit.Security;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Result of connecting and authenticating an SMTP session.\n/// </summary>\npublic sealed class SmtpConnectAuthenticateResult {\n    /// <summary>\n    /// True when both connect and authenticate succeeded.\n    /// </summary>\n    public bool IsSuccess { get; init; }\n\n    /// <summary>\n    /// Effective secure socket options used for the connection.\n    /// </summary>\n    public SecureSocketOptions SecureSocketOptions { get; init; } = SecureSocketOptions.Auto;\n\n    /// <summary>\n    /// Stable error code for connect/auth failures.\n    /// </summary>\n    public string ErrorCode { get; init; } = string.Empty;\n\n    /// <summary>\n    /// Human-readable error text.\n    /// </summary>\n    public string Error { get; init; } = string.Empty;\n\n    /// <summary>\n    /// True when the failure is likely transient.\n    /// </summary>\n    public bool IsTransient { get; init; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpConnectionInfo.cs",
    "content": "using MailKit.Net.Smtp;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Represents details about an SMTP server retrieved when testing connectivity.\n/// </summary>\npublic sealed class SmtpConnectionInfo\n{\n    /// <summary>Server that was tested.</summary>\n    public string Server { get; }\n    /// <summary>Port used during the test.</summary>\n    public int Port { get; }\n    /// <summary>Raw banner line returned by the server.</summary>\n    public string? Banner { get; }\n    /// <summary>Server software parsed from the banner if available.</summary>\n    public string? Software { get; }\n    /// <summary>Capabilities advertised by the server.</summary>\n    public SmtpCapabilities Capabilities { get; }\n    /// <summary>Indicates whether the server kept the connection open after a NOOP.</summary>\n    public bool Persistent { get; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SmtpConnectionInfo\"/> class.\n    /// </summary>\n    public SmtpConnectionInfo(string server, int port, string? banner, string? software, SmtpCapabilities capabilities, bool persistent)\n    {\n        Server = server;\n        Port = port;\n        Banner = banner;\n        Software = software;\n        Capabilities = capabilities;\n        Persistent = persistent;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpConnectionPool.cs",
    "content": "namespace Mailozaurr;\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading;\n\n/// <summary>\n/// Manages SMTP connection pooling.\n/// </summary>\npublic static class SmtpConnectionPool {\n    private sealed class PoolEntry {\n        public readonly ConcurrentBag<ClientSmtp> Bag = new();\n        public int Count;\n    }\n\n    private static readonly ConcurrentDictionary<string, PoolEntry> _connectionPool = new();\n\n    private static int _maxPoolSize = 2;\n    private static int _poolingEnabled = 0;\n\n    /// <summary>Maximum number of pooled connections per server/port.</summary>\n    public static int MaxPoolSize => Volatile.Read(ref _maxPoolSize);\n\n    /// <summary>Enables or disables connection pooling.</summary>\n    public static bool PoolingEnabled => Volatile.Read(ref _poolingEnabled) == 1;\n\n    /// <summary>Sets the maximum number of pooled connections per server/port.</summary>\n    /// <param name=\"value\">Maximum number of pooled connections. Must be at least 1.</param>\n    public static void SetMaxPoolSize(int value) {\n        if (value < 1) {\n            throw new ArgumentOutOfRangeException(nameof(value), value, \"Max pool size must be at least 1.\");\n        }\n\n        Interlocked.Exchange(ref _maxPoolSize, value);\n    }\n\n    /// <summary>Enables or disables connection pooling.</summary>\n    /// <param name=\"enabled\">If true, reuses SMTP connections where possible.</param>\n    public static void SetPoolingEnabled(bool enabled) {\n        var newValue = enabled ? 1 : 0;\n        var previous = Interlocked.Exchange(ref _poolingEnabled, newValue);\n        if (previous == 1 && newValue == 0) {\n            ClearConnectionPool();\n        }\n    }\n\n    /// <summary>Configures pooling settings in a single call.</summary>\n    /// <param name=\"poolingEnabled\">Whether pooling should be enabled.</param>\n    /// <param name=\"maxPoolSize\">Maximum number of pooled connections per endpoint.</param>\n    public static void Configure(bool poolingEnabled, int maxPoolSize) {\n        if (maxPoolSize < 1) {\n            throw new ArgumentOutOfRangeException(nameof(maxPoolSize), maxPoolSize, \"Max pool size must be at least 1.\");\n        }\n\n        SetPoolingEnabled(poolingEnabled);\n        SetMaxPoolSize(maxPoolSize);\n    }\n\n    /// <summary>Number of pooled SMTP clients across all servers.</summary>\n    public static int CurrentPoolSize {\n        get {\n            var total = 0;\n            foreach (var entry in _connectionPool.Values) {\n                total += entry.Count;\n            }\n\n            return total;\n        }\n    }\n\n    /// <summary>Raised whenever the pool size changes.</summary>\n    public static event Action<int>? PoolSizeChanged;\n\n    private static void OnPoolSizeChanged() {\n        var size = CurrentPoolSize;\n        PoolSizeChanged?.Invoke(size);\n    }\n\n    private static string EncodeKeyPart(string? value) {\n        return Convert.ToBase64String(Encoding.UTF8.GetBytes(value ?? string.Empty));\n    }\n\n    private static string BuildKey(string server, int port, string? identity) {\n        var serverKey = EncodeKeyPart(server);\n        if (string.IsNullOrWhiteSpace(identity)) {\n            return $\"{serverKey}:{port}\";\n        }\n        var identityKey = EncodeKeyPart(identity);\n        return $\"{serverKey}:{port}:{identityKey}\";\n    }\n\n    internal static ClientSmtp? TryRentClient(string server, int port, string? identity = null) =>\n        TryRentClient(server, port, identity, PoolingEnabled);\n\n    internal static ClientSmtp? TryRentClient(string server, int port, string? identity, bool poolingEnabled) {\n        if (!poolingEnabled) {\n            return null;\n        }\n\n        var key = BuildKey(server, port, identity);\n        if (_connectionPool.TryGetValue(key, out var entry)) {\n            while (entry.Bag.TryTake(out var pooled)) {\n                Interlocked.Decrement(ref entry.Count);\n                OnPoolSizeChanged();\n                if (pooled.IsConnected) {\n                    return pooled;\n                }\n\n                pooled.Dispose();\n            }\n        }\n\n        return null;\n    }\n\n    internal static void ReturnClient(string server, int port, ClientSmtp client, string? identity = null) =>\n        ReturnClient(server, port, client, identity, PoolingEnabled);\n\n    internal static void ReturnClient(string server, int port, ClientSmtp client, string? identity, bool poolingEnabled) {\n        if (!poolingEnabled) {\n            client.Dispose();\n            return;\n        }\n\n        if (!client.IsConnected) {\n            client.Dispose();\n            return;\n        }\n\n        var key = BuildKey(server, port, identity);\n        var entry = _connectionPool.GetOrAdd(key, _ => new PoolEntry());\n        var current = Interlocked.Increment(ref entry.Count);\n        if (current > MaxPoolSize) {\n            Interlocked.Decrement(ref entry.Count);\n            client.Dispose();\n        } else {\n            entry.Bag.Add(client);\n        }\n\n        OnPoolSizeChanged();\n    }\n\n    /// <summary>Disposes all SMTP clients stored in the connection pool.</summary>\n    public static void ClearConnectionPool() {\n        foreach (var entry in _connectionPool.Values) {\n            while (entry.Bag.TryTake(out var client)) {\n                Interlocked.Decrement(ref entry.Count);\n                if (!client.IsConnected) {\n                    client.Dispose();\n                    continue;\n                }\n\n                client.Dispose();\n            }\n        }\n\n        _connectionPool.Clear();\n        OnPoolSizeChanged();\n    }\n\n    /// <summary>Gets a snapshot of the current connection pool state.</summary>\n    /// <returns>Snapshot containing all pooled connections.</returns>\n    public static SmtpConnectionPoolSnapshot GetSnapshot() {\n        var entries = new List<SmtpConnectionPoolEntry>();\n        foreach (var kv in _connectionPool) {\n            var key = kv.Key;\n            var parts = key.Split(':');\n            var server = parts.Length > 0 ? parts[0] : key;\n            var port = 0;\n            if (parts.Length > 1) {\n                int.TryParse(parts[1], out port);\n            }\n            try {\n                server = Encoding.UTF8.GetString(Convert.FromBase64String(server));\n            } catch (FormatException) {\n                // Keep raw server key if decoding fails.\n            }\n            entries.Add(new SmtpConnectionPoolEntry(server, port, kv.Value.Count));\n        }\n\n        return new SmtpConnectionPoolSnapshot(CurrentPoolSize, entries);\n    }\n}\n\n/// <summary>Information about a single SMTP connection pool entry.</summary>\npublic sealed class SmtpConnectionPoolEntry {\n    /// <summary>Server hostname for the pooled connections.</summary>\n    public string Server { get; }\n\n    /// <summary>TCP port for the pooled connections.</summary>\n    public int Port { get; }\n\n    /// <summary>Number of clients in the pool for this server and port.</summary>\n    public int Count { get; }\n\n    internal SmtpConnectionPoolEntry(string server, int port, int count) {\n        Server = server;\n        Port = port;\n        Count = count;\n    }\n}\n\n/// <summary>Represents a snapshot of the SMTP connection pool.</summary>\npublic sealed class SmtpConnectionPoolSnapshot {\n    /// <summary>Total number of pooled SMTP clients.</summary>\n    public int CurrentPoolSize { get; }\n\n    /// <summary>Entries for each server and port combination.</summary>\n    public IReadOnlyList<SmtpConnectionPoolEntry> Entries { get; }\n\n    internal SmtpConnectionPoolSnapshot(int currentPoolSize, IReadOnlyList<SmtpConnectionPoolEntry> entries) {\n        CurrentPoolSize = currentPoolSize;\n        Entries = entries;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpDuplicateProbeResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Describes duplicate sent-copy probe result for idempotent SMTP send flows.\n/// </summary>\npublic sealed class SmtpDuplicateProbeResult {\n    /// <summary>\n    /// Gets a value indicating whether a matching sent copy was found.\n    /// </summary>\n    public bool IsMatch { get; set; }\n\n    /// <summary>\n    /// Gets a folder full name where the match was detected.\n    /// </summary>\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// Gets a detected or fallback message-id associated with the match.\n    /// </summary>\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// Gets empty match result.\n    /// </summary>\n    public static SmtpDuplicateProbeResult None { get; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Class that holds the result of the SMTP operation\n/// </summary>\n/// <remarks>\n/// Used by various send methods to provide feedback about the\n/// outcome of sending an email.\n/// </remarks>\npublic class SmtpResult {\n    /// <summary>Whether the operation succeeded.</summary>\n    public bool Status { get; set; }\n    /// <summary>The type of email action that was performed.</summary>\n    public EmailAction EmailAction { get; set; }\n    /// <summary>The recipients the message was sent to.</summary>\n    public string SentTo { get; set; }\n    /// <summary>The sender address.</summary>\n    public string SentFrom { get; set; }\n    /// <summary>Identifier of the message associated with the result.</summary>\n    public string? MessageId { get; set; }\n    /// <summary>Optional message returned by the operation.</summary>\n    public string? Message { get; set; }\n    /// <summary>Time taken to perform the action.</summary>\n    public TimeSpan TimeToExecute { get; set; }\n    /// <summary>The server used to send the message.</summary>\n    public string Server { get; set; }\n    /// <summary>The port used to connect.</summary>\n    public int Port { get; set; }\n    /// <summary>Error information if the operation failed.</summary>\n    public string? Error { get; set; }\n    /// <summary>Parsed Graph API error details if available.</summary>\n    public GraphApiErrorResponse? GraphError { get; set; }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SmtpResult\"/> class.\n    /// </summary>\n    /// <param name=\"status\">if set to <c>true</c> [status].</param>\n    /// <param name=\"emailAction\">The email action.</param>\n    /// <param name=\"sentTo\">The sent to.</param>\n    /// <param name=\"sentFrom\">The sent from.</param>\n    /// <param name=\"server\">The server.</param>\n    /// <param name=\"port\">The port.</param>\n    /// <param name=\"timeToExecute\">The time to execute.</param>\n    /// <param name=\"outputMessage\">The output message.</param>\n    /// <param name=\"error\">Error information if the operation failed.</param>\n    public SmtpResult(bool status, EmailAction emailAction, string sentTo, string sentFrom, string server, int port, TimeSpan timeToExecute, string? outputMessage = null, string? error = null) {\n        Status = status;\n        SentTo = sentTo;\n        SentFrom = sentFrom;\n        Server = server;\n        Port = port;\n        TimeToExecute = timeToExecute;\n        Message = outputMessage;\n        EmailAction = emailAction;\n        Error = error;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"SmtpResult\"/> class.\n    /// </summary>\n    /// <param name=\"status\">if set to <c>true</c> [status].</param>\n    /// <param name=\"emailAction\">The email action.</param>\n    /// <param name=\"sentTo\">The sent to.</param>\n    /// <param name=\"sentFrom\">The sent from.</param>\n    /// <param name=\"server\">The server.</param>\n    /// <param name=\"port\">The port.</param>\n    /// <param name=\"timeToExecute\">The time to execute.</param>\n    /// <param name=\"loggingConfigurator\">The logging configurator.</param>\n    public SmtpResult(bool status, EmailAction emailAction, string sentTo, string sentFrom, string server, int port, TimeSpan timeToExecute, LoggingConfigurator? loggingConfigurator) {\n        Status = status;\n        SentTo = sentTo;\n        SentFrom = sentFrom;\n        Server = server;\n        Port = port;\n        TimeToExecute = timeToExecute;\n        EmailAction = emailAction;\n\n        if (loggingConfigurator?.ProtocolLogger != null && loggingConfigurator.LogObject) {\n            Message = Encoding.ASCII.GetString(loggingConfigurator.LogStream?.ToArray() ?? Array.Empty<byte>());\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpSecureMime.cs",
    "content": "﻿//using System.Data.SQLite;\n\n//using MimeKit.Cryptography;\n\n//namespace Mailozaurr;\n//public class MySecureMimeContext : DefaultSecureMimeContext {\n//    public static string DatabasePath { get; set; } = \"C:\\\\Temp\\\\certdb.sqlite\";\n\n//    public MySecureMimeContext() : base(OpenDatabase(DatabasePath)) { }\n\n//    static IX509CertificateDatabase OpenDatabase(string fileName) {\n//        var builder = new SQLiteConnectionStringBuilder();\n//        builder.DateTimeFormat = SQLiteDateFormats.Ticks;\n//        builder.DataSource = fileName;\n\n//        if (!File.Exists(fileName)) {\n//            SQLiteConnection.CreateFile(fileName);\n//        }\n\n//        var sqlite = new SQLiteConnection(builder.ConnectionString);\n//        sqlite.Open();\n\n//        return new SqliteCertificateDatabase(sqlite, \"password\");\n//    }\n//}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpSendExecutionResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Describes normalized SMTP send execution outcome for pipeline consumers.\n/// </summary>\npublic sealed class SmtpSendExecutionResult {\n    /// <summary>\n    /// Gets a value indicating whether send execution completed successfully.\n    /// </summary>\n    public bool Ok { get; set; }\n\n    /// <summary>\n    /// Gets a value indicating whether the message was actually sent.\n    /// </summary>\n    public bool Sent { get; set; }\n\n    /// <summary>\n    /// Gets emitted message-id when available.\n    /// </summary>\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// Gets send error when send execution failed.\n    /// </summary>\n    public string? Error { get; set; }\n\n    /// <summary>\n    /// Gets a value indicating whether append-to-sent completed.\n    /// </summary>\n    public bool AppendedToSent { get; set; }\n\n    /// <summary>\n    /// Gets appended sent folder name when append completed.\n    /// </summary>\n    public string? AppendedSentFolder { get; set; }\n\n    /// <summary>\n    /// Gets append-to-sent error when append failed.\n    /// </summary>\n    public string? AppendError { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpSendPipeline.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Search;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides reusable SMTP send-pipeline primitives for threading/idempotency header shaping.\n/// </summary>\npublic static class SmtpSendPipeline {\n    /// <summary>\n    /// Creates a normalized send execution result contract for pipeline consumers.\n    /// </summary>\n    /// <param name=\"sendRequested\">Whether send was requested by caller.</param>\n    /// <param name=\"sendSucceeded\">Whether send execution succeeded.</param>\n    /// <param name=\"messageId\">Optional emitted message-id.</param>\n    /// <param name=\"sendError\">Optional send error.</param>\n    /// <param name=\"appendedToSent\">Whether append-to-sent succeeded.</param>\n    /// <param name=\"appendedSentFolder\">Optional appended sent folder name.</param>\n    /// <param name=\"appendError\">Optional append error.</param>\n    /// <returns>Normalized execution result.</returns>\n    public static SmtpSendExecutionResult BuildExecutionResult(\n        bool sendRequested,\n        bool sendSucceeded,\n        string? messageId,\n        string? sendError,\n        bool appendedToSent,\n        string? appendedSentFolder,\n        string? appendError) {\n        return new SmtpSendExecutionResult {\n            Ok = sendSucceeded,\n            Sent = sendRequested && sendSucceeded,\n            MessageId = messageId,\n            Error = sendError,\n            AppendedToSent = appendedToSent,\n            AppendedSentFolder = appendedSentFolder,\n            AppendError = appendError\n        };\n    }\n\n    /// <summary>\n    /// Builds send execution result and optionally performs append-to-sent orchestration.\n    /// </summary>\n    /// <param name=\"sendRequested\">Whether send was requested by caller.</param>\n    /// <param name=\"sendSucceeded\">Whether send execution succeeded.</param>\n    /// <param name=\"messageId\">Optional emitted message-id.</param>\n    /// <param name=\"sendError\">Optional send error.</param>\n    /// <param name=\"appendToSentRequested\">Whether append-to-sent was requested.</param>\n    /// <param name=\"appendAsync\">Optional append callback executed only on successful real send + append request.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Normalized execution result with append metadata.</returns>\n    public static async Task<SmtpSendExecutionResult> BuildExecutionResultWithOptionalSentAppendAsync(\n        bool sendRequested,\n        bool sendSucceeded,\n        string? messageId,\n        string? sendError,\n        bool appendToSentRequested,\n        Func<CancellationToken, Task<SmtpAppendExecutionResult>>? appendAsync = null,\n        CancellationToken cancellationToken = default) {\n        var appendedToSent = false;\n        string? appendedSentFolder = null;\n        string? appendError = null;\n\n        if (sendRequested && sendSucceeded && appendToSentRequested) {\n            if (appendAsync is null) {\n                appendError = \"append callback is not configured\";\n            } else {\n                try {\n                    var appendResult = await appendAsync(cancellationToken).ConfigureAwait(false) ?? SmtpAppendExecutionResult.None;\n                    appendedToSent = appendResult.Appended;\n                    appendedSentFolder = appendResult.Folder;\n                    appendError = appendResult.Error;\n                } catch (Exception ex) {\n                    appendError = ex.Message;\n                }\n            }\n        }\n\n        return BuildExecutionResult(\n            sendRequested,\n            sendSucceeded,\n            messageId,\n            sendError,\n            appendedToSent,\n            appendedSentFolder,\n            appendError);\n    }\n\n    /// <summary>\n    /// Applies normalized threading/idempotency headers to a MIME message.\n    /// </summary>\n    /// <param name=\"message\">Message instance to update.</param>\n    /// <param name=\"inReplyToCandidate\">Optional In-Reply-To message-id token.</param>\n    /// <param name=\"referenceCandidates\">Optional References message-id tokens.</param>\n    /// <param name=\"messageId\">Optional deterministic Message-Id token.</param>\n    /// <param name=\"idempotencyHeaderName\">Optional custom idempotency header name.</param>\n    /// <param name=\"idempotencyKey\">Optional idempotency header value.</param>\n    public static void ApplyThreadingHeaders(\n        MimeMessage message,\n        string? inReplyToCandidate,\n        IEnumerable<string?>? referenceCandidates,\n        string? messageId = null,\n        string? idempotencyHeaderName = null,\n        string? idempotencyKey = null) {\n        if (message is null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        if (!string.IsNullOrWhiteSpace(idempotencyHeaderName) &&\n            !string.IsNullOrWhiteSpace(idempotencyKey)) {\n            var headerName = idempotencyHeaderName!.Trim();\n            var headerValue = idempotencyKey!.Trim();\n            message.Headers.Replace(headerName, headerValue);\n        }\n\n        var normalizedMessageId = NormalizeMessageIdToken(messageId);\n        if (normalizedMessageId is { Length: > 0 } ensuredMessageId) {\n            message.MessageId = ensuredMessageId;\n        }\n\n        var inReplyTo = NormalizeMessageIdToken(inReplyToCandidate);\n        if (inReplyTo is { Length: > 0 } ensuredInReplyTo) {\n            message.InReplyTo = ensuredInReplyTo;\n        }\n\n        var refs = new List<string>();\n        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n\n        if (referenceCandidates is not null) {\n            foreach (var candidate in referenceCandidates) {\n                AddReference(candidate);\n            }\n        }\n        AddReference(inReplyTo);\n\n        if (refs.Count == 0) {\n            return;\n        }\n\n        message.References.Clear();\n        foreach (var reference in refs) {\n            message.References.Add(reference);\n        }\n\n        void AddReference(string? candidate) {\n            var token = NormalizeMessageIdToken(candidate);\n            if (token is null || token.Length == 0) {\n                return;\n            }\n            if (seen.Add(token)) {\n                refs.Add(token);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Probes a sent folder for an existing copy using idempotency header, then Message-Id fallback.\n    /// </summary>\n    /// <param name=\"sentFolder\">Opened sent folder to probe.</param>\n    /// <param name=\"idempotencyHeaderName\">Header name used for idempotency tagging.</param>\n    /// <param name=\"idempotencyKey\">Idempotency value to search by.</param>\n    /// <param name=\"idempotentMessageId\">Optional deterministic Message-Id fallback.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Duplicate probe result.</returns>\n    public static async Task<SmtpDuplicateProbeResult> TryFindExistingSentCopyAsync(\n        IMailFolder sentFolder,\n        string idempotencyHeaderName,\n        string idempotencyKey,\n        string? idempotentMessageId,\n        CancellationToken cancellationToken = default) {\n        if (sentFolder is null) {\n            throw new ArgumentNullException(nameof(sentFolder));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyHeaderName)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(idempotencyHeaderName));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyKey)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(idempotencyKey));\n        }\n\n        try {\n            var uids = await sentFolder.SearchAsync(SearchQuery.HeaderContains(idempotencyHeaderName!, idempotencyKey!), cancellationToken).ConfigureAwait(false);\n            if (uids.Count == 0) {\n                var token = NormalizeMessageIdToken(idempotentMessageId);\n                if (!string.IsNullOrWhiteSpace(token)) {\n                    uids = await sentFolder.SearchAsync(SearchQuery.HeaderContains(\"Message-Id\", token!), cancellationToken).ConfigureAwait(false);\n                }\n            }\n\n            if (uids.Count == 0) {\n                return SmtpDuplicateProbeResult.None;\n            }\n\n            string? matchedMessageId = null;\n            try {\n                var message = await sentFolder.GetMessageAsync(uids[0], cancellationToken).ConfigureAwait(false);\n                matchedMessageId = message?.MessageId;\n            } catch {\n                // best-effort\n            }\n\n            return new SmtpDuplicateProbeResult {\n                IsMatch = true,\n                Folder = sentFolder.FullName,\n                MessageId = string.IsNullOrWhiteSpace(matchedMessageId) ? idempotentMessageId : matchedMessageId\n            };\n        } catch {\n            return SmtpDuplicateProbeResult.None;\n        }\n    }\n\n    private static string? NormalizeMessageIdToken(string? value) {\n        if (value is null) {\n            return null;\n        }\n\n        var normalized = value.Trim();\n        if (normalized.Length == 0) {\n            return null;\n        }\n        if (normalized.StartsWith(\"<\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(1);\n        }\n        if (normalized.EndsWith(\">\", StringComparison.Ordinal)) {\n            normalized = normalized.Substring(0, normalized.Length - 1);\n        }\n        normalized = normalized.Trim();\n\n        return string.IsNullOrWhiteSpace(normalized) ? null : normalized;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpSentFolderSessionPipeline.cs",
    "content": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MimeKit;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides IMAP sent-folder session execution primitives for SMTP pipeline consumers.\n/// </summary>\npublic static class SmtpSentFolderSessionPipeline {\n    /// <summary>\n    /// Connects IMAP session, resolves sent folder, probes for duplicate sent copy, and disconnects.\n    /// </summary>\n    /// <param name=\"connectAsync\">Callback used to create and connect IMAP client.</param>\n    /// <param name=\"resolveSentFolderAsync\">Callback used to resolve sent folder from connected client.</param>\n    /// <param name=\"idempotencyHeaderName\">Header name used for idempotency lookup.</param>\n    /// <param name=\"idempotencyKey\">Idempotency key value.</param>\n    /// <param name=\"idempotentMessageId\">Optional deterministic message-id fallback.</param>\n    /// <param name=\"disconnectAsync\">Optional disconnect callback override.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Duplicate-probe execution result.</returns>\n    public static async Task<SmtpDuplicateProbeResult> TryFindExistingSentCopyAsync(\n        Func<CancellationToken, Task<ImapClient>> connectAsync,\n        Func<ImapClient, CancellationToken, Task<IMailFolder>> resolveSentFolderAsync,\n        string idempotencyHeaderName,\n        string idempotencyKey,\n        string? idempotentMessageId,\n        Func<ImapClient, CancellationToken, Task>? disconnectAsync = null,\n        CancellationToken cancellationToken = default) {\n        if (connectAsync is null) {\n            throw new ArgumentNullException(nameof(connectAsync));\n        }\n        if (resolveSentFolderAsync is null) {\n            throw new ArgumentNullException(nameof(resolveSentFolderAsync));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyHeaderName)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(idempotencyHeaderName));\n        }\n        if (string.IsNullOrWhiteSpace(idempotencyKey)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(idempotencyKey));\n        }\n\n        var client = await connectAsync(cancellationToken).ConfigureAwait(false)\n            ?? throw new InvalidOperationException(\"Connected IMAP client is required.\");\n\n        try {\n            var sentFolder = await resolveSentFolderAsync(client, cancellationToken).ConfigureAwait(false)\n                ?? throw new InvalidOperationException(\"Resolved sent folder is required.\");\n\n            return await SmtpSendPipeline.TryFindExistingSentCopyAsync(\n                sentFolder,\n                idempotencyHeaderName,\n                idempotencyKey,\n                idempotentMessageId,\n                cancellationToken).ConfigureAwait(false);\n        } finally {\n            await DisconnectAndDisposeAsync(client, disconnectAsync, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// Connects IMAP session, resolves sent folder, appends message, and disconnects.\n    /// </summary>\n    /// <param name=\"connectAsync\">Callback used to create and connect IMAP client.</param>\n    /// <param name=\"resolveSentFolderAsync\">Callback used to resolve sent folder from connected client.</param>\n    /// <param name=\"message\">Message to append.</param>\n    /// <param name=\"flags\">Message flags used for append operation.</param>\n    /// <param name=\"appendAsync\">Optional append callback override for folder append operation.</param>\n    /// <param name=\"disconnectAsync\">Optional disconnect callback override.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Append execution result.</returns>\n    public static async Task<SmtpAppendExecutionResult> TryAppendToSentAsync(\n        Func<CancellationToken, Task<ImapClient>> connectAsync,\n        Func<ImapClient, CancellationToken, Task<IMailFolder>> resolveSentFolderAsync,\n        MimeMessage message,\n        MessageFlags flags = MessageFlags.Seen,\n        Func<IMailFolder, MimeMessage, MessageFlags, CancellationToken, Task>? appendAsync = null,\n        Func<ImapClient, CancellationToken, Task>? disconnectAsync = null,\n        CancellationToken cancellationToken = default) {\n        if (connectAsync is null) {\n            throw new ArgumentNullException(nameof(connectAsync));\n        }\n        if (resolveSentFolderAsync is null) {\n            throw new ArgumentNullException(nameof(resolveSentFolderAsync));\n        }\n        if (message is null) {\n            throw new ArgumentNullException(nameof(message));\n        }\n\n        var client = await connectAsync(cancellationToken).ConfigureAwait(false)\n            ?? throw new InvalidOperationException(\"Connected IMAP client is required.\");\n\n        try {\n            var sentFolder = await resolveSentFolderAsync(client, cancellationToken).ConfigureAwait(false)\n                ?? throw new InvalidOperationException(\"Resolved sent folder is required.\");\n\n            return await SmtpAppendPipeline.TryAppendToSentAsync(\n                sentFolder,\n                message,\n                flags,\n                appendAsync,\n                cancellationToken).ConfigureAwait(false);\n        } finally {\n            await DisconnectAndDisposeAsync(client, disconnectAsync, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// Connects IMAP session, reads threading metadata for a folder + UID, and disconnects.\n    /// </summary>\n    /// <param name=\"connectAsync\">Callback used to create and connect IMAP client.</param>\n    /// <param name=\"folder\">Folder name containing the message.</param>\n    /// <param name=\"uid\">Message UID in the folder.</param>\n    /// <param name=\"getMetadataAsync\">Optional metadata callback override.</param>\n    /// <param name=\"disconnectAsync\">Optional disconnect callback override.</param>\n    /// <param name=\"cancellationToken\">Cancellation token.</param>\n    /// <returns>Threading metadata when found; otherwise null.</returns>\n    public static async Task<ImapSentMessageOperations.ImapThreadingMetadataResult?> TryGetThreadingMetadataAsync(\n        Func<CancellationToken, Task<ImapClient>> connectAsync,\n        string folder,\n        uint uid,\n        Func<ImapClient, string, uint, CancellationToken, Task<ImapSentMessageOperations.ImapThreadingMetadataResult?>>? getMetadataAsync = null,\n        Func<ImapClient, CancellationToken, Task>? disconnectAsync = null,\n        CancellationToken cancellationToken = default) {\n        if (connectAsync is null) {\n            throw new ArgumentNullException(nameof(connectAsync));\n        }\n        if (string.IsNullOrWhiteSpace(folder)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(folder));\n        }\n        if (uid == 0) {\n            throw new ArgumentOutOfRangeException(nameof(uid), \"uid must be greater than zero.\");\n        }\n\n        var client = await connectAsync(cancellationToken).ConfigureAwait(false)\n            ?? throw new InvalidOperationException(\"Connected IMAP client is required.\");\n\n        try {\n            if (getMetadataAsync is null) {\n                return await ImapSentMessageOperations.GetThreadingMetadataAsync(\n                    client,\n                    folder,\n                    uid,\n                    cancellationToken).ConfigureAwait(false);\n            }\n\n            return await getMetadataAsync(client, folder, uid, cancellationToken).ConfigureAwait(false);\n        } finally {\n            await DisconnectAndDisposeAsync(client, disconnectAsync, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private static async Task DisconnectAndDisposeAsync(\n        ImapClient client,\n        Func<ImapClient, CancellationToken, Task>? disconnectAsync,\n        CancellationToken cancellationToken) {\n        try {\n            if (disconnectAsync is not null) {\n                await disconnectAsync(client, cancellationToken).ConfigureAwait(false);\n            } else if (client.IsConnected) {\n                client.Disconnect(true);\n            }\n        } catch {\n            // best-effort cleanup\n        } finally {\n            client.Dispose();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Smtp/SmtpValidation.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Helper methods for validating SMTP connection and authentication settings.\n/// </summary>\npublic static class SmtpValidation\n{\n    /// <summary>\n    /// Validates the SMTP server and port values.\n    /// </summary>\n    public static bool TryValidateServer(string? server, int port, out string? error)\n    {\n        if (string.IsNullOrWhiteSpace(server))\n        {\n            error = \"SMTP server is required.\";\n            return false;\n        }\n\n        if (port <= 0)\n        {\n            error = \"SMTP port must be greater than 0.\";\n            return false;\n        }\n\n        error = null;\n        return true;\n    }\n\n    /// <summary>\n    /// Validates credentials intended for SMTP authentication.\n    /// </summary>\n    public static bool TryValidateCredentials(string? username, string? password, out string? error)\n    {\n        if (string.IsNullOrWhiteSpace(username))\n        {\n            error = \"SMTP username is required.\";\n            return false;\n        }\n\n        if (string.IsNullOrWhiteSpace(password))\n        {\n            error = \"SMTP password is required.\";\n            return false;\n        }\n\n        error = null;\n        return true;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr/Validator.cs",
    "content": "// ReSharper disable StringLiteralTypo\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing EmailValidation;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for validating email addresses and detecting\n/// disposable domains.\n/// </summary>\npublic static class Validator {\n    private static readonly HashSet<string> DisposableDomains;\n    private static readonly HashSet<string> AllowedDomains;\n\n    static Validator() {\n        DisposableDomains = LoadDomainsFromResource(\"disposable_email_blocklist.conf\");\n        AllowedDomains = LoadDomainsFromResource(\"allowlist.conf\");\n    }\n\n    private static HashSet<string> LoadDomainsFromResource(string resourceName) {\n        var assembly = typeof(Validator).Assembly;\n        using var stream = assembly.GetManifestResourceStream($\"Mailozaurr.Resources.{resourceName}\");\n        if (stream == null) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        using var reader = new StreamReader(stream);\n        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        string? line;\n        while ((line = reader.ReadLine()) != null) {\n            line = line.Trim();\n            if (string.IsNullOrWhiteSpace(line) || line.StartsWith(\"#\")) continue;\n            set.Add(line);\n        }\n        return set;\n    }\n    private static bool IsDisposableEmail(string email) {\n        var atIndex = email.IndexOf('@');\n        if (atIndex < 0 || atIndex == email.Length - 1) {\n            return false;\n        }\n        var domain = email.AsSpan(atIndex + 1);\n        return IsDisposableDomain(domain.ToString());\n    }\n\n    /// <summary>\n    /// Determines whether [is disposable domain] [the specified domain].\n    /// </summary>\n    /// <param name=\"domain\">The domain.</param>\n    /// <returns>\n    ///   <c>true</c> if [is disposable domain] [the specified domain]; otherwise, <c>false</c>.\n    /// </returns>\n    private static bool IsDisposableDomain(string domain) {\n        var normalizedDomain = domain.Trim().ToLowerInvariant();\n        if (AllowedDomains.Contains(normalizedDomain)) {\n            return false;\n        }\n        return DisposableDomains.Contains(normalizedDomain);\n    }\n\n    /// <summary>\n    /// Validates the email. This method will validate the email address and check if it is a disposable email address.\n    /// </summary>\n    /// <param name=\"emailAddress\">The email address.</param>\n    /// <param name=\"allowInternational\">if set to <c>true</c> [allow international].</param>\n    /// <param name=\"allowTopLevelDomains\">if set to <c>true</c> [allow top level domains].</param>\n    /// <returns></returns>\n    public static ValidatedEmail ValidateEmail(string emailAddress, bool allowInternational = false, bool allowTopLevelDomains = false) {\n        bool isDisposable = false;\n        try {\n            var isValid = EmailValidator.TryValidate(emailAddress, allowTopLevelDomains, allowInternational, out EmailValidationError errorReason);\n            if (isValid) {\n                isDisposable = IsDisposableEmail(emailAddress);\n            }\n            return new ValidatedEmail {\n                EmailAddress = emailAddress,\n                IsValid = isValid,\n                IsDisposable = isDisposable,\n                Reason = errorReason.Code,\n                ReasonTokenIndex = errorReason.TokenIndex,\n                ReasonErrorIndex = errorReason.ErrorIndex,\n                Error = \"\"\n            };\n        } catch (FormatException ex) {\n            return new ValidatedEmail {\n                EmailAddress = emailAddress,\n                IsValid = false,\n                IsDisposable = false,\n                Reason = EmailValidationError.None.Code,\n                Error = ex.Message\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/AttachmentSummary.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents normalized attachment metadata.\n/// </summary>\npublic sealed class AttachmentSummary {\n    /// <summary>Owning message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific attachment identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Attachment file name.</summary>\n    public string FileName { get; set; } = string.Empty;\n\n    /// <summary>Attachment content type.</summary>\n    public string? ContentType { get; set; }\n\n    /// <summary>Attachment size in bytes when known.</summary>\n    public long? SizeInBytes { get; set; }\n\n    /// <summary>Whether the attachment is inline content.</summary>\n    public bool IsInline { get; set; }\n\n    /// <summary>Inline content id when applicable.</summary>\n    public string? ContentId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/CommonMessageActionsPreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate dry-run result comparing common mailbox actions for the same message selection.\n/// </summary>\npublic sealed class CommonMessageActionsPreview : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Optional custom destination folder value included in the comparison.</summary>\n    public string? RequestedDestinationFolderId { get; set; }\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>Total duplicate or empty message identifiers removed during normalization.</summary>\n    public int DuplicateOrEmptyCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Total number of action previews included in the bundle.</summary>\n    public int IncludedActionCount { get; set; }\n\n    /// <summary>Total number of action previews that are supported and ready.</summary>\n    public int SucceededActionCount { get; set; }\n\n    /// <summary>Total number of action previews that are unsupported or otherwise blocked.</summary>\n    public int FailedActionCount { get; set; }\n\n    /// <summary>Top-level warnings detected during preview normalization.</summary>\n    public List<string> Warnings { get; set; } = new();\n\n    /// <summary>Included action previews such as read-state, flagged-state, archive, trash, move, and delete.</summary>\n    public List<MessageActionPreviewItem> Actions { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/CommonMessageActionsPreviewRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for previewing a common bundle of message actions side by side.\n/// </summary>\npublic sealed class CommonMessageActionsPreviewRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to preview.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Optional custom destination folder identifier or alias to include as a generic move preview.</summary>\n    public string? DestinationFolderId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DeleteMessagesPreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate dry-run result for a planned message delete.\n/// </summary>\npublic sealed class DeleteMessagesPreview : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>Total duplicate or empty message identifiers removed during normalization.</summary>\n    public int DuplicateOrEmptyCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Optional confirmation token that can be supplied when executing the delete.</summary>\n    public string? ConfirmationToken { get; set; }\n\n    /// <summary>Warnings detected during preview.</summary>\n    public List<string> Warnings { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DeleteMessagesPreviewRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for previewing a message delete without executing it.\n/// </summary>\npublic sealed class DeleteMessagesPreviewRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to preview.</summary>\n    public List<string> MessageIds { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DeleteMessagesRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for deleting one or more messages.\n/// </summary>\npublic sealed class DeleteMessagesRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to delete.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Optional confirmation token from a prior preview for this exact action.</summary>\n    public string? ConfirmationToken { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DraftAttachment.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents an attachment that should be included when composing a draft.\n/// </summary>\npublic sealed class DraftAttachment {\n    /// <summary>Absolute or relative source path.</summary>\n    public string Path { get; set; } = string.Empty;\n\n    /// <summary>Optional explicit attachment name override.</summary>\n    public string? FileName { get; set; }\n\n    /// <summary>Optional explicit content type.</summary>\n    public string? ContentType { get; set; }\n\n    /// <summary>Whether the attachment should be embedded inline.</summary>\n    public bool IsInline { get; set; }\n\n    /// <summary>Content id used for inline attachments.</summary>\n    public string? ContentId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DraftMessage.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a normalized draft message before it is queued or sent.\n/// </summary>\npublic sealed class DraftMessage {\n    /// <summary>Source profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Sender address or display entry.</summary>\n    public MessageRecipient? From { get; set; }\n\n    /// <summary>Primary recipients.</summary>\n    public List<MessageRecipient> To { get; set; } = new();\n\n    /// <summary>Carbon-copy recipients.</summary>\n    public List<MessageRecipient> Cc { get; set; } = new();\n\n    /// <summary>Blind carbon-copy recipients.</summary>\n    public List<MessageRecipient> Bcc { get; set; } = new();\n\n    /// <summary>Reply-to recipients.</summary>\n    public List<MessageRecipient> ReplyTo { get; set; } = new();\n\n    /// <summary>Message subject.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Plain text body.</summary>\n    public string? TextBody { get; set; }\n\n    /// <summary>HTML body.</summary>\n    public string? HtmlBody { get; set; }\n\n    /// <summary>Attachments included with the draft.</summary>\n    public List<DraftAttachment> Attachments { get; set; } = new();\n\n    /// <summary>Optional custom headers.</summary>\n    public Dictionary<string, string> Headers { get; set; } = new(StringComparer.OrdinalIgnoreCase);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/DraftMimeMessageFactory.cs",
    "content": "using MimeKit;\nusing MimeKit.Utils;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Converts normalized draft messages into MIME messages that can be reused across send providers.\n/// </summary>\npublic sealed class DraftMimeMessageFactory : IDraftMimeMessageFactory {\n    /// <inheritdoc />\n    public Task<MimeMessage> CreateAsync(MailProfile profile, DraftMessage draft, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (draft == null) {\n            throw new ArgumentNullException(nameof(draft));\n        }\n\n        cancellationToken.ThrowIfCancellationRequested();\n\n        var message = new MimeMessage {\n            Subject = draft.Subject ?? string.Empty,\n            Date = DateTimeOffset.UtcNow,\n            MessageId = MimeUtils.GenerateMessageId()\n        };\n\n        AddSender(message, profile, draft);\n        AddRecipients(message.To, draft.To);\n        AddRecipients(message.Cc, draft.Cc);\n        AddRecipients(message.Bcc, draft.Bcc);\n        AddRecipients(message.ReplyTo, draft.ReplyTo);\n        if (!message.To.Any() && !message.Cc.Any() && !message.Bcc.Any()) {\n            throw new InvalidOperationException(\"Draft message requires at least one recipient.\");\n        }\n\n        AddHeaders(message, draft.Headers);\n        message.Body = BuildBody(draft, cancellationToken);\n        return Task.FromResult(message);\n    }\n\n    private static void AddSender(MimeMessage message, MailProfile profile, DraftMessage draft) {\n        var sender = draft.From;\n        if (sender != null) {\n            message.From.Add(CreateMailboxAddress(sender));\n            return;\n        }\n\n        if (!string.IsNullOrWhiteSpace(profile.DefaultSender)) {\n            message.From.Add(MailboxAddress.Parse(profile.DefaultSender!.Trim()));\n            return;\n        }\n\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            message.From.Add(MailboxAddress.Parse(profile.DefaultMailbox!.Trim()));\n            return;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' does not define a sender and the draft does not specify one.\");\n    }\n\n    private static void AddRecipients(InternetAddressList target, IEnumerable<MessageRecipient> recipients) {\n        if (target == null) {\n            throw new ArgumentNullException(nameof(target));\n        }\n        if (recipients == null) {\n            return;\n        }\n\n        foreach (var recipient in recipients) {\n            if (recipient == null || string.IsNullOrWhiteSpace(recipient.Address)) {\n                continue;\n            }\n\n            target.Add(CreateMailboxAddress(recipient));\n        }\n    }\n\n    private static MailboxAddress CreateMailboxAddress(MessageRecipient recipient) {\n        if (recipient == null) {\n            throw new ArgumentNullException(nameof(recipient));\n        }\n        if (string.IsNullOrWhiteSpace(recipient.Address)) {\n            throw new InvalidOperationException(\"Recipient address cannot be empty.\");\n        }\n\n        return string.IsNullOrWhiteSpace(recipient.Name)\n            ? MailboxAddress.Parse(recipient.Address.Trim())\n            : new MailboxAddress(recipient.Name!.Trim(), recipient.Address.Trim());\n    }\n\n    private static void AddHeaders(MimeMessage message, IReadOnlyDictionary<string, string> headers) {\n        if (headers == null || headers.Count == 0) {\n            return;\n        }\n\n        foreach (var header in headers) {\n            if (string.IsNullOrWhiteSpace(header.Key) || string.IsNullOrWhiteSpace(header.Value)) {\n                continue;\n            }\n\n            message.Headers.Add(header.Key.Trim(), header.Value.Trim());\n        }\n    }\n\n    private static MimeEntity BuildBody(DraftMessage draft, CancellationToken cancellationToken) {\n        var bodyBuilder = new BodyBuilder();\n        if (!string.IsNullOrWhiteSpace(draft.TextBody)) {\n            bodyBuilder.TextBody = draft.TextBody;\n        }\n        if (!string.IsNullOrWhiteSpace(draft.HtmlBody)) {\n            bodyBuilder.HtmlBody = draft.HtmlBody;\n        }\n\n        foreach (var attachment in draft.Attachments) {\n            cancellationToken.ThrowIfCancellationRequested();\n            if (attachment == null || string.IsNullOrWhiteSpace(attachment.Path)) {\n                continue;\n            }\n\n            var entity = CreateAttachmentEntity(attachment);\n            if (attachment.IsInline) {\n                bodyBuilder.LinkedResources.Add(entity);\n            } else {\n                bodyBuilder.Attachments.Add(entity);\n            }\n        }\n\n        return bodyBuilder.ToMessageBody();\n    }\n\n    private static MimeEntity CreateAttachmentEntity(DraftAttachment attachment) {\n        var fullPath = Path.GetFullPath(attachment.Path);\n        if (!File.Exists(fullPath)) {\n            throw new FileNotFoundException($\"Attachment '{attachment.Path}' was not found.\", fullPath);\n        }\n\n        var fileName = string.IsNullOrWhiteSpace(attachment.FileName)\n            ? Path.GetFileName(fullPath)\n            : attachment.FileName!.Trim();\n        var contentType = ResolveContentType(attachment, fileName);\n        var part = new MimePart(contentType) {\n            Content = new MimeContent(new MemoryStream(File.ReadAllBytes(fullPath), writable: false)),\n            FileName = fileName,\n            ContentDisposition = new ContentDisposition(attachment.IsInline ? ContentDisposition.Inline : ContentDisposition.Attachment),\n            ContentTransferEncoding = ContentEncoding.Base64\n        };\n\n        if (attachment.IsInline) {\n            part.ContentId = string.IsNullOrWhiteSpace(attachment.ContentId)\n                ? MimeUtils.GenerateMessageId()\n                : attachment.ContentId!.Trim();\n        }\n\n        return part;\n    }\n\n    private static ContentType ResolveContentType(DraftAttachment attachment, string fileName) {\n        if (!string.IsNullOrWhiteSpace(attachment.ContentType)) {\n            return ContentType.Parse(attachment.ContentType!.Trim());\n        }\n\n        return ContentType.Parse(MimeTypes.GetMimeType(fileName));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FileMailDraftStore.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Stores reusable drafts in a JSON document on disk.\n/// </summary>\npublic sealed class FileMailDraftStore : IMailDraftStore {\n    private readonly string _filePath;\n    private readonly SemaphoreSlim _gate = new(1, 1);\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <summary>\n    /// Creates a new store using the provided options.\n    /// </summary>\n    public FileMailDraftStore(MailDraftStoreOptions? options = null)\n        : this((options ?? new MailDraftStoreOptions()).GetFilePath()) {\n    }\n\n    /// <summary>\n    /// Creates a new store using the specified file path.\n    /// </summary>\n    public FileMailDraftStore(string filePath) {\n        _filePath = Path.GetFullPath(filePath ?? throw new ArgumentNullException(nameof(filePath)));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailDraft>> GetAllAsync(CancellationToken cancellationToken = default) {\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            return document.Drafts\n                .Select(CloneDraft)\n                .OrderBy(draft => draft.Name, StringComparer.OrdinalIgnoreCase)\n                .ThenBy(draft => draft.Id, StringComparer.OrdinalIgnoreCase)\n                .ToArray();\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<MailDraft?> GetByIdAsync(string draftId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(draftId)) {\n            throw new ArgumentException(\"Draft id is required.\", nameof(draftId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var draft = document.Drafts.FirstOrDefault(existing => string.Equals(existing.Id, draftId, StringComparison.OrdinalIgnoreCase));\n            return draft == null ? null : CloneDraft(draft);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task SaveAsync(MailDraft draft, CancellationToken cancellationToken = default) {\n        ValidateDraft(draft);\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var index = document.Drafts.FindIndex(existing => string.Equals(existing.Id, draft.Id, StringComparison.OrdinalIgnoreCase));\n            var draftToStore = CloneDraft(draft);\n            if (draftToStore.CreatedAt == default) {\n                draftToStore.CreatedAt = DateTimeOffset.UtcNow;\n            }\n            draftToStore.UpdatedAt = DateTimeOffset.UtcNow;\n\n            if (index >= 0) {\n                draftToStore.CreatedAt = document.Drafts[index].CreatedAt == default\n                    ? draftToStore.CreatedAt\n                    : document.Drafts[index].CreatedAt;\n                document.Drafts[index] = draftToStore;\n            } else {\n                document.Drafts.Add(draftToStore);\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<bool> RemoveAsync(string draftId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(draftId)) {\n            throw new ArgumentException(\"Draft id is required.\", nameof(draftId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var removed = document.Drafts.RemoveAll(existing => string.Equals(existing.Id, draftId, StringComparison.OrdinalIgnoreCase)) > 0;\n            if (!removed) {\n                return false;\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n            return true;\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    private async Task<MailDraftStoreDocument> LoadDocumentAsync(CancellationToken cancellationToken) {\n        if (!File.Exists(_filePath)) {\n            return new MailDraftStoreDocument();\n        }\n\n        using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) {\n            var document = await JsonSerializer.DeserializeAsync<MailDraftStoreDocument>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n            return document ?? new MailDraftStoreDocument();\n        }\n    }\n\n    private async Task SaveDocumentAsync(MailDraftStoreDocument document, CancellationToken cancellationToken) {\n        var directory = Path.GetDirectoryName(_filePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            throw new InvalidOperationException(\"Draft store path is invalid.\");\n        }\n\n        Directory.CreateDirectory(directory);\n\n        var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n        try {\n            using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) {\n                await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken).ConfigureAwait(false);\n                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n            }\n\n            if (File.Exists(_filePath)) {\n                File.Delete(_filePath);\n            }\n\n            File.Move(tempPath, _filePath);\n            tempPath = string.Empty;\n        } finally {\n            if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                File.Delete(tempPath);\n            }\n        }\n    }\n\n    private static void ValidateDraft(MailDraft? draft) {\n        if (draft == null) {\n            throw new ArgumentNullException(nameof(draft));\n        }\n        if (string.IsNullOrWhiteSpace(draft.Id)) {\n            throw new InvalidOperationException(\"Draft id is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(draft.Name)) {\n            throw new InvalidOperationException(\"Draft name is required.\");\n        }\n        if (draft.Message == null) {\n            throw new InvalidOperationException(\"Draft message is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(draft.Message.ProfileId)) {\n            throw new InvalidOperationException(\"Draft message profile id is required.\");\n        }\n    }\n\n    private static MailDraft CloneDraft(MailDraft draft) => new() {\n        Id = draft.Id,\n        Name = draft.Name,\n        CreatedAt = draft.CreatedAt,\n        UpdatedAt = draft.UpdatedAt,\n        Message = CloneMessage(draft.Message)\n    };\n\n    private static DraftMessage CloneMessage(DraftMessage message) => new() {\n        ProfileId = message.ProfileId,\n        From = message.From == null ? null : CloneRecipient(message.From),\n        To = message.To.Select(CloneRecipient).ToList(),\n        Cc = message.Cc.Select(CloneRecipient).ToList(),\n        Bcc = message.Bcc.Select(CloneRecipient).ToList(),\n        ReplyTo = message.ReplyTo.Select(CloneRecipient).ToList(),\n        Subject = message.Subject,\n        TextBody = message.TextBody,\n        HtmlBody = message.HtmlBody,\n        Headers = new Dictionary<string, string>(message.Headers, StringComparer.OrdinalIgnoreCase),\n        Attachments = message.Attachments.Select(CloneAttachment).ToList()\n    };\n\n    private static MessageRecipient CloneRecipient(MessageRecipient recipient) => new() {\n        Name = recipient.Name,\n        Address = recipient.Address\n    };\n\n    private static DraftAttachment CloneAttachment(DraftAttachment attachment) => new() {\n        Path = attachment.Path,\n        FileName = attachment.FileName,\n        ContentType = attachment.ContentType,\n        IsInline = attachment.IsInline,\n        ContentId = attachment.ContentId\n    };\n\n    private sealed class MailDraftStoreDocument {\n        public int Version { get; set; } = 1;\n\n        public List<MailDraft> Drafts { get; set; } = new();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FileMailMessageActionPlanBatchStore.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Stores reusable message action plan batches in a JSON document on disk.\n/// </summary>\npublic sealed class FileMailMessageActionPlanBatchStore : IMailMessageActionPlanBatchStore {\n    private readonly string _filePath;\n    private readonly SemaphoreSlim _gate = new(1, 1);\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <summary>\n    /// Creates a new store using the provided options.\n    /// </summary>\n    public FileMailMessageActionPlanBatchStore(MailMessageActionPlanBatchStoreOptions? options = null)\n        : this((options ?? new MailMessageActionPlanBatchStoreOptions()).GetFilePath()) {\n    }\n\n    /// <summary>\n    /// Creates a new store using the specified file path.\n    /// </summary>\n    public FileMailMessageActionPlanBatchStore(string filePath) {\n        _filePath = Path.GetFullPath(filePath ?? throw new ArgumentNullException(nameof(filePath)));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailMessageActionPlanBatch>> GetAllAsync(CancellationToken cancellationToken = default) {\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            return document.Batches\n                .Select(CloneBatch)\n                .OrderBy(batch => batch.Name, StringComparer.OrdinalIgnoreCase)\n                .ThenBy(batch => batch.Id, StringComparer.OrdinalIgnoreCase)\n                .ToArray();\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<MailMessageActionPlanBatch?> GetByIdAsync(string batchId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var batch = document.Batches.FirstOrDefault(existing => string.Equals(existing.Id, batchId, StringComparison.OrdinalIgnoreCase));\n            return batch == null ? null : CloneBatch(batch);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default) {\n        ValidateBatch(batch);\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var index = document.Batches.FindIndex(existing => string.Equals(existing.Id, batch.Id, StringComparison.OrdinalIgnoreCase));\n            var batchToStore = CloneBatch(batch);\n            if (batchToStore.CreatedAt == default) {\n                batchToStore.CreatedAt = DateTimeOffset.UtcNow;\n            }\n            batchToStore.UpdatedAt = DateTimeOffset.UtcNow;\n\n            if (index >= 0) {\n                batchToStore.CreatedAt = document.Batches[index].CreatedAt == default\n                    ? batchToStore.CreatedAt\n                    : document.Batches[index].CreatedAt;\n                document.Batches[index] = batchToStore;\n            } else {\n                document.Batches.Add(batchToStore);\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<bool> RemoveAsync(string batchId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var removed = document.Batches.RemoveAll(existing => string.Equals(existing.Id, batchId, StringComparison.OrdinalIgnoreCase)) > 0;\n            if (!removed) {\n                return false;\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n            return true;\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    private async Task<MailMessageActionPlanBatchStoreDocument> LoadDocumentAsync(CancellationToken cancellationToken) {\n        if (!File.Exists(_filePath)) {\n            return new MailMessageActionPlanBatchStoreDocument();\n        }\n\n        using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) {\n            var document = await JsonSerializer.DeserializeAsync<MailMessageActionPlanBatchStoreDocument>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n            return document ?? new MailMessageActionPlanBatchStoreDocument();\n        }\n    }\n\n    private async Task SaveDocumentAsync(MailMessageActionPlanBatchStoreDocument document, CancellationToken cancellationToken) {\n        var directory = Path.GetDirectoryName(_filePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            throw new InvalidOperationException(\"Action plan batch store path is invalid.\");\n        }\n\n        Directory.CreateDirectory(directory);\n\n        var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n        try {\n            using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) {\n                await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken).ConfigureAwait(false);\n                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n            }\n\n            if (File.Exists(_filePath)) {\n                File.Delete(_filePath);\n            }\n\n            File.Move(tempPath, _filePath);\n            tempPath = string.Empty;\n        } finally {\n            if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                File.Delete(tempPath);\n            }\n        }\n    }\n\n    private static void ValidateBatch(MailMessageActionPlanBatch? batch) {\n        if (batch == null) {\n            throw new ArgumentNullException(nameof(batch));\n        }\n        if (string.IsNullOrWhiteSpace(batch.Id)) {\n            throw new InvalidOperationException(\"Action plan batch id is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(batch.Name)) {\n            throw new InvalidOperationException(\"Action plan batch name is required.\");\n        }\n        if (batch.Plans == null) {\n            throw new InvalidOperationException(\"Action plan batch plans are required.\");\n        }\n    }\n\n    private static MailMessageActionPlanBatch CloneBatch(MailMessageActionPlanBatch batch) => new() {\n        Id = batch.Id,\n        Name = batch.Name,\n        Description = batch.Description,\n        CreatedAt = batch.CreatedAt,\n        UpdatedAt = batch.UpdatedAt,\n        Plans = batch.Plans.Select(ClonePlan).ToList()\n    };\n\n    private static MessageActionExecutionPlan ClonePlan(MessageActionExecutionPlan plan) => new() {\n        Succeeded = plan.Succeeded,\n        Code = plan.Code,\n        Message = plan.Message,\n        Name = plan.Name,\n        Summary = plan.Summary,\n        Action = plan.Action,\n        ExecutionKind = plan.ExecutionKind,\n        ProfileId = plan.ProfileId,\n        MailboxId = plan.MailboxId,\n        FolderId = plan.FolderId,\n        RequestedCount = plan.RequestedCount,\n        UniqueMessageCount = plan.UniqueMessageCount,\n        MessageIds = plan.MessageIds.ToList(),\n        RequestedDestinationFolderId = plan.RequestedDestinationFolderId,\n        Destination = plan.Destination == null\n            ? null\n            : new MailFolderTargetResolution {\n                ProfileId = plan.Destination.ProfileId,\n                MailboxId = plan.Destination.MailboxId,\n                RequestedValue = plan.Destination.RequestedValue,\n                IsAlias = plan.Destination.IsAlias,\n                Alias = plan.Destination.Alias,\n                IsSupported = plan.Destination.IsSupported,\n                IsResolved = plan.Destination.IsResolved,\n                EffectiveFolderId = plan.Destination.EffectiveFolderId,\n                FolderDisplayName = plan.Destination.FolderDisplayName,\n                FolderPath = plan.Destination.FolderPath,\n                Summary = plan.Destination.Summary\n            },\n        DesiredState = plan.DesiredState,\n        ConfirmationToken = plan.ConfirmationToken,\n        ConfirmationProvided = plan.ConfirmationProvided,\n        ConfirmationValidated = plan.ConfirmationValidated,\n        Warnings = plan.Warnings.ToList()\n    };\n\n    private sealed class MailMessageActionPlanBatchStoreDocument {\n        public int Version { get; set; } = 1;\n\n        public List<MailMessageActionPlanBatch> Batches { get; set; } = new();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FileMailProfileStore.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Stores profiles in a JSON document on disk.\n/// </summary>\npublic sealed class FileMailProfileStore : IMailProfileStore {\n    private readonly string _filePath;\n    private readonly SemaphoreSlim _gate = new(1, 1);\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <summary>\n    /// Creates a new store using the provided options.\n    /// </summary>\n    public FileMailProfileStore(MailProfileStoreOptions? options = null)\n        : this((options ?? new MailProfileStoreOptions()).GetFilePath()) {\n    }\n\n    /// <summary>\n    /// Creates a new store using the specified file path.\n    /// </summary>\n    public FileMailProfileStore(string filePath) {\n        _filePath = Path.GetFullPath(filePath ?? throw new ArgumentNullException(nameof(filePath)));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) {\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            return CloneProfiles(document.Profiles);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var profile = document.Profiles.FirstOrDefault(p => string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase));\n            return profile == null ? null : CloneProfile(profile);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        ValidateProfile(profile);\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var existingIndex = document.Profiles.FindIndex(p => string.Equals(p.Id, profile.Id, StringComparison.OrdinalIgnoreCase));\n            var profileToStore = CloneProfile(profile);\n\n            if (profileToStore.IsDefault) {\n                foreach (var existing in document.Profiles) {\n                    existing.IsDefault = false;\n                }\n            }\n\n            if (existingIndex >= 0) {\n                document.Profiles[existingIndex] = profileToStore;\n            } else {\n                document.Profiles.Add(profileToStore);\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var removed = document.Profiles.RemoveAll(p => string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase)) > 0;\n            if (!removed) {\n                return false;\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n            return true;\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    private async Task<MailProfileStoreDocument> LoadDocumentAsync(CancellationToken cancellationToken) {\n        if (!File.Exists(_filePath)) {\n            return new MailProfileStoreDocument();\n        }\n\n        using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) {\n            var document = await JsonSerializer.DeserializeAsync<MailProfileStoreDocument>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n            return document ?? new MailProfileStoreDocument();\n        }\n    }\n\n    private async Task SaveDocumentAsync(MailProfileStoreDocument document, CancellationToken cancellationToken) {\n        var directory = Path.GetDirectoryName(_filePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            throw new InvalidOperationException(\"Profile store path is invalid.\");\n        }\n\n        Directory.CreateDirectory(directory);\n\n        var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n        try {\n            using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) {\n                await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken).ConfigureAwait(false);\n                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n            }\n\n            if (File.Exists(_filePath)) {\n                File.Delete(_filePath);\n            }\n\n            File.Move(tempPath, _filePath);\n            tempPath = string.Empty;\n        } finally {\n            if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                File.Delete(tempPath);\n            }\n        }\n    }\n\n    private static void ValidateProfile(MailProfile? profile) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n\n        if (string.IsNullOrWhiteSpace(profile.Id)) {\n            throw new InvalidOperationException(\"Profile id is required.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(profile.DisplayName)) {\n            throw new InvalidOperationException(\"Profile display name is required.\");\n        }\n    }\n\n    private static IReadOnlyList<MailProfile> CloneProfiles(IEnumerable<MailProfile> profiles) =>\n        profiles.Select(CloneProfile).ToArray();\n\n    private static MailProfile CloneProfile(MailProfile profile) => new() {\n        Id = profile.Id,\n        DisplayName = profile.DisplayName,\n        Description = profile.Description,\n        Kind = profile.Kind,\n        DefaultSender = profile.DefaultSender,\n        DefaultMailbox = profile.DefaultMailbox,\n        IsDefault = profile.IsDefault,\n        Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n        Capabilities = profile.Capabilities == null\n            ? null\n            : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n    };\n\n    private sealed class MailProfileStoreDocument {\n        public int Version { get; set; } = 1;\n\n        public List<MailProfile> Profiles { get; set; } = new();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FileMailSecretStore.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Stores protected secrets in a JSON document on disk.\n/// </summary>\npublic sealed class FileMailSecretStore : IMailSecretStore {\n    private readonly string _filePath;\n    private readonly ICredentialProtector _protector;\n    private readonly SemaphoreSlim _gate = new(1, 1);\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <summary>\n    /// Creates a new store using the default credential protector.\n    /// </summary>\n    public FileMailSecretStore(MailSecretStoreOptions? options = null)\n        : this((options ?? new MailSecretStoreOptions()).GetFilePath(), CredentialProtection.Default) {\n    }\n\n    /// <summary>\n    /// Creates a new store using the specified file path and protector.\n    /// </summary>\n    public FileMailSecretStore(string filePath, ICredentialProtector protector) {\n        _filePath = Path.GetFullPath(filePath ?? throw new ArgumentNullException(nameof(filePath)));\n        _protector = protector ?? throw new ArgumentNullException(nameof(protector));\n    }\n\n    /// <inheritdoc />\n    public async Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n        ValidateKeyPart(profileId, nameof(profileId));\n        ValidateKeyPart(secretName, nameof(secretName));\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            if (!document.Secrets.TryGetValue(CreateKey(profileId, secretName), out var protectedValue)) {\n                return null;\n            }\n\n            return _protector.Unprotect(protectedValue);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n        ValidateKeyPart(profileId, nameof(profileId));\n        ValidateKeyPart(secretName, nameof(secretName));\n        if (secretValue == null) {\n            throw new ArgumentNullException(nameof(secretValue));\n        }\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            document.Secrets[CreateKey(profileId, secretName)] = _protector.Protect(secretValue);\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n        ValidateKeyPart(profileId, nameof(profileId));\n        ValidateKeyPart(secretName, nameof(secretName));\n\n        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);\n        try {\n            var document = await LoadDocumentAsync(cancellationToken).ConfigureAwait(false);\n            var removed = document.Secrets.Remove(CreateKey(profileId, secretName));\n            if (!removed) {\n                return false;\n            }\n\n            await SaveDocumentAsync(document, cancellationToken).ConfigureAwait(false);\n            return true;\n        } finally {\n            _gate.Release();\n        }\n    }\n\n    private async Task<MailSecretStoreDocument> LoadDocumentAsync(CancellationToken cancellationToken) {\n        if (!File.Exists(_filePath)) {\n            return new MailSecretStoreDocument();\n        }\n\n        using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) {\n            var document = await JsonSerializer.DeserializeAsync<MailSecretStoreDocument>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n            return document ?? new MailSecretStoreDocument();\n        }\n    }\n\n    private async Task SaveDocumentAsync(MailSecretStoreDocument document, CancellationToken cancellationToken) {\n        var directory = Path.GetDirectoryName(_filePath);\n        if (string.IsNullOrWhiteSpace(directory)) {\n            throw new InvalidOperationException(\"Secret store path is invalid.\");\n        }\n\n        Directory.CreateDirectory(directory);\n\n        var tempPath = Path.Combine(directory, Path.GetRandomFileName());\n        var backupPath = Path.Combine(directory, Path.GetRandomFileName());\n        try {\n            using (var stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) {\n                await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken).ConfigureAwait(false);\n                await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n            }\n\n            if (File.Exists(_filePath)) {\n                File.Replace(tempPath, _filePath, backupPath);\n                tempPath = string.Empty;\n                if (File.Exists(backupPath)) {\n                    File.Delete(backupPath);\n                }\n                backupPath = string.Empty;\n                return;\n            }\n\n            File.Move(tempPath, _filePath);\n            tempPath = string.Empty;\n        } finally {\n            if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) {\n                File.Delete(tempPath);\n            }\n            if (!string.IsNullOrEmpty(backupPath) && File.Exists(backupPath)) {\n                File.Delete(backupPath);\n            }\n        }\n    }\n\n    private static string CreateKey(string profileId, string secretName) => $\"{profileId.Trim()}::{secretName.Trim()}\";\n\n    private static void ValidateKeyPart(string value, string parameterName) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            throw new ArgumentException(\"A non-empty value is required.\", parameterName);\n        }\n    }\n\n    private sealed class MailSecretStoreDocument {\n        public int Version { get; set; } = 1;\n\n        public Dictionary<string, string> Secrets { get; set; } = new(StringComparer.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FolderRef.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Identifies a folder or folder-like mailbox container.\n/// </summary>\npublic sealed class FolderRef {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Provider-specific folder identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing folder name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Full path or canonical folder name.</summary>\n    public string? Path { get; set; }\n\n    /// <summary>Provider-specific special use marker such as Inbox or Sent.</summary>\n    public string? SpecialUse { get; set; }\n\n    /// <summary>Total message count when known.</summary>\n    public int? MessageCount { get; set; }\n\n    /// <summary>Unread message count when known.</summary>\n    public int? UnreadCount { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/FolderRefCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a folder or folder-like mailbox container.\n/// </summary>\npublic sealed class FolderRefCompact {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Provider-specific folder identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing folder name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Full path or canonical folder name.</summary>\n    public string? Path { get; set; }\n\n    /// <summary>Provider-specific special use marker such as Inbox or Sent.</summary>\n    public string? SpecialUse { get; set; }\n\n    /// <summary>Total message count when known.</summary>\n    public int? MessageCount { get; set; }\n\n    /// <summary>Unread message count when known.</summary>\n    public int? UnreadCount { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GetMessageRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for retrieving a message.\n/// </summary>\npublic sealed class GetMessageRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Whether raw content should be included when available.</summary>\n    public bool IncludeRawContent { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GetMessagesRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for retrieving multiple messages.\n/// </summary>\npublic sealed class GetMessagesRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Whether raw content should be included when available.</summary>\n    public bool IncludeRawContent { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailMailMessageActionHandler.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Gmail mailbox action handler backed by Mailozaurr Gmail helpers.\n/// </summary>\npublic sealed class GmailMailMessageActionHandler : IMailMessageActionHandler {\n    private readonly IGmailSessionFactory _sessionFactory;\n    private readonly Func<GmailSession, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>> _setReadStateAsync;\n    private readonly Func<GmailSession, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>> _moveAsync;\n    private readonly Func<GmailSession, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>> _deleteAsync;\n\n    /// <summary>\n    /// Creates a new Gmail message-action handler.\n    /// </summary>\n    public GmailMailMessageActionHandler(\n        IGmailSessionFactory sessionFactory,\n        Func<GmailSession, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>>? setReadStateAsync = null,\n        Func<GmailSession, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>>? moveAsync = null,\n        Func<GmailSession, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>>? deleteAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _setReadStateAsync = setReadStateAsync ?? DefaultSetReadStateAsync;\n        _moveAsync = moveAsync ?? DefaultMoveAsync;\n        _deleteAsync = deleteAsync ?? DefaultDeleteAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Gmail;\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _setReadStateAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        var results = await session.Browser.SetMessagesFlaggedAsync(\n            request.MessageIds.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToArray(),\n            request.IsFlagged,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, request.IsFlagged ? \"Flagged messages.\" : \"Unflagged messages.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _moveAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _deleteAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<MessageActionResult> DefaultSetReadStateAsync(GmailSession session, MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken) {\n        var results = await session.Browser.SetMessagesSeenAsync(NormalizeIds(request.MessageIds), request.IsRead, cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, request.IsRead ? \"Marked messages as read.\" : \"Marked messages as unread.\");\n    }\n\n    private static async Task<MessageActionResult> DefaultMoveAsync(GmailSession session, MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken) {\n        var results = await session.Browser.MoveMessagesAsync(\n            NormalizeIds(request.MessageIds),\n            request.FolderId,\n            request.DestinationFolderId,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, $\"Moved messages to '{request.DestinationFolderId}'.\");\n    }\n\n    private static async Task<MessageActionResult> DefaultDeleteAsync(GmailSession session, MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken) {\n        var results = await session.Browser.DeleteMessagesAsync(NormalizeIds(request.MessageIds), cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, \"Deleted messages.\");\n    }\n\n    private static IReadOnlyList<string> NormalizeIds(IEnumerable<string> messageIds) =>\n        messageIds.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToArray();\n\n    private static MessageActionResult MapResult(string profileId, IReadOnlyList<MailboxBulkOperationResult> items, string successMessage) {\n        var result = new MessageActionResult {\n            ProfileId = profileId,\n            RequestedCount = items.Count\n        };\n\n        foreach (var item in items) {\n            result.Results.Add(new MessageActionItemResult {\n                MessageId = item.Id,\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Ok ? null : item.Error\n            });\n            if (item.Ok) {\n                result.SucceededCount++;\n            } else {\n                result.FailedCount++;\n            }\n        }\n\n        result.Succeeded = result.FailedCount == 0 && result.SucceededCount > 0;\n        result.Code = result.Succeeded ? null : \"message_action_failed\";\n        result.Message = result.Succeeded\n            ? successMessage\n            : $\"{result.SucceededCount} message action(s) succeeded; {result.FailedCount} failed.\";\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailMailReadHandler.cs",
    "content": "using System.Globalization;\nusing MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Gmail read handler backed by Mailozaurr Gmail helpers.\n/// </summary>\npublic sealed class GmailMailReadHandler : IMailReadHandler {\n    private readonly IGmailSessionFactory _sessionFactory;\n    private readonly Func<GmailSession, MailProfile, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>> _getFoldersAsync;\n    private readonly Func<GmailSession, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>> _searchAsync;\n    private readonly Func<GmailSession, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>> _getMessageAsync;\n    private readonly Func<GmailSession, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>> _saveAttachmentAsync;\n\n    /// <summary>\n    /// Creates a new Gmail read handler.\n    /// </summary>\n    public GmailMailReadHandler(\n        IGmailSessionFactory sessionFactory,\n        Func<GmailSession, MailProfile, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>>? getFoldersAsync = null,\n        Func<GmailSession, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>>? searchAsync = null,\n        Func<GmailSession, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>>? getMessageAsync = null,\n        Func<GmailSession, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>>? saveAttachmentAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _getFoldersAsync = getFoldersAsync ?? DefaultGetFoldersAsync;\n        _searchAsync = searchAsync ?? DefaultSearchAsync;\n        _getMessageAsync = getMessageAsync ?? DefaultGetMessageAsync;\n        _saveAttachmentAsync = saveAttachmentAsync ?? DefaultSaveAttachmentAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Gmail;\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getFoldersAsync(session, profile, query, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _searchAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getMessageAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _saveAttachmentAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<IReadOnlyList<FolderRef>> DefaultGetFoldersAsync(\n        GmailSession session,\n        MailProfile profile,\n        MailFolderQuery query,\n        CancellationToken cancellationToken) {\n        var userId = ResolveUserId(profile, query.MailboxId);\n        var folders = await session.Browser.ListFoldersAsync(cancellationToken).ConfigureAwait(false);\n\n        bool MatchesFilter(GmailMailboxBrowser.GmailMailboxFolderSummary folder) {\n            if (query.RootOnly) {\n                return folder.Name.IndexOf(\"/\", StringComparison.Ordinal) < 0;\n            }\n            if (string.IsNullOrWhiteSpace(query.ParentFolderId)) {\n                return true;\n            }\n\n            var parent = GetParentPath(folder.Name);\n            return string.Equals(parent, query.ParentFolderId, StringComparison.OrdinalIgnoreCase);\n        }\n\n        return folders\n            .Where(MatchesFilter)\n            .Select(folder => new FolderRef {\n                ProfileId = profile.Id,\n                MailboxId = userId,\n                Id = folder.Id,\n                DisplayName = folder.Name,\n                Path = folder.Name,\n                SpecialUse = folder.Type\n            })\n            .OrderBy(folder => folder.Path, StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n    }\n\n    private static async Task<IReadOnlyList<MessageSummary>> DefaultSearchAsync(\n        GmailSession session,\n        MailProfile profile,\n        MailSearchRequest request,\n        CancellationToken cancellationToken) {\n        var folder = ResolveFolder(request.FolderId, profile);\n        var result = await session.Browser.SearchMessagesAsync(\n            new GmailMailboxBrowser.GmailMailboxSearchRequest {\n                Folder = folder,\n                Query = request.QueryText,\n                SubjectContains = request.SubjectContains,\n                FromContains = request.FromContains,\n                ToContains = request.ToContains,\n                UnseenOnly = request.IsRead.HasValue ? !request.IsRead.Value : false,\n                HasAttachment = request.HasAttachments,\n                SinceUtc = request.Since?.UtcDateTime,\n                BeforeUtc = request.Before?.UtcDateTime\n            },\n            request.Limit ?? 100,\n            cancellationToken).ConfigureAwait(false);\n\n        return result.Messages\n            .Select(message => MapSummary(profile.Id, folder, message))\n            .ToArray();\n    }\n\n    private static async Task<MessageDetail?> DefaultGetMessageAsync(\n        GmailSession session,\n        MailProfile profile,\n        GetMessageRequest request,\n        CancellationToken cancellationToken) {\n        var folder = ResolveFolder(request.FolderId, profile);\n        var messageId = request.MessageId.Trim();\n        var getResult = await session.Browser.GetMessageContentAsync(\n            messageId,\n            GetMaxMimeBytes(profile),\n            cancellationToken).ConfigureAwait(false);\n        var summary = MapSummary(\n            profile.Id,\n            folder,\n            await session.Browser.GetMessageSummaryAsync(messageId, cancellationToken).ConfigureAwait(false),\n            getResult.Message);\n\n        var detail = new MessageDetail {\n            ProfileId = profile.Id,\n            Id = summary.Id,\n            Summary = summary,\n            TextBody = getResult.Message.TextBody,\n            HtmlBody = getResult.Message.HtmlBody,\n            Attachments = (await session.Client.ListAttachmentsAsync(session.UserId, messageId, cancellationToken).ConfigureAwait(false))\n                .OfType<GmailAttachmentInfo>()\n                .Select((attachment, index) => new AttachmentSummary {\n                    MessageId = summary.Id,\n                    Id = !string.IsNullOrWhiteSpace(attachment.Id)\n                        ? attachment.Id!.Trim()\n                        : index.ToString(CultureInfo.InvariantCulture),\n                    FileName = string.IsNullOrWhiteSpace(attachment.FileName)\n                        ? (!string.IsNullOrWhiteSpace(attachment.Id)\n                            ? attachment.Id!.Trim()\n                            : index.ToString(CultureInfo.InvariantCulture))\n                        : attachment.FileName!.Trim(),\n                    ContentType = attachment.MimeType\n                })\n                .ToList()\n        };\n\n        if (request.IncludeRawContent) {\n            using var stream = new MemoryStream();\n            await getResult.Message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n            stream.Position = 0;\n            using var reader = new StreamReader(stream);\n            detail.RawContent = reader.ReadToEnd();\n        }\n\n        return detail;\n    }\n\n    private static async Task<OperationResult> DefaultSaveAttachmentAsync(\n        GmailSession session,\n        MailProfile profile,\n        SaveAttachmentRequest request,\n        CancellationToken cancellationToken) {\n        var attachments = await session.Client.ListAttachmentsAsync(session.UserId, request.MessageId, cancellationToken).ConfigureAwait(false);\n        if (attachments == null || attachments.Count == 0) {\n            return OperationResult.Failure(\"attachment_not_found\", \"Message has no attachments.\");\n        }\n\n        var resolved = ResolveAttachment(attachments, request.AttachmentId);\n        if (resolved == null || string.IsNullOrWhiteSpace(resolved.Id)) {\n            return OperationResult.Failure(\"attachment_not_found\", $\"Attachment '{request.AttachmentId}' was not found.\");\n        }\n\n        var fileName = string.IsNullOrWhiteSpace(resolved.FileName) ? resolved.Id!.Trim() : resolved.FileName!.Trim();\n        var destinationPath = ResolveDestinationPath(request.DestinationPath, fileName);\n        if (File.Exists(destinationPath) && !request.Overwrite) {\n            return OperationResult.Failure(\"destination_exists\", $\"Destination '{destinationPath}' already exists.\");\n        }\n\n        var bytes = await session.Client.DownloadAttachmentAsync(\n            session.UserId,\n            request.MessageId,\n            resolved.Id!.Trim(),\n            cancellationToken).ConfigureAwait(false);\n\n        cancellationToken.ThrowIfCancellationRequested();\n        using (var stream = File.Create(destinationPath)) {\n#if NETSTANDARD2_0\n            await stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);\n#else\n            await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);\n#endif\n        }\n        return OperationResult.Success($\"Attachment saved to '{destinationPath}'.\");\n    }\n\n    private static MessageSummary MapSummary(\n        string profileId,\n        string folderId,\n        GmailMailboxBrowser.GmailMailboxMessageSummary message,\n        MimeMessage? mimeMessage = null) => new() {\n        ProfileId = profileId,\n        Id = message.NativeId,\n        ThreadId = message.NativeThreadId,\n        FolderId = folderId,\n        Subject = message.Subject ?? mimeMessage?.Subject,\n        Preview = mimeMessage?.TextBody,\n        From = MapRecipients(message.From, mimeMessage?.From.Mailboxes),\n        To = MapRecipients(message.To, mimeMessage?.To.Mailboxes),\n        ReceivedAt = message.DateUtc,\n        SentAt = mimeMessage?.Date,\n        IsRead = message.Seen,\n        HasAttachments = message.HasAttachments\n    };\n\n    private static List<MessageRecipient> MapRecipients(string? raw, IEnumerable<MailboxAddress>? fallback) {\n        if (!string.IsNullOrWhiteSpace(raw)) {\n            var normalizedRaw = raw!.Trim();\n            try {\n                var parsed = InternetAddressList.Parse(normalizedRaw);\n                var recipients = parsed.Mailboxes.Select(mailbox => new MessageRecipient {\n                    Name = mailbox.Name,\n                    Address = mailbox.Address ?? string.Empty\n                }).ToList();\n                if (recipients.Count > 0) {\n                    return recipients;\n                }\n            } catch {\n                return new List<MessageRecipient> {\n                    new() {\n                        Address = normalizedRaw\n                    }\n                };\n            }\n        }\n\n        return fallback?\n            .Select(mailbox => new MessageRecipient {\n                Name = mailbox.Name,\n                Address = mailbox.Address ?? string.Empty\n            })\n            .ToList() ?? new List<MessageRecipient>();\n    }\n\n    private static string ResolveFolder(string? folderId, MailProfile profile) {\n        if (!string.IsNullOrWhiteSpace(folderId)) {\n            return folderId!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Folder, out var folder) && !string.IsNullOrWhiteSpace(folder)) {\n            return folder!.Trim();\n        }\n\n        return \"INBOX\";\n    }\n\n    private static string ResolveUserId(MailProfile profile, string? mailboxOverride = null) {\n        if (!string.IsNullOrWhiteSpace(mailboxOverride)) {\n            return mailboxOverride!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) && !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox!.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n\n        return \"me\";\n    }\n\n    private static int GetMaxMimeBytes(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.MaxBodyBytes, out var raw) &&\n            int.TryParse(raw, out var value) &&\n            value > 0) {\n            return value;\n        }\n\n        return GmailMailboxBrowser.DefaultMaxMimeBytes;\n    }\n\n    private static GmailAttachmentInfo? ResolveAttachment(IList<GmailAttachmentInfo> attachments, string attachmentId) {\n        if (int.TryParse(attachmentId, out var index) && index >= 0 && index < attachments.Count) {\n            return attachments[index];\n        }\n\n        return attachments.FirstOrDefault(attachment =>\n            string.Equals(attachment.Id, attachmentId, StringComparison.OrdinalIgnoreCase) ||\n            string.Equals(attachment.FileName, attachmentId, StringComparison.OrdinalIgnoreCase));\n    }\n\n    private static string ResolveDestinationPath(string requestedPath, string fileName) {\n        var destinationPath = Path.GetFullPath(requestedPath);\n        if (Directory.Exists(destinationPath)) {\n            return Path.Combine(destinationPath, fileName);\n        }\n\n        var directory = Path.GetDirectoryName(destinationPath);\n        if (!string.IsNullOrWhiteSpace(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        return destinationPath;\n    }\n\n    private static string? GetParentPath(string path) {\n        var separatorIndex = path.LastIndexOf('/');\n        if (separatorIndex <= 0) {\n            return null;\n        }\n\n        return path.Substring(0, separatorIndex);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailMailSendHandler.cs",
    "content": "using MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Gmail send handler backed by Mailozaurr Gmail helpers.\n/// </summary>\npublic sealed class GmailMailSendHandler : IMailSendHandler {\n    private readonly IGmailSessionFactory _sessionFactory;\n    private readonly IDraftMimeMessageFactory _draftMimeMessageFactory;\n    private readonly IPendingMessageRepository? _pendingMessageRepository;\n    private readonly Func<GmailSession, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<GmailMessage>> _sendAsync;\n\n    /// <summary>\n    /// Creates a new Gmail send handler.\n    /// </summary>\n    public GmailMailSendHandler(\n        IGmailSessionFactory sessionFactory,\n        IDraftMimeMessageFactory? draftMimeMessageFactory = null,\n        IPendingMessageRepository? pendingMessageRepository = null,\n        Func<GmailSession, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<GmailMessage>>? sendAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _draftMimeMessageFactory = draftMimeMessageFactory ?? new DraftMimeMessageFactory();\n        _pendingMessageRepository = pendingMessageRepository;\n        _sendAsync = sendAsync ?? DefaultSendAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Gmail;\n\n    /// <inheritdoc />\n    public async Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n        if (request.NotBefore.HasValue) {\n            throw new NotSupportedException(\"Scheduled Gmail sends are not yet supported by the application send handler.\");\n        }\n\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        var message = await _draftMimeMessageFactory.CreateAsync(profile, request.Message, cancellationToken).ConfigureAwait(false);\n        var queueEnabled = _pendingMessageRepository != null && request.PreferQueue && !request.RequireImmediateSend;\n        session.Client.PendingMessageRepository = queueEnabled ? _pendingMessageRepository : null;\n\n        try {\n            var providerResult = await _sendAsync(session, profile, request, message, cancellationToken).ConfigureAwait(false);\n            return new SendResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind,\n                ProviderMessageId = providerResult.Id,\n                QueueMessageId = message.MessageId,\n                Message = \"Message sent successfully.\"\n            };\n        } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n            throw;\n        } catch (Exception ex) when (queueEnabled && !string.IsNullOrWhiteSpace(message.MessageId)) {\n            var queued = await _pendingMessageRepository!.GetByMessageIdAsync(message.MessageId!, cancellationToken).ConfigureAwait(false);\n            if (queued != null) {\n                return new SendResult {\n                    Succeeded = true,\n                    ProfileId = profile.Id,\n                    ProfileKind = profile.Kind,\n                    Queued = true,\n                    QueueMessageId = queued.MessageId,\n                    Message = $\"Message queued after send failure: {ex.Message}\"\n                };\n            }\n\n            throw;\n        }\n    }\n\n    private static Task<GmailMessage> DefaultSendAsync(\n        GmailSession session,\n        MailProfile profile,\n        SendMessageRequest request,\n        MimeMessage message,\n        CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return session.Client.SendAsync(session.UserId, message, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailProfileBootstrapRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request used to create or update a reusable Gmail profile.\n/// </summary>\npublic sealed class GmailProfileBootstrapRequest {\n    /// <summary>Stable profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Human-readable display name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Optional operator-focused description.</summary>\n    public string? Description { get; set; }\n\n    /// <summary>Mailbox address or Gmail user id. Defaults to <c>me</c> when omitted.</summary>\n    public string? Mailbox { get; set; }\n\n    /// <summary>Optional default sender address. Falls back to the mailbox when omitted.</summary>\n    public string? DefaultSender { get; set; }\n\n    /// <summary>When true, marks the saved profile as the default profile.</summary>\n    public bool IsDefault { get; set; }\n\n    /// <summary>Optional Google OAuth client identifier.</summary>\n    public string? ClientId { get; set; }\n\n    /// <summary>Optional Google OAuth client secret to store securely.</summary>\n    public string? ClientSecret { get; set; }\n\n    /// <summary>Optional secret reference for the client secret, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? ClientSecretReference { get; set; }\n\n    /// <summary>Optional refresh token to store securely.</summary>\n    public string? RefreshToken { get; set; }\n\n    /// <summary>Optional secret reference for the refresh token, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? RefreshTokenReference { get; set; }\n\n    /// <summary>Optional explicit access token to store securely.</summary>\n    public string? AccessToken { get; set; }\n\n    /// <summary>Optional secret reference for the access token, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? AccessTokenReference { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailProfileLoginRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request used to authenticate an existing Gmail profile.\n/// </summary>\npublic sealed class GmailProfileLoginRequest {\n    /// <summary>Stable profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional Gmail account override used for the login flow.</summary>\n    public string? GmailAccount { get; set; }\n\n    /// <summary>Optional OAuth client identifier override.</summary>\n    public string? ClientId { get; set; }\n\n    /// <summary>Optional OAuth client secret override.</summary>\n    public string? ClientSecret { get; set; }\n\n    /// <summary>Optional secret reference for the client secret, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? ClientSecretReference { get; set; }\n\n    /// <summary>Optional scopes override. Defaults to the Mailozaurr Gmail mail scope.</summary>\n    public IReadOnlyList<string>? Scopes { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailSession.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents an authenticated Gmail API session for a resolved mailbox context.\n/// </summary>\npublic sealed class GmailSession : IDisposable {\n    /// <summary>\n    /// Creates a new Gmail session wrapper.\n    /// </summary>\n    public GmailSession(GmailApiClient client, string userId) {\n        Client = client ?? throw new ArgumentNullException(nameof(client));\n        UserId = string.IsNullOrWhiteSpace(userId) ? \"me\" : userId.Trim();\n        Browser = new GmailMailboxBrowser(Client, UserId);\n    }\n\n    /// <summary>Gmail API client.</summary>\n    public GmailApiClient Client { get; }\n\n    /// <summary>Resolved mailbox user id used for Gmail requests.</summary>\n    public string UserId { get; }\n\n    /// <summary>High-level Gmail mailbox browser.</summary>\n    public GmailMailboxBrowser Browser { get; }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        Client.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailSessionFactory.cs",
    "content": "using System.Globalization;\nusing System.Text;\nusing System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Builds Gmail API sessions from reusable profile and secret stores.\n/// </summary>\npublic sealed class GmailSessionFactory : IGmailSessionFactory {\n    private static readonly Uri GoogleTokenEndpoint = new(\"https://oauth2.googleapis.com/token\");\n    private readonly IMailSecretStore _secretStore;\n    private readonly Func<GmailRefreshRequest, CancellationToken, Task<OAuthCredential>> _refreshCredentialAsync;\n    private readonly Func<GmailSessionRequest, CancellationToken, Task<GmailSession>> _connectAsync;\n\n    /// <summary>\n    /// Creates a new Gmail session factory.\n    /// </summary>\n    public GmailSessionFactory(\n        IMailSecretStore secretStore,\n        Func<GmailRefreshRequest, CancellationToken, Task<OAuthCredential>>? refreshCredentialAsync = null,\n        Func<GmailSessionRequest, CancellationToken, Task<GmailSession>>? connectAsync = null) {\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n        _refreshCredentialAsync = refreshCredentialAsync ?? DefaultRefreshCredentialAsync;\n        _connectAsync = connectAsync ?? DefaultConnectAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n\n        var userId = ResolveUserId(profile);\n        var (credential, refreshRequest) = await ResolveCredentialAsync(profile, userId, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(credential.UserName)) {\n            credential.UserName = userId;\n        }\n        if (credential.ExpiresOn == default) {\n            credential.ExpiresOn = DateTimeOffset.MaxValue;\n        }\n\n        Func<CancellationToken, Task<string>>? refreshDelegate = null;\n        if (refreshRequest != null) {\n            refreshDelegate = async token => {\n                var refreshed = await _refreshCredentialAsync(refreshRequest, token).ConfigureAwait(false);\n                credential.AccessToken = refreshed.AccessToken;\n                credential.ExpiresOn = refreshed.ExpiresOn;\n                credential.RefreshToken = refreshed.RefreshToken ?? credential.RefreshToken;\n                return refreshed.AccessToken;\n            };\n        }\n\n        return await _connectAsync(new GmailSessionRequest {\n            UserId = userId,\n            Credential = credential,\n            RefreshAccessTokenAsync = refreshDelegate\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<(OAuthCredential Credential, GmailRefreshRequest? RefreshRequest)> ResolveCredentialAsync(\n        MailProfile profile,\n        string userId,\n        CancellationToken cancellationToken) {\n        var accessToken = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n        var refreshToken = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false);\n        var tokenExpiresOn = TryResolveTokenExpiration(profile);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId);\n        var clientSecret = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n\n        GmailRefreshRequest? refreshRequest = null;\n        if (!string.IsNullOrWhiteSpace(refreshToken) &&\n            !string.IsNullOrWhiteSpace(clientId) &&\n            !string.IsNullOrWhiteSpace(clientSecret)) {\n            var normalizedClientId = clientId!.Trim();\n            var normalizedClientSecret = clientSecret!.Trim();\n            var normalizedRefreshToken = refreshToken!.Trim();\n            refreshRequest = new GmailRefreshRequest {\n                UserId = userId,\n                ClientId = normalizedClientId,\n                ClientSecret = normalizedClientSecret,\n                RefreshToken = normalizedRefreshToken\n            };\n        }\n\n        if (!string.IsNullOrWhiteSpace(accessToken) && !ShouldRefreshToken(tokenExpiresOn)) {\n            var normalizedAccessToken = accessToken!.Trim();\n            var normalizedRefreshToken = string.IsNullOrWhiteSpace(refreshToken) ? null : refreshToken!.Trim();\n            var normalizedClientId = string.IsNullOrWhiteSpace(clientId) ? null : clientId!.Trim();\n            var normalizedClientSecret = string.IsNullOrWhiteSpace(clientSecret) ? null : clientSecret!.Trim();\n            return (new OAuthCredential {\n                UserName = userId,\n                AccessToken = normalizedAccessToken,\n                RefreshToken = normalizedRefreshToken,\n                ClientId = normalizedClientId,\n                ClientSecret = normalizedClientSecret,\n                ExpiresOn = tokenExpiresOn ?? DateTimeOffset.MaxValue\n            }, refreshRequest);\n        }\n\n        if (refreshRequest == null) {\n            throw new InvalidOperationException(\n                $\"Gmail profile '{profile.Id}' requires secret '{MailSecretNames.AccessToken}' or the combination of secret '{MailSecretNames.RefreshToken}', setting '{MailProfileSettingsKeys.ClientId}', and secret '{MailSecretNames.ClientSecret}'.\");\n        }\n\n        var refreshed = await _refreshCredentialAsync(refreshRequest, cancellationToken).ConfigureAwait(false);\n        refreshed.UserName = userId;\n        refreshed.ClientId ??= refreshRequest.ClientId;\n        refreshed.ClientSecret ??= refreshRequest.ClientSecret;\n        refreshed.RefreshToken ??= refreshRequest.RefreshToken;\n        return (refreshed, refreshRequest);\n    }\n\n    private static async Task<OAuthCredential> DefaultRefreshCredentialAsync(\n        GmailRefreshRequest request,\n        CancellationToken cancellationToken) {\n        using var client = new HttpClient();\n        using var content = new FormUrlEncodedContent(new Dictionary<string, string> {\n            [\"client_id\"] = request.ClientId,\n            [\"client_secret\"] = request.ClientSecret,\n            [\"refresh_token\"] = request.RefreshToken,\n            [\"grant_type\"] = \"refresh_token\"\n        });\n        using var response = await client.PostAsync(GoogleTokenEndpoint, content, cancellationToken).ConfigureAwait(false);\n#if NET5_0_OR_GREATER\n        var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);\n#else\n        var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n#endif\n        if (!response.IsSuccessStatusCode) {\n            throw new InvalidOperationException($\"Gmail token refresh failed with status {(int)response.StatusCode}: {body}\");\n        }\n\n        using var document = JsonDocument.Parse(body);\n        var root = document.RootElement;\n        if (!root.TryGetProperty(\"access_token\", out var accessTokenElement) ||\n            accessTokenElement.ValueKind != JsonValueKind.String) {\n            throw new InvalidDataException(\"Gmail token refresh response did not contain an access_token.\");\n        }\n\n        var accessToken = accessTokenElement.GetString();\n        if (string.IsNullOrWhiteSpace(accessToken)) {\n            throw new InvalidDataException(\"Gmail token refresh response contained an empty access_token.\");\n        }\n\n        var expiresOn = DateTimeOffset.MaxValue;\n        if (root.TryGetProperty(\"expires_in\", out var expiresInElement)) {\n            if (expiresInElement.ValueKind == JsonValueKind.Number &&\n                expiresInElement.TryGetInt64(out var expiresInSeconds)) {\n                expiresOn = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds);\n            } else if (expiresInElement.ValueKind == JsonValueKind.String &&\n                       long.TryParse(expiresInElement.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedExpiresInSeconds)) {\n                expiresOn = DateTimeOffset.UtcNow.AddSeconds(parsedExpiresInSeconds);\n            }\n        }\n\n        string? refreshToken = null;\n        if (root.TryGetProperty(\"refresh_token\", out var refreshTokenElement) &&\n            refreshTokenElement.ValueKind == JsonValueKind.String) {\n            refreshToken = refreshTokenElement.GetString();\n        }\n\n        return new OAuthCredential {\n            UserName = request.UserId,\n            AccessToken = accessToken!.Trim(),\n            RefreshToken = string.IsNullOrWhiteSpace(refreshToken) ? request.RefreshToken : refreshToken!.Trim(),\n            ClientId = request.ClientId,\n            ClientSecret = request.ClientSecret,\n            ExpiresOn = expiresOn\n        };\n    }\n\n    private static Task<GmailSession> DefaultConnectAsync(GmailSessionRequest request, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return Task.FromResult(new GmailSession(new GmailApiClient(request.Credential, request.RefreshAccessTokenAsync), request.UserId));\n    }\n\n    private static string ResolveUserId(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) &&\n            !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n\n        return \"me\";\n    }\n\n    private static DateTimeOffset? TryResolveTokenExpiration(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.TokenExpiresOn, out var expiresOnValue) &&\n            !string.IsNullOrWhiteSpace(expiresOnValue) &&\n            DateTimeOffset.TryParse(expiresOnValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresOn)) {\n            return expiresOn;\n        }\n\n        return null;\n    }\n\n    private static bool ShouldRefreshToken(DateTimeOffset? expiresOn) =>\n        expiresOn.HasValue && expiresOn.Value <= DateTimeOffset.UtcNow.Add(MailProfileAuthDefaults.TokenRefreshWindow);\n\n    /// <summary>\n    /// Represents the refresh-token input required to mint a Gmail access token.\n    /// </summary>\n    public sealed class GmailRefreshRequest {\n        /// <summary>Resolved Gmail user id.</summary>\n        public string UserId { get; set; } = \"me\";\n\n        /// <summary>OAuth client id.</summary>\n        public string ClientId { get; set; } = string.Empty;\n\n        /// <summary>OAuth client secret.</summary>\n        public string ClientSecret { get; set; } = string.Empty;\n\n        /// <summary>OAuth refresh token.</summary>\n        public string RefreshToken { get; set; } = string.Empty;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GmailSessionRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the resolved session input required to connect to Gmail.\n/// </summary>\npublic sealed class GmailSessionRequest {\n    /// <summary>Resolved Gmail mailbox user id.</summary>\n    public string UserId { get; set; } = \"me\";\n\n    /// <summary>OAuth credential used to authenticate Gmail requests.</summary>\n    public OAuthCredential Credential { get; set; } = new();\n\n    /// <summary>Optional access-token refresh delegate.</summary>\n    public Func<CancellationToken, Task<string>>? RefreshAccessTokenAsync { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphMailMessageActionHandler.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Graph mailbox action handler backed by Mailozaurr Graph helpers.\n/// </summary>\npublic sealed class GraphMailMessageActionHandler : IMailMessageActionHandler {\n    private readonly IGraphSessionFactory _sessionFactory;\n    private readonly Func<GraphSession, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>> _setReadStateAsync;\n    private readonly Func<GraphSession, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>> _moveAsync;\n    private readonly Func<GraphSession, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>> _deleteAsync;\n\n    /// <summary>\n    /// Creates a new Graph message-action handler.\n    /// </summary>\n    public GraphMailMessageActionHandler(\n        IGraphSessionFactory sessionFactory,\n        Func<GraphSession, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>>? setReadStateAsync = null,\n        Func<GraphSession, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>>? moveAsync = null,\n        Func<GraphSession, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>>? deleteAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _setReadStateAsync = setReadStateAsync ?? DefaultSetReadStateAsync;\n        _moveAsync = moveAsync ?? DefaultMoveAsync;\n        _deleteAsync = deleteAsync ?? DefaultDeleteAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Graph;\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _setReadStateAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        var browser = new GraphMailboxBrowser(session.Client);\n        var results = await browser.SetMessagesFlaggedAsync(\n            request.MessageIds.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToArray(),\n            request.IsFlagged,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, request.IsFlagged ? \"Flagged messages.\" : \"Unflagged messages.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _moveAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _deleteAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<MessageActionResult> DefaultSetReadStateAsync(GraphSession session, MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken) {\n        var browser = new GraphMailboxBrowser(session.Client);\n        var results = await browser.SetMessagesSeenAsync(NormalizeIds(request.MessageIds), request.IsRead, cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, request.IsRead ? \"Marked messages as read.\" : \"Marked messages as unread.\");\n    }\n\n    private static async Task<MessageActionResult> DefaultMoveAsync(GraphSession session, MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken) {\n        var browser = new GraphMailboxBrowser(session.Client);\n        var results = await browser.MoveMessagesAsync(NormalizeIds(request.MessageIds), request.DestinationFolderId, cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, $\"Moved messages to '{request.DestinationFolderId}'.\");\n    }\n\n    private static async Task<MessageActionResult> DefaultDeleteAsync(GraphSession session, MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken) {\n        var browser = new GraphMailboxBrowser(session.Client);\n        var results = await browser.DeleteMessagesAsync(NormalizeIds(request.MessageIds), cancellationToken: cancellationToken).ConfigureAwait(false);\n        return MapResult(profile.Id, results, \"Deleted messages.\");\n    }\n\n    private static IReadOnlyList<string> NormalizeIds(IEnumerable<string> messageIds) =>\n        messageIds.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToArray();\n\n    private static MessageActionResult MapResult(string profileId, IReadOnlyList<MailboxBulkOperationResult> items, string successMessage) {\n        var result = new MessageActionResult {\n            ProfileId = profileId,\n            RequestedCount = items.Count\n        };\n\n        foreach (var item in items) {\n            result.Results.Add(new MessageActionItemResult {\n                MessageId = item.Id,\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Ok ? null : item.Error\n            });\n            if (item.Ok) {\n                result.SucceededCount++;\n            } else {\n                result.FailedCount++;\n            }\n        }\n\n        result.Succeeded = result.FailedCount == 0 && result.SucceededCount > 0;\n        result.Code = result.Succeeded ? null : \"message_action_failed\";\n        result.Message = result.Succeeded\n            ? successMessage\n            : $\"{result.SucceededCount} message action(s) succeeded; {result.FailedCount} failed.\";\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphMailReadHandler.cs",
    "content": "using System.Globalization;\nusing MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Graph read handler backed by Mailozaurr Graph helpers.\n/// </summary>\npublic sealed class GraphMailReadHandler : IMailReadHandler {\n    private const string SummarySelect = \"id,subject,receivedDateTime,from,toRecipients,internetMessageId,hasAttachments,isRead,flag,conversationId\";\n    private readonly IGraphSessionFactory _sessionFactory;\n    private readonly Func<GraphSession, MailProfile, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>> _getFoldersAsync;\n    private readonly Func<GraphSession, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>> _searchAsync;\n    private readonly Func<GraphSession, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>> _getMessageAsync;\n    private readonly Func<GraphSession, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>> _saveAttachmentAsync;\n\n    /// <summary>\n    /// Creates a new Graph read handler.\n    /// </summary>\n    public GraphMailReadHandler(\n        IGraphSessionFactory sessionFactory,\n        Func<GraphSession, MailProfile, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>>? getFoldersAsync = null,\n        Func<GraphSession, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>>? searchAsync = null,\n        Func<GraphSession, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>>? getMessageAsync = null,\n        Func<GraphSession, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>>? saveAttachmentAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _getFoldersAsync = getFoldersAsync ?? DefaultGetFoldersAsync;\n        _searchAsync = searchAsync ?? DefaultSearchAsync;\n        _getMessageAsync = getMessageAsync ?? DefaultGetMessageAsync;\n        _saveAttachmentAsync = saveAttachmentAsync ?? DefaultSaveAttachmentAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Graph;\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getFoldersAsync(session, profile, query, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _searchAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getMessageAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _saveAttachmentAsync(session, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<IReadOnlyList<FolderRef>> DefaultGetFoldersAsync(\n        GraphSession session,\n        MailProfile profile,\n        MailFolderQuery query,\n        CancellationToken cancellationToken) {\n        var userId = ResolveUserId(profile, query.MailboxId);\n        var folders = await session.Client.ListMailFoldersRecursiveAsync(userId, cancellationToken: cancellationToken).ConfigureAwait(false);\n        var nodes = folders.ToDictionary(\n            folder => folder.Id,\n            folder => new GraphFolderNode(\n                folder.Id,\n                folder.DisplayName,\n                folder.ParentFolderId,\n                folder.WellKnownName,\n                folder.TotalItemCount,\n                folder.UnreadItemCount),\n            StringComparer.Ordinal);\n\n        string BuildPath(string id) {\n            if (!nodes.TryGetValue(id, out var node)) {\n                return id;\n            }\n\n            var parts = new List<string>();\n            var current = node;\n            var guard = 0;\n            while (guard++ < 100) {\n                parts.Add(current.DisplayName);\n                if (string.IsNullOrWhiteSpace(current.ParentId) ||\n                    !nodes.TryGetValue(current.ParentId!, out current)) {\n                    break;\n                }\n            }\n            parts.Reverse();\n            return string.Join(\"/\", parts);\n        }\n\n        bool MatchesFilter(GraphFolderNode node) {\n            if (query.RootOnly) {\n                return string.IsNullOrWhiteSpace(node.ParentId);\n            }\n            if (string.IsNullOrWhiteSpace(query.ParentFolderId)) {\n                return true;\n            }\n\n            if (string.Equals(node.Id, query.ParentFolderId, StringComparison.OrdinalIgnoreCase)) {\n                return true;\n            }\n\n            var currentParentId = node.ParentId;\n            var guard = 0;\n            while (!string.IsNullOrWhiteSpace(currentParentId) && guard++ < 100) {\n                if (string.Equals(currentParentId, query.ParentFolderId, StringComparison.OrdinalIgnoreCase)) {\n                    return true;\n                }\n\n                if (!nodes.TryGetValue(currentParentId!, out var parentNode)) {\n                    break;\n                }\n\n                currentParentId = parentNode.ParentId;\n            }\n\n            return false;\n        }\n\n        return nodes.Values\n            .Where(MatchesFilter)\n            .Select(node => new FolderRef {\n                ProfileId = profile.Id,\n                MailboxId = userId,\n                Id = node.Id,\n                DisplayName = node.DisplayName,\n                Path = BuildPath(node.Id),\n                SpecialUse = node.WellKnownName,\n                MessageCount = node.TotalItemCount,\n                UnreadCount = node.UnreadItemCount\n            })\n            .OrderBy(folder => folder.Path, StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n    }\n\n    private static async Task<IReadOnlyList<MessageSummary>> DefaultSearchAsync(\n        GraphSession session,\n        MailProfile profile,\n        MailSearchRequest request,\n        CancellationToken cancellationToken) {\n        var userId = ResolveUserId(profile, request.MailboxId);\n        var folder = ResolveFolder(request.FolderId, profile);\n        var filterParts = new List<string>();\n\n        if (request.IsRead.HasValue) {\n            filterParts.Add(\"isRead eq \" + (request.IsRead.Value ? \"true\" : \"false\"));\n        }\n        if (request.HasAttachments) {\n            filterParts.Add(\"hasAttachments eq true\");\n        }\n        if (request.Since.HasValue) {\n            filterParts.Add(\"receivedDateTime ge \" + request.Since.Value.ToUniversalTime().ToString(\"o\", CultureInfo.InvariantCulture));\n        }\n        if (request.Before.HasValue) {\n            filterParts.Add(\"receivedDateTime lt \" + request.Before.Value.ToUniversalTime().ToString(\"o\", CultureInfo.InvariantCulture));\n        }\n\n        var searchTerms = new List<string>();\n        AddSearchToken(searchTerms, request.QueryText);\n        AddSearchToken(searchTerms, request.SubjectContains);\n        AddSearchToken(searchTerms, request.FromContains);\n        AddSearchToken(searchTerms, request.ToContains);\n\n        var page = await session.Client.ListMessagesAsync(\n            folder,\n            userId: userId,\n            top: request.Limit ?? 100,\n            skip: null,\n            select: SummarySelect,\n            orderBy: \"receivedDateTime desc\",\n            filter: filterParts.Count == 0 ? null : string.Join(\" and \", filterParts),\n            search: searchTerms.Count == 0 ? null : string.Join(\" \", searchTerms),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return page.Items\n            .Select(message => MapSummary(profile.Id, folder, message))\n            .ToArray();\n    }\n\n    private static async Task<MessageDetail?> DefaultGetMessageAsync(\n        GraphSession session,\n        MailProfile profile,\n        GetMessageRequest request,\n        CancellationToken cancellationToken) {\n        var userId = ResolveUserId(profile, request.MailboxId);\n        var folder = ResolveFolder(request.FolderId, profile);\n        var maxMimeBytes = GetMaxMimeBytes(profile);\n        var metadata = await session.Client.GetMessageAsync(\n            request.MessageId,\n            userId: userId,\n            select: SummarySelect,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n        var mimeBytes = await session.Client.GetMessageMimeAsync(\n            request.MessageId,\n            userId: userId,\n            maxBytes: maxMimeBytes,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        MimeMessage message;\n        try {\n            message = MimeMessage.Load(new MemoryStream(mimeBytes, writable: false));\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to parse Graph MIME message.\", ex);\n        }\n\n        var summary = MapSummary(profile.Id, folder, metadata, message);\n        var detail = new MessageDetail {\n            ProfileId = profile.Id,\n            Id = summary.Id,\n            Summary = summary,\n            TextBody = message.TextBody,\n            HtmlBody = message.HtmlBody,\n            Attachments = message.Attachments.Select((attachment, index) => new AttachmentSummary {\n                MessageId = summary.Id,\n                Id = index.ToString(CultureInfo.InvariantCulture),\n                FileName = MimeAttachmentStorage.GetAttachmentFileName(attachment),\n                ContentType = attachment.ContentType?.MimeType\n            }).ToList()\n        };\n\n        if (request.IncludeRawContent) {\n            using var stream = new MemoryStream(mimeBytes, writable: false);\n            using var reader = new StreamReader(stream);\n            detail.RawContent = reader.ReadToEnd();\n        }\n\n        return detail;\n    }\n\n    private static async Task<OperationResult> DefaultSaveAttachmentAsync(\n        GraphSession session,\n        MailProfile profile,\n        SaveAttachmentRequest request,\n        CancellationToken cancellationToken) {\n        var userId = ResolveUserId(profile, request.MailboxId);\n        var maxMimeBytes = GetMaxMimeBytes(profile);\n        var mimeBytes = await session.Client.GetMessageMimeAsync(\n            request.MessageId,\n            userId: userId,\n            maxBytes: maxMimeBytes,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        MimeMessage message;\n        try {\n            message = MimeMessage.Load(new MemoryStream(mimeBytes, writable: false));\n        } catch (Exception ex) {\n            throw new InvalidDataException(\"Failed to parse Graph MIME message.\", ex);\n        }\n\n        var attachments = message.Attachments.ToList();\n        if (attachments.Count == 0) {\n            return OperationResult.Failure(\"attachment_not_found\", \"Message has no attachments.\");\n        }\n\n        var attachment = MimeAttachmentStorage.ResolveAttachment(attachments, request.AttachmentId);\n        if (attachment == null) {\n            return OperationResult.Failure(\"attachment_not_found\", $\"Attachment '{request.AttachmentId}' was not found.\");\n        }\n\n        var destinationPath = MimeAttachmentStorage.ResolveDestinationPath(request.DestinationPath, attachment);\n        if (File.Exists(destinationPath) && !request.Overwrite) {\n            return OperationResult.Failure(\"destination_exists\", $\"Destination '{destinationPath}' already exists.\");\n        }\n\n        MimeAttachmentStorage.SaveAttachment(attachment, destinationPath);\n        return OperationResult.Success($\"Attachment saved to '{destinationPath}'.\");\n    }\n\n    private static MessageSummary MapSummary(\n        string profileId,\n        string folderId,\n        GraphMailMessage message,\n        MimeMessage? mimeMessage = null) => new() {\n        ProfileId = profileId,\n        Id = message.Id,\n        ThreadId = message.ConversationId,\n        FolderId = folderId,\n        Subject = string.IsNullOrWhiteSpace(message.Subject) ? mimeMessage?.Subject : message.Subject,\n        Preview = mimeMessage?.TextBody,\n        From = MapRecipients(message.From, mimeMessage?.From.Mailboxes),\n        To = MapRecipients(message.ToRecipients, mimeMessage?.To.Mailboxes),\n        ReceivedAt = message.ReceivedDateTime ?? mimeMessage?.Date,\n        SentAt = mimeMessage?.Date,\n        IsRead = message.IsRead,\n        HasAttachments = message.HasAttachments ?? mimeMessage?.Attachments.Any() ?? false\n    };\n\n    private static List<MessageRecipient> MapRecipients(GraphEmailAddress? sender, IEnumerable<MailboxAddress>? fallback) {\n        if (sender?.Email != null && !string.IsNullOrWhiteSpace(sender.Email.Address)) {\n            return new List<MessageRecipient> {\n                new() {\n                    Name = sender.Email.Name,\n                    Address = sender.Email.Address\n                }\n            };\n        }\n\n        return fallback?\n            .Select(mailbox => new MessageRecipient {\n                Name = mailbox.Name,\n                Address = mailbox.Address ?? string.Empty\n            })\n            .ToList() ?? new List<MessageRecipient>();\n    }\n\n    private static List<MessageRecipient> MapRecipients(IEnumerable<GraphEmailAddress>? recipients, IEnumerable<MailboxAddress>? fallback) {\n        var output = recipients?\n            .Where(recipient => recipient.Email != null && !string.IsNullOrWhiteSpace(recipient.Email.Address))\n            .Select(recipient => new MessageRecipient {\n                Name = recipient.Email.Name,\n                Address = recipient.Email.Address\n            })\n            .ToList();\n        if (output != null && output.Count > 0) {\n            return output;\n        }\n\n        return fallback?\n            .Select(mailbox => new MessageRecipient {\n                Name = mailbox.Name,\n                Address = mailbox.Address ?? string.Empty\n            })\n            .ToList() ?? new List<MessageRecipient>();\n    }\n\n    private static string ResolveFolder(string? folderId, MailProfile profile) {\n        if (!string.IsNullOrWhiteSpace(folderId)) {\n            return folderId!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Folder, out var folder) && !string.IsNullOrWhiteSpace(folder)) {\n            return folder!.Trim();\n        }\n\n        return \"inbox\";\n    }\n\n    private static string ResolveUserId(MailProfile profile, string? mailboxOverride = null) {\n        if (!string.IsNullOrWhiteSpace(mailboxOverride)) {\n            return mailboxOverride!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) && !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox!.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n\n        return \"me\";\n    }\n\n    private static int GetMaxMimeBytes(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.MaxBodyBytes, out var raw) &&\n            int.TryParse(raw, out var value) &&\n            value > 0) {\n            return value;\n        }\n\n        return GraphMailboxBrowser.DefaultMaxMimeBytes;\n    }\n\n    private static void AddSearchToken(List<string> tokens, string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return;\n        }\n\n        var normalized = value!.Trim();\n        if (normalized.Length == 0) {\n            return;\n        }\n\n        tokens.Add(normalized.Replace(\"\\\"\", string.Empty));\n    }\n\n    private sealed class GraphFolderNode {\n        public GraphFolderNode(\n            string id,\n            string displayName,\n            string? parentId,\n            string? wellKnownName,\n            int? totalItemCount,\n            int? unreadItemCount) {\n            Id = id;\n            DisplayName = displayName;\n            ParentId = parentId;\n            WellKnownName = wellKnownName;\n            TotalItemCount = totalItemCount;\n            UnreadItemCount = unreadItemCount;\n        }\n\n        public string Id { get; }\n\n        public string DisplayName { get; }\n\n        public string? ParentId { get; }\n\n        public string? WellKnownName { get; }\n\n        public int? TotalItemCount { get; }\n\n        public int? UnreadItemCount { get; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphMailSendHandler.cs",
    "content": "using System.Globalization;\nusing System.IO;\nusing MimeKit;\nusing MimeKit.Utils;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized Graph send handler backed by Mailozaurr Graph helpers.\n/// </summary>\npublic sealed class GraphMailSendHandler : IMailSendHandler {\n    private readonly IGraphSessionFactory _sessionFactory;\n    private readonly IDraftMimeMessageFactory _draftMimeMessageFactory;\n    private readonly IPendingMessageRepository? _pendingMessageRepository;\n    private readonly Func<GraphSession, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<GraphMessage>> _sendAsync;\n\n    /// <summary>\n    /// Creates a new Graph send handler.\n    /// </summary>\n    public GraphMailSendHandler(\n        IGraphSessionFactory sessionFactory,\n        IDraftMimeMessageFactory? draftMimeMessageFactory = null,\n        IPendingMessageRepository? pendingMessageRepository = null,\n        Func<GraphSession, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<GraphMessage>>? sendAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _draftMimeMessageFactory = draftMimeMessageFactory ?? new DraftMimeMessageFactory();\n        _pendingMessageRepository = pendingMessageRepository;\n        _sendAsync = sendAsync ?? DefaultSendAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Graph;\n\n    /// <inheritdoc />\n    public async Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        using var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        var message = await _draftMimeMessageFactory.CreateAsync(profile, request.Message, cancellationToken).ConfigureAwait(false);\n        var shouldQueue = _pendingMessageRepository != null && request.PreferQueue && !request.RequireImmediateSend;\n        if (request.NotBefore.HasValue || shouldQueue) {\n            if (_pendingMessageRepository == null) {\n                throw new NotSupportedException(\"Queue-backed Graph sends require a pending-message repository.\");\n            }\n\n            var queuedRecord = await QueueMessageAsync(\n                profile,\n                session,\n                message,\n                request.NotBefore,\n                cancellationToken).ConfigureAwait(false);\n            return new SendResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind,\n                Queued = true,\n                QueueMessageId = queuedRecord.MessageId,\n                Message = \"Message queued successfully.\"\n            };\n        }\n\n        var providerResult = await _sendAsync(session, profile, request, message, cancellationToken).ConfigureAwait(false);\n\n        return new SendResult {\n            Succeeded = true,\n            ProfileId = profile.Id,\n            ProfileKind = profile.Kind,\n            ProviderMessageId = providerResult.Id,\n            QueueMessageId = message.MessageId,\n            Message = \"Message sent successfully.\"\n        };\n    }\n\n    private static async Task<GraphMessage> DefaultSendAsync(\n        GraphSession session,\n        MailProfile profile,\n        SendMessageRequest request,\n        MimeMessage message,\n        CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return await GraphMimeMessageSender.SendAsync(session.Client, session.UserId, message, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<PendingMessageRecord> QueueMessageAsync(\n        MailProfile profile,\n        GraphSession session,\n        MimeMessage message,\n        DateTimeOffset? notBefore,\n        CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(message.MessageId)) {\n            message.MessageId = MimeUtils.GenerateMessageId();\n        }\n\n        using var stream = new MemoryStream();\n        await message.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord {\n            MessageId = message.MessageId!,\n            MimeMessage = Convert.ToBase64String(stream.ToArray()),\n            Timestamp = now,\n            NextAttemptAt = notBefore?.ToUniversalTime() ?? now,\n            Provider = EmailProvider.Graph\n        };\n\n        record.ProviderData[GraphPendingMessageSender.UserIdKey] = session.UserId;\n        var credential = session.Credential;\n        var userName = !string.IsNullOrWhiteSpace(credential?.UserName)\n            ? credential!.UserName\n            : session.UserId;\n        record.ProviderData[GraphPendingMessageSender.UserNameKey] = userName;\n\n        if (!string.IsNullOrWhiteSpace(credential?.AccessToken)) {\n            record.ProviderData[GraphPendingMessageSender.AccessTokenProtectedKey] =\n                CredentialProtection.Default.Protect(credential!.AccessToken);\n        }\n\n        if (credential != null) {\n            record.ProviderData[GraphPendingMessageSender.ExpiresOnKey] =\n                (credential.ExpiresOn == default ? DateTimeOffset.MaxValue : credential.ExpiresOn).ToString(\"o\", CultureInfo.InvariantCulture);\n        }\n\n        var graphCredential = session.GraphCredential;\n        if (!string.IsNullOrWhiteSpace(graphCredential?.ClientId)) {\n            record.ProviderData[GraphPendingMessageSender.ClientIdKey] = graphCredential!.ClientId;\n        }\n        if (!string.IsNullOrWhiteSpace(graphCredential?.DirectoryId)) {\n            record.ProviderData[GraphPendingMessageSender.TenantIdKey] = graphCredential!.DirectoryId;\n        }\n        if (!string.IsNullOrWhiteSpace(graphCredential?.ClientSecret)) {\n            record.ProviderData[GraphPendingMessageSender.ClientSecretProtectedKey] =\n                CredentialProtection.Default.Protect(graphCredential!.ClientSecret!);\n        }\n        if (!string.IsNullOrWhiteSpace(graphCredential?.CertificatePath)) {\n            record.ProviderData[GraphPendingMessageSender.CertificatePathKey] = graphCredential!.CertificatePath!;\n        }\n        if (!string.IsNullOrWhiteSpace(graphCredential?.CertificatePassword)) {\n            record.ProviderData[GraphPendingMessageSender.CertificatePasswordProtectedKey] =\n                CredentialProtection.Default.Protect(graphCredential!.CertificatePassword!);\n        }\n\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantId);\n        if (!string.IsNullOrWhiteSpace(tenantId) &&\n            !record.ProviderData.ContainsKey(GraphPendingMessageSender.TenantIdKey)) {\n            record.ProviderData[GraphPendingMessageSender.TenantIdKey] = tenantId.Trim();\n        }\n\n        await _pendingMessageRepository!.SaveAsync(record, cancellationToken).ConfigureAwait(false);\n        return record;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphProfileBootstrapRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request used to create or update a reusable Microsoft Graph profile.\n/// </summary>\npublic sealed class GraphProfileBootstrapRequest {\n    /// <summary>Stable profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Human-readable display name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Optional operator-focused description.</summary>\n    public string? Description { get; set; }\n\n    /// <summary>Mailbox address or user principal name.</summary>\n    public string Mailbox { get; set; } = string.Empty;\n\n    /// <summary>Optional default sender address. Falls back to <see cref=\"Mailbox\" /> when omitted.</summary>\n    public string? DefaultSender { get; set; }\n\n    /// <summary>When true, marks the saved profile as the default profile.</summary>\n    public bool IsDefault { get; set; }\n\n    /// <summary>Optional Graph client/application identifier.</summary>\n    public string? ClientId { get; set; }\n\n    /// <summary>Optional Graph tenant/directory identifier.</summary>\n    public string? TenantId { get; set; }\n\n    /// <summary>Optional confidential-client secret to store securely.</summary>\n    public string? ClientSecret { get; set; }\n\n    /// <summary>Optional secret reference for the client secret, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? ClientSecretReference { get; set; }\n\n    /// <summary>Optional explicit access token to store securely.</summary>\n    public string? AccessToken { get; set; }\n\n    /// <summary>Optional secret reference for the access token, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? AccessTokenReference { get; set; }\n\n    /// <summary>Optional certificate path for certificate-based authentication.</summary>\n    public string? CertificatePath { get; set; }\n\n    /// <summary>Optional certificate password to store securely.</summary>\n    public string? CertificatePassword { get; set; }\n\n    /// <summary>Optional secret reference for the certificate password, in the form <c>&lt;profile-id&gt;:&lt;secret-name&gt;</c> or <c>&lt;secret-name&gt;</c>.</summary>\n    public string? CertificatePasswordReference { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphProfileLoginRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request used to authenticate an existing Microsoft Graph profile.\n/// </summary>\npublic sealed class GraphProfileLoginRequest {\n    /// <summary>Stable profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional login hint override for the interactive flow.</summary>\n    public string? Login { get; set; }\n\n    /// <summary>Optional mailbox override that should be persisted with the profile.</summary>\n    public string? Mailbox { get; set; }\n\n    /// <summary>Optional application/client identifier override.</summary>\n    public string? ClientId { get; set; }\n\n    /// <summary>Optional tenant/directory identifier override.</summary>\n    public string? TenantId { get; set; }\n\n    /// <summary>Optional redirect URI override. Defaults to the Mailozaurr Graph native-client redirect URI.</summary>\n    public string? RedirectUri { get; set; }\n\n    /// <summary>Optional scopes override. Defaults to Mail.ReadWrite, Mail.Send, email, and offline_access.</summary>\n    public IReadOnlyList<string>? Scopes { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphSession.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents an authenticated Graph API session for a resolved mailbox context.\n/// </summary>\npublic sealed class GraphSession : IDisposable {\n    /// <summary>\n    /// Creates a new Graph session wrapper.\n    /// </summary>\n    public GraphSession(\n        GraphApiClient client,\n        string userId,\n        OAuthCredential? credential = null,\n        GraphCredential? graphCredential = null) {\n        Client = client ?? throw new ArgumentNullException(nameof(client));\n        UserId = string.IsNullOrWhiteSpace(userId) ? \"me\" : userId.Trim();\n        Credential = credential;\n        GraphCredential = graphCredential;\n    }\n\n    /// <summary>Graph API client.</summary>\n    public GraphApiClient Client { get; }\n\n    /// <summary>Resolved mailbox user id used for Graph requests.</summary>\n    public string UserId { get; }\n\n    /// <summary>Resolved OAuth credential used to authenticate the session.</summary>\n    public OAuthCredential? Credential { get; }\n\n    /// <summary>Optional Graph credential metadata that can mint a fresh access token later.</summary>\n    public GraphCredential? GraphCredential { get; }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        Client.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphSessionFactory.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Builds Graph API sessions from reusable profile and secret stores.\n/// </summary>\npublic sealed class GraphSessionFactory : IGraphSessionFactory {\n    private readonly IMailSecretStore _secretStore;\n    private readonly Func<MailProfile, GraphCredential, CancellationToken, Task<OAuthCredential>> _acquireCredentialAsync;\n    private readonly Func<MailProfile, CancellationToken, Task<OAuthCredential?>> _acquireSilentCredentialAsync;\n    private readonly Func<GraphSessionRequest, CancellationToken, Task<GraphSession>> _connectAsync;\n\n    /// <summary>\n    /// Creates a new Graph session factory.\n    /// </summary>\n    public GraphSessionFactory(\n        IMailSecretStore secretStore,\n        Func<MailProfile, GraphCredential, CancellationToken, Task<OAuthCredential>>? acquireCredentialAsync = null,\n        Func<MailProfile, CancellationToken, Task<OAuthCredential?>>? acquireSilentCredentialAsync = null,\n        Func<GraphSessionRequest, CancellationToken, Task<GraphSession>>? connectAsync = null) {\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n        _acquireCredentialAsync = acquireCredentialAsync ?? DefaultAcquireCredentialAsync;\n        _acquireSilentCredentialAsync = acquireSilentCredentialAsync ?? DefaultAcquireSilentCredentialAsync;\n        _connectAsync = connectAsync ?? DefaultConnectAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n\n        var userId = ResolveUserId(profile);\n        var credential = await ResolveCredentialAsync(profile, userId, cancellationToken).ConfigureAwait(false);\n        var graphCredential = await TryBuildGraphCredentialAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(credential.UserName)) {\n            credential.UserName = userId;\n        }\n        if (credential.ExpiresOn == default) {\n            credential.ExpiresOn = DateTimeOffset.MaxValue;\n        }\n\n        return await _connectAsync(new GraphSessionRequest {\n            UserId = userId,\n            Credential = credential,\n            GraphCredential = graphCredential\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<OAuthCredential> ResolveCredentialAsync(MailProfile profile, string userId, CancellationToken cancellationToken) {\n        var accessToken = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n        var tokenExpiresOn = TryResolveTokenExpiration(profile);\n        if (!string.IsNullOrWhiteSpace(accessToken)) {\n            if (!ShouldRefreshToken(tokenExpiresOn)) {\n                return new OAuthCredential {\n                    UserName = userId,\n                    AccessToken = accessToken!.Trim(),\n                    RefreshToken = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false),\n                    ClientId = profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ? clientId : null,\n                    ClientSecret = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false),\n                    ExpiresOn = tokenExpiresOn ?? DateTimeOffset.MaxValue\n                };\n            }\n        }\n\n        if (CanUseInteractiveSilentRefresh(profile)) {\n            var refreshedCredential = await _acquireSilentCredentialAsync(profile, cancellationToken).ConfigureAwait(false);\n            if (refreshedCredential != null && !string.IsNullOrWhiteSpace(refreshedCredential.AccessToken)) {\n                refreshedCredential.UserName = string.IsNullOrWhiteSpace(refreshedCredential.UserName) ? userId : refreshedCredential.UserName;\n                if (refreshedCredential.ExpiresOn == default) {\n                    refreshedCredential.ExpiresOn = DateTimeOffset.MaxValue;\n                }\n                return refreshedCredential;\n            }\n        }\n\n        if (!string.IsNullOrWhiteSpace(accessToken)) {\n            return new OAuthCredential {\n                UserName = userId,\n                AccessToken = accessToken!.Trim(),\n                RefreshToken = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false),\n                ClientId = profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ? clientId : null,\n                ClientSecret = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false),\n                ExpiresOn = tokenExpiresOn ?? DateTimeOffset.MaxValue\n            };\n        }\n\n        var graphCredential = await BuildGraphCredentialAsync(profile, cancellationToken).ConfigureAwait(false);\n        var credential = await _acquireCredentialAsync(profile, graphCredential, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(credential.AccessToken)) {\n            throw new InvalidOperationException($\"Graph profile '{profile.Id}' did not produce an access token.\");\n        }\n\n        return credential;\n    }\n\n    private async Task<GraphCredential> BuildGraphCredentialAsync(MailProfile profile, CancellationToken cancellationToken) {\n        var clientId = GetRequiredSetting(profile, MailProfileSettingsKeys.ClientId);\n        var tenantId = GetRequiredSetting(profile, MailProfileSettingsKeys.TenantId);\n        var clientSecret = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n        var certificatePassword = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.CertificatePassword, cancellationToken).ConfigureAwait(false);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.CertificatePath, out var certificatePath);\n\n        if (string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(certificatePath)) {\n            throw new InvalidOperationException(\n                $\"Graph profile '{profile.Id}' requires either secret '{MailSecretNames.AccessToken}', secret '{MailSecretNames.ClientSecret}', or setting '{MailProfileSettingsKeys.CertificatePath}'.\");\n        }\n\n        return new GraphCredential {\n            ClientId = clientId,\n            DirectoryId = tenantId,\n            ClientSecret = string.IsNullOrWhiteSpace(clientSecret) ? null : clientSecret!.Trim(),\n            CertificatePath = string.IsNullOrWhiteSpace(certificatePath) ? null : certificatePath!.Trim(),\n            CertificatePassword = string.IsNullOrWhiteSpace(certificatePassword) ? null : certificatePassword!.Trim()\n        };\n    }\n\n    private async Task<GraphCredential?> TryBuildGraphCredentialAsync(MailProfile profile, CancellationToken cancellationToken) {\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ||\n            string.IsNullOrWhiteSpace(clientId) ||\n            !profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantId) ||\n            string.IsNullOrWhiteSpace(tenantId)) {\n            return null;\n        }\n\n        var clientSecret = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n        var certificatePassword = await _secretStore.GetSecretAsync(profile.Id, MailSecretNames.CertificatePassword, cancellationToken).ConfigureAwait(false);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.CertificatePath, out var certificatePath);\n\n        if (string.IsNullOrWhiteSpace(clientSecret) && string.IsNullOrWhiteSpace(certificatePath)) {\n            return null;\n        }\n\n        return new GraphCredential {\n            ClientId = clientId.Trim(),\n            DirectoryId = tenantId.Trim(),\n            ClientSecret = string.IsNullOrWhiteSpace(clientSecret) ? null : clientSecret!.Trim(),\n            CertificatePath = string.IsNullOrWhiteSpace(certificatePath) ? null : certificatePath!.Trim(),\n            CertificatePassword = string.IsNullOrWhiteSpace(certificatePassword) ? null : certificatePassword!.Trim()\n        };\n    }\n\n    private static async Task<OAuthCredential> DefaultAcquireCredentialAsync(\n        MailProfile profile,\n        GraphCredential graphCredential,\n        CancellationToken cancellationToken) {\n        var authorization = await MicrosoftGraphUtils.ConnectO365GraphAsync(\n            graphCredential,\n            graphCredential.DirectoryId,\n            \"https://graph.microsoft.com\",\n            cancellationToken).ConfigureAwait(false);\n        var accessToken = NormalizeAccessToken(authorization);\n        return new OAuthCredential {\n            UserName = ResolveUserId(profile),\n            AccessToken = accessToken,\n            ClientId = graphCredential.ClientId,\n            ClientSecret = graphCredential.ClientSecret,\n            ExpiresOn = DateTimeOffset.MaxValue\n        };\n    }\n\n    private static Task<OAuthCredential?> DefaultAcquireSilentCredentialAsync(MailProfile profile, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ||\n            string.IsNullOrWhiteSpace(clientId) ||\n            !profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantId) ||\n            string.IsNullOrWhiteSpace(tenantId)) {\n            return Task.FromResult<OAuthCredential?>(null);\n        }\n\n        var redirectUri = profile.Settings.TryGetValue(MailProfileSettingsKeys.RedirectUri, out var storedRedirectUri) &&\n                          !string.IsNullOrWhiteSpace(storedRedirectUri)\n            ? storedRedirectUri.Trim()\n            : MailProfileAuthDefaults.GraphRedirectUri;\n        var login = ResolveLoginHint(profile);\n        return OAuthHelpers.TryAcquireO365TokenSilentAsync(\n            login,\n            clientId.Trim(),\n            tenantId.Trim(),\n            redirectUri,\n            MailProfileAuthDefaults.GraphScopes);\n    }\n\n    private static Task<GraphSession> DefaultConnectAsync(GraphSessionRequest request, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return Task.FromResult(new GraphSession(\n            new GraphApiClient(request.Credential),\n            request.UserId,\n            request.Credential,\n            request.GraphCredential));\n    }\n\n    private static string ResolveUserId(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) &&\n            !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n\n        return \"me\";\n    }\n\n    private static string? ResolveLoginHint(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.LoginHint, out var loginHint) &&\n            !string.IsNullOrWhiteSpace(loginHint)) {\n            return loginHint.Trim();\n        }\n\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) &&\n            !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n\n        return null;\n    }\n\n    private static DateTimeOffset? TryResolveTokenExpiration(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.TokenExpiresOn, out var expiresOnValue) &&\n            !string.IsNullOrWhiteSpace(expiresOnValue) &&\n            DateTimeOffset.TryParse(expiresOnValue, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind, out var expiresOn)) {\n            return expiresOn;\n        }\n\n        return null;\n    }\n\n    private static bool ShouldRefreshToken(DateTimeOffset? expiresOn) =>\n        expiresOn.HasValue && expiresOn.Value <= DateTimeOffset.UtcNow.Add(MailProfileAuthDefaults.TokenRefreshWindow);\n\n    private static bool CanUseInteractiveSilentRefresh(MailProfile profile) {\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthFlow, out var authFlow) ||\n            !string.Equals(authFlow, MailProfileAuthFlowNames.Interactive, StringComparison.OrdinalIgnoreCase)) {\n            return false;\n        }\n\n        return profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) &&\n               !string.IsNullOrWhiteSpace(clientId) &&\n               profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantId) &&\n               !string.IsNullOrWhiteSpace(tenantId);\n    }\n\n    private static string GetRequiredSetting(MailProfile profile, string key) {\n        if (profile.Settings.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value.Trim();\n        }\n\n        throw new InvalidOperationException($\"Graph profile '{profile.Id}' is missing required setting '{key}'.\");\n    }\n\n    private static string NormalizeAccessToken(string authorizationHeaderValue) {\n        if (string.IsNullOrWhiteSpace(authorizationHeaderValue)) {\n            throw new InvalidOperationException(\"Graph authorization did not return a token.\");\n        }\n\n        var trimmed = authorizationHeaderValue.Trim();\n        const string bearerPrefix = \"Bearer \";\n        return trimmed.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)\n            ? trimmed.Substring(bearerPrefix.Length).Trim()\n            : trimmed;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/GraphSessionRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the resolved session input required to connect to Microsoft Graph.\n/// </summary>\npublic sealed class GraphSessionRequest {\n    /// <summary>Resolved Graph mailbox user id.</summary>\n    public string UserId { get; set; } = \"me\";\n\n    /// <summary>OAuth credential used to authenticate Graph requests.</summary>\n    public OAuthCredential Credential { get; set; } = new();\n\n    /// <summary>Optional Graph credential metadata used to mint new access tokens.</summary>\n    public GraphCredential? GraphCredential { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IDraftMimeMessageFactory.cs",
    "content": "using MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Builds reusable MIME messages from normalized draft contracts.\n/// </summary>\npublic interface IDraftMimeMessageFactory {\n    /// <summary>\n    /// Creates a MIME message for the provided profile and draft.\n    /// </summary>\n    Task<MimeMessage> CreateAsync(MailProfile profile, DraftMessage draft, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IGmailSessionFactory.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated Gmail API sessions from reusable profile definitions.\n/// </summary>\npublic interface IGmailSessionFactory {\n    /// <summary>Creates an authenticated Gmail session.</summary>\n    Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IGraphSessionFactory.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated Graph API sessions from reusable profile definitions.\n/// </summary>\npublic interface IGraphSessionFactory {\n    /// <summary>Creates an authenticated Graph session.</summary>\n    Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IImapSessionFactory.cs",
    "content": "using MailKit.Net.Imap;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated IMAP sessions from stored profiles.\n/// </summary>\npublic interface IImapSessionFactory {\n    /// <summary>\n    /// Connects an authenticated IMAP client for the provided profile.\n    /// </summary>\n    Task<ImapClient> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailDraftExchangeService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Imports and exports reusable drafts to stable external files.\n/// </summary>\npublic interface IMailDraftExchangeService {\n    /// <summary>Loads a draft from an external file.</summary>\n    Task<MailDraft> LoadAsync(string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves a draft to an external file.</summary>\n    Task SaveAsync(string path, MailDraft draft, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailDraftService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides reusable draft lifecycle operations.\n/// </summary>\npublic interface IMailDraftService {\n    /// <summary>Lists all saved drafts.</summary>\n    Task<IReadOnlyList<MailDraft>> GetDraftsAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Lists all saved drafts using a lightweight projection.</summary>\n    Task<IReadOnlyList<MailDraftCompact>> GetDraftsCompactAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a draft by id.</summary>\n    Task<MailDraft?> GetDraftAsync(string draftId, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a draft by id using a lightweight projection.</summary>\n    Task<MailDraftCompact?> GetDraftCompactAsync(string draftId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a draft.</summary>\n    Task<OperationResult> SaveAsync(MailDraft draft, CancellationToken cancellationToken = default);\n\n    /// <summary>Deletes a draft by id.</summary>\n    Task<OperationResult> DeleteAsync(string draftId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailDraftStore.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Persists reusable outbound drafts.\n/// </summary>\npublic interface IMailDraftStore {\n    /// <summary>Lists all saved drafts.</summary>\n    Task<IReadOnlyList<MailDraft>> GetAllAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a saved draft by id.</summary>\n    Task<MailDraft?> GetByIdAsync(string draftId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a draft.</summary>\n    Task SaveAsync(MailDraft draft, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a saved draft by id.</summary>\n    Task<bool> RemoveAsync(string draftId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailFolderAliasService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides provider-neutral folder alias discovery for reusable adapters.\n/// </summary>\npublic interface IMailFolderAliasService {\n    /// <summary>Lists supported folder aliases for a profile.</summary>\n    Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(\n        string profileId,\n        string? mailboxId = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Resolves a requested folder target to an alias or provider-specific destination.</summary>\n    Task<MailFolderTargetResolution> ResolveAsync(\n        string profileId,\n        string targetFolderId,\n        string? mailboxId = null,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionBatchService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Executes one or more normalized message action plans and returns a shared summary.\n/// </summary>\npublic interface IMailMessageActionBatchService {\n    /// <summary>Executes a batch of normalized message action plans.</summary>\n    Task<MessageActionBatchExecutionResult> ExecuteAsync(\n        IReadOnlyList<MessageActionExecutionPlan> plans,\n        bool continueOnError = true,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionHandler.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Handles normalized mailbox message actions for a specific profile kind.\n/// </summary>\npublic interface IMailMessageActionHandler {\n    /// <summary>Profile kind handled by this instance.</summary>\n    MailProfileKind Kind { get; }\n\n    /// <summary>Sets read/unread state for messages.</summary>\n    Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Sets flagged/starred state for messages.</summary>\n    Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Moves messages to a destination folder.</summary>\n    Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Deletes messages.</summary>\n    Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionPlanBatchStore.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Persists reusable message action plan batches.\n/// </summary>\npublic interface IMailMessageActionPlanBatchStore {\n    /// <summary>Lists all saved plan batches.</summary>\n    Task<IReadOnlyList<MailMessageActionPlanBatch>> GetAllAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a saved plan batch by id.</summary>\n    Task<MailMessageActionPlanBatch?> GetByIdAsync(string batchId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a plan batch.</summary>\n    Task SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a saved plan batch by id.</summary>\n    Task<bool> RemoveAsync(string batchId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionPlanExchangeService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Imports and exports normalized message action plans to stable external files.\n/// </summary>\npublic interface IMailMessageActionPlanExchangeService {\n    /// <summary>Loads one normalized action plan from an external file.</summary>\n    Task<MessageActionExecutionPlan> LoadAsync(string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves one normalized action plan to an external file.</summary>\n    Task SaveAsync(string path, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default);\n\n    /// <summary>Loads a batch of normalized action plans from an external file.</summary>\n    Task<IReadOnlyList<MessageActionExecutionPlan>> LoadBatchAsync(string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves a batch of normalized action plans to an external file.</summary>\n    Task SaveBatchAsync(string path, IReadOnlyList<MessageActionExecutionPlan> plans, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionPlanRegistryService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides reusable lifecycle operations for persisted message action plan batches.\n/// </summary>\npublic interface IMailMessageActionPlanRegistryService {\n    /// <summary>Lists all saved plan batches.</summary>\n    Task<IReadOnlyList<MailMessageActionPlanBatch>> GetBatchesAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default);\n\n    /// <summary>Lists all saved plan batches using a lightweight projection.</summary>\n    Task<IReadOnlyList<MailMessageActionPlanBatchCompact>> GetBatchesCompactAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default);\n\n    /// <summary>Lists all saved plan batches using a richer summary projection.</summary>\n    Task<IReadOnlyList<MailMessageActionPlanBatchSummary>> GetBatchesSummaryAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a batch by id.</summary>\n    Task<MailMessageActionPlanBatch?> GetBatchAsync(string batchId, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a batch by id using a lightweight projection.</summary>\n    Task<MailMessageActionPlanBatchCompact?> GetBatchCompactAsync(string batchId, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a batch by id using a richer summary projection.</summary>\n    Task<MailMessageActionPlanBatchSummary?> GetBatchSummaryAsync(string batchId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a persisted plan batch.</summary>\n    Task<OperationResult> SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default);\n\n    /// <summary>Builds and saves a persisted plan batch from a common mailbox action selection.</summary>\n    Task<OperationResult> CreateCommonBatchAsync(\n        string batchId,\n        string name,\n        CommonMessageActionsPreviewRequest request,\n        IReadOnlyList<string>? actions = null,\n        string? description = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Builds and saves a persisted plan batch from an existing common mailbox action preview bundle.</summary>\n    Task<OperationResult> CreateCommonBatchFromPreviewAsync(\n        string batchId,\n        string name,\n        CommonMessageActionsPreview preview,\n        IReadOnlyList<string>? actions = null,\n        string? description = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Clones an existing persisted batch to a new identifier and name.</summary>\n    Task<OperationResult> CloneAsync(string sourceBatchId, string targetBatchId, string name, string? description = null, CancellationToken cancellationToken = default);\n\n    /// <summary>Clones an existing persisted batch while applying shared profile/mailbox/folder/destination transforms.</summary>\n    Task<OperationResult> TransformCloneAsync(\n        string sourceBatchId,\n        string targetBatchId,\n        string name,\n        MessageActionPlanBatchTransformRequest transform,\n        string? description = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Previews a stored batch transform and reports what would change before cloning and saving it.</summary>\n    Task<MailMessageActionPlanBatchTransformPreview> PreviewTransformCloneAsync(\n        string sourceBatchId,\n        MessageActionPlanBatchTransformRequest transform,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Appends one normalized plan to an existing persisted batch.</summary>\n    Task<OperationResult> AppendPlanAsync(string batchId, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default);\n\n    /// <summary>Loads one normalized plan from a file and appends it to an existing persisted batch.</summary>\n    Task<OperationResult> AppendImportedPlanAsync(string batchId, string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Replaces one plan inside an existing persisted batch by zero-based index.</summary>\n    Task<OperationResult> ReplacePlanAtAsync(string batchId, int index, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default);\n\n    /// <summary>Loads one normalized plan from a file and replaces a stored plan by zero-based index.</summary>\n    Task<OperationResult> ReplaceImportedPlanAtAsync(string batchId, int index, string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes one plan from an existing persisted batch by zero-based index.</summary>\n    Task<OperationResult> RemovePlanAtAsync(string batchId, int index, CancellationToken cancellationToken = default);\n\n    /// <summary>Deletes a persisted plan batch by id.</summary>\n    Task<OperationResult> DeleteAsync(string batchId, CancellationToken cancellationToken = default);\n\n    /// <summary>Imports a persisted plan batch from an external batch file.</summary>\n    Task<OperationResult> ImportAsync(string batchId, string name, string path, string? description = null, CancellationToken cancellationToken = default);\n\n    /// <summary>Exports a persisted plan batch to an external batch file.</summary>\n    Task<OperationResult> ExportAsync(string batchId, string path, CancellationToken cancellationToken = default);\n\n    /// <summary>Executes a persisted plan batch through the shared batch executor.</summary>\n    Task<MessageActionBatchExecutionResult> ExecuteAsync(string batchId, bool continueOnError = true, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionPlanService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates reusable execution plans from previewable message actions and can execute those plans.\n/// </summary>\npublic interface IMailMessageActionPlanService {\n    /// <summary>Creates a normalized execution plan for a requested message action.</summary>\n    Task<MessageActionExecutionPlan> CreatePlanAsync(MessageActionExecutionPlanRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Executes a previously created normalized execution plan.</summary>\n    Task<MessageActionResult> ExecuteAsync(MessageActionExecutionPlan plan, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionPreviewService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides reusable dry-run previews for mailbox message actions.\n/// </summary>\npublic interface IMailMessageActionPreviewService {\n    /// <summary>Previews a read/unread state change without executing it.</summary>\n    Task<MessageStateChangePreview> PreviewReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Previews a flagged/unflagged state change without executing it.</summary>\n    Task<MessageStateChangePreview> PreviewFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Previews a common bundle of state-change and mailbox actions side by side without executing them.</summary>\n    Task<CommonMessageActionsPreview> PreviewCommonActionsAsync(CommonMessageActionsPreviewRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Previews a message move without executing it.</summary>\n    Task<MoveMessagesPreview> PreviewMoveAsync(MoveMessagesPreviewRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Previews a message delete without executing it.</summary>\n    Task<DeleteMessagesPreview> PreviewDeleteAsync(DeleteMessagesPreviewRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Previews the standard set of mailbox actions side by side without executing them.</summary>\n    Task<StandardMessageActionsPreview> PreviewStandardActionsAsync(StandardMessageActionsPreviewRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailMessageActionService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides normalized mailbox message actions such as read-state changes, move, and delete.\n/// </summary>\npublic interface IMailMessageActionService {\n    /// <summary>Sets read/unread state for messages.</summary>\n    Task<MessageActionResult> SetReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Sets flagged/starred state for messages.</summary>\n    Task<MessageActionResult> SetFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Moves messages to a destination folder.</summary>\n    Task<MessageActionResult> MoveAsync(MoveMessagesRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Deletes messages.</summary>\n    Task<MessageActionResult> DeleteAsync(DeleteMessagesRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileAuthService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Performs reusable profile authentication flows and persists resulting credentials.\n/// </summary>\npublic interface IMailProfileAuthService {\n    /// <summary>Returns the current persisted authentication status for a saved profile.</summary>\n    Task<MailProfileAuthStatus?> GetStatusAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Authenticates a saved Gmail profile and persists the resulting tokens.</summary>\n    Task<MailProfileAuthenticationResult> LoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Authenticates a saved Microsoft Graph profile and persists the resulting access token.</summary>\n    Task<MailProfileAuthenticationResult> LoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Refreshes or reauthenticates a saved profile using its persisted metadata.</summary>\n    Task<MailProfileAuthenticationResult> RefreshAsync(string profileId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileBootstrapService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates or updates reusable provider profiles using higher-level bootstrap requests.\n/// </summary>\npublic interface IMailProfileBootstrapService {\n    /// <summary>Creates or updates a Microsoft Graph profile and any associated secrets.</summary>\n    Task<OperationResult> SaveGraphProfileAsync(GraphProfileBootstrapRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Creates or updates a Gmail profile and any associated secrets.</summary>\n    Task<OperationResult> SaveGmailProfileAsync(GmailProfileBootstrapRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileConnectionService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Tests whether saved profiles can establish a live provider connection.\n/// </summary>\npublic interface IMailProfileConnectionService {\n    /// <summary>Runs a connection test for a saved profile.</summary>\n    Task<MailProfileConnectionTestResult> TestAsync(\n        string profileId,\n        MailProfileConnectionTestScope scope = MailProfileConnectionTestScope.Auto,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileOverviewService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Produces reusable high-level summaries for saved profiles.\n/// </summary>\npublic interface IMailProfileOverviewService {\n    /// <summary>Builds summary views for all saved profiles.</summary>\n    Task<IReadOnlyList<MailProfileOverview>> GetOverviewsAsync(\n        MailProfileOverviewQuery? query = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Builds lightweight summary views for all saved profiles.</summary>\n    Task<IReadOnlyList<MailProfileOverviewCompact>> GetCompactOverviewsAsync(\n        MailProfileOverviewQuery? query = null,\n        CancellationToken cancellationToken = default);\n\n    /// <summary>Builds a summary view for a saved profile.</summary>\n    Task<MailProfileOverview?> GetOverviewAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Builds a lightweight summary view for a saved profile.</summary>\n    Task<MailProfileOverviewCompact?> GetCompactOverviewAsync(string profileId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileSecretService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Manages secrets associated with saved mail profiles.\n/// </summary>\npublic interface IMailProfileSecretService {\n    /// <summary>Saves or replaces a secret for an existing profile.</summary>\n    Task<OperationResult> SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or replaces a secret for an existing profile, optionally by copying from another stored secret reference.</summary>\n    Task<OperationResult> SetSecretAsync(string profileId, string secretName, string? secretValue, string? secretReference, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a secret from an existing profile.</summary>\n    Task<OperationResult> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Manages profile lifecycle operations for application adapters.\n/// </summary>\npublic interface IMailProfileService {\n    /// <summary>Returns all profiles.</summary>\n    Task<IReadOnlyList<MailProfile>> GetProfilesAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Returns a profile by identifier.</summary>\n    Task<MailProfile?> GetProfileAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Validates a profile definition.</summary>\n    Task<MailProfileValidationResult> ValidateAsync(MailProfile profile, CancellationToken cancellationToken = default);\n\n    /// <summary>Inspects a saved profile and reports configuration or authentication gaps.</summary>\n    Task<MailProfileValidationResult> DiagnoseAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a profile.</summary>\n    Task<OperationResult> SaveAsync(MailProfile profile, CancellationToken cancellationToken = default);\n\n    /// <summary>Deletes a profile.</summary>\n    Task<OperationResult> DeleteAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Marks a profile as the default profile.</summary>\n    Task<OperationResult> SetDefaultAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Returns the effective capabilities for a profile.</summary>\n    Task<ProfileCapabilities?> GetCapabilitiesAsync(string profileId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailProfileStore.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Persists reusable mail profiles.\n/// </summary>\npublic interface IMailProfileStore {\n    /// <summary>Returns all known profiles.</summary>\n    Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>Returns a profile by identifier.</summary>\n    Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves or updates a profile.</summary>\n    Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a profile by identifier.</summary>\n    Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailQueueService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides reusable access to queued outbound messages.\n/// </summary>\npublic interface IMailQueueService {\n    /// <summary>\n    /// Lists queued messages.\n    /// </summary>\n    Task<IReadOnlyList<QueuedMessageSummary>> ListAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Lists queued messages using a lightweight projection.\n    /// </summary>\n    Task<IReadOnlyList<QueuedMessageCompact>> ListCompactAsync(CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Gets a queued message by its message identifier.\n    /// </summary>\n    Task<QueuedMessageSummary?> GetAsync(string messageId, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Gets a queued message by its message identifier using a lightweight projection.\n    /// </summary>\n    Task<QueuedMessageCompact?> GetCompactAsync(string messageId, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Removes a queued message.\n    /// </summary>\n    Task<OperationResult> RemoveAsync(string messageId, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Processes all due queued messages.\n    /// </summary>\n    Task<QueueProcessResult> ProcessAsync(CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailReadHandler.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Handles normalized read operations for a specific profile kind.\n/// </summary>\npublic interface IMailReadHandler {\n    /// <summary>Profile kind handled by this instance.</summary>\n    MailProfileKind Kind { get; }\n\n    /// <summary>Lists folders.</summary>\n    Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default);\n\n    /// <summary>Searches messages.</summary>\n    Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Gets a detailed message view.</summary>\n    Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves an attachment.</summary>\n    Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailReadService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides normalized read-oriented mailbox operations.\n/// </summary>\npublic interface IMailReadService {\n    /// <summary>Lists folders or folder-like containers.</summary>\n    Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default);\n\n    /// <summary>Lists folders or folder-like containers using a lightweight projection.</summary>\n    Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default);\n\n    /// <summary>Searches for messages.</summary>\n    Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Searches for messages using a lightweight projection.</summary>\n    Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Lists attachments associated with a specific message.</summary>\n    Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Retrieves a detailed message view.</summary>\n    Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Retrieves detailed message views for multiple message identifiers.</summary>\n    Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Retrieves a lightweight detailed message view.</summary>\n    Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Retrieves lightweight detailed message views for multiple message identifiers.</summary>\n    Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves one or more attachments associated with a specific message.</summary>\n    Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves one or more attachments across multiple messages.</summary>\n    Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>Saves an attachment to the requested destination.</summary>\n    Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailSecretStore.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Stores secrets associated with mail profiles.\n/// </summary>\npublic interface IMailSecretStore {\n    /// <summary>Retrieves a secret value by profile id and secret name.</summary>\n    Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default);\n\n    /// <summary>Stores or replaces a secret value.</summary>\n    Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default);\n\n    /// <summary>Removes a secret value.</summary>\n    Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailSendHandler.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Handles normalized send operations for a specific profile kind.\n/// </summary>\npublic interface IMailSendHandler {\n    /// <summary>Profile kind handled by this instance.</summary>\n    MailProfileKind Kind { get; }\n\n    /// <summary>Sends or queues the message for the provided profile.</summary>\n    Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/IMailSendService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides normalized send-oriented mailbox operations.\n/// </summary>\npublic interface IMailSendService {\n    /// <summary>Sends or queues a message based on the request.</summary>\n    Task<SendResult> SendAsync(SendMessageRequest request, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ISmtpSessionFactory.cs",
    "content": "using Mailozaurr;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated SMTP sessions from reusable profile definitions.\n/// </summary>\npublic interface ISmtpSessionFactory {\n    /// <summary>Creates an authenticated SMTP session.</summary>\n    Task<Smtp> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ImapMailMessageActionHandler.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized IMAP mailbox action handler backed by Mailozaurr IMAP helpers.\n/// </summary>\npublic sealed class ImapMailMessageActionHandler : IMailMessageActionHandler {\n    private readonly IImapSessionFactory _sessionFactory;\n    private readonly Func<ImapClient, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>> _setReadStateAsync;\n    private readonly Func<ImapClient, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>> _moveAsync;\n    private readonly Func<ImapClient, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>> _deleteAsync;\n\n    /// <summary>\n    /// Creates a new IMAP message-action handler.\n    /// </summary>\n    public ImapMailMessageActionHandler(\n        IImapSessionFactory sessionFactory,\n        Func<ImapClient, MailProfile, SetReadStateRequest, CancellationToken, Task<MessageActionResult>>? setReadStateAsync = null,\n        Func<ImapClient, MailProfile, MoveMessagesRequest, CancellationToken, Task<MessageActionResult>>? moveAsync = null,\n        Func<ImapClient, MailProfile, DeleteMessagesRequest, CancellationToken, Task<MessageActionResult>>? deleteAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _setReadStateAsync = setReadStateAsync ?? DefaultSetReadStateAsync;\n        _moveAsync = moveAsync ?? DefaultMoveAsync;\n        _deleteAsync = deleteAsync ?? DefaultDeleteAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Imap;\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _setReadStateAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        var folder = client.GetCachedFolder(ResolveFolder(request.FolderId, profile), FolderAccess.ReadWrite);\n        var operation = await ImapBulkFlagOperations.SetFlagsAsync(\n            folder,\n            ParseUids(request.MessageIds),\n            MessageFlags.Flagged,\n            add: request.IsFlagged,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new MessageActionResult {\n            Succeeded = operation.Results.All(item => item.Ok) && operation.Updated > 0,\n            Code = operation.Results.All(item => item.Ok) && operation.Updated > 0 ? null : \"message_action_failed\",\n            Message = operation.Results.All(item => item.Ok) && operation.Updated > 0\n                ? (request.IsFlagged ? \"Flagged messages.\" : \"Unflagged messages.\")\n                : $\"{operation.Updated} message action(s) succeeded; {operation.Results.Count(item => !item.Ok)} failed.\",\n            ProfileId = profile.Id,\n            RequestedCount = operation.Requested,\n            SucceededCount = operation.Updated,\n            FailedCount = operation.Results.Count(item => !item.Ok),\n            Results = operation.Results.Select(item => new MessageActionItemResult {\n                MessageId = item.Uid.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Error\n            }).ToList()\n        };\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _moveAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _deleteAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<MessageActionResult> DefaultSetReadStateAsync(ImapClient client, MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken) {\n        var folder = client.GetCachedFolder(ResolveFolder(request.FolderId, profile), FolderAccess.ReadWrite);\n        var operation = await ImapBulkFlagOperations.SetFlagsAsync(\n            folder,\n            ParseUids(request.MessageIds),\n            MessageFlags.Seen,\n            add: request.IsRead,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new MessageActionResult {\n            Succeeded = operation.Results.All(item => item.Ok) && operation.Updated > 0,\n            Code = operation.Results.All(item => item.Ok) && operation.Updated > 0 ? null : \"message_action_failed\",\n            Message = operation.Results.All(item => item.Ok) && operation.Updated > 0\n                ? (request.IsRead ? \"Marked messages as read.\" : \"Marked messages as unread.\")\n                : $\"{operation.Updated} message action(s) succeeded; {operation.Results.Count(item => !item.Ok)} failed.\",\n            ProfileId = profile.Id,\n            RequestedCount = operation.Requested,\n            SucceededCount = operation.Updated,\n            FailedCount = operation.Results.Count(item => !item.Ok),\n            Results = operation.Results.Select(item => new MessageActionItemResult {\n                MessageId = item.Uid.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Error\n            }).ToList()\n        };\n    }\n\n    private static async Task<MessageActionResult> DefaultMoveAsync(ImapClient client, MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken) {\n        var operation = await ImapMoveOperations.MoveAsync(\n            client,\n            ResolveFolder(request.FolderId, profile),\n            request.DestinationFolderId,\n            ParseUids(request.MessageIds),\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new MessageActionResult {\n            Succeeded = operation.Results.All(item => item.Ok) && operation.Moved > 0,\n            Code = operation.Results.All(item => item.Ok) && operation.Moved > 0 ? null : \"message_action_failed\",\n            Message = operation.Results.All(item => item.Ok) && operation.Moved > 0\n                ? $\"Moved messages to '{request.DestinationFolderId}'.\"\n                : $\"{operation.Moved} message action(s) succeeded; {operation.Results.Count(item => !item.Ok)} failed.\",\n            ProfileId = profile.Id,\n            RequestedCount = operation.Requested,\n            SucceededCount = operation.Moved,\n            FailedCount = operation.Results.Count(item => !item.Ok),\n            Results = operation.Results.Select(item => new MessageActionItemResult {\n                MessageId = item.Uid.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Error\n            }).ToList()\n        };\n    }\n\n    private static async Task<MessageActionResult> DefaultDeleteAsync(ImapClient client, MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken) {\n        var folder = client.GetCachedFolder(ResolveFolder(request.FolderId, profile), FolderAccess.ReadWrite);\n        var operation = await ImapDeleteOperations.DeleteAsync(\n            folder,\n            ParseUids(request.MessageIds),\n            expunge: true,\n            cancellationToken: cancellationToken).ConfigureAwait(false);\n\n        return new MessageActionResult {\n            Succeeded = operation.Results.All(item => item.Ok) && operation.Deleted > 0,\n            Code = operation.Results.All(item => item.Ok) && operation.Deleted > 0 ? null : \"message_action_failed\",\n            Message = operation.Results.All(item => item.Ok) && operation.Deleted > 0\n                ? \"Deleted messages.\"\n                : $\"{operation.Deleted} message action(s) succeeded; {operation.Results.Count(item => !item.Ok)} failed.\",\n            ProfileId = profile.Id,\n            RequestedCount = operation.Requested,\n            SucceededCount = operation.Deleted,\n            FailedCount = operation.Results.Count(item => !item.Ok),\n            Results = operation.Results.Select(item => new MessageActionItemResult {\n                MessageId = item.Uid.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),\n                Succeeded = item.Ok,\n                Code = item.Ok ? null : \"message_action_failed\",\n                Message = item.Error\n            }).ToList()\n        };\n    }\n\n    private static IReadOnlyList<UniqueId> ParseUids(IEnumerable<string> messageIds) =>\n        messageIds.Where(id => !string.IsNullOrWhiteSpace(id)).Select(ParseUid).ToArray();\n\n    private static UniqueId ParseUid(string value) {\n        if (uint.TryParse(value.Trim(), out var uid)) {\n            return new UniqueId(uid);\n        }\n\n        throw new InvalidOperationException($\"Message id '{value}' is not a valid IMAP UID.\");\n    }\n\n    private static string ResolveFolder(string? folderId, MailProfile profile) {\n        if (!string.IsNullOrWhiteSpace(folderId)) {\n            return folderId!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Folder, out var folder) && !string.IsNullOrWhiteSpace(folder)) {\n            return folder!.Trim();\n        }\n        return \"INBOX\";\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ImapMailReadHandler.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized IMAP read handler backed by Mailozaurr IMAP helpers.\n/// </summary>\npublic sealed class ImapMailReadHandler : IMailReadHandler {\n    private readonly IImapSessionFactory _sessionFactory;\n    private readonly Func<ImapClient, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>> _getFoldersAsync;\n    private readonly Func<ImapClient, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>> _searchAsync;\n    private readonly Func<ImapClient, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>> _getMessageAsync;\n    private readonly Func<ImapClient, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>> _saveAttachmentAsync;\n\n    /// <summary>\n    /// Creates a new IMAP read handler.\n    /// </summary>\n    public ImapMailReadHandler(\n        IImapSessionFactory sessionFactory,\n        Func<ImapClient, MailFolderQuery, CancellationToken, Task<IReadOnlyList<FolderRef>>>? getFoldersAsync = null,\n        Func<ImapClient, MailProfile, MailSearchRequest, CancellationToken, Task<IReadOnlyList<MessageSummary>>>? searchAsync = null,\n        Func<ImapClient, MailProfile, GetMessageRequest, CancellationToken, Task<MessageDetail?>>? getMessageAsync = null,\n        Func<ImapClient, MailProfile, SaveAttachmentRequest, CancellationToken, Task<OperationResult>>? saveAttachmentAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _getFoldersAsync = getFoldersAsync ?? DefaultGetFoldersAsync;\n        _searchAsync = searchAsync ?? DefaultSearchAsync;\n        _getMessageAsync = getMessageAsync ?? DefaultGetMessageAsync;\n        _saveAttachmentAsync = saveAttachmentAsync ?? DefaultSaveAttachmentAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Imap;\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getFoldersAsync(client, query, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _searchAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _getMessageAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n        using var client = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _saveAttachmentAsync(client, profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task<IReadOnlyList<FolderRef>> DefaultGetFoldersAsync(\n        ImapClient client,\n        MailFolderQuery query,\n        CancellationToken cancellationToken) {\n        var results = new List<FolderRef>();\n        if (query.RootOnly) {\n            await foreach (var folder in ImapRootFolderEnumerator.EnumerateAsync(client, cancellationToken).ConfigureAwait(false)) {\n                results.Add(MapFolder(folder, query.ProfileId));\n            }\n            return results;\n        }\n\n        var root = client.PersonalNamespaces.Count > 0\n            ? client.GetFolder(client.PersonalNamespaces[0])\n            : client.GetFolder(string.Empty);\n        await CollectFoldersAsync(root, results, query.ProfileId, query.ParentFolderId, cancellationToken).ConfigureAwait(false);\n        return results;\n    }\n\n    private static async Task CollectFoldersAsync(\n        IMailFolder root,\n        List<FolderRef> results,\n        string profileId,\n        string? parentFolderId,\n        CancellationToken cancellationToken) {\n        var folders = await root.GetSubfoldersAsync(false, cancellationToken).ConfigureAwait(false);\n        foreach (var folder in folders) {\n            if (!string.IsNullOrWhiteSpace(parentFolderId) &&\n                !string.Equals(folder.ParentFolder?.FullName, parentFolderId, StringComparison.OrdinalIgnoreCase) &&\n                !string.Equals(folder.FullName, parentFolderId, StringComparison.OrdinalIgnoreCase)) {\n                if (folder.Attributes.HasFlag(FolderAttributes.HasChildren)) {\n                    await CollectFoldersAsync(folder, results, profileId, parentFolderId, cancellationToken).ConfigureAwait(false);\n                }\n                continue;\n            }\n\n            results.Add(MapFolder(folder, profileId));\n            if (folder.Attributes.HasFlag(FolderAttributes.HasChildren)) {\n                await CollectFoldersAsync(folder, results, profileId, null, cancellationToken).ConfigureAwait(false);\n            }\n        }\n    }\n\n    private static FolderRef MapFolder(IMailFolder folder, string profileId) => new() {\n        ProfileId = profileId,\n        Id = folder.FullName,\n        DisplayName = folder.Name,\n        Path = folder.FullName,\n        MailboxId = null,\n        SpecialUse = folder.Attributes.HasFlag(FolderAttributes.Inbox) ? \"Inbox\" : null,\n        MessageCount = folder.IsOpen ? folder.Count : (int?)null,\n        UnreadCount = folder.IsOpen ? folder.Unread : (int?)null\n    };\n\n    private static async Task<IReadOnlyList<MessageSummary>> DefaultSearchAsync(\n        ImapClient client,\n        MailProfile profile,\n        MailSearchRequest request,\n        CancellationToken cancellationToken) {\n        var messages = await MailboxSearcher.SearchImapAsync(\n            client,\n            folder: ResolveFolder(request.FolderId, profile),\n            subject: request.SubjectContains,\n            fromContains: request.FromContains,\n            toContains: request.ToContains,\n            hasAttachment: request.HasAttachments,\n            since: request.Since?.UtcDateTime,\n            before: request.Before?.UtcDateTime,\n            maxResults: request.Limit ?? 0,\n            cancellationToken: cancellationToken,\n            queryString: request.QueryText).ConfigureAwait(false);\n\n        return messages.Select(message => MapSummary(profile.Id, ResolveFolder(request.FolderId, profile), message)).ToArray();\n    }\n\n    private static async Task<MessageDetail?> DefaultGetMessageAsync(\n        ImapClient client,\n        MailProfile profile,\n        GetMessageRequest request,\n        CancellationToken cancellationToken) {\n        var folder = ResolveFolder(request.FolderId, profile);\n        var uid = ParseUid(request.MessageId);\n        var maxBodyBytes = GetMaxBodyBytes(profile);\n        var result = await ImapMessageReader.ReadAsync(\n            client,\n            new ImapMessageReadRequest(uid, folder, maxBodyBytes),\n            cancellationToken).ConfigureAwait(false);\n\n        var summary = new MessageSummary {\n            ProfileId = profile.Id,\n            Id = result.Uid.ToString(System.Globalization.CultureInfo.InvariantCulture),\n            FolderId = result.Folder,\n            Subject = result.Subject,\n            From = SplitAddresses(result.From),\n            To = SplitAddresses(result.To),\n            ReceivedAt = result.DateUtc,\n            HasAttachments = result.HasAttachments\n        };\n\n        var detail = new MessageDetail {\n            ProfileId = profile.Id,\n            Id = summary.Id,\n            Summary = summary,\n            TextBody = result.TextBody,\n            HtmlBody = result.HtmlBody,\n            Attachments = result.Attachments.Select((attachment, index) => new AttachmentSummary {\n                MessageId = summary.Id,\n                Id = index.ToString(System.Globalization.CultureInfo.InvariantCulture),\n                FileName = attachment.FileName,\n                ContentType = attachment.ContentType\n            }).ToList()\n        };\n\n        if (request.IncludeRawContent) {\n            var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadOnly);\n            var message = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n            using var stream = new MemoryStream();\n            message.WriteTo(stream);\n            stream.Position = 0;\n            using var reader = new StreamReader(stream);\n            detail.RawContent = reader.ReadToEnd();\n        }\n\n        return detail;\n    }\n\n    private static async Task<OperationResult> DefaultSaveAttachmentAsync(\n        ImapClient client,\n        MailProfile profile,\n        SaveAttachmentRequest request,\n        CancellationToken cancellationToken) {\n        var folder = ResolveFolder(request.FolderId, profile);\n        var uid = ParseUid(request.MessageId);\n        var mailFolder = client.GetCachedFolder(folder, FolderAccess.ReadOnly);\n        var message = await mailFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false);\n        var attachments = message.Attachments.ToList();\n        if (attachments.Count == 0) {\n            return OperationResult.Failure(\"attachment_not_found\", \"Message has no attachments.\");\n        }\n\n        var attachment = ResolveAttachment(attachments, request.AttachmentId);\n        if (attachment == null) {\n            return OperationResult.Failure(\"attachment_not_found\", $\"Attachment '{request.AttachmentId}' was not found.\");\n        }\n\n        var destinationPath = MimeAttachmentStorage.ResolveDestinationPath(request.DestinationPath, attachment);\n        if (File.Exists(destinationPath) && !request.Overwrite) {\n            return OperationResult.Failure(\"destination_exists\", $\"Destination '{destinationPath}' already exists.\");\n        }\n\n        MimeAttachmentStorage.SaveAttachment(attachment, destinationPath);\n        return OperationResult.Success($\"Attachment saved to '{destinationPath}'.\");\n    }\n\n    private static MessageSummary MapSummary(string profileId, string folder, ImapEmailMessage message) => new() {\n        ProfileId = profileId,\n        Id = message.Uid.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),\n        FolderId = folder,\n        Subject = message.Message.Subject,\n        Preview = message.Message.TextBody,\n        From = message.Message.From.Mailboxes.Select(mailbox => new MessageRecipient {\n            Name = mailbox.Name,\n            Address = mailbox.Address ?? string.Empty\n        }).ToList(),\n        To = message.Message.To.Mailboxes.Select(mailbox => new MessageRecipient {\n            Name = mailbox.Name,\n            Address = mailbox.Address ?? string.Empty\n        }).ToList(),\n        SentAt = message.Message.Date,\n        ReceivedAt = message.Message.Date,\n        HasAttachments = message.Message.Attachments.Any(),\n        Priority = message.Message.Priority switch {\n            MimeKit.MessagePriority.Urgent => MessagePriority.High,\n            MimeKit.MessagePriority.NonUrgent => MessagePriority.Low,\n            _ => MessagePriority.Normal\n        }\n    };\n\n    private static UniqueId ParseUid(string value) {\n        if (uint.TryParse(value, out var uid)) {\n            return new UniqueId(uid);\n        }\n\n        throw new InvalidOperationException($\"Message id '{value}' is not a valid IMAP UID.\");\n    }\n\n    private static string ResolveFolder(string? folderId, MailProfile profile) {\n        if (!string.IsNullOrWhiteSpace(folderId)) {\n            return folderId!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Folder, out var folder) && !string.IsNullOrWhiteSpace(folder)) {\n            return folder!.Trim();\n        }\n        return \"INBOX\";\n    }\n\n    private static long GetMaxBodyBytes(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.MaxBodyBytes, out var raw) &&\n            long.TryParse(raw, out var value) &&\n            value > 0) {\n            return value;\n        }\n\n        return 256 * 1024;\n    }\n\n    private static List<MessageRecipient> SplitAddresses(string? addresses) {\n        if (string.IsNullOrWhiteSpace(addresses)) {\n            return new List<MessageRecipient>();\n        }\n\n        var normalized = addresses!;\n        return normalized\n            .Split(new[] { \", \" }, StringSplitOptions.RemoveEmptyEntries)\n            .Select(address => new MessageRecipient { Address = address })\n            .ToList();\n    }\n\n    private static MimeEntity? ResolveAttachment(IReadOnlyList<MimeEntity> attachments, string attachmentId) =>\n        MimeAttachmentStorage.ResolveAttachment(attachments, attachmentId);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ImapSessionFactory.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated IMAP sessions using profile settings and stored secrets.\n/// </summary>\npublic sealed class ImapSessionFactory : IImapSessionFactory {\n    private readonly IMailSecretStore? _secretStore;\n    private readonly Func<ImapSessionRequest, CancellationToken, Task<ImapClient>> _connectAsync;\n\n    /// <summary>\n    /// Creates a new IMAP session factory.\n    /// </summary>\n    public ImapSessionFactory(\n        IMailSecretStore? secretStore = null,\n        Func<ImapSessionRequest, CancellationToken, Task<ImapClient>>? connectAsync = null) {\n        _secretStore = secretStore;\n        _connectAsync = connectAsync ?? ((request, cancellationToken) => ImapSessionService.ConnectAsync(request, cancellationToken));\n    }\n\n    /// <inheritdoc />\n    public async Task<ImapClient> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (profile.Kind != MailProfileKind.Imap) {\n            throw new InvalidOperationException($\"Profile '{profile.Id}' is not an IMAP profile.\");\n        }\n\n        var request = await CreateRequestAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _connectAsync(request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<ImapSessionRequest> CreateRequestAsync(MailProfile profile, CancellationToken cancellationToken) {\n        var server = RequireSetting(profile, MailProfileSettingsKeys.Server);\n        var port = GetIntSetting(profile, MailProfileSettingsKeys.Port) ?? 993;\n        var authMode = GetAuthMode(profile);\n        var userName = ResolveUserName(profile);\n        var secret = await ResolveSecretAsync(profile, authMode, cancellationToken).ConfigureAwait(false);\n\n        return new ImapSessionRequest {\n            Connection = new ImapConnectionRequest(\n                server,\n                port,\n                GetSecureSocketOptions(profile),\n                GetIntSetting(profile, MailProfileSettingsKeys.Timeout) ?? 30000,\n                GetBoolSetting(profile, MailProfileSettingsKeys.SkipCertificateRevocation) ?? false,\n                GetBoolSetting(profile, MailProfileSettingsKeys.SkipCertificateValidation) ?? false,\n                GetIntSetting(profile, MailProfileSettingsKeys.RetryCount) ?? 3,\n                GetIntSetting(profile, MailProfileSettingsKeys.RetryDelayMilliseconds) ?? 500,\n                GetDoubleSetting(profile, MailProfileSettingsKeys.RetryDelayBackoff) ?? 2.0),\n            UserName = userName,\n            Secret = secret,\n            AuthMode = authMode\n        };\n    }\n\n    private async Task<string> ResolveSecretAsync(MailProfile profile, ProtocolAuthMode authMode, CancellationToken cancellationToken) {\n        var primarySecretName = authMode == ProtocolAuthMode.OAuth2\n            ? MailSecretNames.AccessToken\n            : MailSecretNames.Password;\n        var fallbackSecretName = authMode == ProtocolAuthMode.OAuth2\n            ? MailSecretNames.Password\n            : null;\n\n        var secret = _secretStore == null\n            ? null\n            : await _secretStore.GetSecretAsync(profile.Id, primarySecretName, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(secret) && fallbackSecretName != null && _secretStore != null) {\n            secret = await _secretStore.GetSecretAsync(profile.Id, fallbackSecretName, cancellationToken).ConfigureAwait(false);\n        }\n\n        if (string.IsNullOrWhiteSpace(secret)) {\n            throw new InvalidOperationException($\"Secret '{primarySecretName}' is required for profile '{profile.Id}'.\");\n        }\n\n        return secret!;\n    }\n\n    private static string ResolveUserName(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.UserName, out var userName) && !string.IsNullOrWhiteSpace(userName)) {\n            return userName.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) && !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            var defaultMailbox = profile.DefaultMailbox;\n            return defaultMailbox!.Trim();\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' requires '{MailProfileSettingsKeys.UserName}' or a mailbox value.\");\n    }\n\n    private static string RequireSetting(MailProfile profile, string key) {\n        if (profile.Settings.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value.Trim();\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' requires setting '{key}'.\");\n    }\n\n    private static ProtocolAuthMode GetAuthMode(MailProfile profile) {\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthMode, out var rawMode);\n        return ProtocolAuth.ParseMode(rawMode, ProtocolAuthMode.Basic);\n    }\n\n    private static SecureSocketOptions GetSecureSocketOptions(MailProfile profile) {\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.SecureSocketOptions, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return SecureSocketOptions.Auto;\n        }\n\n        if (Enum.TryParse<SecureSocketOptions>(raw, ignoreCase: true, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid secure socket option '{raw}'.\");\n    }\n\n    private static int? GetIntSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (int.TryParse(raw, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid integer setting '{key}'.\");\n    }\n\n    private static double? GetDoubleSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (double.TryParse(raw, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid numeric setting '{key}'.\");\n    }\n\n    private static bool? GetBoolSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (bool.TryParse(raw, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid boolean setting '{key}'.\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/JsonMailDraftExchangeService.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Imports and exports drafts as JSON documents.\n/// </summary>\npublic sealed class JsonMailDraftExchangeService : IMailDraftExchangeService {\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <inheritdoc />\n    public async Task<MailDraft> LoadAsync(string path, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Draft path is required.\", nameof(path));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        using var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);\n        var draft = await JsonSerializer.DeserializeAsync<MailDraft>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        if (draft == null) {\n            throw new InvalidDataException($\"Draft file '{fullPath}' did not contain a valid draft document.\");\n        }\n\n        return draft;\n    }\n\n    /// <inheritdoc />\n    public async Task SaveAsync(string path, MailDraft draft, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Draft path is required.\", nameof(path));\n        }\n        if (draft == null) {\n            throw new ArgumentNullException(nameof(draft));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        var directory = Path.GetDirectoryName(fullPath);\n        if (!string.IsNullOrWhiteSpace(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None);\n        await JsonSerializer.SerializeAsync(stream, draft, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/JsonMailMessageActionPlanExchangeService.cs",
    "content": "using System.Text.Json;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Imports and exports normalized message action plans as JSON documents.\n/// </summary>\npublic sealed class JsonMailMessageActionPlanExchangeService : IMailMessageActionPlanExchangeService {\n    private static readonly JsonSerializerOptions SerializerOptions = new() {\n        PropertyNameCaseInsensitive = true,\n        WriteIndented = true\n    };\n\n    /// <inheritdoc />\n    public async Task<MessageActionExecutionPlan> LoadAsync(string path, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Action plan path is required.\", nameof(path));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        using var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);\n        var plan = await JsonSerializer.DeserializeAsync<MessageActionExecutionPlan>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        if (plan == null) {\n            throw new InvalidDataException($\"Action plan file '{fullPath}' did not contain a valid action plan document.\");\n        }\n\n        return plan;\n    }\n\n    /// <inheritdoc />\n    public async Task SaveAsync(string path, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Action plan path is required.\", nameof(path));\n        }\n        if (plan == null) {\n            throw new ArgumentNullException(nameof(plan));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        var directory = Path.GetDirectoryName(fullPath);\n        if (!string.IsNullOrWhiteSpace(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None);\n        await JsonSerializer.SerializeAsync(stream, plan, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageActionExecutionPlan>> LoadBatchAsync(string path, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Action plan batch path is required.\", nameof(path));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        using var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);\n        var plans = await JsonSerializer.DeserializeAsync<List<MessageActionExecutionPlan>>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        if (plans == null) {\n            throw new InvalidDataException($\"Action plan batch file '{fullPath}' did not contain a valid action plan array.\");\n        }\n\n        return plans;\n    }\n\n    /// <inheritdoc />\n    public async Task SaveBatchAsync(string path, IReadOnlyList<MessageActionExecutionPlan> plans, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Action plan batch path is required.\", nameof(path));\n        }\n        if (plans == null) {\n            throw new ArgumentNullException(nameof(plans));\n        }\n\n        var fullPath = Path.GetFullPath(path);\n        var directory = Path.GetDirectoryName(fullPath);\n        if (!string.IsNullOrWhiteSpace(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None);\n        await JsonSerializer.SerializeAsync(stream, plans, SerializerOptions, cancellationToken).ConfigureAwait(false);\n        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ListAttachmentsRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for listing attachments associated with a message.\n/// </summary>\npublic sealed class ListAttachmentsRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier when the provider requires folder scoping.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailApplication.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a composed Mailozaurr application service graph for adapters.\n/// </summary>\npublic sealed class MailApplication {\n    internal MailApplication(\n        IMailProfileStore profileStore,\n        IMailSecretStore secretStore,\n        IMailDraftStore draftStore,\n        IMailProfileService profiles,\n        IMailProfileOverviewService profileOverview,\n        IMailProfileConnectionService profileConnections,\n        IMailProfileSecretService profileSecrets,\n        IMailProfileBootstrapService profileBootstrap,\n        IMailProfileAuthService profileAuth,\n        IMailFolderAliasService folderAliases,\n        IMailDraftService drafts,\n        IMailDraftExchangeService draftExchange,\n        IMailReadService read,\n        IMailMessageActionPreviewService messageActionPreview,\n        IMailMessageActionPlanService messageActionPlans,\n        IMailMessageActionPlanExchangeService messageActionPlanExchange,\n        IMailMessageActionPlanRegistryService messageActionPlanRegistry,\n        IMailMessageActionBatchService messageActionBatch,\n        IMailMessageActionService messageActions,\n        IMailSendService send,\n        IMailQueueService queue,\n        IReadOnlyList<IMailReadHandler> readHandlers,\n        IReadOnlyList<IMailMessageActionHandler> messageActionHandlers,\n        IReadOnlyList<IMailSendHandler> sendHandlers) {\n        ProfileStore = profileStore;\n        SecretStore = secretStore;\n        DraftStore = draftStore;\n        Profiles = profiles;\n        ProfileOverview = profileOverview;\n        ProfileConnections = profileConnections;\n        ProfileSecrets = profileSecrets;\n        ProfileBootstrap = profileBootstrap;\n        ProfileAuth = profileAuth;\n        FolderAliases = folderAliases;\n        Drafts = drafts;\n        DraftExchange = draftExchange;\n        Read = read;\n        MessageActionPreview = messageActionPreview;\n        MessageActionPlans = messageActionPlans;\n        MessageActionPlanExchange = messageActionPlanExchange;\n        MessageActionPlanRegistry = messageActionPlanRegistry;\n        MessageActionBatch = messageActionBatch;\n        MessageActions = messageActions;\n        Send = send;\n        Queue = queue;\n        ReadHandlers = readHandlers;\n        MessageActionHandlers = messageActionHandlers;\n        SendHandlers = sendHandlers;\n    }\n\n    /// <summary>Profile persistence service.</summary>\n    public IMailProfileStore ProfileStore { get; }\n\n    /// <summary>Secret persistence service.</summary>\n    public IMailSecretStore SecretStore { get; }\n\n    /// <summary>Draft persistence service.</summary>\n    public IMailDraftStore DraftStore { get; }\n\n    /// <summary>Profile lifecycle service.</summary>\n    public IMailProfileService Profiles { get; }\n\n    /// <summary>Aggregated profile overview service.</summary>\n    public IMailProfileOverviewService ProfileOverview { get; }\n\n    /// <summary>Live profile connection-test service.</summary>\n    public IMailProfileConnectionService ProfileConnections { get; }\n\n    /// <summary>Profile secret lifecycle service.</summary>\n    public IMailProfileSecretService ProfileSecrets { get; }\n\n    /// <summary>Higher-level profile bootstrap workflows.</summary>\n    public IMailProfileBootstrapService ProfileBootstrap { get; }\n\n    /// <summary>Higher-level profile authentication workflows.</summary>\n    public IMailProfileAuthService ProfileAuth { get; }\n\n    /// <summary>Provider-neutral folder alias discovery service.</summary>\n    public IMailFolderAliasService FolderAliases { get; }\n\n    /// <summary>Draft lifecycle service.</summary>\n    public IMailDraftService Drafts { get; }\n\n    /// <summary>Draft import/export service.</summary>\n    public IMailDraftExchangeService DraftExchange { get; }\n\n    /// <summary>Normalized read service.</summary>\n    public IMailReadService Read { get; }\n\n    /// <summary>Normalized dry-run mailbox action preview service.</summary>\n    public IMailMessageActionPreviewService MessageActionPreview { get; }\n\n    /// <summary>Normalized mailbox action planning service.</summary>\n    public IMailMessageActionPlanService MessageActionPlans { get; }\n\n    /// <summary>Normalized mailbox action plan import/export service.</summary>\n    public IMailMessageActionPlanExchangeService MessageActionPlanExchange { get; }\n\n    /// <summary>Normalized persisted mailbox action plan batch registry service.</summary>\n    public IMailMessageActionPlanRegistryService MessageActionPlanRegistry { get; }\n\n    /// <summary>Normalized mailbox action batch execution service.</summary>\n    public IMailMessageActionBatchService MessageActionBatch { get; }\n\n    /// <summary>Normalized mailbox message action service.</summary>\n    public IMailMessageActionService MessageActions { get; }\n\n    /// <summary>Normalized send service.</summary>\n    public IMailSendService Send { get; }\n\n    /// <summary>Normalized outbound queue service.</summary>\n    public IMailQueueService Queue { get; }\n\n    /// <summary>Registered read handlers.</summary>\n    public IReadOnlyList<IMailReadHandler> ReadHandlers { get; }\n\n    /// <summary>Registered message-action handlers.</summary>\n    public IReadOnlyList<IMailMessageActionHandler> MessageActionHandlers { get; }\n\n    /// <summary>Registered send handlers.</summary>\n    public IReadOnlyList<IMailSendHandler> SendHandlers { get; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailApplicationBuilder.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Builds a composed <see cref=\"MailApplication\" /> with reusable default services.\n/// </summary>\npublic sealed class MailApplicationBuilder {\n    private readonly List<IMailReadHandler> _readHandlers = new();\n    private readonly List<IMailMessageActionHandler> _messageActionHandlers = new();\n    private readonly List<IMailSendHandler> _sendHandlers = new();\n    private MailApplicationOptions _options = new();\n    private IMailProfileStore? _profileStore;\n    private IMailSecretStore? _secretStore;\n    private IMailDraftStore? _draftStore;\n    private IMailMessageActionPlanBatchStore? _messageActionPlanBatchStore;\n    private IMailProfileService? _profileService;\n    private IMailProfileOverviewService? _profileOverviewService;\n    private IMailProfileConnectionService? _profileConnectionService;\n    private IMailProfileSecretService? _profileSecretService;\n    private IMailProfileBootstrapService? _profileBootstrapService;\n    private IMailProfileAuthService? _profileAuthService;\n    private IMailFolderAliasService? _folderAliasService;\n    private IMailDraftService? _draftService;\n    private IMailDraftExchangeService? _draftExchangeService;\n    private IMailReadService? _readService;\n    private IMailMessageActionPreviewService? _messageActionPreviewService;\n    private IMailMessageActionPlanService? _messageActionPlanService;\n    private IMailMessageActionPlanExchangeService? _messageActionPlanExchangeService;\n    private IMailMessageActionPlanRegistryService? _messageActionPlanRegistryService;\n    private IMailMessageActionBatchService? _messageActionBatchService;\n    private IMailMessageActionService? _messageActionService;\n    private IMailSendService? _sendService;\n    private IMailQueueService? _queueService;\n    private IDraftMimeMessageFactory? _draftMimeMessageFactory;\n    private IPendingMessageRepository? _pendingMessageRepository;\n    private IImapSessionFactory? _imapSessionFactory;\n    private IGraphSessionFactory? _graphSessionFactory;\n    private IGmailSessionFactory? _gmailSessionFactory;\n    private ISmtpSessionFactory? _smtpSessionFactory;\n\n    /// <summary>\n    /// Creates a new builder with default options.\n    /// </summary>\n    public MailApplicationBuilder() {\n    }\n\n    /// <summary>\n    /// Creates a new builder with the provided options.\n    /// </summary>\n    public MailApplicationBuilder(MailApplicationOptions options) {\n        _options = options ?? throw new ArgumentNullException(nameof(options));\n    }\n\n    /// <summary>Replaces the current options.</summary>\n    public MailApplicationBuilder Configure(MailApplicationOptions options) {\n        _options = options ?? throw new ArgumentNullException(nameof(options));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile store.</summary>\n    public MailApplicationBuilder UseProfileStore(IMailProfileStore profileStore) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        return this;\n    }\n\n    /// <summary>Uses an explicit secret store.</summary>\n    public MailApplicationBuilder UseSecretStore(IMailSecretStore secretStore) {\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n        return this;\n    }\n\n    /// <summary>Uses an explicit draft store.</summary>\n    public MailApplicationBuilder UseDraftStore(IMailDraftStore draftStore) {\n        _draftStore = draftStore ?? throw new ArgumentNullException(nameof(draftStore));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action plan batch store.</summary>\n    public MailApplicationBuilder UseMessageActionPlanBatchStore(IMailMessageActionPlanBatchStore messageActionPlanBatchStore) {\n        _messageActionPlanBatchStore = messageActionPlanBatchStore ?? throw new ArgumentNullException(nameof(messageActionPlanBatchStore));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile service.</summary>\n    public MailApplicationBuilder UseProfileService(IMailProfileService profileService) {\n        _profileService = profileService ?? throw new ArgumentNullException(nameof(profileService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile overview service.</summary>\n    public MailApplicationBuilder UseProfileOverviewService(IMailProfileOverviewService profileOverviewService) {\n        _profileOverviewService = profileOverviewService ?? throw new ArgumentNullException(nameof(profileOverviewService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile connection-test service.</summary>\n    public MailApplicationBuilder UseProfileConnectionService(IMailProfileConnectionService profileConnectionService) {\n        _profileConnectionService = profileConnectionService ?? throw new ArgumentNullException(nameof(profileConnectionService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile secret service.</summary>\n    public MailApplicationBuilder UseProfileSecretService(IMailProfileSecretService profileSecretService) {\n        _profileSecretService = profileSecretService ?? throw new ArgumentNullException(nameof(profileSecretService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile bootstrap service.</summary>\n    public MailApplicationBuilder UseProfileBootstrapService(IMailProfileBootstrapService profileBootstrapService) {\n        _profileBootstrapService = profileBootstrapService ?? throw new ArgumentNullException(nameof(profileBootstrapService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit profile authentication service.</summary>\n    public MailApplicationBuilder UseProfileAuthService(IMailProfileAuthService profileAuthService) {\n        _profileAuthService = profileAuthService ?? throw new ArgumentNullException(nameof(profileAuthService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit folder alias discovery service.</summary>\n    public MailApplicationBuilder UseFolderAliasService(IMailFolderAliasService folderAliasService) {\n        _folderAliasService = folderAliasService ?? throw new ArgumentNullException(nameof(folderAliasService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit draft service.</summary>\n    public MailApplicationBuilder UseDraftService(IMailDraftService draftService) {\n        _draftService = draftService ?? throw new ArgumentNullException(nameof(draftService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit draft exchange service.</summary>\n    public MailApplicationBuilder UseDraftExchangeService(IMailDraftExchangeService draftExchangeService) {\n        _draftExchangeService = draftExchangeService ?? throw new ArgumentNullException(nameof(draftExchangeService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit read service.</summary>\n    public MailApplicationBuilder UseReadService(IMailReadService readService) {\n        _readService = readService ?? throw new ArgumentNullException(nameof(readService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action preview service.</summary>\n    public MailApplicationBuilder UseMessageActionPreviewService(IMailMessageActionPreviewService messageActionPreviewService) {\n        _messageActionPreviewService = messageActionPreviewService ?? throw new ArgumentNullException(nameof(messageActionPreviewService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action planning service.</summary>\n    public MailApplicationBuilder UseMessageActionPlanService(IMailMessageActionPlanService messageActionPlanService) {\n        _messageActionPlanService = messageActionPlanService ?? throw new ArgumentNullException(nameof(messageActionPlanService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action plan exchange service.</summary>\n    public MailApplicationBuilder UseMessageActionPlanExchangeService(IMailMessageActionPlanExchangeService messageActionPlanExchangeService) {\n        _messageActionPlanExchangeService = messageActionPlanExchangeService ?? throw new ArgumentNullException(nameof(messageActionPlanExchangeService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action plan registry service.</summary>\n    public MailApplicationBuilder UseMessageActionPlanRegistryService(IMailMessageActionPlanRegistryService messageActionPlanRegistryService) {\n        _messageActionPlanRegistryService = messageActionPlanRegistryService ?? throw new ArgumentNullException(nameof(messageActionPlanRegistryService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action batch execution service.</summary>\n    public MailApplicationBuilder UseMessageActionBatchService(IMailMessageActionBatchService messageActionBatchService) {\n        _messageActionBatchService = messageActionBatchService ?? throw new ArgumentNullException(nameof(messageActionBatchService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit message-action service.</summary>\n    public MailApplicationBuilder UseMessageActionService(IMailMessageActionService messageActionService) {\n        _messageActionService = messageActionService ?? throw new ArgumentNullException(nameof(messageActionService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit send service.</summary>\n    public MailApplicationBuilder UseSendService(IMailSendService sendService) {\n        _sendService = sendService ?? throw new ArgumentNullException(nameof(sendService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit queue service.</summary>\n    public MailApplicationBuilder UseQueueService(IMailQueueService queueService) {\n        _queueService = queueService ?? throw new ArgumentNullException(nameof(queueService));\n        return this;\n    }\n\n    /// <summary>Uses an explicit draft MIME message factory.</summary>\n    public MailApplicationBuilder UseDraftMimeMessageFactory(IDraftMimeMessageFactory draftMimeMessageFactory) {\n        _draftMimeMessageFactory = draftMimeMessageFactory ?? throw new ArgumentNullException(nameof(draftMimeMessageFactory));\n        return this;\n    }\n\n    /// <summary>Uses an explicit pending-message repository.</summary>\n    public MailApplicationBuilder UsePendingMessageRepository(IPendingMessageRepository pendingMessageRepository) {\n        _pendingMessageRepository = pendingMessageRepository ?? throw new ArgumentNullException(nameof(pendingMessageRepository));\n        return this;\n    }\n\n    /// <summary>Uses an explicit IMAP session factory.</summary>\n    public MailApplicationBuilder UseImapSessionFactory(IImapSessionFactory imapSessionFactory) {\n        _imapSessionFactory = imapSessionFactory ?? throw new ArgumentNullException(nameof(imapSessionFactory));\n        return this;\n    }\n\n    /// <summary>Uses an explicit Graph session factory.</summary>\n    public MailApplicationBuilder UseGraphSessionFactory(IGraphSessionFactory graphSessionFactory) {\n        _graphSessionFactory = graphSessionFactory ?? throw new ArgumentNullException(nameof(graphSessionFactory));\n        return this;\n    }\n\n    /// <summary>Uses an explicit Gmail session factory.</summary>\n    public MailApplicationBuilder UseGmailSessionFactory(IGmailSessionFactory gmailSessionFactory) {\n        _gmailSessionFactory = gmailSessionFactory ?? throw new ArgumentNullException(nameof(gmailSessionFactory));\n        return this;\n    }\n\n    /// <summary>Uses an explicit SMTP session factory.</summary>\n    public MailApplicationBuilder UseSmtpSessionFactory(ISmtpSessionFactory smtpSessionFactory) {\n        _smtpSessionFactory = smtpSessionFactory ?? throw new ArgumentNullException(nameof(smtpSessionFactory));\n        return this;\n    }\n\n    /// <summary>Adds a read handler.</summary>\n    public MailApplicationBuilder AddReadHandler(IMailReadHandler handler) {\n        _readHandlers.Add(handler ?? throw new ArgumentNullException(nameof(handler)));\n        return this;\n    }\n\n    /// <summary>Adds a message-action handler.</summary>\n    public MailApplicationBuilder AddMessageActionHandler(IMailMessageActionHandler handler) {\n        _messageActionHandlers.Add(handler ?? throw new ArgumentNullException(nameof(handler)));\n        return this;\n    }\n\n    /// <summary>Adds a send handler.</summary>\n    public MailApplicationBuilder AddSendHandler(IMailSendHandler handler) {\n        _sendHandlers.Add(handler ?? throw new ArgumentNullException(nameof(handler)));\n        return this;\n    }\n\n    /// <summary>\n    /// Builds the composed application.\n    /// </summary>\n    public MailApplication Build() {\n        var profileStore = _profileStore ?? new FileMailProfileStore(_options.ProfileStore);\n        var secretStore = _secretStore ?? new FileMailSecretStore(_options.SecretStore);\n        var draftStore = _draftStore ?? new FileMailDraftStore(_options.DraftStore);\n        var messageActionPlanBatchStore = _messageActionPlanBatchStore ?? new FileMailMessageActionPlanBatchStore(_options.ActionPlanBatchStore);\n        var profileService = _profileService ?? new MailProfileService(profileStore, secretStore);\n        var imapSessionFactory = _imapSessionFactory ?? new ImapSessionFactory(secretStore);\n        var graphSessionFactory = _graphSessionFactory ?? new GraphSessionFactory(secretStore);\n        var gmailSessionFactory = _gmailSessionFactory ?? new GmailSessionFactory(secretStore);\n        var smtpSessionFactory = _smtpSessionFactory ?? new SmtpSessionFactory(secretStore);\n        var profileSecretService = _profileSecretService ?? new MailProfileSecretService(profileStore, secretStore);\n        var profileBootstrapService = _profileBootstrapService ?? new MailProfileBootstrapService(profileService, profileSecretService, secretStore);\n        var profileAuthService = _profileAuthService ?? new MailProfileAuthService(profileService, profileSecretService, secretStore);\n        var profileOverviewService = _profileOverviewService ?? new MailProfileOverviewService(profileService, profileAuthService);\n        var profileConnectionService = _profileConnectionService ?? new MailProfileConnectionService(profileStore, imapSessionFactory, graphSessionFactory, gmailSessionFactory, smtpSessionFactory);\n        var draftService = _draftService ?? new MailDraftService(draftStore, profileStore);\n        var draftExchangeService = _draftExchangeService ?? new JsonMailDraftExchangeService();\n        var draftMimeMessageFactory = _draftMimeMessageFactory ?? new DraftMimeMessageFactory();\n        var pendingMessageRepository = _pendingMessageRepository ?? new FilePendingMessageRepository(_options.PendingMessageStore);\n\n        var readHandlers = new List<IMailReadHandler>(_readHandlers);\n        if (_options.EnableImapReadHandler && !readHandlers.Any(handler => handler.Kind == MailProfileKind.Imap)) {\n            readHandlers.Add(new ImapMailReadHandler(imapSessionFactory));\n        }\n        if (_options.EnableGraphReadHandler && !readHandlers.Any(handler => handler.Kind == MailProfileKind.Graph)) {\n            readHandlers.Add(new GraphMailReadHandler(graphSessionFactory));\n        }\n        if (_options.EnableGmailReadHandler && !readHandlers.Any(handler => handler.Kind == MailProfileKind.Gmail)) {\n            readHandlers.Add(new GmailMailReadHandler(gmailSessionFactory));\n        }\n\n        var messageActionHandlers = new List<IMailMessageActionHandler>(_messageActionHandlers);\n        if (!messageActionHandlers.Any(handler => handler.Kind == MailProfileKind.Imap)) {\n            messageActionHandlers.Add(new ImapMailMessageActionHandler(imapSessionFactory));\n        }\n        if (!messageActionHandlers.Any(handler => handler.Kind == MailProfileKind.Graph)) {\n            messageActionHandlers.Add(new GraphMailMessageActionHandler(graphSessionFactory));\n        }\n        if (!messageActionHandlers.Any(handler => handler.Kind == MailProfileKind.Gmail)) {\n            messageActionHandlers.Add(new GmailMailMessageActionHandler(gmailSessionFactory));\n        }\n\n        var sendHandlers = new List<IMailSendHandler>(_sendHandlers);\n        if (_options.EnableGraphSendHandler && !sendHandlers.Any(handler => handler.Kind == MailProfileKind.Graph)) {\n            sendHandlers.Add(new GraphMailSendHandler(graphSessionFactory, draftMimeMessageFactory, pendingMessageRepository));\n        }\n        if (_options.EnableGmailSendHandler && !sendHandlers.Any(handler => handler.Kind == MailProfileKind.Gmail)) {\n            sendHandlers.Add(new GmailMailSendHandler(gmailSessionFactory, draftMimeMessageFactory, pendingMessageRepository));\n        }\n        if (_options.EnableSmtpSendHandler && !sendHandlers.Any(handler => handler.Kind == MailProfileKind.Smtp)) {\n            sendHandlers.Add(new SmtpMailSendHandler(smtpSessionFactory, draftMimeMessageFactory, pendingMessageRepository));\n        }\n\n        var readService = _readService ?? new RoutedMailReadService(profileStore, readHandlers);\n        var folderAliasService = _folderAliasService ?? new MailFolderAliasService(profileStore, readService);\n        var messageActionPreviewService = _messageActionPreviewService ?? new MailMessageActionPreviewService(profileStore, folderAliasService);\n        var messageActionService = _messageActionService ?? new RoutedMailMessageActionService(profileStore, messageActionHandlers, folderAliasService);\n        var messageActionPlanService = _messageActionPlanService ?? new MailMessageActionPlanService(messageActionPreviewService, messageActionService);\n        var messageActionPlanExchangeService = _messageActionPlanExchangeService ?? new JsonMailMessageActionPlanExchangeService();\n        var messageActionBatchService = _messageActionBatchService ?? new MailMessageActionBatchService(messageActionPlanService);\n        var messageActionPlanRegistryService = _messageActionPlanRegistryService ?? new MailMessageActionPlanRegistryService(messageActionPlanBatchStore, messageActionPlanExchangeService, messageActionPreviewService, messageActionPlanService, messageActionBatchService, profileStore);\n        var sendService = _sendService ?? new RoutedMailSendService(profileStore, sendHandlers);\n        var queueService = _queueService ?? new PendingMailQueueService(pendingMessageRepository);\n\n        return new MailApplication(\n            profileStore,\n            secretStore,\n            draftStore,\n            profileService,\n            profileOverviewService,\n            profileConnectionService,\n            profileSecretService,\n            profileBootstrapService,\n            profileAuthService,\n            folderAliasService,\n            draftService,\n            draftExchangeService,\n            readService,\n            messageActionPreviewService,\n            messageActionPlanService,\n            messageActionPlanExchangeService,\n            messageActionPlanRegistryService,\n            messageActionBatchService,\n            messageActionService,\n            sendService,\n            queueService,\n            readHandlers.AsReadOnly(),\n            messageActionHandlers.AsReadOnly(),\n            sendHandlers.AsReadOnly());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailApplicationOptions.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Configures the default Mailozaurr application composition.\n/// </summary>\npublic sealed class MailApplicationOptions {\n    /// <summary>Options used for the default profile store.</summary>\n    public MailProfileStoreOptions ProfileStore { get; set; } = new();\n\n    /// <summary>Options used for the default secret store.</summary>\n    public MailSecretStoreOptions SecretStore { get; set; } = new();\n\n    /// <summary>Options used for the default draft store.</summary>\n    public MailDraftStoreOptions DraftStore { get; set; } = new();\n\n    /// <summary>Options used for the default reusable action plan batch store.</summary>\n    public MailMessageActionPlanBatchStoreOptions ActionPlanBatchStore { get; set; } = new();\n\n    /// <summary>Options used for the default pending-message repository.</summary>\n    public PendingMessageRepositoryOptions PendingMessageStore { get; set; } = new();\n\n    /// <summary>Whether the built-in IMAP read handler should be registered.</summary>\n    public bool EnableImapReadHandler { get; set; } = true;\n\n    /// <summary>Whether the built-in Graph read handler should be registered.</summary>\n    public bool EnableGraphReadHandler { get; set; } = true;\n\n    /// <summary>Whether the built-in Graph send handler should be registered.</summary>\n    public bool EnableGraphSendHandler { get; set; } = true;\n\n    /// <summary>Whether the built-in Gmail read handler should be registered.</summary>\n    public bool EnableGmailReadHandler { get; set; } = true;\n\n    /// <summary>Whether the built-in Gmail send handler should be registered.</summary>\n    public bool EnableGmailSendHandler { get; set; } = true;\n\n    /// <summary>Whether the built-in SMTP send handler should be registered.</summary>\n    public bool EnableSmtpSendHandler { get; set; } = true;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailApplicationPaths.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Resolves default storage paths used by application-layer services.\n/// </summary>\npublic static class MailApplicationPaths {\n    private const string RootDirectoryName = \"Mailozaurr\";\n    private const string ProfilesSubDirectoryName = \"Profiles\";\n    private const string SecretsSubDirectoryName = \"Secrets\";\n    private const string DraftsSubDirectoryName = \"Drafts\";\n    private const string ActionPlanBatchesSubDirectoryName = \"ActionPlanBatches\";\n    private const string ProfilesOverrideDirectoryVariable = \"MAILOZAURR_PROFILE_DIRECTORY\";\n    private const string SecretsOverrideDirectoryVariable = \"MAILOZAURR_SECRET_DIRECTORY\";\n    private const string DraftsOverrideDirectoryVariable = \"MAILOZAURR_DRAFT_DIRECTORY\";\n    private const string ActionPlanBatchesOverrideDirectoryVariable = \"MAILOZAURR_ACTION_PLAN_DIRECTORY\";\n\n    /// <summary>\n    /// Resolves the default directory used to store profile configuration.\n    /// </summary>\n    public static string ResolveProfilesDirectory() => ResolveDirectory(\n        ProfilesOverrideDirectoryVariable,\n        ProfilesSubDirectoryName);\n\n    /// <summary>\n    /// Resolves the default directory used to store protected secrets.\n    /// </summary>\n    public static string ResolveSecretsDirectory() => ResolveDirectory(\n        SecretsOverrideDirectoryVariable,\n        SecretsSubDirectoryName);\n\n    /// <summary>\n    /// Resolves the default directory used to store reusable drafts.\n    /// </summary>\n    public static string ResolveDraftsDirectory() => ResolveDirectory(\n        DraftsOverrideDirectoryVariable,\n        DraftsSubDirectoryName);\n\n    /// <summary>\n    /// Resolves the default directory used to store reusable action plan batches.\n    /// </summary>\n    public static string ResolveActionPlanBatchesDirectory() => ResolveDirectory(\n        ActionPlanBatchesOverrideDirectoryVariable,\n        ActionPlanBatchesSubDirectoryName);\n\n    private static string ResolveDirectory(string overrideVariableName, string subDirectoryName) {\n        var overrideDirectory = Environment.GetEnvironmentVariable(overrideVariableName);\n        if (!string.IsNullOrWhiteSpace(overrideDirectory)) {\n            return Path.GetFullPath(overrideDirectory);\n        }\n\n        var baseDirectory = GetBaseDirectory();\n        return Path.Combine(baseDirectory, RootDirectoryName, subDirectoryName);\n    }\n\n    private static string GetBaseDirectory() {\n        var candidates = new[] {\n            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),\n            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)\n        };\n\n        foreach (var candidate in candidates) {\n            if (!string.IsNullOrWhiteSpace(candidate)) {\n                return candidate;\n            }\n        }\n\n        return AppContext.BaseDirectory;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailCapability.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes normalized capabilities that a profile can expose across adapters.\n/// </summary>\n[Flags]\npublic enum MailCapability {\n    /// <summary>No capabilities.</summary>\n    None = 0,\n\n    /// <summary>List folders or mailbox containers.</summary>\n    ListFolders = 1 << 0,\n\n    /// <summary>Search messages.</summary>\n    SearchMessages = 1 << 1,\n\n    /// <summary>Read message details.</summary>\n    ReadMessages = 1 << 2,\n\n    /// <summary>Save attachments to disk or other storage.</summary>\n    SaveAttachments = 1 << 3,\n\n    /// <summary>Mark messages read, unread, flagged, or equivalent.</summary>\n    MarkMessages = 1 << 4,\n\n    /// <summary>Move messages between folders or labels.</summary>\n    MoveMessages = 1 << 5,\n\n    /// <summary>Delete messages.</summary>\n    DeleteMessages = 1 << 6,\n\n    /// <summary>Send messages.</summary>\n    SendMessages = 1 << 7,\n\n    /// <summary>Wait for or subscribe to new messages.</summary>\n    WaitForMessages = 1 << 8,\n\n    /// <summary>Manage inbox rules or equivalent server-side rules.</summary>\n    ManageRules = 1 << 9,\n\n    /// <summary>Manage calendar events exposed through the same provider.</summary>\n    ManageEvents = 1 << 10,\n\n    /// <summary>Manage mailbox permissions.</summary>\n    ManagePermissions = 1 << 11,\n\n    /// <summary>Work with threaded conversations.</summary>\n    UseThreads = 1 << 12,\n\n    /// <summary>Work with labels or label-like grouping.</summary>\n    UseLabels = 1 << 13,\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailCapabilityCatalog.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Provides the default normalized capability map for each profile kind.\n/// </summary>\npublic static class MailCapabilityCatalog {\n    /// <summary>\n    /// Returns the default capabilities for a given profile kind.\n    /// </summary>\n    public static ProfileCapabilities For(MailProfileKind kind) {\n        var capabilities = kind switch {\n            MailProfileKind.Imap => MailCapability.ListFolders\n                | MailCapability.SearchMessages\n                | MailCapability.ReadMessages\n                | MailCapability.SaveAttachments\n                | MailCapability.MarkMessages\n                | MailCapability.MoveMessages\n                | MailCapability.DeleteMessages\n                | MailCapability.WaitForMessages,\n            MailProfileKind.Pop3 => MailCapability.SearchMessages\n                | MailCapability.ReadMessages\n                | MailCapability.SaveAttachments\n                | MailCapability.DeleteMessages\n                | MailCapability.WaitForMessages,\n            MailProfileKind.Graph => MailCapability.ListFolders\n                | MailCapability.SearchMessages\n                | MailCapability.ReadMessages\n                | MailCapability.SaveAttachments\n                | MailCapability.MarkMessages\n                | MailCapability.MoveMessages\n                | MailCapability.DeleteMessages\n                | MailCapability.SendMessages\n                | MailCapability.WaitForMessages\n                | MailCapability.ManageRules\n                | MailCapability.ManageEvents\n                | MailCapability.ManagePermissions,\n            MailProfileKind.Gmail => MailCapability.ListFolders\n                | MailCapability.SearchMessages\n                | MailCapability.ReadMessages\n                | MailCapability.SaveAttachments\n                | MailCapability.MarkMessages\n                | MailCapability.MoveMessages\n                | MailCapability.DeleteMessages\n                | MailCapability.SendMessages\n                | MailCapability.UseThreads\n                | MailCapability.UseLabels,\n            MailProfileKind.Smtp => MailCapability.SendMessages,\n            MailProfileKind.SendGrid => MailCapability.SendMessages,\n            MailProfileKind.Mailgun => MailCapability.SendMessages,\n            MailProfileKind.Ses => MailCapability.SendMessages,\n            _ => MailCapability.None,\n        };\n\n        return new ProfileCapabilities(kind, capabilities);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailDraft.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a persisted reusable draft.\n/// </summary>\npublic sealed class MailDraft {\n    /// <summary>Stable draft identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing draft name.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>When the draft was first created.</summary>\n    public DateTimeOffset CreatedAt { get; set; }\n\n    /// <summary>When the draft was last updated.</summary>\n    public DateTimeOffset UpdatedAt { get; set; }\n\n    /// <summary>Normalized draft message payload.</summary>\n    public DraftMessage Message { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailDraftCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a persisted reusable draft.\n/// </summary>\npublic sealed class MailDraftCompact {\n    /// <summary>Stable draft identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing draft name.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Associated profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional subject line.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Primary recipient count.</summary>\n    public int ToCount { get; set; }\n\n    /// <summary>Attachment count.</summary>\n    public int AttachmentCount { get; set; }\n\n    /// <summary>When the draft was last updated.</summary>\n    public DateTimeOffset UpdatedAt { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailDraftService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Implements reusable draft lifecycle operations.\n/// </summary>\npublic sealed class MailDraftService : IMailDraftService {\n    private readonly IMailDraftStore _store;\n    private readonly IMailProfileStore _profileStore;\n\n    /// <summary>\n    /// Creates a new draft service.\n    /// </summary>\n    public MailDraftService(IMailDraftStore store, IMailProfileStore profileStore) {\n        _store = store ?? throw new ArgumentNullException(nameof(store));\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n    }\n\n    /// <inheritdoc />\n    public Task<IReadOnlyList<MailDraft>> GetDraftsAsync(CancellationToken cancellationToken = default) =>\n        _store.GetAllAsync(cancellationToken);\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailDraftCompact>> GetDraftsCompactAsync(CancellationToken cancellationToken = default) =>\n        (await _store.GetAllAsync(cancellationToken).ConfigureAwait(false))\n        .Select(ToCompact)\n        .ToArray();\n\n    /// <inheritdoc />\n    public Task<MailDraft?> GetDraftAsync(string draftId, CancellationToken cancellationToken = default) =>\n        _store.GetByIdAsync(draftId, cancellationToken);\n\n    /// <inheritdoc />\n    public async Task<MailDraftCompact?> GetDraftCompactAsync(string draftId, CancellationToken cancellationToken = default) {\n        var draft = await _store.GetByIdAsync(draftId, cancellationToken).ConfigureAwait(false);\n        return draft == null ? null : ToCompact(draft);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAsync(MailDraft draft, CancellationToken cancellationToken = default) {\n        var validation = await ValidateAsync(draft, cancellationToken).ConfigureAwait(false);\n        if (!validation.Succeeded) {\n            return validation;\n        }\n\n        await _store.SaveAsync(draft, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success($\"Draft '{draft.Id}' saved.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> DeleteAsync(string draftId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(draftId)) {\n            throw new ArgumentException(\"Draft id is required.\", nameof(draftId));\n        }\n\n        var removed = await _store.RemoveAsync(draftId.Trim(), cancellationToken).ConfigureAwait(false);\n        return removed\n            ? OperationResult.Success($\"Draft '{draftId.Trim()}' deleted.\")\n            : OperationResult.Failure(\"draft_not_found\", $\"Draft '{draftId.Trim()}' was not found.\");\n    }\n\n    private async Task<OperationResult> ValidateAsync(MailDraft? draft, CancellationToken cancellationToken) {\n        if (draft == null) {\n            return OperationResult.Failure(\"draft_invalid\", \"Draft is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(draft.Id)) {\n            return OperationResult.Failure(\"draft_invalid\", \"Draft id is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(draft.Name)) {\n            return OperationResult.Failure(\"draft_invalid\", \"Draft name is required.\");\n        }\n        if (draft.Message == null) {\n            return OperationResult.Failure(\"draft_invalid\", \"Draft message is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(draft.Message.ProfileId)) {\n            return OperationResult.Failure(\"draft_invalid\", \"Draft profile id is required.\");\n        }\n\n        var profile = await _profileStore.GetByIdAsync(draft.Message.ProfileId, cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return OperationResult.Failure(\"draft_profile_not_found\", $\"Profile '{draft.Message.ProfileId}' was not found.\");\n        }\n\n        return OperationResult.Success();\n    }\n\n    private static MailDraftCompact ToCompact(MailDraft draft) => new() {\n        Id = draft.Id,\n        Name = draft.Name,\n        ProfileId = draft.Message.ProfileId,\n        Subject = draft.Message.Subject,\n        ToCount = draft.Message.To.Count,\n        AttachmentCount = draft.Message.Attachments.Count,\n        UpdatedAt = draft.UpdatedAt,\n        Summary = $\"{draft.Id} [{draft.Message.ProfileId}] {draft.Name}\"\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailDraftStoreOptions.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Configures where persisted drafts are stored.\n/// </summary>\npublic sealed class MailDraftStoreOptions {\n    /// <summary>Directory containing the draft store file.</summary>\n    public string? DirectoryPath { get; set; }\n\n    /// <summary>File name used to persist drafts.</summary>\n    public string FileName { get; set; } = \"drafts.json\";\n\n    /// <summary>\n    /// Resolves the file path that should be used by the draft store.\n    /// </summary>\n    public string GetFilePath() {\n        var directory = string.IsNullOrWhiteSpace(DirectoryPath)\n            ? MailApplicationPaths.ResolveDraftsDirectory()\n            : Path.GetFullPath(DirectoryPath);\n        return Path.Combine(directory, FileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailFolderAliasService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of provider-neutral folder alias discovery.\n/// </summary>\npublic sealed class MailFolderAliasService : IMailFolderAliasService {\n    private static readonly AliasDefinition[] KnownAliases = {\n        new(MailFolderAliases.Inbox, \"Inbox\", MatchMode.ReadOrList, new[] { \"inbox\" }, new[] { \"inbox\" }),\n        new(MailFolderAliases.Archive, \"Archive\", MatchMode.Move, new[] { \"archive\", \"allmail\" }, new[] { \"archive\", \"allmail\" }),\n        new(MailFolderAliases.Trash, \"Trash\", MatchMode.Move, new[] { \"trash\", \"deleteditems\", \"bin\" }, new[] { \"trash\", \"deleteditems\", \"deleteditem\", \"bin\" }),\n        new(MailFolderAliases.Sent, \"Sent\", MatchMode.ListOnly, new[] { \"sent\", \"sentitems\" }, new[] { \"sent\", \"sentitems\", \"sentmail\" }),\n        new(MailFolderAliases.Drafts, \"Drafts\", MatchMode.ListOnly, new[] { \"drafts\", \"draft\" }, new[] { \"drafts\", \"draft\" }),\n        new(MailFolderAliases.Junk, \"Junk\", MatchMode.MoveOrList, new[] { \"junk\", \"junkemail\", \"spam\" }, new[] { \"junk\", \"junkemail\", \"spam\" })\n    };\n\n    private readonly IMailProfileStore _profileStore;\n    private readonly IMailReadService _read;\n\n    /// <summary>\n    /// Creates a new folder alias service.\n    /// </summary>\n    public MailFolderAliasService(IMailProfileStore profileStore, IMailReadService read) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _read = read ?? throw new ArgumentNullException(nameof(read));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(\n        string profileId,\n        string? mailboxId = null,\n        CancellationToken cancellationToken = default) {\n        var profile = await GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);\n        var capabilities = profile.GetCapabilities();\n        var folders = await TryGetFoldersAsync(profile, mailboxId, capabilities, cancellationToken).ConfigureAwait(false);\n        var results = new List<MailFolderAliasSummary>();\n        foreach (var alias in KnownAliases) {\n            if (!IsSupported(alias.Mode, capabilities)) {\n                continue;\n            }\n\n            var resolved = TryResolveAlias(alias, folders);\n            results.Add(new MailFolderAliasSummary {\n                ProfileId = profile.Id,\n                MailboxId = mailboxId,\n                Alias = alias.Alias,\n                DisplayName = alias.DisplayName,\n                IsSupported = true,\n                IsResolved = resolved != null,\n                FolderId = resolved?.Id,\n                FolderDisplayName = resolved?.DisplayName,\n                FolderPath = resolved?.Path,\n                SpecialUse = resolved?.SpecialUse,\n                Summary = resolved == null\n                    ? $\"{alias.Alias} [alias-only]\"\n                    : $\"{alias.Alias} -> {resolved.Path ?? resolved.DisplayName ?? resolved.Id}\"\n            });\n        }\n\n        return results;\n    }\n\n    /// <inheritdoc />\n    public async Task<MailFolderTargetResolution> ResolveAsync(\n        string profileId,\n        string targetFolderId,\n        string? mailboxId = null,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(targetFolderId)) {\n            throw new ArgumentException(\"Target folder id is required.\", nameof(targetFolderId));\n        }\n\n        var normalizedTarget = targetFolderId.Trim();\n        var canonicalAlias = MailFolderAliases.Canonicalize(normalizedTarget);\n        if (canonicalAlias == null) {\n            return new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = normalizedTarget,\n                IsAlias = false,\n                IsSupported = true,\n                IsResolved = true,\n                EffectiveFolderId = normalizedTarget,\n                Summary = $\"{normalizedTarget} [explicit]\"\n            };\n        }\n\n        var alias = (await GetAliasesAsync(profileId, mailboxId, cancellationToken).ConfigureAwait(false))\n            .FirstOrDefault(item => string.Equals(item.Alias, canonicalAlias, StringComparison.OrdinalIgnoreCase));\n\n        if (alias == null) {\n            return new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = normalizedTarget,\n                IsAlias = true,\n                Alias = canonicalAlias,\n                IsSupported = false,\n                IsResolved = false,\n                EffectiveFolderId = canonicalAlias,\n                Summary = $\"{canonicalAlias} [unsupported]\"\n            };\n        }\n\n        return new MailFolderTargetResolution {\n            ProfileId = alias.ProfileId,\n            MailboxId = alias.MailboxId,\n            RequestedValue = normalizedTarget,\n            IsAlias = true,\n            Alias = alias.Alias,\n            IsSupported = alias.IsSupported,\n            IsResolved = alias.IsResolved,\n            EffectiveFolderId = !string.IsNullOrWhiteSpace(alias.FolderId) ? alias.FolderId! : alias.Alias,\n            FolderDisplayName = alias.FolderDisplayName,\n            FolderPath = alias.FolderPath,\n            SpecialUse = alias.SpecialUse,\n            Summary = alias.IsResolved\n                ? $\"{alias.Alias} -> {alias.FolderPath ?? alias.FolderDisplayName ?? alias.FolderId}\"\n                : $\"{alias.Alias} [alias-only]\"\n        };\n    }\n\n    private async Task<MailProfile> GetProfileAsync(string profileId, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        return profile ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    private async Task<IReadOnlyList<FolderRef>> TryGetFoldersAsync(\n        MailProfile profile,\n        string? mailboxId,\n        ProfileCapabilities capabilities,\n        CancellationToken cancellationToken) {\n        if (!capabilities.Supports(MailCapability.ListFolders)) {\n            return Array.Empty<FolderRef>();\n        }\n\n        try {\n            return await _read.GetFoldersAsync(new MailFolderQuery {\n                ProfileId = profile.Id,\n                MailboxId = mailboxId\n            }, cancellationToken).ConfigureAwait(false);\n        } catch {\n            return Array.Empty<FolderRef>();\n        }\n    }\n\n    private static bool IsSupported(MatchMode mode, ProfileCapabilities capabilities) => mode switch {\n        MatchMode.Move => capabilities.Supports(MailCapability.MoveMessages),\n        MatchMode.ListOnly => capabilities.Supports(MailCapability.ListFolders),\n        MatchMode.MoveOrList => capabilities.Supports(MailCapability.MoveMessages) || capabilities.Supports(MailCapability.ListFolders),\n        MatchMode.ReadOrList => capabilities.Supports(MailCapability.ListFolders) || capabilities.Supports(MailCapability.ReadMessages) || capabilities.Supports(MailCapability.SearchMessages),\n        _ => false\n    };\n\n    private static FolderRef? TryResolveAlias(AliasDefinition alias, IReadOnlyList<FolderRef> folders) {\n        if (folders.Count == 0) {\n            return null;\n        }\n\n        foreach (var folder in folders) {\n            if (Matches(folder.SpecialUse, alias.SpecialUseMatches)) {\n                return folder;\n            }\n        }\n\n        foreach (var folder in folders) {\n            if (Matches(folder.Path, alias.NameMatches) || Matches(folder.DisplayName, alias.NameMatches)) {\n                return folder;\n            }\n        }\n\n        return null;\n    }\n\n    private static bool Matches(string? value, IReadOnlyList<string> candidates) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return false;\n        }\n\n        var normalizedValue = Normalize(value!);\n        foreach (var candidate in candidates) {\n            if (!string.IsNullOrWhiteSpace(candidate) && normalizedValue == Normalize(candidate!)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static string Normalize(string value) {\n        var buffer = new char[value.Length];\n        var index = 0;\n        foreach (var character in value) {\n            if (char.IsLetterOrDigit(character)) {\n                buffer[index++] = char.ToLowerInvariant(character);\n            }\n        }\n\n        return new string(buffer, 0, index);\n    }\n\n    private sealed class AliasDefinition {\n        public AliasDefinition(\n            string alias,\n            string displayName,\n            MatchMode mode,\n            IReadOnlyList<string> specialUseMatches,\n            IReadOnlyList<string> nameMatches) {\n            Alias = alias;\n            DisplayName = displayName;\n            Mode = mode;\n            SpecialUseMatches = specialUseMatches;\n            NameMatches = nameMatches;\n        }\n\n        public string Alias { get; }\n\n        public string DisplayName { get; }\n\n        public MatchMode Mode { get; }\n\n        public IReadOnlyList<string> SpecialUseMatches { get; }\n\n        public IReadOnlyList<string> NameMatches { get; }\n    }\n\n    private enum MatchMode {\n        Move,\n        ListOnly,\n        MoveOrList,\n        ReadOrList\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailFolderAliasSummary.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes a provider-neutral folder alias and its resolved provider target when known.\n/// </summary>\npublic sealed class MailFolderAliasSummary {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Provider-neutral alias such as Inbox, Archive, or Trash.</summary>\n    public string Alias { get; set; } = string.Empty;\n\n    /// <summary>User-facing alias display name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Indicates whether the alias can be used by the selected profile.</summary>\n    public bool IsSupported { get; set; }\n\n    /// <summary>Indicates whether the alias was resolved to a provider folder.</summary>\n    public bool IsResolved { get; set; }\n\n    /// <summary>Resolved provider folder identifier when known.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Resolved provider folder display name when known.</summary>\n    public string? FolderDisplayName { get; set; }\n\n    /// <summary>Resolved provider folder path when known.</summary>\n    public string? FolderPath { get; set; }\n\n    /// <summary>Resolved provider special-use marker when known.</summary>\n    public string? SpecialUse { get; set; }\n\n    /// <summary>Human-readable summary for lightweight CLI and MCP output.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailFolderAliases.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Shared well-known folder aliases used across providers.\n/// </summary>\npublic static class MailFolderAliases {\n    private static readonly string[] KnownAliases = {\n        Inbox,\n        Archive,\n        Trash,\n        Sent,\n        Drafts,\n        Junk\n    };\n\n    /// <summary>Inbox folder alias.</summary>\n    public const string Inbox = \"Inbox\";\n\n    /// <summary>Archive folder alias.</summary>\n    public const string Archive = \"Archive\";\n\n    /// <summary>Trash/deleted-items folder alias.</summary>\n    public const string Trash = \"Trash\";\n\n    /// <summary>Sent items folder alias.</summary>\n    public const string Sent = \"Sent\";\n\n    /// <summary>Drafts folder alias.</summary>\n    public const string Drafts = \"Drafts\";\n\n    /// <summary>Junk or spam folder alias.</summary>\n    public const string Junk = \"Junk\";\n\n    /// <summary>Returns all known provider-neutral folder aliases.</summary>\n    public static IReadOnlyList<string> All => KnownAliases;\n\n    /// <summary>Returns the canonical alias value when the input matches a known alias.</summary>\n    public static string? Canonicalize(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n\n        var normalizedValue = value!.Trim();\n        foreach (var alias in KnownAliases) {\n            if (string.Equals(alias, normalizedValue, StringComparison.OrdinalIgnoreCase)) {\n                return alias;\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailFolderQuery.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for listing folders or folder-like mailbox containers.\n/// </summary>\npublic sealed class MailFolderQuery {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional parent folder identifier.</summary>\n    public string? ParentFolderId { get; set; }\n\n    /// <summary>Whether only top-level folders should be returned.</summary>\n    public bool RootOnly { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailFolderTargetResolution.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes how a requested folder target resolves for a specific profile and mailbox.\n/// </summary>\npublic sealed class MailFolderTargetResolution {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>The original requested destination folder value.</summary>\n    public string RequestedValue { get; set; } = string.Empty;\n\n    /// <summary>True when the requested value matched a known provider-neutral alias.</summary>\n    public bool IsAlias { get; set; }\n\n    /// <summary>The canonical provider-neutral alias when <see cref=\"IsAlias\" /> is true.</summary>\n    public string? Alias { get; set; }\n\n    /// <summary>True when the target is supported for the selected profile.</summary>\n    public bool IsSupported { get; set; }\n\n    /// <summary>True when a provider-specific folder target was resolved.</summary>\n    public bool IsResolved { get; set; }\n\n    /// <summary>The effective folder identifier that should be used for actions.</summary>\n    public string EffectiveFolderId { get; set; } = string.Empty;\n\n    /// <summary>The resolved provider folder display name when known.</summary>\n    public string? FolderDisplayName { get; set; }\n\n    /// <summary>The resolved provider folder path when known.</summary>\n    public string? FolderPath { get; set; }\n\n    /// <summary>The resolved provider special-use marker when known.</summary>\n    public string? SpecialUse { get; set; }\n\n    /// <summary>Human-readable summary for lightweight CLI and MCP output.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionBatchService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Executes batches of normalized message action plans through the shared planning service.\n/// </summary>\npublic sealed class MailMessageActionBatchService : IMailMessageActionBatchService {\n    private readonly IMailMessageActionPlanService _planService;\n\n    /// <summary>\n    /// Creates a new message action batch execution service.\n    /// </summary>\n    public MailMessageActionBatchService(IMailMessageActionPlanService planService) {\n        _planService = planService ?? throw new ArgumentNullException(nameof(planService));\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionBatchExecutionResult> ExecuteAsync(\n        IReadOnlyList<MessageActionExecutionPlan> plans,\n        bool continueOnError = true,\n        CancellationToken cancellationToken = default) {\n        if (plans == null) {\n            throw new ArgumentNullException(nameof(plans));\n        }\n\n        var result = new MessageActionBatchExecutionResult {\n            RequestedPlanCount = plans.Count\n        };\n\n        for (var index = 0; index < plans.Count; index++) {\n            var plan = plans[index];\n            if (plan == null) {\n                result.FailedPlanCount++;\n                result.AttemptedPlanCount++;\n                result.Results.Add(new MessageActionBatchExecutionItemResult {\n                    Index = index,\n                    Succeeded = false,\n                    Code = \"plan_required\",\n                    Message = \"A plan entry was null.\",\n                    FailedCount = 1\n                });\n                if (!continueOnError) {\n                    AddSkippedItems(result, plans, index + 1);\n                    break;\n                }\n\n                continue;\n            }\n\n            var execution = await _planService.ExecuteAsync(plan, cancellationToken).ConfigureAwait(false);\n            result.AttemptedPlanCount++;\n            if (execution.Succeeded) {\n                result.SucceededPlanCount++;\n            } else {\n                result.FailedPlanCount++;\n            }\n\n            result.Results.Add(new MessageActionBatchExecutionItemResult {\n                Index = index,\n                Action = plan.Action,\n                ExecutionKind = plan.ExecutionKind,\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.RequestedCount,\n                Succeeded = execution.Succeeded,\n                Code = execution.Code,\n                Message = execution.Message,\n                SucceededCount = execution.SucceededCount,\n                FailedCount = execution.FailedCount\n            });\n\n            if (!execution.Succeeded && !continueOnError) {\n                AddSkippedItems(result, plans, index + 1);\n                break;\n            }\n        }\n\n        result.Succeeded = result.FailedPlanCount == 0;\n        result.Code = result.Succeeded ? null : \"batch_execution_failed\";\n        result.Message = result.Succeeded\n            ? $\"Executed {result.AttemptedPlanCount} action plan(s) successfully.\"\n            : $\"Executed {result.AttemptedPlanCount} action plan(s): {result.SucceededPlanCount} succeeded, {result.FailedPlanCount} failed, {result.SkippedPlanCount} skipped.\";\n        return result;\n    }\n\n    private static void AddSkippedItems(MessageActionBatchExecutionResult result, IReadOnlyList<MessageActionExecutionPlan> plans, int startIndex) {\n        for (var index = startIndex; index < plans.Count; index++) {\n            var plan = plans[index];\n            result.SkippedPlanCount++;\n            result.Results.Add(new MessageActionBatchExecutionItemResult {\n                Index = index,\n                Action = plan?.Action ?? string.Empty,\n                ExecutionKind = plan?.ExecutionKind ?? string.Empty,\n                ProfileId = plan?.ProfileId ?? string.Empty,\n                RequestedCount = plan?.RequestedCount ?? 0,\n                Succeeded = false,\n                Code = \"skipped_after_failure\",\n                Message = \"Skipped because an earlier plan failed and continueOnError was false.\"\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatch.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a persisted reusable batch of normalized message action plans.\n/// </summary>\npublic sealed class MailMessageActionPlanBatch {\n    /// <summary>Stable batch identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing batch name.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Optional operator-facing description.</summary>\n    public string? Description { get; set; }\n\n    /// <summary>When the batch was first created.</summary>\n    public DateTimeOffset CreatedAt { get; set; }\n\n    /// <summary>When the batch was last updated.</summary>\n    public DateTimeOffset UpdatedAt { get; set; }\n\n    /// <summary>The normalized plans contained in this reusable batch.</summary>\n    public List<MessageActionExecutionPlan> Plans { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a persisted reusable message action plan batch.\n/// </summary>\npublic sealed class MailMessageActionPlanBatchCompact {\n    /// <summary>Stable batch identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing batch name.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Total plans in the batch.</summary>\n    public int PlanCount { get; set; }\n\n    /// <summary>Total plans currently ready for execution.</summary>\n    public int ReadyPlanCount { get; set; }\n\n    /// <summary>Total distinct profiles referenced by the stored plans.</summary>\n    public int ProfileCount { get; set; }\n\n    /// <summary>Lightweight human-readable plan names available in the batch.</summary>\n    public List<string> PlanNames { get; set; } = new();\n\n    /// <summary>Last updated timestamp for the batch.</summary>\n    public DateTimeOffset UpdatedAt { get; set; }\n\n    /// <summary>Human-readable summary.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchQuery.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Optional filters for persisted action-plan batch queries.\n/// </summary>\npublic sealed class MailMessageActionPlanBatchQuery {\n    /// <summary>Optional human-readable plan names that must exist in the returned batch.</summary>\n    public List<string> PlanNames { get; set; } = new();\n\n    /// <summary>Optional profile identifiers that must be referenced by the returned batch.</summary>\n    public List<string> ProfileIds { get; set; } = new();\n\n    /// <summary>Optional normalized action names that must be referenced by the returned batch.</summary>\n    public List<string> Actions { get; set; } = new();\n\n    /// <summary>Optional sort order for the returned batches.</summary>\n    public MailMessageActionPlanBatchSortBy SortBy { get; set; } = MailMessageActionPlanBatchSortBy.Id;\n\n    /// <summary>When true, reverses the selected sort order.</summary>\n    public bool Descending { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchSortBy.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Supported sort orders for stored action-plan batch queries.\n/// </summary>\npublic enum MailMessageActionPlanBatchSortBy {\n    /// <summary>Sort by stable batch identifier.</summary>\n    Id,\n\n    /// <summary>Sort by user-facing batch name.</summary>\n    Name,\n\n    /// <summary>Sort by total plan count.</summary>\n    PlanCount,\n\n    /// <summary>Sort by ready plan count.</summary>\n    ReadyPlanCount,\n\n    /// <summary>Sort by last updated timestamp.</summary>\n    UpdatedAt,\n\n    /// <summary>Sort by the number of distinct action types referenced by the batch.</summary>\n    ActionTypeCount\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchStoreOptions.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Configures where persisted reusable message action plan batches are stored.\n/// </summary>\npublic sealed class MailMessageActionPlanBatchStoreOptions {\n    /// <summary>Directory containing the batch store file.</summary>\n    public string? DirectoryPath { get; set; }\n\n    /// <summary>File name used to persist batches.</summary>\n    public string FileName { get; set; } = \"action-plan-batches.json\";\n\n    /// <summary>\n    /// Resolves the file path that should be used by the batch store.\n    /// </summary>\n    public string GetFilePath() {\n        var directory = string.IsNullOrWhiteSpace(DirectoryPath)\n            ? MailApplicationPaths.ResolveActionPlanBatchesDirectory()\n            : Path.GetFullPath(DirectoryPath);\n        return Path.Combine(directory, FileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchSummary.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Rich lightweight summary of a persisted reusable message action plan batch.\n/// </summary>\npublic sealed class MailMessageActionPlanBatchSummary {\n    /// <summary>Stable batch identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing batch name.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Total plans in the batch.</summary>\n    public int PlanCount { get; set; }\n\n    /// <summary>Total plans currently ready for execution.</summary>\n    public int ReadyPlanCount { get; set; }\n\n    /// <summary>Distinct profile identifiers referenced by the stored plans.</summary>\n    public List<string> ProfileIds { get; set; } = new();\n\n    /// <summary>Per-action plan counts keyed by normalized action name.</summary>\n    public Dictionary<string, int> ActionCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);\n\n    /// <summary>Lightweight human-readable plan names available in the batch.</summary>\n    public List<string> PlanNames { get; set; } = new();\n\n    /// <summary>Last updated timestamp for the batch.</summary>\n    public DateTimeOffset UpdatedAt { get; set; }\n\n    /// <summary>Human-readable summary.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanBatchTransformPreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Dry-run preview for cloning and transforming a stored message-action plan batch.\n/// </summary>\npublic sealed class MailMessageActionPlanBatchTransformPreview : OperationResult {\n    /// <summary>Source batch identifier.</summary>\n    public string SourceBatchId { get; set; } = string.Empty;\n\n    /// <summary>Source batch name when available.</summary>\n    public string? SourceBatchName { get; set; }\n\n    /// <summary>Total plans contained in the source batch.</summary>\n    public int PlanCount { get; set; }\n\n    /// <summary>Total transformed plans whose effective values would change.</summary>\n    public int ChangedPlanCount { get; set; }\n\n    /// <summary>Total transformed plans whose confirmation token would be regenerated.</summary>\n    public int ConfirmationTokenChangedCount { get; set; }\n\n    /// <summary>Whether the requested target profile exists when a profile remap was requested.</summary>\n    public bool? TargetProfileExists { get; set; }\n\n    /// <summary>Target profile kind when a profile remap was requested and resolved.</summary>\n    public MailProfileKind? TargetProfileKind { get; set; }\n\n    /// <summary>Validation or transform warnings.</summary>\n    public List<string> Warnings { get; set; } = new();\n\n    /// <summary>Validation errors that would block saving the transformed clone.</summary>\n    public List<string> Errors { get; set; } = new();\n\n    /// <summary>Per-plan transform preview items.</summary>\n    public List<MessageActionPlanBatchTransformPreviewItem> Plans { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanRegistryService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Implements reusable lifecycle operations for persisted message action plan batches.\n/// </summary>\npublic sealed class MailMessageActionPlanRegistryService : IMailMessageActionPlanRegistryService {\n    private readonly IMailMessageActionPlanBatchStore _store;\n    private readonly IMailMessageActionPlanExchangeService _exchangeService;\n    private readonly IMailMessageActionPreviewService _previewService;\n    private readonly IMailMessageActionPlanService _planService;\n    private readonly IMailMessageActionBatchService _batchService;\n    private readonly IMailProfileStore _profileStore;\n\n    /// <summary>\n    /// Creates a new persisted action plan registry service.\n    /// </summary>\n    public MailMessageActionPlanRegistryService(\n        IMailMessageActionPlanBatchStore store,\n        IMailMessageActionPlanExchangeService exchangeService,\n        IMailMessageActionPreviewService previewService,\n        IMailMessageActionPlanService planService,\n        IMailMessageActionBatchService batchService,\n        IMailProfileStore profileStore) {\n        _store = store ?? throw new ArgumentNullException(nameof(store));\n        _exchangeService = exchangeService ?? throw new ArgumentNullException(nameof(exchangeService));\n        _previewService = previewService ?? throw new ArgumentNullException(nameof(previewService));\n        _planService = planService ?? throw new ArgumentNullException(nameof(planService));\n        _batchService = batchService ?? throw new ArgumentNullException(nameof(batchService));\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailMessageActionPlanBatch>> GetBatchesAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) =>\n        ApplyBatchQuery(await _store.GetAllAsync(cancellationToken).ConfigureAwait(false), query);\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailMessageActionPlanBatchCompact>> GetBatchesCompactAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) =>\n        ApplyBatchQuery(await _store.GetAllAsync(cancellationToken).ConfigureAwait(false), query)\n        .Select(ToCompact)\n        .ToArray();\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailMessageActionPlanBatchSummary>> GetBatchesSummaryAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) =>\n        ApplyBatchQuery(await _store.GetAllAsync(cancellationToken).ConfigureAwait(false), query)\n        .Select(ToSummary)\n        .ToArray();\n\n    /// <inheritdoc />\n    public Task<MailMessageActionPlanBatch?> GetBatchAsync(string batchId, CancellationToken cancellationToken = default) =>\n        _store.GetByIdAsync(batchId, cancellationToken);\n\n    /// <inheritdoc />\n    public async Task<MailMessageActionPlanBatchCompact?> GetBatchCompactAsync(string batchId, CancellationToken cancellationToken = default) {\n        var batch = await _store.GetByIdAsync(batchId, cancellationToken).ConfigureAwait(false);\n        return batch == null ? null : ToCompact(batch);\n    }\n\n    /// <inheritdoc />\n    public async Task<MailMessageActionPlanBatchSummary?> GetBatchSummaryAsync(string batchId, CancellationToken cancellationToken = default) {\n        var batch = await _store.GetByIdAsync(batchId, cancellationToken).ConfigureAwait(false);\n        return batch == null ? null : ToSummary(batch);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default) {\n        var validation = await ValidateAsync(batch, cancellationToken).ConfigureAwait(false);\n        if (!validation.Succeeded) {\n            return validation;\n        }\n\n        await _store.SaveAsync(batch, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success($\"Action plan batch '{batch.Id}' saved.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> CreateCommonBatchAsync(\n        string batchId,\n        string name,\n        CommonMessageActionsPreviewRequest request,\n        IReadOnlyList<string>? actions = null,\n        string? description = null,\n        CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var preview = await _previewService.PreviewCommonActionsAsync(request, cancellationToken).ConfigureAwait(false);\n        return await CreateCommonBatchFromPreviewAsync(batchId, name, preview, actions, description, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> CreateCommonBatchFromPreviewAsync(\n        string batchId,\n        string name,\n        CommonMessageActionsPreview preview,\n        IReadOnlyList<string>? actions = null,\n        string? description = null,\n        CancellationToken cancellationToken = default) {\n        if (preview == null) {\n            throw new ArgumentNullException(nameof(preview));\n        }\n\n        var selectedActions = SelectCommonActions(preview, actions);\n        var plans = new List<MessageActionExecutionPlan>();\n        foreach (var action in selectedActions) {\n            var plan = await _planService.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n                Action = action.Action,\n                ProfileId = preview.ProfileId,\n                MailboxId = preview.MailboxId,\n                FolderId = preview.FolderId,\n                MessageIds = preview.MessageIds.ToList(),\n                DestinationFolderId = ResolveDestinationFolderId(action, preview.RequestedDestinationFolderId),\n                ConfirmationToken = action.ConfirmationToken\n            }, cancellationToken).ConfigureAwait(false);\n\n            if (plan.Succeeded) {\n                plans.Add(plan);\n            }\n        }\n\n        if (plans.Count == 0) {\n            return OperationResult.Failure(\"no_supported_actions\", \"No supported action plans could be created for this message selection.\");\n        }\n\n        return await SaveAsync(new MailMessageActionPlanBatch {\n            Id = batchId,\n            Name = name,\n            Description = description,\n            Plans = plans\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> CloneAsync(string sourceBatchId, string targetBatchId, string name, string? description = null, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(sourceBatchId)) {\n            throw new ArgumentException(\"Source batch id is required.\", nameof(sourceBatchId));\n        }\n        if (string.IsNullOrWhiteSpace(targetBatchId)) {\n            throw new ArgumentException(\"Target batch id is required.\", nameof(targetBatchId));\n        }\n        if (string.IsNullOrWhiteSpace(name)) {\n            throw new ArgumentException(\"Batch name is required.\", nameof(name));\n        }\n\n        var source = await _store.GetByIdAsync(sourceBatchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (source == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{sourceBatchId.Trim()}' was not found.\");\n        }\n\n        return await SaveAsync(new MailMessageActionPlanBatch {\n            Id = targetBatchId.Trim(),\n            Name = name.Trim(),\n            Description = description ?? source.Description,\n            Plans = source.Plans.Select(ClonePlan).ToList()\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> TransformCloneAsync(\n        string sourceBatchId,\n        string targetBatchId,\n        string name,\n        MessageActionPlanBatchTransformRequest transform,\n        string? description = null,\n        CancellationToken cancellationToken = default) {\n        if (transform == null) {\n            throw new ArgumentNullException(nameof(transform));\n        }\n        if (string.IsNullOrWhiteSpace(sourceBatchId)) {\n            throw new ArgumentException(\"Source batch id is required.\", nameof(sourceBatchId));\n        }\n        if (string.IsNullOrWhiteSpace(targetBatchId)) {\n            throw new ArgumentException(\"Target batch id is required.\", nameof(targetBatchId));\n        }\n        if (string.IsNullOrWhiteSpace(name)) {\n            throw new ArgumentException(\"Batch name is required.\", nameof(name));\n        }\n\n        var source = await _store.GetByIdAsync(sourceBatchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (source == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{sourceBatchId.Trim()}' was not found.\");\n        }\n\n        var selectedPlans = SelectPlans(source, transform, out var selectionError);\n        if (selectionError != null) {\n            return selectionError;\n        }\n\n        return await SaveAsync(new MailMessageActionPlanBatch {\n            Id = targetBatchId.Trim(),\n            Name = name.Trim(),\n            Description = description ?? source.Description,\n            Plans = selectedPlans.Select(selection => TransformPlan(selection.Plan, transform)).ToList()\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MailMessageActionPlanBatchTransformPreview> PreviewTransformCloneAsync(\n        string sourceBatchId,\n        MessageActionPlanBatchTransformRequest transform,\n        CancellationToken cancellationToken = default) {\n        if (transform == null) {\n            throw new ArgumentNullException(nameof(transform));\n        }\n        if (string.IsNullOrWhiteSpace(sourceBatchId)) {\n            throw new ArgumentException(\"Source batch id is required.\", nameof(sourceBatchId));\n        }\n\n        var normalizedSourceBatchId = sourceBatchId.Trim();\n        var source = await _store.GetByIdAsync(normalizedSourceBatchId, cancellationToken).ConfigureAwait(false);\n        if (source == null) {\n            return new MailMessageActionPlanBatchTransformPreview {\n                Succeeded = false,\n                Code = \"action_plan_batch_not_found\",\n                Message = $\"Action plan batch '{normalizedSourceBatchId}' was not found.\",\n                SourceBatchId = normalizedSourceBatchId\n            };\n        }\n\n        var selectedPlans = SelectPlans(source, transform, out var selectionError);\n\n        var preview = new MailMessageActionPlanBatchTransformPreview {\n            Succeeded = selectionError == null,\n            Code = selectionError?.Code,\n            SourceBatchId = source.Id,\n            SourceBatchName = source.Name,\n            PlanCount = selectedPlans.Count,\n            Message = selectionError?.Message\n        };\n\n        if (selectionError != null && !string.IsNullOrWhiteSpace(selectionError.Message)) {\n            preview.Errors.Add(selectionError.Message!);\n        }\n\n        if (!string.IsNullOrWhiteSpace(transform.ProfileId)) {\n            var targetProfileId = transform.ProfileId!.Trim();\n            var targetProfile = await _profileStore.GetByIdAsync(targetProfileId, cancellationToken).ConfigureAwait(false);\n            preview.TargetProfileExists = targetProfile != null;\n            preview.TargetProfileKind = targetProfile?.Kind;\n            if (targetProfile == null) {\n                preview.Succeeded = false;\n                preview.Code = \"action_plan_profile_not_found\";\n                preview.Errors.Add($\"Profile '{targetProfileId}' was not found.\");\n            }\n        }\n\n        foreach (var selection in selectedPlans) {\n            var sourcePlan = selection.Plan;\n            var transformedPlan = TransformPlan(sourcePlan, transform);\n            var willChange =\n                !string.Equals(sourcePlan.ProfileId, transformedPlan.ProfileId, StringComparison.Ordinal) ||\n                !string.Equals(sourcePlan.MailboxId, transformedPlan.MailboxId, StringComparison.Ordinal) ||\n                !string.Equals(sourcePlan.FolderId, transformedPlan.FolderId, StringComparison.Ordinal) ||\n                !string.Equals(sourcePlan.RequestedDestinationFolderId, transformedPlan.RequestedDestinationFolderId, StringComparison.Ordinal);\n            var tokenChanged = !string.Equals(sourcePlan.ConfirmationToken, transformedPlan.ConfirmationToken, StringComparison.Ordinal);\n\n            if (willChange) {\n                preview.ChangedPlanCount++;\n            }\n            if (tokenChanged) {\n                preview.ConfirmationTokenChangedCount++;\n            }\n\n            preview.Plans.Add(new MessageActionPlanBatchTransformPreviewItem {\n                Index = selection.Index,\n                Action = sourcePlan.Action,\n                ExecutionKind = sourcePlan.ExecutionKind,\n                SourceProfileId = sourcePlan.ProfileId,\n                TargetProfileId = transformedPlan.ProfileId,\n                SourceMailboxId = sourcePlan.MailboxId,\n                TargetMailboxId = transformedPlan.MailboxId,\n                SourceFolderId = sourcePlan.FolderId,\n                TargetFolderId = transformedPlan.FolderId,\n                SourceDestinationFolderId = sourcePlan.RequestedDestinationFolderId,\n                TargetDestinationFolderId = transformedPlan.RequestedDestinationFolderId,\n                WillChange = willChange,\n                ConfirmationTokenWillChange = tokenChanged,\n                Summary = BuildTransformSummary(sourcePlan, transformedPlan, willChange, tokenChanged)\n            });\n        }\n\n        if (preview.ChangedPlanCount == 0) {\n            preview.Warnings.Add(\"The requested transform would not change any stored plan values.\");\n        }\n\n        preview.Message = preview.Succeeded\n            ? $\"Previewed transform for {preview.PlanCount} plan(s); {preview.ChangedPlanCount} would change and {preview.ConfirmationTokenChangedCount} confirmation token(s) would be regenerated.\"\n            : preview.Message ?? $\"Transform preview for batch '{source.Id}' found validation issues.\";\n        return preview;\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> AppendPlanAsync(string batchId, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n        if (plan == null) {\n            throw new ArgumentNullException(nameof(plan));\n        }\n\n        var batch = await _store.GetByIdAsync(batchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (batch == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{batchId.Trim()}' was not found.\");\n        }\n\n        batch.Plans.Add(ClonePlan(plan));\n        return await SaveAsync(batch, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> AppendImportedPlanAsync(string batchId, string path, CancellationToken cancellationToken = default) {\n        var plan = await _exchangeService.LoadAsync(path, cancellationToken).ConfigureAwait(false);\n        return await AppendPlanAsync(batchId, plan, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> ReplacePlanAtAsync(string batchId, int index, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n        if (plan == null) {\n            throw new ArgumentNullException(nameof(plan));\n        }\n\n        var batch = await _store.GetByIdAsync(batchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (batch == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{batchId.Trim()}' was not found.\");\n        }\n        if (index < 0 || index >= batch.Plans.Count) {\n            return OperationResult.Failure(\"action_plan_batch_index_invalid\", $\"Action plan batch '{batch.Id}' does not contain a plan at index {index}.\");\n        }\n\n        batch.Plans[index] = ClonePlan(plan);\n        return await SaveAsync(batch, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> ReplaceImportedPlanAtAsync(string batchId, int index, string path, CancellationToken cancellationToken = default) {\n        var plan = await _exchangeService.LoadAsync(path, cancellationToken).ConfigureAwait(false);\n        return await ReplacePlanAtAsync(batchId, index, plan, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> RemovePlanAtAsync(string batchId, int index, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n\n        var batch = await _store.GetByIdAsync(batchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (batch == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{batchId.Trim()}' was not found.\");\n        }\n        if (index < 0 || index >= batch.Plans.Count) {\n            return OperationResult.Failure(\"action_plan_batch_index_invalid\", $\"Action plan batch '{batch.Id}' does not contain a plan at index {index}.\");\n        }\n        if (batch.Plans.Count == 1) {\n            return OperationResult.Failure(\"action_plan_batch_invalid\", \"Cannot remove the last plan from a batch. Delete the batch instead.\");\n        }\n\n        batch.Plans.RemoveAt(index);\n        return await SaveAsync(batch, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> DeleteAsync(string batchId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n\n        var normalizedId = batchId.Trim();\n        var removed = await _store.RemoveAsync(normalizedId, cancellationToken).ConfigureAwait(false);\n        return removed\n            ? OperationResult.Success($\"Action plan batch '{normalizedId}' deleted.\")\n            : OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{normalizedId}' was not found.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> ImportAsync(string batchId, string name, string path, string? description = null, CancellationToken cancellationToken = default) {\n        var plans = await _exchangeService.LoadBatchAsync(path, cancellationToken).ConfigureAwait(false);\n        return await SaveAsync(new MailMessageActionPlanBatch {\n            Id = batchId,\n            Name = name,\n            Description = description,\n            Plans = plans.ToList()\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> ExportAsync(string batchId, string path, CancellationToken cancellationToken = default) {\n        var batch = await _store.GetByIdAsync(batchId, cancellationToken).ConfigureAwait(false);\n        if (batch == null) {\n            return OperationResult.Failure(\"action_plan_batch_not_found\", $\"Action plan batch '{batchId}' was not found.\");\n        }\n\n        await _exchangeService.SaveBatchAsync(path, batch.Plans, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success($\"Action plan batch '{batch.Id}' exported.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionBatchExecutionResult> ExecuteAsync(string batchId, bool continueOnError = true, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(batchId)) {\n            throw new ArgumentException(\"Batch id is required.\", nameof(batchId));\n        }\n\n        var batch = await _store.GetByIdAsync(batchId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (batch == null) {\n            return new MessageActionBatchExecutionResult {\n                Succeeded = false,\n                Code = \"action_plan_batch_not_found\",\n                Message = $\"Action plan batch '{batchId.Trim()}' was not found.\"\n            };\n        }\n\n        return await _batchService.ExecuteAsync(batch.Plans, continueOnError, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<OperationResult> ValidateAsync(MailMessageActionPlanBatch? batch, CancellationToken cancellationToken) {\n        if (batch == null) {\n            return OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(batch.Id)) {\n            return OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch id is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(batch.Name)) {\n            return OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch name is required.\");\n        }\n        if (batch.Plans == null || batch.Plans.Count == 0) {\n            return OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch must contain at least one plan.\");\n        }\n\n        foreach (var plan in batch.Plans) {\n            if (plan == null) {\n                return OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch cannot contain null plans.\");\n            }\n            if (string.IsNullOrWhiteSpace(plan.ProfileId)) {\n                return OperationResult.Failure(\"action_plan_batch_invalid\", \"Each stored plan must include a profile id.\");\n            }\n\n            var profile = await _profileStore.GetByIdAsync(plan.ProfileId, cancellationToken).ConfigureAwait(false);\n            if (profile == null) {\n                return OperationResult.Failure(\"action_plan_profile_not_found\", $\"Profile '{plan.ProfileId}' was not found.\");\n            }\n        }\n\n        return OperationResult.Success();\n    }\n\n    private static MessageActionExecutionPlan ClonePlan(MessageActionExecutionPlan plan) => new() {\n        Succeeded = plan.Succeeded,\n        Code = plan.Code,\n        Message = plan.Message,\n        Name = plan.Name,\n        Summary = plan.Summary,\n        Action = plan.Action,\n        ExecutionKind = plan.ExecutionKind,\n        ProfileId = plan.ProfileId,\n        MailboxId = plan.MailboxId,\n        FolderId = plan.FolderId,\n        RequestedCount = plan.RequestedCount,\n        UniqueMessageCount = plan.UniqueMessageCount,\n        MessageIds = plan.MessageIds.ToList(),\n        RequestedDestinationFolderId = plan.RequestedDestinationFolderId,\n        Destination = plan.Destination == null\n            ? null\n            : new MailFolderTargetResolution {\n                ProfileId = plan.Destination.ProfileId,\n                MailboxId = plan.Destination.MailboxId,\n                RequestedValue = plan.Destination.RequestedValue,\n                IsAlias = plan.Destination.IsAlias,\n                Alias = plan.Destination.Alias,\n                IsSupported = plan.Destination.IsSupported,\n                IsResolved = plan.Destination.IsResolved,\n                EffectiveFolderId = plan.Destination.EffectiveFolderId,\n                FolderDisplayName = plan.Destination.FolderDisplayName,\n                FolderPath = plan.Destination.FolderPath,\n                Summary = plan.Destination.Summary\n            },\n        DesiredState = plan.DesiredState,\n        ConfirmationToken = plan.ConfirmationToken,\n        ConfirmationProvided = plan.ConfirmationProvided,\n        ConfirmationValidated = plan.ConfirmationValidated,\n        Warnings = plan.Warnings.ToList()\n    };\n\n    private static MessageActionExecutionPlan TransformPlan(MessageActionExecutionPlan plan, MessageActionPlanBatchTransformRequest transform) {\n        var cloned = ClonePlan(plan);\n        var transformedProfileId = string.IsNullOrWhiteSpace(transform.ProfileId) ? cloned.ProfileId : transform.ProfileId!.Trim();\n        var transformedMailboxId = string.IsNullOrWhiteSpace(transform.MailboxId) ? cloned.MailboxId : transform.MailboxId!.Trim();\n        var transformedFolderId = string.IsNullOrWhiteSpace(transform.FolderId) ? cloned.FolderId : transform.FolderId!.Trim();\n\n        cloned.ProfileId = transformedProfileId;\n        cloned.MailboxId = transformedMailboxId;\n        cloned.FolderId = transformedFolderId;\n\n        if (cloned.Destination != null) {\n            cloned.Destination.ProfileId = transformedProfileId;\n            cloned.Destination.MailboxId = transformedMailboxId;\n        }\n\n        if (!string.IsNullOrWhiteSpace(transform.DestinationFolderId) && string.Equals(cloned.ExecutionKind, \"Move\", StringComparison.OrdinalIgnoreCase)) {\n            var transformedDestination = transform.DestinationFolderId!.Trim();\n            cloned.RequestedDestinationFolderId = transformedDestination;\n            cloned.Destination = null;\n        }\n\n        cloned.ConfirmationProvided = false;\n        cloned.ConfirmationValidated = true;\n        cloned.ConfirmationToken = CreateConfirmationToken(cloned);\n        cloned.Summary = BuildStoredPlanSummary(cloned);\n        return cloned;\n    }\n\n    private static IReadOnlyList<MessageActionPreviewItem> SelectCommonActions(CommonMessageActionsPreview preview, IReadOnlyList<string>? actions) {\n        if (actions == null || actions.Count == 0) {\n            return preview.Actions\n                .Where(action => action.Succeeded)\n                .ToArray();\n        }\n\n        var selected = new List<MessageActionPreviewItem>();\n        foreach (var actionName in actions\n                     .Where(action => !string.IsNullOrWhiteSpace(action))\n                     .Select(action => action.Trim())\n                     .Distinct(StringComparer.OrdinalIgnoreCase)) {\n            var match = preview.Actions.FirstOrDefault(action =>\n                string.Equals(action.Action, actionName, StringComparison.OrdinalIgnoreCase) &&\n                action.Succeeded);\n            if (match != null) {\n                selected.Add(match);\n            }\n        }\n\n        return selected;\n    }\n\n    private static string? ResolveDestinationFolderId(MessageActionPreviewItem action, string? fallbackDestinationFolderId) =>\n        string.Equals(action.Action, \"move\", StringComparison.OrdinalIgnoreCase)\n            ? action.Destination?.EffectiveFolderId ?? action.RequestedDestinationFolderId ?? fallbackDestinationFolderId\n            : null;\n\n    private static string? CreateConfirmationToken(MessageActionExecutionPlan plan) =>\n        plan.ExecutionKind switch {\n            \"Move\" => !string.IsNullOrWhiteSpace(plan.Destination?.EffectiveFolderId ?? plan.RequestedDestinationFolderId)\n                ? MessageActionConfirmationTokens.CreateMoveToken(\n                    plan.ProfileId,\n                    plan.MailboxId,\n                    plan.FolderId,\n                    plan.MessageIds,\n                    plan.Destination?.EffectiveFolderId ?? plan.RequestedDestinationFolderId!)\n                : null,\n            \"Delete\" => MessageActionConfirmationTokens.CreateDeleteToken(\n                plan.ProfileId,\n                plan.MailboxId,\n                plan.FolderId,\n                plan.MessageIds),\n            \"SetReadState\" when plan.DesiredState.HasValue => MessageActionConfirmationTokens.CreateReadStateToken(\n                plan.ProfileId,\n                plan.MailboxId,\n                plan.FolderId,\n                plan.MessageIds,\n                plan.DesiredState.Value),\n            \"SetFlaggedState\" when plan.DesiredState.HasValue => MessageActionConfirmationTokens.CreateFlaggedStateToken(\n                plan.ProfileId,\n                plan.MailboxId,\n                plan.FolderId,\n                plan.MessageIds,\n                plan.DesiredState.Value),\n            _ => plan.ConfirmationToken\n        };\n\n    private static string BuildTransformSummary(MessageActionExecutionPlan sourcePlan, MessageActionExecutionPlan transformedPlan, bool willChange, bool tokenChanged) {\n        var changes = new List<string>();\n        if (!string.Equals(sourcePlan.ProfileId, transformedPlan.ProfileId, StringComparison.Ordinal)) {\n            changes.Add($\"profile {sourcePlan.ProfileId}->{transformedPlan.ProfileId}\");\n        }\n        if (!string.Equals(sourcePlan.MailboxId, transformedPlan.MailboxId, StringComparison.Ordinal)) {\n            changes.Add($\"mailbox {(sourcePlan.MailboxId ?? \"<none>\")}->{(transformedPlan.MailboxId ?? \"<none>\")}\");\n        }\n        if (!string.Equals(sourcePlan.FolderId, transformedPlan.FolderId, StringComparison.Ordinal)) {\n            changes.Add($\"folder {(sourcePlan.FolderId ?? \"<none>\")}->{(transformedPlan.FolderId ?? \"<none>\")}\");\n        }\n        if (!string.Equals(sourcePlan.RequestedDestinationFolderId, transformedPlan.RequestedDestinationFolderId, StringComparison.Ordinal)) {\n            changes.Add($\"destination {(sourcePlan.RequestedDestinationFolderId ?? \"<none>\")}->{(transformedPlan.RequestedDestinationFolderId ?? \"<none>\")}\");\n        }\n        if (tokenChanged) {\n            changes.Add(\"confirmation token regenerated\");\n        }\n\n        return willChange || tokenChanged\n            ? $\"{sourcePlan.Action}: {string.Join(\", \", changes)}\"\n            : $\"{sourcePlan.Action}: unchanged\";\n    }\n\n    private static IReadOnlyList<SelectedPlan> SelectPlans(\n        MailMessageActionPlanBatch source,\n        MessageActionPlanBatchTransformRequest transform,\n        out OperationResult? error) {\n        error = null;\n\n        var selected = new Dictionary<int, SelectedPlan>();\n        var hasIndexes = transform.PlanIndexes != null && transform.PlanIndexes.Count > 0;\n        var hasNames = transform.PlanNames != null && transform.PlanNames.Count > 0;\n        IEnumerable<int> requestedIndexes = hasIndexes ? transform.PlanIndexes! : Array.Empty<int>();\n        IEnumerable<string> requestedNames = hasNames ? transform.PlanNames! : Array.Empty<string>();\n\n        if (!hasIndexes && !hasNames) {\n            return source.Plans\n                .Select((plan, index) => new SelectedPlan(index, plan))\n                .ToArray();\n        }\n\n        if (hasIndexes) {\n            foreach (var index in requestedIndexes.Distinct()) {\n                if (index < 0 || index >= source.Plans.Count) {\n                    error = OperationResult.Failure(\"action_plan_batch_index_invalid\", $\"Action plan batch '{source.Id}' does not contain a plan at index {index}.\");\n                    return Array.Empty<SelectedPlan>();\n                }\n\n                selected[index] = new SelectedPlan(index, source.Plans[index]);\n            }\n        }\n\n        if (hasNames) {\n            foreach (var requestedName in requestedNames\n                         .Where(name => !string.IsNullOrWhiteSpace(name))\n                         .Select(name => name.Trim())\n                         .Distinct(StringComparer.OrdinalIgnoreCase)) {\n                var matches = source.Plans\n                    .Select((plan, index) => new SelectedPlan(index, plan))\n                    .Where(selection => PlanMatchesName(selection.Plan, requestedName))\n                    .ToArray();\n                if (matches.Length == 0) {\n                    error = OperationResult.Failure(\"action_plan_batch_name_invalid\", $\"Action plan batch '{source.Id}' does not contain a plan named '{requestedName}'.\");\n                    return Array.Empty<SelectedPlan>();\n                }\n\n                foreach (var match in matches) {\n                    selected[match.Index] = match;\n                }\n            }\n        }\n\n        if (selected.Count == 0) {\n            error = OperationResult.Failure(\"action_plan_batch_invalid\", \"Action plan batch transform must include at least one plan.\");\n            return Array.Empty<SelectedPlan>();\n        }\n\n        return selected\n            .OrderBy(item => item.Key)\n            .Select(item => item.Value)\n            .ToArray();\n    }\n\n    private static MailMessageActionPlanBatchCompact ToCompact(MailMessageActionPlanBatch batch) {\n        var profileCount = batch.Plans\n            .Select(plan => plan.ProfileId)\n            .Where(profileId => !string.IsNullOrWhiteSpace(profileId))\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .Count();\n        var readyPlanCount = batch.Plans.Count(plan => plan.Succeeded);\n\n        return new MailMessageActionPlanBatchCompact {\n            Id = batch.Id,\n            Name = batch.Name,\n            PlanCount = batch.Plans.Count,\n            ReadyPlanCount = readyPlanCount,\n            ProfileCount = profileCount,\n            PlanNames = batch.Plans\n                .Select(plan => !string.IsNullOrWhiteSpace(plan.Name) ? plan.Name : BuildStoredPlanSummary(plan))\n                .Distinct(StringComparer.OrdinalIgnoreCase)\n                .ToList(),\n            UpdatedAt = batch.UpdatedAt,\n            Summary = $\"{batch.Id} ({batch.Plans.Count} plan(s), {readyPlanCount} ready)\"\n        };\n    }\n\n    private static MailMessageActionPlanBatchSummary ToSummary(MailMessageActionPlanBatch batch) {\n        var profileIds = batch.Plans\n            .Select(plan => plan.ProfileId)\n            .Where(profileId => !string.IsNullOrWhiteSpace(profileId))\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .OrderBy(profileId => profileId, StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var readyPlanCount = batch.Plans.Count(plan => plan.Succeeded);\n        var actionCounts = batch.Plans\n            .Where(plan => !string.IsNullOrWhiteSpace(plan.Action))\n            .GroupBy(plan => plan.Action, StringComparer.OrdinalIgnoreCase)\n            .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)\n            .ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);\n\n        return new MailMessageActionPlanBatchSummary {\n            Id = batch.Id,\n            Name = batch.Name,\n            PlanCount = batch.Plans.Count,\n            ReadyPlanCount = readyPlanCount,\n            ProfileIds = profileIds,\n            ActionCounts = actionCounts,\n            PlanNames = batch.Plans\n                .Select(plan => !string.IsNullOrWhiteSpace(plan.Name) ? plan.Name : BuildStoredPlanSummary(plan))\n                .Distinct(StringComparer.OrdinalIgnoreCase)\n                .ToList(),\n            UpdatedAt = batch.UpdatedAt,\n            Summary = $\"{batch.Id} ({batch.Plans.Count} plan(s), {readyPlanCount} ready, {actionCounts.Count} action type(s))\"\n        };\n    }\n\n    private static IReadOnlyList<MailMessageActionPlanBatch> ApplyBatchQuery(\n        IReadOnlyList<MailMessageActionPlanBatch> batches,\n        MailMessageActionPlanBatchQuery? query) {\n        if (query == null) {\n            return batches;\n        }\n\n        var requestedPlanNames = query.PlanNames\n            .Where(name => !string.IsNullOrWhiteSpace(name))\n            .Select(name => name.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n        var requestedProfileIds = query.ProfileIds\n            .Where(profileId => !string.IsNullOrWhiteSpace(profileId))\n            .Select(profileId => profileId.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n        var requestedActions = query.Actions\n            .Where(action => !string.IsNullOrWhiteSpace(action))\n            .Select(action => action.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n\n        IEnumerable<MailMessageActionPlanBatch> filtered = batches;\n        if (requestedPlanNames.Length > 0 || requestedProfileIds.Length > 0 || requestedActions.Length > 0) {\n            filtered = filtered.Where(batch =>\n                (requestedPlanNames.Length == 0 || batch.Plans.Any(plan => requestedPlanNames.Any(requestedName => PlanMatchesName(plan, requestedName)))) &&\n                (requestedProfileIds.Length == 0 || batch.Plans.Any(plan => requestedProfileIds.Any(requestedProfileId => string.Equals(plan.ProfileId, requestedProfileId, StringComparison.OrdinalIgnoreCase)))) &&\n                (requestedActions.Length == 0 || batch.Plans.Any(plan => requestedActions.Any(requestedAction => string.Equals(plan.Action, requestedAction, StringComparison.OrdinalIgnoreCase)))));\n        }\n\n        var ordered = ApplyBatchQuerySort(filtered, query.SortBy, query.Descending);\n        return ordered.ToArray();\n    }\n\n    private static IEnumerable<MailMessageActionPlanBatch> ApplyBatchQuerySort(\n        IEnumerable<MailMessageActionPlanBatch> batches,\n        MailMessageActionPlanBatchSortBy sortBy,\n        bool descending) {\n        Func<MailMessageActionPlanBatch, object> keySelector = sortBy switch {\n            MailMessageActionPlanBatchSortBy.Name => batch => batch.Name,\n            MailMessageActionPlanBatchSortBy.PlanCount => batch => batch.Plans.Count,\n            MailMessageActionPlanBatchSortBy.ReadyPlanCount => batch => batch.Plans.Count(plan => plan.Succeeded),\n            MailMessageActionPlanBatchSortBy.UpdatedAt => batch => batch.UpdatedAt,\n            MailMessageActionPlanBatchSortBy.ActionTypeCount => batch => batch.Plans\n                .Select(plan => plan.Action)\n                .Where(action => !string.IsNullOrWhiteSpace(action))\n                .Distinct(StringComparer.OrdinalIgnoreCase)\n                .Count(),\n            _ => batch => batch.Id\n        };\n\n        var ordered = descending\n            ? batches.OrderByDescending(keySelector)\n            : batches.OrderBy(keySelector);\n\n        return ordered.ThenBy(batch => batch.Id, StringComparer.OrdinalIgnoreCase);\n    }\n\n    private static bool PlanMatchesName(MessageActionExecutionPlan plan, string requestedName) =>\n        (!string.IsNullOrWhiteSpace(plan.Name) && string.Equals(plan.Name, requestedName, StringComparison.OrdinalIgnoreCase)) ||\n        (!string.IsNullOrWhiteSpace(plan.Summary) && string.Equals(plan.Summary, requestedName, StringComparison.OrdinalIgnoreCase));\n\n    private static string BuildStoredPlanSummary(MessageActionExecutionPlan plan) {\n        var messageCount = plan.UniqueMessageCount == 1 ? \"1 message\" : $\"{plan.UniqueMessageCount} messages\";\n        return !string.IsNullOrWhiteSpace(plan.Name)\n            ? $\"{plan.Name} ({messageCount})\"\n            : $\"{plan.Action} ({messageCount})\";\n    }\n\n    private sealed class SelectedPlan {\n        public SelectedPlan(int index, MessageActionExecutionPlan plan) {\n            Index = index;\n            Plan = plan;\n        }\n\n        public int Index { get; }\n\n        public MessageActionExecutionPlan Plan { get; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPlanService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates normalized execution plans from previewable message actions and can execute those plans.\n/// </summary>\npublic sealed class MailMessageActionPlanService : IMailMessageActionPlanService {\n    private readonly IMailMessageActionPreviewService _previewService;\n    private readonly IMailMessageActionService _messageActionService;\n\n    /// <summary>\n    /// Creates a new message action planning service.\n    /// </summary>\n    public MailMessageActionPlanService(IMailMessageActionPreviewService previewService, IMailMessageActionService messageActionService) {\n        _previewService = previewService ?? throw new ArgumentNullException(nameof(previewService));\n        _messageActionService = messageActionService ?? throw new ArgumentNullException(nameof(messageActionService));\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionExecutionPlan> CreatePlanAsync(MessageActionExecutionPlanRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n        if (string.IsNullOrWhiteSpace(request.Action)) {\n            throw new ArgumentException(\"Action is required.\", nameof(request));\n        }\n\n        var action = request.Action.Trim().ToLowerInvariant();\n        return action switch {\n            \"mark-read\" => CreateStatePlan(\n                action,\n                \"SetReadState\",\n                request,\n                await _previewService.PreviewReadStateAsync(new SetReadStateRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    IsRead = true\n                }, cancellationToken).ConfigureAwait(false)),\n            \"mark-unread\" => CreateStatePlan(\n                action,\n                \"SetReadState\",\n                request,\n                await _previewService.PreviewReadStateAsync(new SetReadStateRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    IsRead = false\n                }, cancellationToken).ConfigureAwait(false)),\n            \"flag\" => CreateStatePlan(\n                action,\n                \"SetFlaggedState\",\n                request,\n                await _previewService.PreviewFlaggedStateAsync(new SetFlaggedStateRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    IsFlagged = true\n                }, cancellationToken).ConfigureAwait(false)),\n            \"unflag\" => CreateStatePlan(\n                action,\n                \"SetFlaggedState\",\n                request,\n                await _previewService.PreviewFlaggedStateAsync(new SetFlaggedStateRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    IsFlagged = false\n                }, cancellationToken).ConfigureAwait(false)),\n            \"archive\" => CreateMovePlan(\n                action,\n                request,\n                await _previewService.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    DestinationFolderId = MailFolderAliases.Archive\n                }, cancellationToken).ConfigureAwait(false)),\n            \"trash\" => CreateMovePlan(\n                action,\n                request,\n                await _previewService.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    DestinationFolderId = MailFolderAliases.Trash\n                }, cancellationToken).ConfigureAwait(false)),\n            \"move\" => CreateMovePlan(\n                action,\n                request,\n                await _previewService.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    DestinationFolderId = request.DestinationFolderId ?? string.Empty\n                }, cancellationToken).ConfigureAwait(false)),\n            \"delete\" => CreateDeletePlan(\n                action,\n                request,\n                await _previewService.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds\n                }, cancellationToken).ConfigureAwait(false)),\n            _ => throw new InvalidOperationException($\"Unsupported action '{request.Action}'.\")\n        };\n    }\n\n    /// <inheritdoc />\n    public Task<MessageActionResult> ExecuteAsync(MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n        if (plan == null) {\n            throw new ArgumentNullException(nameof(plan));\n        }\n        if (!plan.Succeeded) {\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = false,\n                Code = plan.Code ?? \"plan_not_ready\",\n                Message = plan.Message ?? \"The action plan is not ready for execution.\",\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.RequestedCount,\n                FailedCount = plan.UniqueMessageCount\n            });\n        }\n\n        return plan.ExecutionKind switch {\n            \"SetReadState\" => _messageActionService.SetReadStateAsync(new SetReadStateRequest {\n                ProfileId = plan.ProfileId,\n                MailboxId = plan.MailboxId,\n                FolderId = plan.FolderId,\n                MessageIds = plan.MessageIds.ToList(),\n                IsRead = plan.DesiredState == true,\n                ConfirmationToken = plan.ConfirmationToken\n            }, cancellationToken),\n            \"SetFlaggedState\" => _messageActionService.SetFlaggedStateAsync(new SetFlaggedStateRequest {\n                ProfileId = plan.ProfileId,\n                MailboxId = plan.MailboxId,\n                FolderId = plan.FolderId,\n                MessageIds = plan.MessageIds.ToList(),\n                IsFlagged = plan.DesiredState == true,\n                ConfirmationToken = plan.ConfirmationToken\n            }, cancellationToken),\n            \"Move\" => _messageActionService.MoveAsync(new MoveMessagesRequest {\n                ProfileId = plan.ProfileId,\n                MailboxId = plan.MailboxId,\n                FolderId = plan.FolderId,\n                MessageIds = plan.MessageIds.ToList(),\n                DestinationFolderId = plan.Destination?.EffectiveFolderId ?? plan.RequestedDestinationFolderId ?? string.Empty,\n                ConfirmationToken = plan.ConfirmationToken\n            }, cancellationToken),\n            \"Delete\" => _messageActionService.DeleteAsync(new DeleteMessagesRequest {\n                ProfileId = plan.ProfileId,\n                MailboxId = plan.MailboxId,\n                FolderId = plan.FolderId,\n                MessageIds = plan.MessageIds.ToList(),\n                ConfirmationToken = plan.ConfirmationToken\n            }, cancellationToken),\n            _ => throw new InvalidOperationException($\"Unsupported execution kind '{plan.ExecutionKind}'.\")\n        };\n    }\n\n    private static MessageActionExecutionPlan CreateStatePlan(\n        string action,\n        string executionKind,\n        MessageActionExecutionPlanRequest request,\n        MessageStateChangePreview preview) {\n        var plan = CreateBasePlan(action, executionKind, request, preview.ProfileId, preview.RequestedCount, preview.UniqueMessageCount, preview.MessageIds);\n        plan.DesiredState = preview.DesiredState;\n        plan.ConfirmationToken = preview.ConfirmationToken;\n        plan.Warnings.AddRange(preview.Warnings);\n        return FinalizePlan(plan, request.ConfirmationToken, preview.Succeeded, preview.Code, preview.Message);\n    }\n\n    private static MessageActionExecutionPlan CreateMovePlan(\n        string action,\n        MessageActionExecutionPlanRequest request,\n        MoveMessagesPreview preview) {\n        var plan = CreateBasePlan(action, \"Move\", request, preview.ProfileId, preview.RequestedCount, preview.UniqueMessageCount, preview.MessageIds);\n        plan.RequestedDestinationFolderId = preview.RequestedDestinationFolderId;\n        plan.Destination = preview.Destination;\n        plan.ConfirmationToken = preview.ConfirmationToken;\n        plan.Warnings.AddRange(preview.Warnings);\n        return FinalizePlan(plan, request.ConfirmationToken, preview.Succeeded, preview.Code, preview.Message);\n    }\n\n    private static MessageActionExecutionPlan CreateDeletePlan(\n        string action,\n        MessageActionExecutionPlanRequest request,\n        DeleteMessagesPreview preview) {\n        var plan = CreateBasePlan(action, \"Delete\", request, preview.ProfileId, preview.RequestedCount, preview.UniqueMessageCount, preview.MessageIds);\n        plan.ConfirmationToken = preview.ConfirmationToken;\n        plan.Warnings.AddRange(preview.Warnings);\n        return FinalizePlan(plan, request.ConfirmationToken, preview.Succeeded, preview.Code, preview.Message);\n    }\n\n    private static MessageActionExecutionPlan CreateBasePlan(\n        string action,\n        string executionKind,\n        MessageActionExecutionPlanRequest request,\n        string profileId,\n        int requestedCount,\n        int uniqueMessageCount,\n        List<string> messageIds) =>\n        new() {\n            Action = action,\n            ExecutionKind = executionKind,\n            ProfileId = profileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            RequestedCount = requestedCount,\n            UniqueMessageCount = uniqueMessageCount,\n            MessageIds = messageIds\n        };\n\n    private static MessageActionExecutionPlan FinalizePlan(\n        MessageActionExecutionPlan plan,\n        string? providedConfirmationToken,\n        bool previewSucceeded,\n        string? previewCode,\n        string? previewMessage) {\n        plan.Name = BuildPlanName(plan);\n        plan.ConfirmationProvided = !string.IsNullOrWhiteSpace(providedConfirmationToken);\n\n        if (!previewSucceeded) {\n            plan.Succeeded = false;\n            plan.Code = previewCode;\n            plan.Message = previewMessage;\n            plan.ConfirmationValidated = !plan.ConfirmationProvided;\n            plan.Summary = BuildPlanSummary(plan);\n            return plan;\n        }\n\n        if (plan.ConfirmationProvided) {\n            var providedToken = providedConfirmationToken!;\n            providedToken = providedToken.Trim();\n            if (!string.Equals(providedToken, plan.ConfirmationToken, StringComparison.Ordinal)) {\n                plan.Succeeded = false;\n                plan.Code = \"confirmation_token_mismatch\";\n                plan.Message = \"The supplied confirmation token does not match this normalized action plan.\";\n                plan.ConfirmationValidated = false;\n                plan.Summary = BuildPlanSummary(plan);\n                return plan;\n            }\n        }\n\n        plan.Succeeded = true;\n        plan.Code = null;\n        plan.ConfirmationValidated = !plan.ConfirmationProvided || string.Equals(providedConfirmationToken, plan.ConfirmationToken, StringComparison.Ordinal);\n        plan.Message = $\"Execution plan ready for '{plan.Action}' on {plan.UniqueMessageCount} message(s).\";\n        plan.Summary = BuildPlanSummary(plan);\n        return plan;\n    }\n\n    private static string BuildPlanName(MessageActionExecutionPlan plan) =>\n        plan.Action switch {\n            \"mark-read\" => \"Mark as read\",\n            \"mark-unread\" => \"Mark as unread\",\n            \"flag\" => \"Flag\",\n            \"unflag\" => \"Unflag\",\n            \"archive\" => \"Archive\",\n            \"trash\" => \"Trash\",\n            \"move\" => !string.IsNullOrWhiteSpace(plan.RequestedDestinationFolderId)\n                ? $\"Move to {plan.RequestedDestinationFolderId}\"\n                : \"Move\",\n            \"delete\" => \"Delete\",\n            _ => plan.Action\n        };\n\n    private static string BuildPlanSummary(MessageActionExecutionPlan plan) {\n        var messageCount = plan.UniqueMessageCount == 1 ? \"1 message\" : $\"{plan.UniqueMessageCount} messages\";\n        return plan.Action switch {\n            \"move\" when !string.IsNullOrWhiteSpace(plan.RequestedDestinationFolderId) => $\"{BuildPlanName(plan)} ({messageCount})\",\n            _ => $\"{BuildPlanName(plan)} ({messageCount})\"\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailMessageActionPreviewService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of reusable dry-run mailbox action previews.\n/// </summary>\npublic sealed class MailMessageActionPreviewService : IMailMessageActionPreviewService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IMailFolderAliasService _folderAliases;\n\n    /// <summary>\n    /// Creates a new message action preview service.\n    /// </summary>\n    public MailMessageActionPreviewService(IMailProfileStore profileStore, IMailFolderAliasService folderAliases) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _folderAliases = folderAliases ?? throw new ArgumentNullException(nameof(folderAliases));\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageStateChangePreview> PreviewReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        return CreateReadStatePreview(request, context);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageStateChangePreview> PreviewFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        return CreateFlaggedStatePreview(request, context);\n    }\n\n    /// <inheritdoc />\n    public async Task<CommonMessageActionsPreview> PreviewCommonActionsAsync(CommonMessageActionsPreviewRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        var preview = CreateCommonPreview(request, context);\n\n        preview.Actions.Add(ToActionItem(\n            \"mark-read\",\n            \"Mark as read\",\n            CreateReadStatePreview(new SetReadStateRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                IsRead = true\n            }, context)));\n\n        preview.Actions.Add(ToActionItem(\n            \"mark-unread\",\n            \"Mark as unread\",\n            CreateReadStatePreview(new SetReadStateRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                IsRead = false\n            }, context)));\n\n        preview.Actions.Add(ToActionItem(\n            \"flag\",\n            \"Flag\",\n            CreateFlaggedStatePreview(new SetFlaggedStateRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                IsFlagged = true\n            }, context)));\n\n        preview.Actions.Add(ToActionItem(\n            \"unflag\",\n            \"Unflag\",\n            CreateFlaggedStatePreview(new SetFlaggedStateRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                IsFlagged = false\n            }, context)));\n\n        var standardPreview = await PreviewStandardActionsAsync(new StandardMessageActionsPreviewRequest {\n            ProfileId = request.ProfileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            MessageIds = request.MessageIds,\n            DestinationFolderId = request.DestinationFolderId\n        }, cancellationToken).ConfigureAwait(false);\n\n        preview.Actions.AddRange(standardPreview.Actions);\n        preview.IncludedActionCount = preview.Actions.Count;\n        preview.SucceededActionCount = preview.Actions.Count(action => action.Succeeded);\n        preview.FailedActionCount = preview.IncludedActionCount - preview.SucceededActionCount;\n        preview.Succeeded = preview.SucceededActionCount > 0;\n        preview.Code = preview.Succeeded ? null : \"no_supported_actions\";\n        preview.Message = preview.Succeeded\n            ? $\"Prepared {preview.IncludedActionCount} action preview(s) for {preview.UniqueMessageCount} message(s); {preview.SucceededActionCount} supported.\"\n            : $\"No previewed actions are currently supported for profile '{preview.ProfileId}'.\";\n        return preview;\n    }\n\n    /// <inheritdoc />\n    public async Task<MoveMessagesPreview> PreviewMoveAsync(MoveMessagesPreviewRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        return await CreateMovePreviewAsync(request, context, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<DeleteMessagesPreview> PreviewDeleteAsync(DeleteMessagesPreviewRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        return CreateDeletePreview(request, context);\n    }\n\n    /// <inheritdoc />\n    public async Task<StandardMessageActionsPreview> PreviewStandardActionsAsync(StandardMessageActionsPreviewRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var context = await CreateContextAsync(request.ProfileId, request.MessageIds, cancellationToken).ConfigureAwait(false);\n        var preview = CreateStandardPreview(request, context);\n\n        preview.Actions.Add(ToActionItem(\n            \"archive\",\n            \"Archive\",\n            await CreateMovePreviewAsync(new MoveMessagesPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                DestinationFolderId = MailFolderAliases.Archive\n            }, context, cancellationToken).ConfigureAwait(false)));\n\n        preview.Actions.Add(ToActionItem(\n            \"trash\",\n            \"Trash\",\n            await CreateMovePreviewAsync(new MoveMessagesPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds,\n                DestinationFolderId = MailFolderAliases.Trash\n            }, context, cancellationToken).ConfigureAwait(false)));\n\n        if (!string.IsNullOrWhiteSpace(request.DestinationFolderId)) {\n            var trimmedDestination = request.DestinationFolderId!;\n            trimmedDestination = trimmedDestination.Trim();\n            preview.Actions.Add(ToActionItem(\n                \"move\",\n                $\"Move to '{trimmedDestination}'\",\n                await CreateMovePreviewAsync(new MoveMessagesPreviewRequest {\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    MessageIds = request.MessageIds,\n                    DestinationFolderId = trimmedDestination\n                }, context, cancellationToken).ConfigureAwait(false)));\n        }\n\n        preview.Actions.Add(ToActionItem(\n            \"delete\",\n            \"Delete\",\n            CreateDeletePreview(new DeleteMessagesPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds\n            }, context)));\n\n        preview.IncludedActionCount = preview.Actions.Count;\n        preview.SucceededActionCount = preview.Actions.Count(action => action.Succeeded);\n        preview.FailedActionCount = preview.IncludedActionCount - preview.SucceededActionCount;\n        preview.Succeeded = preview.SucceededActionCount > 0;\n        preview.Code = preview.Succeeded ? null : \"no_supported_actions\";\n        preview.Message = preview.Succeeded\n            ? $\"Prepared {preview.IncludedActionCount} action preview(s) for {preview.UniqueMessageCount} message(s); {preview.SucceededActionCount} supported.\"\n            : $\"No previewed actions are currently supported for profile '{preview.ProfileId}'.\";\n        return preview;\n    }\n\n    private async Task<PreviewContext> CreateContextAsync(string profileId, IReadOnlyList<string> messageIds, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n        }\n\n        var normalizedMessageIds = NormalizeMessageIds(messageIds);\n        return new PreviewContext(profile, messageIds.Count, normalizedMessageIds);\n    }\n\n    private async Task<MoveMessagesPreview> CreateMovePreviewAsync(\n        MoveMessagesPreviewRequest request,\n        PreviewContext context,\n        CancellationToken cancellationToken) {\n        var preview = CreateMovePreview(request, context.Profile.Id, context.NormalizedMessageIds, context.RequestedCount);\n\n        if (!context.Profile.GetCapabilities().Supports(MailCapability.MoveMessages)) {\n            preview.Succeeded = false;\n            preview.Code = \"move_not_supported\";\n            preview.Message = $\"Profile '{context.Profile.Id}' does not support '{MailCapability.MoveMessages}'.\";\n            return preview;\n        }\n\n        if (context.NormalizedMessageIds.Count == 0) {\n            preview.Succeeded = false;\n            preview.Code = \"messages_required\";\n            preview.Message = \"At least one non-empty message id is required.\";\n            return preview;\n        }\n\n        var resolution = await _folderAliases.ResolveAsync(context.Profile.Id, request.DestinationFolderId, request.MailboxId, cancellationToken).ConfigureAwait(false);\n        preview.Destination = resolution;\n        if (!resolution.IsSupported) {\n            preview.Succeeded = false;\n            preview.Code = \"destination_not_supported\";\n            preview.Message = resolution.Summary;\n            return preview;\n        }\n\n        if (!resolution.IsResolved && resolution.IsAlias) {\n            preview.Warnings.Add($\"Destination alias '{resolution.Alias}' could not be resolved to a concrete folder and will be used as-is.\");\n        }\n\n        preview.ConfirmationToken = MessageActionConfirmationTokens.CreateMoveToken(\n            context.Profile.Id,\n            request.MailboxId,\n            request.FolderId,\n            context.NormalizedMessageIds,\n            resolution.EffectiveFolderId);\n        preview.Succeeded = true;\n        preview.Code = null;\n        preview.Message = $\"Move preview ready for {preview.UniqueMessageCount} message(s) to '{resolution.EffectiveFolderId}'.\";\n        return preview;\n    }\n\n    private DeleteMessagesPreview CreateDeletePreview(DeleteMessagesPreviewRequest request, PreviewContext context) {\n        var preview = CreateDeletePreview(request, context.Profile.Id, context.NormalizedMessageIds, context.RequestedCount);\n\n        if (!context.Profile.GetCapabilities().Supports(MailCapability.DeleteMessages)) {\n            preview.Succeeded = false;\n            preview.Code = \"delete_not_supported\";\n            preview.Message = $\"Profile '{context.Profile.Id}' does not support '{MailCapability.DeleteMessages}'.\";\n            return preview;\n        }\n\n        if (context.NormalizedMessageIds.Count == 0) {\n            preview.Succeeded = false;\n            preview.Code = \"messages_required\";\n            preview.Message = \"At least one non-empty message id is required.\";\n            return preview;\n        }\n\n        preview.ConfirmationToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            context.Profile.Id,\n            request.MailboxId,\n            request.FolderId,\n            context.NormalizedMessageIds);\n        preview.Succeeded = true;\n        preview.Code = null;\n        preview.Message = $\"Delete preview ready for {preview.UniqueMessageCount} message(s).\";\n        return preview;\n    }\n\n    private MessageStateChangePreview CreateReadStatePreview(SetReadStateRequest request, PreviewContext context) {\n        var preview = CreateStatePreview(\n            context,\n            request.MailboxId,\n            request.FolderId,\n            \"read-state\",\n            request.IsRead);\n        if (!context.Profile.GetCapabilities().Supports(MailCapability.MarkMessages)) {\n            preview.Succeeded = false;\n            preview.Code = \"mark_not_supported\";\n            preview.Message = $\"Profile '{context.Profile.Id}' does not support '{MailCapability.MarkMessages}'.\";\n            return preview;\n        }\n\n        if (context.NormalizedMessageIds.Count == 0) {\n            preview.Succeeded = false;\n            preview.Code = \"messages_required\";\n            preview.Message = \"At least one non-empty message id is required.\";\n            return preview;\n        }\n\n        preview.ConfirmationToken = MessageActionConfirmationTokens.CreateReadStateToken(\n            context.Profile.Id,\n            request.MailboxId,\n            request.FolderId,\n            context.NormalizedMessageIds,\n            request.IsRead);\n        preview.Succeeded = true;\n        preview.Message = request.IsRead\n            ? $\"Read-state preview ready for {preview.UniqueMessageCount} message(s).\"\n            : $\"Unread-state preview ready for {preview.UniqueMessageCount} message(s).\";\n        return preview;\n    }\n\n    private MessageStateChangePreview CreateFlaggedStatePreview(SetFlaggedStateRequest request, PreviewContext context) {\n        var preview = CreateStatePreview(\n            context,\n            request.MailboxId,\n            request.FolderId,\n            \"flagged-state\",\n            request.IsFlagged);\n        if (!context.Profile.GetCapabilities().Supports(MailCapability.MarkMessages)) {\n            preview.Succeeded = false;\n            preview.Code = \"mark_not_supported\";\n            preview.Message = $\"Profile '{context.Profile.Id}' does not support '{MailCapability.MarkMessages}'.\";\n            return preview;\n        }\n\n        if (context.NormalizedMessageIds.Count == 0) {\n            preview.Succeeded = false;\n            preview.Code = \"messages_required\";\n            preview.Message = \"At least one non-empty message id is required.\";\n            return preview;\n        }\n\n        preview.ConfirmationToken = MessageActionConfirmationTokens.CreateFlaggedStateToken(\n            context.Profile.Id,\n            request.MailboxId,\n            request.FolderId,\n            context.NormalizedMessageIds,\n            request.IsFlagged);\n        preview.Succeeded = true;\n        preview.Message = request.IsFlagged\n            ? $\"Flag preview ready for {preview.UniqueMessageCount} message(s).\"\n            : $\"Unflag preview ready for {preview.UniqueMessageCount} message(s).\";\n        return preview;\n    }\n\n    private static List<string> NormalizeMessageIds(IReadOnlyList<string> messageIds) =>\n        messageIds\n            .Where(id => !string.IsNullOrWhiteSpace(id))\n            .Select(id => id.Trim())\n            .Distinct(StringComparer.Ordinal)\n            .ToList();\n\n    private static MoveMessagesPreview CreateMovePreview(\n        MoveMessagesPreviewRequest request,\n        string profileId,\n        List<string> normalizedMessageIds,\n        int requestedCount) {\n        var preview = new MoveMessagesPreview {\n            ProfileId = profileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            RequestedDestinationFolderId = request.DestinationFolderId,\n            RequestedCount = requestedCount,\n            MessageIds = normalizedMessageIds,\n            UniqueMessageCount = normalizedMessageIds.Count,\n            DuplicateOrEmptyCount = requestedCount - normalizedMessageIds.Count\n        };\n\n        if (preview.DuplicateOrEmptyCount > 0) {\n            preview.Warnings.Add($\"Ignored {preview.DuplicateOrEmptyCount} duplicate or empty message id value(s).\");\n        }\n\n        return preview;\n    }\n\n    private static DeleteMessagesPreview CreateDeletePreview(\n        DeleteMessagesPreviewRequest request,\n        string profileId,\n        List<string> normalizedMessageIds,\n        int requestedCount) {\n        var preview = new DeleteMessagesPreview {\n            ProfileId = profileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            RequestedCount = requestedCount,\n            MessageIds = normalizedMessageIds,\n            UniqueMessageCount = normalizedMessageIds.Count,\n            DuplicateOrEmptyCount = requestedCount - normalizedMessageIds.Count\n        };\n\n        if (preview.DuplicateOrEmptyCount > 0) {\n            preview.Warnings.Add($\"Ignored {preview.DuplicateOrEmptyCount} duplicate or empty message id value(s).\");\n        }\n\n        return preview;\n    }\n\n    private static StandardMessageActionsPreview CreateStandardPreview(StandardMessageActionsPreviewRequest request, PreviewContext context) {\n        var requestedDestinationFolderId = string.IsNullOrWhiteSpace(request.DestinationFolderId)\n            ? null\n            : request.DestinationFolderId!.Trim();\n        var preview = new StandardMessageActionsPreview {\n            ProfileId = context.Profile.Id,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            RequestedDestinationFolderId = requestedDestinationFolderId,\n            RequestedCount = context.RequestedCount,\n            MessageIds = context.NormalizedMessageIds,\n            UniqueMessageCount = context.NormalizedMessageIds.Count,\n            DuplicateOrEmptyCount = context.RequestedCount - context.NormalizedMessageIds.Count\n        };\n\n        if (preview.DuplicateOrEmptyCount > 0) {\n            preview.Warnings.Add($\"Ignored {preview.DuplicateOrEmptyCount} duplicate or empty message id value(s).\");\n        }\n\n        return preview;\n    }\n\n    private static MessageStateChangePreview CreateStatePreview(\n        PreviewContext context,\n        string? mailboxId,\n        string? folderId,\n        string action,\n        bool desiredState) {\n        var preview = new MessageStateChangePreview {\n            ProfileId = context.Profile.Id,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            Action = action,\n            DesiredState = desiredState,\n            RequestedCount = context.RequestedCount,\n            MessageIds = context.NormalizedMessageIds,\n            UniqueMessageCount = context.NormalizedMessageIds.Count,\n            DuplicateOrEmptyCount = context.RequestedCount - context.NormalizedMessageIds.Count\n        };\n\n        if (preview.DuplicateOrEmptyCount > 0) {\n            preview.Warnings.Add($\"Ignored {preview.DuplicateOrEmptyCount} duplicate or empty message id value(s).\");\n        }\n\n        return preview;\n    }\n\n    private static MessageActionPreviewItem ToActionItem(string action, string displayName, MoveMessagesPreview preview) =>\n        new() {\n            Action = action,\n            DisplayName = displayName,\n            Succeeded = preview.Succeeded,\n            Code = preview.Code,\n            Message = preview.Message,\n            RequestedDestinationFolderId = preview.RequestedDestinationFolderId,\n            Destination = preview.Destination,\n            ConfirmationToken = preview.ConfirmationToken,\n            Warnings = new List<string>(preview.Warnings)\n        };\n\n    private static MessageActionPreviewItem ToActionItem(string action, string displayName, DeleteMessagesPreview preview) =>\n        new() {\n            Action = action,\n            DisplayName = displayName,\n            Succeeded = preview.Succeeded,\n            Code = preview.Code,\n            Message = preview.Message,\n            ConfirmationToken = preview.ConfirmationToken,\n            Warnings = new List<string>(preview.Warnings)\n        };\n\n    private static MessageActionPreviewItem ToActionItem(string action, string displayName, MessageStateChangePreview preview) =>\n        new() {\n            Action = action,\n            DisplayName = displayName,\n            Succeeded = preview.Succeeded,\n            Code = preview.Code,\n            Message = preview.Message,\n            DesiredState = preview.DesiredState,\n            ConfirmationToken = preview.ConfirmationToken,\n            Warnings = new List<string>(preview.Warnings)\n        };\n\n    private static CommonMessageActionsPreview CreateCommonPreview(CommonMessageActionsPreviewRequest request, PreviewContext context) {\n        var requestedDestinationFolderId = string.IsNullOrWhiteSpace(request.DestinationFolderId)\n            ? null\n            : request.DestinationFolderId!.Trim();\n        var preview = new CommonMessageActionsPreview {\n            ProfileId = context.Profile.Id,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            RequestedDestinationFolderId = requestedDestinationFolderId,\n            RequestedCount = context.RequestedCount,\n            MessageIds = context.NormalizedMessageIds,\n            UniqueMessageCount = context.NormalizedMessageIds.Count,\n            DuplicateOrEmptyCount = context.RequestedCount - context.NormalizedMessageIds.Count\n        };\n\n        if (preview.DuplicateOrEmptyCount > 0) {\n            preview.Warnings.Add($\"Ignored {preview.DuplicateOrEmptyCount} duplicate or empty message id value(s).\");\n        }\n\n        return preview;\n    }\n\n    private sealed class PreviewContext {\n        public PreviewContext(MailProfile profile, int requestedCount, List<string> normalizedMessageIds) {\n            Profile = profile;\n            RequestedCount = requestedCount;\n            NormalizedMessageIds = normalizedMessageIds;\n        }\n\n        public MailProfile Profile { get; }\n\n        public int RequestedCount { get; }\n\n        public List<string> NormalizedMessageIds { get; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfile.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a reusable profile definition that can be shared by CLI, MCP, GUI, and PowerShell adapters.\n/// </summary>\npublic sealed class MailProfile {\n    /// <summary>Stable profile identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing profile name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Optional description for human operators.</summary>\n    public string? Description { get; set; }\n\n    /// <summary>Technology or provider behind the profile.</summary>\n    public MailProfileKind Kind { get; set; } = MailProfileKind.Unknown;\n\n    /// <summary>Default sender address when the profile supports sending.</summary>\n    public string? DefaultSender { get; set; }\n\n    /// <summary>Default mailbox address or principal name, where applicable.</summary>\n    public string? DefaultMailbox { get; set; }\n\n    /// <summary>Whether this profile should be treated as the default choice.</summary>\n    public bool IsDefault { get; set; }\n\n    /// <summary>Non-secret profile settings.</summary>\n    public Dictionary<string, string> Settings { get; set; } = new(StringComparer.OrdinalIgnoreCase);\n\n    /// <summary>\n    /// Explicit capability override when present.\n    /// </summary>\n    public ProfileCapabilities? Capabilities { get; set; }\n\n    /// <summary>\n    /// Returns the effective capabilities for this profile.\n    /// </summary>\n    public ProfileCapabilities GetCapabilities() => Capabilities ?? ProfileCapabilities.CreateDefault(Kind);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileAuthDefaults.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Shared defaults used by profile-based authentication flows.\n/// </summary>\npublic static class MailProfileAuthDefaults {\n    /// <summary>Default Gmail mail scope.</summary>\n    public static readonly IReadOnlyList<string> GmailScopes = new[] { \"https://mail.google.com/\" };\n\n    /// <summary>Default Microsoft Graph mail scopes.</summary>\n    public static readonly IReadOnlyList<string> GraphScopes = new[] {\n        \"email\",\n        \"offline_access\",\n        \"https://graph.microsoft.com/Mail.ReadWrite\",\n        \"https://graph.microsoft.com/Mail.Send\"\n    };\n\n    /// <summary>Default redirect URI used by native Microsoft identity flows.</summary>\n    public const string GraphRedirectUri = \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n\n    /// <summary>Refresh window used when deciding whether a token should be reacquired.</summary>\n    public static readonly TimeSpan TokenRefreshWindow = TimeSpan.FromMinutes(5);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileAuthFlowNames.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Well-known authentication flow names persisted with reusable profiles.\n/// </summary>\npublic static class MailProfileAuthFlowNames {\n    /// <summary>Interactive user login backed by provider token cache.</summary>\n    public const string Interactive = \"interactive\";\n\n    /// <summary>Manually supplied access token.</summary>\n    public const string ManualToken = \"manualToken\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileAuthService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of reusable profile authentication workflows.\n/// </summary>\npublic sealed class MailProfileAuthService : IMailProfileAuthService {\n    private readonly IMailProfileService _profiles;\n    private readonly IMailProfileSecretService _profileSecrets;\n    private readonly IMailSecretStore _secretStore;\n    private readonly Func<GmailProfileLoginRequest, CancellationToken, Task<OAuthCredential>> _loginGmailAsync;\n    private readonly Func<GraphProfileLoginRequest, CancellationToken, Task<OAuthCredential>> _loginGraphAsync;\n\n    /// <summary>\n    /// Creates a new profile authentication service.\n    /// </summary>\n    public MailProfileAuthService(\n        IMailProfileService profiles,\n        IMailProfileSecretService profileSecrets,\n        IMailSecretStore secretStore,\n        Func<GmailProfileLoginRequest, CancellationToken, Task<OAuthCredential>>? loginGmailAsync = null,\n        Func<GraphProfileLoginRequest, CancellationToken, Task<OAuthCredential>>? loginGraphAsync = null) {\n        _profiles = profiles ?? throw new ArgumentNullException(nameof(profiles));\n        _profileSecrets = profileSecrets ?? throw new ArgumentNullException(nameof(profileSecrets));\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n        _loginGmailAsync = loginGmailAsync ?? DefaultLoginGmailAsync;\n        _loginGraphAsync = loginGraphAsync ?? DefaultLoginGraphAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileAuthStatus?> GetStatusAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profiles.GetProfileAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return null;\n        }\n\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthFlow, out var authFlow);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.LoginHint, out var loginHint);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailboxSetting);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantId);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.CertificatePath, out var certificatePath);\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthMode, out var authMode);\n\n        var accessToken = await TryReadSecretAsync(profile.Id, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n        var refreshToken = await TryReadSecretAsync(profile.Id, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false);\n        var clientSecret = await TryReadSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n        var certificatePassword = await TryReadSecretAsync(profile.Id, MailSecretNames.CertificatePassword, cancellationToken).ConfigureAwait(false);\n        var password = await TryReadSecretAsync(profile.Id, MailSecretNames.Password, cancellationToken).ConfigureAwait(false);\n\n        var tokenExpiresOn = TryParseTimestamp(profile.Settings.TryGetValue(MailProfileSettingsKeys.TokenExpiresOn, out var rawTokenExpiresOn) ? rawTokenExpiresOn : null);\n        var mailbox = FirstNonEmpty(mailboxSetting, profile.DefaultMailbox, profile.DefaultSender);\n        var hasAccessToken = !string.IsNullOrWhiteSpace(accessToken);\n        var hasRefreshToken = !string.IsNullOrWhiteSpace(refreshToken);\n        var hasClientSecret = !string.IsNullOrWhiteSpace(clientSecret);\n        var hasCertificatePath = !string.IsNullOrWhiteSpace(certificatePath);\n        var hasCertificatePassword = !string.IsNullOrWhiteSpace(certificatePassword);\n        var hasPassword = !string.IsNullOrWhiteSpace(password);\n        var hasClientId = !string.IsNullOrWhiteSpace(clientId);\n        var hasTenantId = !string.IsNullOrWhiteSpace(tenantId);\n        var isTokenExpired = tokenExpiresOn.HasValue && tokenExpiresOn.Value <= DateTimeOffset.UtcNow;\n\n        var mode = DetermineMode(profile, authFlow, authMode, hasAccessToken, hasRefreshToken, hasClientSecret, hasCertificatePath, hasPassword);\n        var canLoginInteractively = DetermineCanLoginInteractively(profile.Kind, hasClientId, hasTenantId, hasClientSecret, mailbox);\n        var canRefresh = DetermineCanRefresh(profile.Kind, authFlow, hasClientId, hasTenantId, hasClientSecret, mailbox);\n\n        return new MailProfileAuthStatus {\n            ProfileId = profile.Id,\n            ProfileKind = profile.Kind,\n            AuthFlow = authFlow,\n            Mode = mode,\n            LoginHint = loginHint,\n            Mailbox = mailbox,\n            TokenExpiresOn = tokenExpiresOn,\n            HasAccessToken = hasAccessToken,\n            HasRefreshToken = hasRefreshToken,\n            HasClientSecret = hasClientSecret,\n            HasCertificatePath = hasCertificatePath,\n            HasCertificatePassword = hasCertificatePassword,\n            HasPassword = hasPassword,\n            HasClientId = hasClientId,\n            HasTenantId = hasTenantId,\n            IsTokenExpired = isTokenExpired,\n            CanRefresh = canRefresh,\n            CanLoginInteractively = canLoginInteractively,\n            Summary = BuildSummary(profile, mode, mailbox, hasAccessToken, tokenExpiresOn, isTokenExpired, canRefresh)\n        };\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileAuthenticationResult> LoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profileResult = await LoadProfileAsync(request.ProfileId, MailProfileKind.Gmail, cancellationToken).ConfigureAwait(false);\n        if (profileResult.Error != null) {\n            return profileResult.Error;\n        }\n\n        var profile = profileResult.Profile!;\n        string? requestClientSecret;\n        try {\n            requestClientSecret = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profile.Id,\n                MailSecretNames.ClientSecret,\n                request.ClientSecret,\n                request.ClientSecretReference,\n                cancellationToken).ConfigureAwait(false);\n        } catch (InvalidOperationException ex) {\n            return Failure(\"secret_reference_invalid\", ex.Message, profile.Id, MailProfileKind.Gmail);\n        }\n        var gmailAccount = FirstNonEmpty(\n            request.GmailAccount,\n            profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailboxSetting) ? mailboxSetting : null,\n            profile.DefaultMailbox);\n        var clientId = FirstNonEmpty(\n            request.ClientId,\n            profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientIdSetting) ? clientIdSetting : null);\n        var clientSecret = FirstNonEmpty(\n            requestClientSecret,\n            await TryReadSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false));\n\n        if (string.IsNullOrWhiteSpace(gmailAccount)) {\n            return Failure(\"gmail_account_required\", \"Gmail login requires an account address.\", profile.Id, MailProfileKind.Gmail);\n        }\n        if (string.IsNullOrWhiteSpace(clientId)) {\n            return Failure(\"client_id_required\", \"Gmail login requires a client id.\", profile.Id, MailProfileKind.Gmail);\n        }\n        if (string.IsNullOrWhiteSpace(clientSecret)) {\n            return Failure(\"client_secret_required\", \"Gmail login requires a client secret.\", profile.Id, MailProfileKind.Gmail);\n        }\n\n        var effectiveRequest = new GmailProfileLoginRequest {\n            ProfileId = profile.Id,\n            GmailAccount = gmailAccount,\n            ClientId = clientId,\n            ClientSecret = clientSecret,\n            ClientSecretReference = null,\n            Scopes = NormalizeScopes(request.Scopes, MailProfileAuthDefaults.GmailScopes)\n        };\n        var credential = await _loginGmailAsync(effectiveRequest, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(credential.AccessToken)) {\n            return Failure(\"access_token_missing\", \"Gmail authentication did not return an access token.\", profile.Id, MailProfileKind.Gmail);\n        }\n\n        profile.Settings[MailProfileSettingsKeys.Mailbox] = gmailAccount!;\n        profile.Settings[MailProfileSettingsKeys.ClientId] = clientId!;\n        profile.Settings[MailProfileSettingsKeys.AuthFlow] = MailProfileAuthFlowNames.Interactive;\n        profile.Settings[MailProfileSettingsKeys.LoginHint] = gmailAccount!;\n        UpsertTokenExpiration(profile, credential.ExpiresOn);\n        profile.DefaultMailbox ??= gmailAccount;\n        profile.DefaultSender ??= gmailAccount;\n\n        var saveResult = await _profiles.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (!saveResult.Succeeded) {\n            return FromOperation(saveResult, profile.Id, MailProfileKind.Gmail);\n        }\n\n        var secretResult = await PersistSecretAsync(profile.Id, MailSecretNames.ClientSecret, clientSecret, cancellationToken).ConfigureAwait(false);\n        if (secretResult != null) {\n            return secretResult;\n        }\n        secretResult = await PersistSecretAsync(profile.Id, MailSecretNames.AccessToken, credential.AccessToken, cancellationToken).ConfigureAwait(false);\n        if (secretResult != null) {\n            return secretResult;\n        }\n        if (!string.IsNullOrWhiteSpace(credential.RefreshToken)) {\n            secretResult = await PersistSecretAsync(profile.Id, MailSecretNames.RefreshToken, credential.RefreshToken, cancellationToken).ConfigureAwait(false);\n            if (secretResult != null) {\n                return secretResult;\n            }\n        }\n\n        return Success(\"Gmail login completed.\", profile.Id, MailProfileKind.Gmail, credential);\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileAuthenticationResult> LoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profileResult = await LoadProfileAsync(request.ProfileId, MailProfileKind.Graph, cancellationToken).ConfigureAwait(false);\n        if (profileResult.Error != null) {\n            return profileResult.Error;\n        }\n\n        var profile = profileResult.Profile!;\n        var login = FirstNonEmpty(request.Login, profile.DefaultMailbox, profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailboxSetting) ? mailboxSetting : null);\n        var mailbox = FirstNonEmpty(request.Mailbox, profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var existingMailbox) ? existingMailbox : null, profile.DefaultMailbox, login);\n        var clientId = FirstNonEmpty(request.ClientId, profile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientIdSetting) ? clientIdSetting : null);\n        var tenantId = FirstNonEmpty(request.TenantId, profile.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var tenantIdSetting) ? tenantIdSetting : null);\n        var redirectUri = FirstNonEmpty(request.RedirectUri, profile.Settings.TryGetValue(MailProfileSettingsKeys.RedirectUri, out var redirectUriSetting) ? redirectUriSetting : null)\n            ?? MailProfileAuthDefaults.GraphRedirectUri;\n\n        if (string.IsNullOrWhiteSpace(clientId)) {\n            return Failure(\"client_id_required\", \"Graph login requires a client id.\", profile.Id, MailProfileKind.Graph);\n        }\n        if (string.IsNullOrWhiteSpace(tenantId)) {\n            return Failure(\"tenant_id_required\", \"Graph login requires a tenant id.\", profile.Id, MailProfileKind.Graph);\n        }\n\n        var effectiveRequest = new GraphProfileLoginRequest {\n            ProfileId = profile.Id,\n            Login = login,\n            Mailbox = mailbox,\n            ClientId = clientId,\n            TenantId = tenantId,\n            RedirectUri = redirectUri,\n            Scopes = NormalizeScopes(request.Scopes, MailProfileAuthDefaults.GraphScopes)\n        };\n        var credential = await _loginGraphAsync(effectiveRequest, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(credential.AccessToken)) {\n            return Failure(\"access_token_missing\", \"Graph authentication did not return an access token.\", profile.Id, MailProfileKind.Graph);\n        }\n\n        profile.Settings[MailProfileSettingsKeys.ClientId] = clientId!;\n        profile.Settings[MailProfileSettingsKeys.TenantId] = tenantId!;\n        profile.Settings[MailProfileSettingsKeys.RedirectUri] = redirectUri;\n        profile.Settings[MailProfileSettingsKeys.AuthFlow] = MailProfileAuthFlowNames.Interactive;\n        UpsertSetting(profile.Settings, MailProfileSettingsKeys.LoginHint, login);\n        UpsertTokenExpiration(profile, credential.ExpiresOn);\n        if (!string.IsNullOrWhiteSpace(mailbox)) {\n            profile.Settings[MailProfileSettingsKeys.Mailbox] = mailbox!;\n            profile.DefaultMailbox ??= mailbox;\n            profile.DefaultSender ??= mailbox;\n        } else if (!string.IsNullOrWhiteSpace(credential.UserName)) {\n            profile.DefaultMailbox ??= credential.UserName;\n        }\n\n        var saveResult = await _profiles.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (!saveResult.Succeeded) {\n            return FromOperation(saveResult, profile.Id, MailProfileKind.Graph);\n        }\n\n        var secretResult = await PersistSecretAsync(profile.Id, MailSecretNames.AccessToken, credential.AccessToken, cancellationToken).ConfigureAwait(false);\n        if (secretResult != null) {\n            return secretResult;\n        }\n\n        return Success(\"Graph login completed.\", profile.Id, MailProfileKind.Graph, credential);\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileAuthenticationResult> RefreshAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            return Failure(\"profile_required\", \"Profile id is required.\", profileId, MailProfileKind.Unknown);\n        }\n\n        var profile = await _profiles.GetProfileAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return Failure(\"profile_not_found\", \"Profile was not found.\", profileId, MailProfileKind.Unknown);\n        }\n\n        switch (profile.Kind) {\n            case MailProfileKind.Gmail:\n                return await LoginGmailAsync(new GmailProfileLoginRequest {\n                    ProfileId = profile.Id\n                }, cancellationToken).ConfigureAwait(false);\n            case MailProfileKind.Graph:\n                return await LoginGraphAsync(new GraphProfileLoginRequest {\n                    ProfileId = profile.Id\n                }, cancellationToken).ConfigureAwait(false);\n            default:\n                return Failure(\n                    \"refresh_not_supported\",\n                    $\"Profile kind '{profile.Kind}' does not support shared refresh-auth.\",\n                    profile.Id,\n                    profile.Kind);\n        }\n    }\n\n    private async Task<(MailProfile? Profile, MailProfileAuthenticationResult? Error)> LoadProfileAsync(string profileId, MailProfileKind expectedKind, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            return (null, Failure(\"profile_required\", \"Profile id is required.\", profileId, expectedKind));\n        }\n\n        var profile = await _profiles.GetProfileAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return (null, Failure(\"profile_not_found\", \"Profile was not found.\", profileId, expectedKind));\n        }\n        if (profile.Kind != expectedKind) {\n            return (null, Failure(\"profile_kind_mismatch\", $\"Profile '{profile.Id}' is not a {expectedKind} profile.\", profile.Id, profile.Kind));\n        }\n\n        return (CloneProfile(profile), null);\n    }\n\n    private Task<string?> TryReadSecretAsync(string profileId, string secretName, CancellationToken cancellationToken) =>\n        _secretStore.GetSecretAsync(profileId, secretName, cancellationToken);\n\n    private async Task<MailProfileAuthenticationResult?> PersistSecretAsync(string profileId, string secretName, string? secretValue, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(secretValue)) {\n            return null;\n        }\n\n        var normalizedSecretValue = secretValue!.Trim();\n        var result = await _profileSecrets.SetSecretAsync(profileId, secretName, normalizedSecretValue, cancellationToken).ConfigureAwait(false);\n        return result.Succeeded\n            ? null\n            : FromOperation(result, profileId, MailProfileKind.Unknown);\n    }\n\n    private static Task<OAuthCredential> DefaultLoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return OAuthHelpers.AcquireGoogleTokenCachedAsync(\n            request.GmailAccount!,\n            request.ClientId!,\n            request.ClientSecret!,\n            request.Scopes ?? MailProfileAuthDefaults.GmailScopes);\n    }\n\n    private static Task<OAuthCredential> DefaultLoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        return OAuthHelpers.AcquireO365TokenCachedAsync(\n            request.Login,\n            request.ClientId!,\n            request.TenantId!,\n            request.RedirectUri!,\n            request.Scopes ?? MailProfileAuthDefaults.GraphScopes);\n    }\n\n    private static MailProfile CloneProfile(MailProfile profile) => new() {\n        Id = profile.Id,\n        DisplayName = profile.DisplayName,\n        Description = profile.Description,\n        Kind = profile.Kind,\n        DefaultSender = profile.DefaultSender,\n        DefaultMailbox = profile.DefaultMailbox,\n        IsDefault = profile.IsDefault,\n        Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n        Capabilities = profile.Capabilities == null\n            ? null\n            : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n    };\n\n    private static IReadOnlyList<string> NormalizeScopes(IReadOnlyList<string>? scopes, IReadOnlyList<string> fallback) =>\n        (scopes == null || scopes.Count == 0\n            ? fallback\n            : scopes.Where(scope => !string.IsNullOrWhiteSpace(scope)).Select(scope => scope.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray())!;\n\n    private static string? FirstNonEmpty(params string?[] values) {\n        foreach (var value in values) {\n            if (!string.IsNullOrWhiteSpace(value)) {\n                return value!.Trim();\n            }\n        }\n\n        return null;\n    }\n\n    private static DateTimeOffset? TryParseTimestamp(string? value) =>\n        DateTimeOffset.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind, out var timestamp)\n            ? timestamp\n            : null;\n\n    private static string DetermineMode(\n        MailProfile profile,\n        string? authFlow,\n        string? authMode,\n        bool hasAccessToken,\n        bool hasRefreshToken,\n        bool hasClientSecret,\n        bool hasCertificatePath,\n        bool hasPassword) {\n        if (string.Equals(authFlow, MailProfileAuthFlowNames.Interactive, StringComparison.OrdinalIgnoreCase)) {\n            return \"interactive\";\n        }\n\n        if (string.Equals(authFlow, MailProfileAuthFlowNames.ManualToken, StringComparison.OrdinalIgnoreCase)) {\n            return \"manualToken\";\n        }\n\n        return profile.Kind switch {\n            MailProfileKind.Graph when hasClientSecret || hasCertificatePath => \"appOnly\",\n            MailProfileKind.Graph when hasAccessToken => \"manualToken\",\n            MailProfileKind.Gmail when hasRefreshToken && hasClientSecret => \"interactive\",\n            MailProfileKind.Gmail when hasAccessToken => \"manualToken\",\n            MailProfileKind.Imap or MailProfileKind.Pop3 or MailProfileKind.Smtp\n                when string.Equals(authMode, \"oauth2\", StringComparison.OrdinalIgnoreCase) => \"oauth2\",\n            MailProfileKind.Imap or MailProfileKind.Pop3 or MailProfileKind.Smtp\n                when hasPassword => \"basic\",\n            _ => \"unknown\"\n        };\n    }\n\n    private static bool DetermineCanLoginInteractively(\n        MailProfileKind kind,\n        bool hasClientId,\n        bool hasTenantId,\n        bool hasClientSecret,\n        string? mailbox) =>\n        kind switch {\n            MailProfileKind.Gmail => hasClientId && hasClientSecret && !string.IsNullOrWhiteSpace(mailbox),\n            MailProfileKind.Graph => hasClientId && hasTenantId,\n            _ => false\n        };\n\n    private static bool DetermineCanRefresh(\n        MailProfileKind kind,\n        string? authFlow,\n        bool hasClientId,\n        bool hasTenantId,\n        bool hasClientSecret,\n        string? mailbox) =>\n        kind switch {\n            MailProfileKind.Gmail => hasClientId && hasClientSecret && !string.IsNullOrWhiteSpace(mailbox),\n            MailProfileKind.Graph => string.Equals(authFlow, MailProfileAuthFlowNames.Interactive, StringComparison.OrdinalIgnoreCase) && hasClientId && hasTenantId,\n            _ => false\n        };\n\n    private static string BuildSummary(\n        MailProfile profile,\n        string mode,\n        string? mailbox,\n        bool hasAccessToken,\n        DateTimeOffset? tokenExpiresOn,\n        bool isTokenExpired,\n        bool canRefresh) {\n        var mailboxSegment = string.IsNullOrWhiteSpace(mailbox) ? \"mailbox not set\" : $\"mailbox={mailbox}\";\n        var tokenSegment = !hasAccessToken\n            ? \"token missing\"\n            : tokenExpiresOn == null\n                ? \"token present\"\n                : isTokenExpired\n                    ? $\"token expired {tokenExpiresOn:O}\"\n                    : $\"token expires {tokenExpiresOn:O}\";\n        var refreshSegment = canRefresh ? \"refresh available\" : \"refresh unavailable\";\n        return $\"{profile.Id} [{profile.Kind}] auth={mode}, {mailboxSegment}, {tokenSegment}, {refreshSegment}.\";\n    }\n\n    private static void UpsertSetting(IDictionary<string, string> settings, string key, string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            settings.Remove(key);\n            return;\n        }\n\n        settings[key] = value!.Trim();\n    }\n\n    private static void UpsertTokenExpiration(MailProfile profile, DateTimeOffset expiresOn) {\n        if (expiresOn == default || expiresOn == DateTimeOffset.MaxValue) {\n            profile.Settings.Remove(MailProfileSettingsKeys.TokenExpiresOn);\n            return;\n        }\n\n        profile.Settings[MailProfileSettingsKeys.TokenExpiresOn] = expiresOn.ToString(\"o\", System.Globalization.CultureInfo.InvariantCulture);\n    }\n\n    private static MailProfileAuthenticationResult Success(string message, string profileId, MailProfileKind kind, OAuthCredential credential) => new() {\n        Succeeded = true,\n        Message = message,\n        ProfileId = profileId,\n        ProfileKind = kind,\n        UserName = credential.UserName,\n        ExpiresOn = credential.ExpiresOn == default ? null : credential.ExpiresOn\n    };\n\n    private static MailProfileAuthenticationResult Failure(string code, string message, string? profileId, MailProfileKind kind) => new() {\n        Succeeded = false,\n        Code = code,\n        Message = message,\n        ProfileId = string.IsNullOrWhiteSpace(profileId) ? null : profileId,\n        ProfileKind = kind\n    };\n\n    private static MailProfileAuthenticationResult FromOperation(OperationResult result, string profileId, MailProfileKind kind) => new() {\n        Succeeded = result.Succeeded,\n        Code = result.Code,\n        Message = result.Message,\n        ProfileId = profileId,\n        ProfileKind = kind\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileAuthStatus.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes the persisted authentication state for a reusable Mailozaurr profile.\n/// </summary>\npublic sealed class MailProfileAuthStatus {\n    /// <summary>The stable profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>The provider kind associated with the profile.</summary>\n    public MailProfileKind ProfileKind { get; set; }\n\n    /// <summary>The persisted shared auth-flow marker, when available.</summary>\n    public string? AuthFlow { get; set; }\n\n    /// <summary>A normalized auth mode summary such as interactive, appOnly, manualToken, basic, or unknown.</summary>\n    public string Mode { get; set; } = \"unknown\";\n\n    /// <summary>An optional persisted login hint used by interactive OAuth flows.</summary>\n    public string? LoginHint { get; set; }\n\n    /// <summary>The effective mailbox or account identifier associated with the profile.</summary>\n    public string? Mailbox { get; set; }\n\n    /// <summary>The persisted access-token expiration timestamp, when known.</summary>\n    public DateTimeOffset? TokenExpiresOn { get; set; }\n\n    /// <summary>Whether an access token is currently stored.</summary>\n    public bool HasAccessToken { get; set; }\n\n    /// <summary>Whether a refresh token is currently stored.</summary>\n    public bool HasRefreshToken { get; set; }\n\n    /// <summary>Whether a client secret is currently stored.</summary>\n    public bool HasClientSecret { get; set; }\n\n    /// <summary>Whether a certificate path is configured.</summary>\n    public bool HasCertificatePath { get; set; }\n\n    /// <summary>Whether a certificate password is currently stored.</summary>\n    public bool HasCertificatePassword { get; set; }\n\n    /// <summary>Whether a basic password secret is currently stored.</summary>\n    public bool HasPassword { get; set; }\n\n    /// <summary>Whether a client id is configured.</summary>\n    public bool HasClientId { get; set; }\n\n    /// <summary>Whether a tenant id is configured.</summary>\n    public bool HasTenantId { get; set; }\n\n    /// <summary>Whether the persisted token is already expired.</summary>\n    public bool IsTokenExpired { get; set; }\n\n    /// <summary>Whether the shared auth service can perform refresh-auth for this profile.</summary>\n    public bool CanRefresh { get; set; }\n\n    /// <summary>Whether the shared auth service has enough information to run an interactive login flow.</summary>\n    public bool CanLoginInteractively { get; set; }\n\n    /// <summary>A short human-readable summary of the current auth posture.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileAuthenticationResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the result of an authentication flow for a saved profile.\n/// </summary>\npublic sealed class MailProfileAuthenticationResult : OperationResult {\n    /// <summary>The profile identifier the login result applies to.</summary>\n    public string? ProfileId { get; set; }\n\n    /// <summary>The provider kind associated with the authenticated profile.</summary>\n    public MailProfileKind ProfileKind { get; set; } = MailProfileKind.Unknown;\n\n    /// <summary>The resolved user/account name returned by the authentication flow.</summary>\n    public string? UserName { get; set; }\n\n    /// <summary>The access-token expiry when known.</summary>\n    public DateTimeOffset? ExpiresOn { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileBootstrapService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of reusable provider bootstrap workflows.\n/// </summary>\npublic sealed class MailProfileBootstrapService : IMailProfileBootstrapService {\n    private readonly IMailProfileService _profiles;\n    private readonly IMailProfileSecretService _profileSecrets;\n    private readonly IMailSecretStore _secretStore;\n\n    /// <summary>\n    /// Creates a new profile bootstrap service.\n    /// </summary>\n    public MailProfileBootstrapService(IMailProfileService profiles, IMailProfileSecretService profileSecrets, IMailSecretStore secretStore) {\n        _profiles = profiles ?? throw new ArgumentNullException(nameof(profiles));\n        _profileSecrets = profileSecrets ?? throw new ArgumentNullException(nameof(profileSecrets));\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveGraphProfileAsync(GraphProfileBootstrapRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profileId = request.ProfileId.Trim();\n        var displayName = request.DisplayName.Trim();\n        var mailbox = request.Mailbox.Trim();\n        var defaultSender = string.IsNullOrWhiteSpace(request.DefaultSender) ? mailbox : request.DefaultSender!.Trim();\n        var existing = await _profiles.GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);\n        string? clientSecret;\n        string? accessToken;\n        string? certificatePassword;\n        try {\n            clientSecret = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.ClientSecret,\n                request.ClientSecret,\n                request.ClientSecretReference,\n                cancellationToken).ConfigureAwait(false);\n            accessToken = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.AccessToken,\n                request.AccessToken,\n                request.AccessTokenReference,\n                cancellationToken).ConfigureAwait(false);\n            certificatePassword = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.CertificatePassword,\n                request.CertificatePassword,\n                request.CertificatePasswordReference,\n                cancellationToken).ConfigureAwait(false);\n        } catch (InvalidOperationException ex) {\n            return OperationResult.Failure(\"secret_reference_invalid\", ex.Message);\n        }\n\n        var effectiveClientId = FirstNonEmpty(request.ClientId, existing?.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var existingClientId) == true ? existingClientId : null);\n        var effectiveTenantId = FirstNonEmpty(request.TenantId, existing?.Settings.TryGetValue(MailProfileSettingsKeys.TenantId, out var existingTenantId) == true ? existingTenantId : null);\n        var effectiveCertificatePath = FirstNonEmpty(request.CertificatePath, existing?.Settings.TryGetValue(MailProfileSettingsKeys.CertificatePath, out var existingCertificatePath) == true ? existingCertificatePath : null);\n        var hasAccessToken = !string.IsNullOrWhiteSpace(accessToken) ||\n                             await HasStoredSecretAsync(profileId, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n        var hasClientSecret = !string.IsNullOrWhiteSpace(clientSecret) ||\n                              await HasStoredSecretAsync(profileId, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n        var hasCertificate = !string.IsNullOrWhiteSpace(effectiveCertificatePath);\n\n        if (profileId.Length == 0) {\n            return OperationResult.Failure(\"profile_required\", \"Profile id is required.\");\n        }\n        if (displayName.Length == 0) {\n            return OperationResult.Failure(\"display_name_required\", \"Display name is required.\");\n        }\n        if (mailbox.Length == 0) {\n            return OperationResult.Failure(\"mailbox_required\", \"Mailbox is required.\");\n        }\n        if (!hasAccessToken &&\n            (string.IsNullOrWhiteSpace(effectiveClientId) || string.IsNullOrWhiteSpace(effectiveTenantId) || (!hasClientSecret && !hasCertificate))) {\n            return OperationResult.Failure(\n                \"graph_auth_required\",\n                \"Graph profiles require either an access token or a client-id and tenant-id with a client secret or certificate path.\");\n        }\n        if (!string.IsNullOrWhiteSpace(certificatePassword) && !hasCertificate) {\n            return OperationResult.Failure(\"certificate_path_required\", \"Certificate password requires a certificate path.\");\n        }\n\n        var profile = existing == null\n            ? new MailProfile {\n                Id = profileId,\n                Kind = MailProfileKind.Graph\n            }\n            : CloneProfile(existing);\n\n        profile.DisplayName = displayName;\n        profile.Description = request.Description ?? profile.Description;\n        profile.Kind = MailProfileKind.Graph;\n        profile.DefaultMailbox = mailbox;\n        profile.DefaultSender = defaultSender;\n        profile.IsDefault = request.IsDefault;\n        profile.Settings[MailProfileSettingsKeys.Mailbox] = mailbox;\n\n        UpsertSetting(profile.Settings, MailProfileSettingsKeys.ClientId, effectiveClientId);\n        UpsertSetting(profile.Settings, MailProfileSettingsKeys.TenantId, effectiveTenantId);\n        UpsertSetting(profile.Settings, MailProfileSettingsKeys.CertificatePath, effectiveCertificatePath);\n\n        var saveResult = await _profiles.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (!saveResult.Succeeded) {\n            return saveResult;\n        }\n\n        if (!string.IsNullOrWhiteSpace(clientSecret)) {\n            var clientSecretResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.ClientSecret, clientSecret!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!clientSecretResult.Succeeded) {\n                return clientSecretResult;\n            }\n        }\n        if (!string.IsNullOrWhiteSpace(accessToken)) {\n            var accessTokenResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.AccessToken, accessToken!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!accessTokenResult.Succeeded) {\n                return accessTokenResult;\n            }\n        }\n        if (!string.IsNullOrWhiteSpace(certificatePassword)) {\n            var certificatePasswordResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.CertificatePassword, certificatePassword!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!certificatePasswordResult.Succeeded) {\n                return certificatePasswordResult;\n            }\n        }\n\n        return OperationResult.Success(\"Graph profile saved.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveGmailProfileAsync(GmailProfileBootstrapRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profileId = request.ProfileId.Trim();\n        var displayName = request.DisplayName.Trim();\n        var mailbox = string.IsNullOrWhiteSpace(request.Mailbox) ? \"me\" : request.Mailbox!.Trim();\n        var existing = await _profiles.GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);\n        string? clientSecret;\n        string? refreshToken;\n        string? accessToken;\n        try {\n            clientSecret = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.ClientSecret,\n                request.ClientSecret,\n                request.ClientSecretReference,\n                cancellationToken).ConfigureAwait(false);\n            refreshToken = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.RefreshToken,\n                request.RefreshToken,\n                request.RefreshTokenReference,\n                cancellationToken).ConfigureAwait(false);\n            accessToken = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                MailSecretNames.AccessToken,\n                request.AccessToken,\n                request.AccessTokenReference,\n                cancellationToken).ConfigureAwait(false);\n        } catch (InvalidOperationException ex) {\n            return OperationResult.Failure(\"secret_reference_invalid\", ex.Message);\n        }\n        var effectiveClientId = FirstNonEmpty(request.ClientId, existing?.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var existingClientId) == true ? existingClientId : null);\n        var hasAccessToken = !string.IsNullOrWhiteSpace(accessToken) ||\n                             await HasStoredSecretAsync(profileId, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n        var hasRefreshToken = !string.IsNullOrWhiteSpace(refreshToken) ||\n                              await HasStoredSecretAsync(profileId, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false);\n        var hasClientSecret = !string.IsNullOrWhiteSpace(clientSecret) ||\n                              await HasStoredSecretAsync(profileId, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n\n        if (profileId.Length == 0) {\n            return OperationResult.Failure(\"profile_required\", \"Profile id is required.\");\n        }\n        if (displayName.Length == 0) {\n            return OperationResult.Failure(\"display_name_required\", \"Display name is required.\");\n        }\n        if (!hasAccessToken && (!hasRefreshToken || string.IsNullOrWhiteSpace(effectiveClientId) || !hasClientSecret)) {\n            return OperationResult.Failure(\n                \"gmail_auth_required\",\n                \"Gmail profiles require either an access token or a refresh token with a client id and client secret.\");\n        }\n\n        var defaultSender = string.IsNullOrWhiteSpace(request.DefaultSender)\n            ? (string.Equals(mailbox, \"me\", StringComparison.OrdinalIgnoreCase) ? existing?.DefaultSender : mailbox)\n            : request.DefaultSender!.Trim();\n\n        var profile = existing == null\n            ? new MailProfile {\n                Id = profileId,\n                Kind = MailProfileKind.Gmail\n            }\n            : CloneProfile(existing);\n\n        profile.DisplayName = displayName;\n        profile.Description = request.Description ?? profile.Description;\n        profile.Kind = MailProfileKind.Gmail;\n        profile.DefaultMailbox = mailbox;\n        profile.DefaultSender = defaultSender;\n        profile.IsDefault = request.IsDefault;\n        profile.Settings[MailProfileSettingsKeys.Mailbox] = mailbox;\n\n        UpsertSetting(profile.Settings, MailProfileSettingsKeys.ClientId, effectiveClientId);\n\n        var saveResult = await _profiles.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (!saveResult.Succeeded) {\n            return saveResult;\n        }\n\n        if (!string.IsNullOrWhiteSpace(clientSecret)) {\n            var clientSecretResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.ClientSecret, clientSecret!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!clientSecretResult.Succeeded) {\n                return clientSecretResult;\n            }\n        }\n        if (!string.IsNullOrWhiteSpace(refreshToken)) {\n            var refreshTokenResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.RefreshToken, refreshToken!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!refreshTokenResult.Succeeded) {\n                return refreshTokenResult;\n            }\n        }\n        if (!string.IsNullOrWhiteSpace(accessToken)) {\n            var accessTokenResult = await _profileSecrets.SetSecretAsync(profile.Id, MailSecretNames.AccessToken, accessToken!.Trim(), cancellationToken).ConfigureAwait(false);\n            if (!accessTokenResult.Succeeded) {\n                return accessTokenResult;\n            }\n        }\n\n        return OperationResult.Success(\"Gmail profile saved.\");\n    }\n\n    private static MailProfile CloneProfile(MailProfile profile) => new() {\n        Id = profile.Id,\n        DisplayName = profile.DisplayName,\n        Description = profile.Description,\n        Kind = profile.Kind,\n        DefaultSender = profile.DefaultSender,\n        DefaultMailbox = profile.DefaultMailbox,\n        IsDefault = profile.IsDefault,\n        Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n        Capabilities = profile.Capabilities == null\n            ? null\n            : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n    };\n\n    private static void UpsertSetting(IDictionary<string, string> settings, string key, string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            settings.Remove(key);\n            return;\n        }\n\n        var normalizedValue = value!.Trim();\n        settings[key] = normalizedValue;\n    }\n\n    private static string? FirstNonEmpty(params string?[] values) {\n        foreach (var value in values) {\n            if (!string.IsNullOrWhiteSpace(value)) {\n                var normalizedValue = value!.Trim();\n                return normalizedValue;\n            }\n        }\n\n        return null;\n    }\n\n    private async Task<bool> HasStoredSecretAsync(string profileId, string secretName, CancellationToken cancellationToken) =>\n        !string.IsNullOrWhiteSpace(await _secretStore.GetSecretAsync(profileId, secretName, cancellationToken).ConfigureAwait(false));\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileConnectionService.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of live profile connection tests.\n/// </summary>\npublic sealed class MailProfileConnectionService : IMailProfileConnectionService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IImapSessionFactory? _imapSessionFactory;\n    private readonly IGraphSessionFactory? _graphSessionFactory;\n    private readonly IGmailSessionFactory? _gmailSessionFactory;\n    private readonly ISmtpSessionFactory? _smtpSessionFactory;\n    private readonly Func<ImapClient, CancellationToken, Task> _probeImapAsync;\n    private readonly Func<ImapClient, CancellationToken, Task> _probeImapMailboxAsync;\n    private readonly Func<GraphSession, CancellationToken, Task> _probeGraphAsync;\n    private readonly Func<GraphSession, CancellationToken, Task> _probeGraphMailboxAsync;\n    private readonly Func<GmailSession, CancellationToken, Task> _probeGmailAsync;\n    private readonly Func<GmailSession, CancellationToken, Task> _probeGmailMailboxAsync;\n    private readonly Func<Smtp, CancellationToken, Task> _probeSmtpAsync;\n\n    /// <summary>\n    /// Creates a new connection-test service.\n    /// </summary>\n    public MailProfileConnectionService(\n        IMailProfileStore profileStore,\n        IImapSessionFactory? imapSessionFactory = null,\n        IGraphSessionFactory? graphSessionFactory = null,\n        IGmailSessionFactory? gmailSessionFactory = null,\n        ISmtpSessionFactory? smtpSessionFactory = null,\n        Func<ImapClient, CancellationToken, Task>? probeImapAsync = null,\n        Func<ImapClient, CancellationToken, Task>? probeImapMailboxAsync = null,\n        Func<GraphSession, CancellationToken, Task>? probeGraphAsync = null,\n        Func<GraphSession, CancellationToken, Task>? probeGraphMailboxAsync = null,\n        Func<GmailSession, CancellationToken, Task>? probeGmailAsync = null,\n        Func<GmailSession, CancellationToken, Task>? probeGmailMailboxAsync = null,\n        Func<Smtp, CancellationToken, Task>? probeSmtpAsync = null) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _imapSessionFactory = imapSessionFactory;\n        _graphSessionFactory = graphSessionFactory;\n        _gmailSessionFactory = gmailSessionFactory;\n        _smtpSessionFactory = smtpSessionFactory;\n        _probeImapAsync = probeImapAsync ?? DefaultProbeImapAsync;\n        _probeImapMailboxAsync = probeImapMailboxAsync ?? DefaultProbeImapMailboxAsync;\n        _probeGraphAsync = probeGraphAsync ?? DefaultProbeGraphAsync;\n        _probeGraphMailboxAsync = probeGraphMailboxAsync ?? DefaultProbeGraphMailboxAsync;\n        _probeGmailAsync = probeGmailAsync ?? DefaultProbeGmailAsync;\n        _probeGmailMailboxAsync = probeGmailMailboxAsync ?? DefaultProbeGmailMailboxAsync;\n        _probeSmtpAsync = probeSmtpAsync ?? DefaultProbeSmtpAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileConnectionTestResult> TestAsync(\n        string profileId,\n        MailProfileConnectionTestScope scope = MailProfileConnectionTestScope.Auto,\n        CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            return Failure(\"profile_required\", \"Profile id is required.\", profileId, MailProfileKind.Unknown, scope, MailProfileConnectionTestScope.Auto);\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return Failure(\"profile_not_found\", \"Profile was not found.\", profileId, MailProfileKind.Unknown, scope, MailProfileConnectionTestScope.Auto);\n        }\n\n        var effectiveScope = ResolveScope(profile, scope);\n        try {\n            return profile.Kind switch {\n                MailProfileKind.Imap => await TestImapAsync(profile, scope, effectiveScope, cancellationToken).ConfigureAwait(false),\n                MailProfileKind.Graph => await TestGraphAsync(profile, scope, effectiveScope, cancellationToken).ConfigureAwait(false),\n                MailProfileKind.Gmail => await TestGmailAsync(profile, scope, effectiveScope, cancellationToken).ConfigureAwait(false),\n                MailProfileKind.Smtp => await TestSmtpAsync(profile, scope, effectiveScope, cancellationToken).ConfigureAwait(false),\n                _ => Failure(\"connection_test_not_supported\", $\"Live connection testing is not supported for profile kind '{profile.Kind}'.\", profile.Id, profile.Kind, scope, effectiveScope)\n            };\n        } catch (OperationCanceledException) {\n            throw;\n        } catch (Exception ex) {\n            return Failure(\"connection_test_failed\", ex.Message, profile.Id, profile.Kind, scope, effectiveScope);\n        }\n    }\n\n    private async Task<MailProfileConnectionTestResult> TestImapAsync(\n        MailProfile profile,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope effectiveScope,\n        CancellationToken cancellationToken) {\n        if (_imapSessionFactory == null) {\n            return Failure(\"connection_test_not_supported\", \"IMAP connection testing is not configured.\", profile.Id, profile.Kind, requestedScope, effectiveScope);\n        }\n\n        using var client = await _imapSessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (effectiveScope == MailProfileConnectionTestScope.Mailbox) {\n            await _probeImapMailboxAsync(client, cancellationToken).ConfigureAwait(false);\n            return Success(profile, \"openInbox\", ResolveTarget(profile), \"IMAP mailbox probe succeeded.\", requestedScope, effectiveScope);\n        }\n\n        await _probeImapAsync(client, cancellationToken).ConfigureAwait(false);\n        return Success(profile, \"connect\", ResolveTarget(profile), \"IMAP authentication succeeded.\", requestedScope, effectiveScope);\n    }\n\n    private async Task<MailProfileConnectionTestResult> TestGraphAsync(\n        MailProfile profile,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope effectiveScope,\n        CancellationToken cancellationToken) {\n        if (_graphSessionFactory == null) {\n            return Failure(\"connection_test_not_supported\", \"Graph connection testing is not configured.\", profile.Id, profile.Kind, requestedScope, effectiveScope);\n        }\n\n        using var session = await _graphSessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (effectiveScope == MailProfileConnectionTestScope.Mailbox) {\n            await _probeGraphMailboxAsync(session, cancellationToken).ConfigureAwait(false);\n            return Success(profile, \"listFolders\", session.UserId, \"Graph mailbox probe succeeded.\", requestedScope, effectiveScope);\n        }\n\n        await _probeGraphAsync(session, cancellationToken).ConfigureAwait(false);\n        return Success(profile, \"connect\", session.UserId, effectiveScope == MailProfileConnectionTestScope.Send ? \"Graph send preflight succeeded.\" : \"Graph authentication succeeded.\", requestedScope, effectiveScope);\n    }\n\n    private async Task<MailProfileConnectionTestResult> TestGmailAsync(\n        MailProfile profile,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope effectiveScope,\n        CancellationToken cancellationToken) {\n        if (_gmailSessionFactory == null) {\n            return Failure(\"connection_test_not_supported\", \"Gmail connection testing is not configured.\", profile.Id, profile.Kind, requestedScope, effectiveScope);\n        }\n\n        using var session = await _gmailSessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (effectiveScope == MailProfileConnectionTestScope.Mailbox) {\n            await _probeGmailMailboxAsync(session, cancellationToken).ConfigureAwait(false);\n            return Success(profile, \"listFolders\", session.UserId, \"Gmail mailbox probe succeeded.\", requestedScope, effectiveScope);\n        }\n\n        await _probeGmailAsync(session, cancellationToken).ConfigureAwait(false);\n        return Success(profile, \"getProfile\", session.UserId, effectiveScope == MailProfileConnectionTestScope.Send ? \"Gmail send preflight succeeded.\" : \"Gmail authentication succeeded.\", requestedScope, effectiveScope);\n    }\n\n    private async Task<MailProfileConnectionTestResult> TestSmtpAsync(\n        MailProfile profile,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope effectiveScope,\n        CancellationToken cancellationToken) {\n        if (_smtpSessionFactory == null) {\n            return Failure(\"connection_test_not_supported\", \"SMTP connection testing is not configured.\", profile.Id, profile.Kind, requestedScope, effectiveScope);\n        }\n\n        var smtp = await _smtpSessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        try {\n            await _probeSmtpAsync(smtp, cancellationToken).ConfigureAwait(false);\n            return Success(\n                profile,\n                \"connect\",\n                ResolveTarget(profile),\n                effectiveScope == MailProfileConnectionTestScope.Send ? \"SMTP send preflight succeeded.\" : \"SMTP authentication succeeded.\",\n                requestedScope,\n                effectiveScope);\n        } finally {\n            smtp.Dispose();\n        }\n    }\n\n    private static Task DefaultProbeImapAsync(ImapClient client, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        if (client == null) {\n            throw new ArgumentNullException(nameof(client));\n        }\n        if (!client.IsConnected || !client.IsAuthenticated) {\n            throw new InvalidOperationException(\"IMAP client is not connected and authenticated.\");\n        }\n        return Task.CompletedTask;\n    }\n\n    private static async Task DefaultProbeImapMailboxAsync(ImapClient client, CancellationToken cancellationToken) {\n        await DefaultProbeImapAsync(client, cancellationToken).ConfigureAwait(false);\n        var inbox = client.Inbox ?? throw new InvalidOperationException(\"IMAP client does not expose an inbox folder.\");\n        await inbox.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);\n    }\n\n    private static async Task DefaultProbeGraphAsync(GraphSession session, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        _ = session.UserId;\n        await Task.CompletedTask;\n    }\n\n    private static async Task DefaultProbeGraphMailboxAsync(GraphSession session, CancellationToken cancellationToken) {\n        var folders = await session.Client.ListMailFoldersRecursiveAsync(session.UserId, top: 1, maxRequests: 1, cancellationToken: cancellationToken).ConfigureAwait(false);\n        _ = folders.Count;\n    }\n\n    private static async Task DefaultProbeGmailAsync(GmailSession session, CancellationToken cancellationToken) {\n        var profile = await session.Browser.GetProfileAsync(cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(profile.EmailAddress)) {\n            throw new InvalidOperationException(\"Gmail profile probe did not return an email address.\");\n        }\n    }\n\n    private static async Task DefaultProbeGmailMailboxAsync(GmailSession session, CancellationToken cancellationToken) {\n        var folders = await session.Browser.ListFoldersAsync(cancellationToken).ConfigureAwait(false);\n        _ = folders.Count;\n    }\n\n    private static Task DefaultProbeSmtpAsync(Smtp smtp, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        if (smtp == null) {\n            throw new ArgumentNullException(nameof(smtp));\n        }\n        if (!smtp.Client.IsConnected) {\n            throw new InvalidOperationException(\"SMTP client is not connected.\");\n        }\n        return Task.CompletedTask;\n    }\n\n    private static string? ResolveTarget(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) &&\n            !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultSender)) {\n            return profile.DefaultSender!.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.UserName, out var userName) &&\n            !string.IsNullOrWhiteSpace(userName)) {\n            return userName.Trim();\n        }\n\n        return null;\n    }\n\n    private static MailProfileConnectionTestScope ResolveScope(MailProfile profile, MailProfileConnectionTestScope scope) =>\n        scope switch {\n            MailProfileConnectionTestScope.Auto => profile.Kind switch {\n                MailProfileKind.Imap => MailProfileConnectionTestScope.Mailbox,\n                MailProfileKind.Graph => MailProfileConnectionTestScope.Mailbox,\n                MailProfileKind.Gmail => MailProfileConnectionTestScope.Mailbox,\n                MailProfileKind.Smtp => MailProfileConnectionTestScope.Send,\n                _ => MailProfileConnectionTestScope.Auth\n            },\n            MailProfileConnectionTestScope.Mailbox when profile.Kind == MailProfileKind.Smtp => MailProfileConnectionTestScope.Send,\n            MailProfileConnectionTestScope.Send when profile.Kind == MailProfileKind.Imap => MailProfileConnectionTestScope.Auth,\n            MailProfileConnectionTestScope.Send when profile.Kind == MailProfileKind.Pop3 => MailProfileConnectionTestScope.Auth,\n            _ => scope\n        };\n\n    private static MailProfileConnectionTestResult Success(\n        MailProfile profile,\n        string probe,\n        string? target,\n        string message,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope executedScope) => new() {\n        Succeeded = true,\n        Message = message,\n        ProfileId = profile.Id,\n        ProfileKind = profile.Kind,\n        Probe = probe,\n        Target = target,\n        RequestedScope = requestedScope,\n        ExecutedScope = executedScope\n    };\n\n    private static MailProfileConnectionTestResult Failure(\n        string code,\n        string message,\n        string? profileId,\n        MailProfileKind kind,\n        MailProfileConnectionTestScope requestedScope,\n        MailProfileConnectionTestScope executedScope) => new() {\n        Succeeded = false,\n        Code = code,\n        Message = message,\n        ProfileId = string.IsNullOrWhiteSpace(profileId) ? null : profileId,\n        ProfileKind = kind,\n        RequestedScope = requestedScope,\n        ExecutedScope = executedScope\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileConnectionTestResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the outcome of a reusable provider connection test.\n/// </summary>\npublic sealed class MailProfileConnectionTestResult : OperationResult {\n    /// <summary>The tested profile identifier.</summary>\n    public string? ProfileId { get; set; }\n\n    /// <summary>The tested profile kind.</summary>\n    public MailProfileKind ProfileKind { get; set; }\n\n    /// <summary>The provider action used to verify connectivity.</summary>\n    public string? Probe { get; set; }\n\n    /// <summary>The resolved mailbox or account target used for the test, when applicable.</summary>\n    public string? Target { get; set; }\n\n    /// <summary>The requested test scope.</summary>\n    public MailProfileConnectionTestScope RequestedScope { get; set; }\n\n    /// <summary>The scope that was actually executed.</summary>\n    public MailProfileConnectionTestScope ExecutedScope { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileConnectionTestScope.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes the depth of a live profile connection test.\n/// </summary>\npublic enum MailProfileConnectionTestScope {\n    /// <summary>Uses the provider-appropriate default probe.</summary>\n    Auto,\n\n    /// <summary>Verifies session creation and authentication only.</summary>\n    Auth,\n\n    /// <summary>Verifies a lightweight mailbox read operation.</summary>\n    Mailbox,\n\n    /// <summary>Verifies a non-destructive send-path preflight.</summary>\n    Send\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileKind.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Identifies the mailbox or transport technology behind a profile.\n/// </summary>\npublic enum MailProfileKind {\n    /// <summary>Unknown or not yet configured.</summary>\n    Unknown = 0,\n\n    /// <summary>IMAP mailbox access.</summary>\n    Imap,\n\n    /// <summary>POP3 mailbox access.</summary>\n    Pop3,\n\n    /// <summary>Microsoft Graph mailbox access.</summary>\n    Graph,\n\n    /// <summary>Gmail API mailbox access.</summary>\n    Gmail,\n\n    /// <summary>SMTP transport.</summary>\n    Smtp,\n\n    /// <summary>SendGrid transport.</summary>\n    SendGrid,\n\n    /// <summary>Mailgun transport.</summary>\n    Mailgun,\n\n    /// <summary>Amazon SES transport.</summary>\n    Ses,\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileKindParser.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Parses human-friendly profile kind values.\n/// </summary>\npublic static class MailProfileKindParser {\n    /// <summary>\n    /// Parses a profile kind string.\n    /// </summary>\n    public static MailProfileKind Parse(string? value) {\n        if (TryParse(value, out var kind)) {\n            return kind;\n        }\n\n        throw new InvalidOperationException($\"Unknown profile kind '{value}'.\");\n    }\n\n    /// <summary>\n    /// Attempts to parse a profile kind string.\n    /// </summary>\n    public static bool TryParse(string? value, out MailProfileKind kind) {\n        var normalized = (value ?? string.Empty).Trim();\n        if (normalized.Length == 0) {\n            kind = MailProfileKind.Unknown;\n            return false;\n        }\n\n        switch (normalized.ToLowerInvariant()) {\n            case \"imap\":\n                kind = MailProfileKind.Imap;\n                return true;\n            case \"pop3\":\n                kind = MailProfileKind.Pop3;\n                return true;\n            case \"graph\":\n            case \"microsoft-graph\":\n            case \"msgraph\":\n                kind = MailProfileKind.Graph;\n                return true;\n            case \"gmail\":\n                kind = MailProfileKind.Gmail;\n                return true;\n            case \"smtp\":\n                kind = MailProfileKind.Smtp;\n                return true;\n            case \"sendgrid\":\n                kind = MailProfileKind.SendGrid;\n                return true;\n            case \"mailgun\":\n                kind = MailProfileKind.Mailgun;\n                return true;\n            case \"ses\":\n            case \"amazon-ses\":\n                kind = MailProfileKind.Ses;\n                return true;\n            default:\n                kind = MailProfileKind.Unknown;\n                return false;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileOverview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregated summary of a saved Mailozaurr profile for operator and agent surfaces.\n/// </summary>\npublic sealed class MailProfileOverview {\n    /// <summary>The saved profile.</summary>\n    public MailProfile Profile { get; set; } = new();\n\n    /// <summary>Normalized capability map for the profile.</summary>\n    public ProfileCapabilities? Capabilities { get; set; }\n\n    /// <summary>Whether the profile supports mailbox read/search operations.</summary>\n    public bool SupportsRead { get; set; }\n\n    /// <summary>Whether the profile supports outbound sending.</summary>\n    public bool SupportsSend { get; set; }\n\n    /// <summary>Persisted authentication status when available.</summary>\n    public MailProfileAuthStatus? AuthStatus { get; set; }\n\n    /// <summary>Provider-readiness diagnosis.</summary>\n    public MailProfileValidationResult? Readiness { get; set; }\n\n    /// <summary>Whether the saved profile currently appears ready to use.</summary>\n    public bool IsReady { get; set; }\n\n    /// <summary>Total validation or readiness errors surfaced for the profile.</summary>\n    public int ErrorCount { get; set; }\n\n    /// <summary>Total validation or readiness warnings surfaced for the profile.</summary>\n    public int WarningCount { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileOverviewCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a profile overview for list and agent scenarios.\n/// </summary>\npublic sealed class MailProfileOverviewCompact {\n    /// <summary>The stable profile identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>The human-readable profile name.</summary>\n    public string? DisplayName { get; set; }\n\n    /// <summary>The profile provider kind.</summary>\n    public MailProfileKind Kind { get; set; }\n\n    /// <summary>Whether the profile is marked as default.</summary>\n    public bool IsDefault { get; set; }\n\n    /// <summary>Whether the profile supports mailbox reads.</summary>\n    public bool SupportsRead { get; set; }\n\n    /// <summary>Whether the profile supports sending.</summary>\n    public bool SupportsSend { get; set; }\n\n    /// <summary>The persisted auth mode when known.</summary>\n    public string? AuthMode { get; set; }\n\n    /// <summary>Whether the profile currently appears ready to use.</summary>\n    public bool IsReady { get; set; }\n\n    /// <summary>Total readiness or validation errors.</summary>\n    public int ErrorCount { get; set; }\n\n    /// <summary>Total readiness or validation warnings.</summary>\n    public int WarningCount { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileOverviewQuery.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Optional filters for bulk profile overview queries.\n/// </summary>\npublic sealed class MailProfileOverviewQuery {\n    /// <summary>Optional profile kind filter.</summary>\n    public MailProfileKind? Kind { get; set; }\n\n    /// <summary>Optional sort key for the returned overviews.</summary>\n    public MailProfileOverviewSortBy SortBy { get; set; } = MailProfileOverviewSortBy.Id;\n\n    /// <summary>When true, reverses the selected sort order.</summary>\n    public bool Descending { get; set; }\n\n    /// <summary>When true, only returns ready profiles.</summary>\n    public bool ReadyOnly { get; set; }\n\n    /// <summary>When true, only returns profiles that support reading.</summary>\n    public bool CanReadOnly { get; set; }\n\n    /// <summary>When true, only returns profiles that support sending.</summary>\n    public bool CanSendOnly { get; set; }\n\n    /// <summary>When true, only returns profiles marked as default.</summary>\n    public bool DefaultOnly { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileOverviewService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of reusable profile overview aggregation.\n/// </summary>\npublic sealed class MailProfileOverviewService : IMailProfileOverviewService {\n    private readonly IMailProfileService _profiles;\n    private readonly IMailProfileAuthService _profileAuth;\n\n    /// <summary>\n    /// Creates a new overview service.\n    /// </summary>\n    public MailProfileOverviewService(IMailProfileService profiles, IMailProfileAuthService profileAuth) {\n        _profiles = profiles ?? throw new ArgumentNullException(nameof(profiles));\n        _profileAuth = profileAuth ?? throw new ArgumentNullException(nameof(profileAuth));\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailProfileOverview>> GetOverviewsAsync(\n        MailProfileOverviewQuery? query = null,\n        CancellationToken cancellationToken = default) {\n        var profiles = await _profiles.GetProfilesAsync(cancellationToken).ConfigureAwait(false);\n        var results = new List<MailProfileOverview>(profiles.Count);\n        foreach (var profile in profiles) {\n            if (!MatchesProfileFilter(profile, query)) {\n                continue;\n            }\n\n            var overview = await BuildOverviewAsync(profile, cancellationToken).ConfigureAwait(false);\n            if (!MatchesOverviewFilter(overview, query)) {\n                continue;\n            }\n\n            results.Add(overview);\n        }\n\n        return SortOverviews(results, query);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MailProfileOverviewCompact>> GetCompactOverviewsAsync(\n        MailProfileOverviewQuery? query = null,\n        CancellationToken cancellationToken = default) =>\n        (await GetOverviewsAsync(query, cancellationToken).ConfigureAwait(false))\n        .Select(ToCompact)\n        .ToArray();\n\n    /// <inheritdoc />\n    public async Task<MailProfileOverview?> GetOverviewAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profiles.GetProfileAsync(profileId.Trim(), cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return null;\n        }\n\n        return await BuildOverviewAsync(profile, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MailProfileOverviewCompact?> GetCompactOverviewAsync(string profileId, CancellationToken cancellationToken = default) {\n        var overview = await GetOverviewAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return overview == null ? null : ToCompact(overview);\n    }\n\n    private async Task<MailProfileOverview> BuildOverviewAsync(MailProfile profile, CancellationToken cancellationToken) {\n        var capabilities = await _profiles.GetCapabilitiesAsync(profile.Id, cancellationToken).ConfigureAwait(false);\n        var authStatus = await _profileAuth.GetStatusAsync(profile.Id, cancellationToken).ConfigureAwait(false);\n        var readiness = await _profiles.DiagnoseAsync(profile.Id, cancellationToken).ConfigureAwait(false);\n        var supportsRead = capabilities?.Supports(MailCapability.SearchMessages | MailCapability.ReadMessages) == true;\n        var supportsSend = capabilities?.Supports(MailCapability.SendMessages) == true;\n\n        return new MailProfileOverview {\n            Profile = profile,\n            Capabilities = capabilities,\n            SupportsRead = supportsRead,\n            SupportsSend = supportsSend,\n            AuthStatus = authStatus,\n            Readiness = readiness,\n            IsReady = readiness.Succeeded,\n            ErrorCount = readiness.Errors.Count,\n            WarningCount = readiness.Warnings.Count,\n            Summary = BuildSummary(profile, authStatus, readiness, supportsRead, supportsSend)\n        };\n    }\n\n    private static string BuildSummary(\n        MailProfile profile,\n        MailProfileAuthStatus? authStatus,\n        MailProfileValidationResult readiness,\n        bool supportsRead,\n        bool supportsSend) {\n        var authMode = authStatus?.Mode ?? \"unknown\";\n        var readinessState = readiness.Succeeded ? \"ready\" : \"needs-attention\";\n        var readState = supportsRead ? \"yes\" : \"no\";\n        var sendState = supportsSend ? \"yes\" : \"no\";\n        var warningSuffix = readiness.Warnings.Count > 0 ? $\", warnings={readiness.Warnings.Count}\" : string.Empty;\n        var errorSuffix = readiness.Errors.Count > 0 ? $\", errors={readiness.Errors.Count}\" : string.Empty;\n        return $\"{profile.Id} [{profile.Kind}] read={readState}, send={sendState}, auth={authMode}, readiness={readinessState}{warningSuffix}{errorSuffix}.\";\n    }\n\n    private static bool MatchesProfileFilter(MailProfile profile, MailProfileOverviewQuery? query) {\n        if (query == null) {\n            return true;\n        }\n\n        if (query.Kind.HasValue && profile.Kind != query.Kind.Value) {\n            return false;\n        }\n\n        if (query.DefaultOnly && !profile.IsDefault) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static bool MatchesOverviewFilter(MailProfileOverview overview, MailProfileOverviewQuery? query) {\n        if (query == null) {\n            return true;\n        }\n\n        if (query.ReadyOnly && !overview.IsReady) {\n            return false;\n        }\n\n        if (query.CanReadOnly && !overview.SupportsRead) {\n            return false;\n        }\n\n        if (query.CanSendOnly && !overview.SupportsSend) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static IReadOnlyList<MailProfileOverview> SortOverviews(\n        IReadOnlyList<MailProfileOverview> overviews,\n        MailProfileOverviewQuery? query) {\n        if (overviews.Count <= 1) {\n            return overviews;\n        }\n\n        var sortBy = query?.SortBy ?? MailProfileOverviewSortBy.Id;\n        var descending = query?.Descending == true;\n\n        IOrderedEnumerable<MailProfileOverview> ordered = sortBy switch {\n            MailProfileOverviewSortBy.Kind => overviews\n                .OrderBy(overview => overview.Profile.Kind)\n                .ThenBy(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase),\n            MailProfileOverviewSortBy.Readiness => overviews\n                .OrderBy(overview => overview.IsReady)\n                .ThenByDescending(overview => overview.ErrorCount)\n                .ThenByDescending(overview => overview.WarningCount)\n                .ThenBy(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase),\n            _ => overviews.OrderBy(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase)\n        };\n\n        if (descending) {\n            ordered = sortBy switch {\n                MailProfileOverviewSortBy.Kind => overviews\n                    .OrderByDescending(overview => overview.Profile.Kind)\n                    .ThenByDescending(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase),\n                MailProfileOverviewSortBy.Readiness => overviews\n                    .OrderByDescending(overview => overview.IsReady)\n                    .ThenBy(overview => overview.ErrorCount)\n                    .ThenBy(overview => overview.WarningCount)\n                    .ThenBy(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase),\n                _ => overviews.OrderByDescending(overview => overview.Profile.Id, StringComparer.OrdinalIgnoreCase)\n            };\n        }\n\n        return ordered.ToArray();\n    }\n\n    private static MailProfileOverviewCompact ToCompact(MailProfileOverview overview) => new() {\n        Id = overview.Profile.Id,\n        DisplayName = overview.Profile.DisplayName,\n        Kind = overview.Profile.Kind,\n        IsDefault = overview.Profile.IsDefault,\n        SupportsRead = overview.SupportsRead,\n        SupportsSend = overview.SupportsSend,\n        AuthMode = overview.AuthStatus?.Mode,\n        IsReady = overview.IsReady,\n        ErrorCount = overview.ErrorCount,\n        WarningCount = overview.WarningCount,\n        Summary = overview.Summary\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileOverviewSortBy.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Supported sort keys for bulk profile overview queries.\n/// </summary>\npublic enum MailProfileOverviewSortBy {\n    /// <summary>Sort by profile identifier.</summary>\n    Id = 0,\n\n    /// <summary>Sort by profile kind, then identifier.</summary>\n    Kind = 1,\n\n    /// <summary>Sort by readiness, with profiles needing attention first.</summary>\n    Readiness = 2,\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileSecretService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of profile secret lifecycle operations.\n/// </summary>\npublic sealed class MailProfileSecretService : IMailProfileSecretService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IMailSecretStore _secretStore;\n\n    /// <summary>\n    /// Creates a new profile secret service.\n    /// </summary>\n    public MailProfileSecretService(IMailProfileStore profileStore, IMailSecretStore secretStore) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _secretStore = secretStore ?? throw new ArgumentNullException(nameof(secretStore));\n    }\n\n    /// <inheritdoc />\n    public Task<OperationResult> SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) =>\n        SetSecretAsync(profileId, secretName, secretValue, null, cancellationToken);\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SetSecretAsync(\n        string profileId,\n        string secretName,\n        string? secretValue,\n        string? secretReference,\n        CancellationToken cancellationToken = default) {\n        var validationResult = await ValidateAsync(profileId, secretName, cancellationToken).ConfigureAwait(false);\n        if (!validationResult.Succeeded) {\n            return validationResult;\n        }\n\n        string? resolvedSecret;\n        try {\n            resolvedSecret = await MailSecretReferenceResolver.ResolveAsync(\n                _secretStore,\n                profileId,\n                secretName,\n                secretValue,\n                secretReference,\n                cancellationToken).ConfigureAwait(false);\n        } catch (InvalidOperationException ex) {\n            return OperationResult.Failure(\"secret_reference_invalid\", ex.Message);\n        }\n\n        if (string.IsNullOrWhiteSpace(resolvedSecret)) {\n            return OperationResult.Failure(\"secret_value_required\", \"Secret value is required.\");\n        }\n\n        await _secretStore.SetSecretAsync(profileId, secretName, resolvedSecret!, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Secret saved.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n        var validationResult = await ValidateAsync(profileId, secretName, cancellationToken).ConfigureAwait(false);\n        if (!validationResult.Succeeded) {\n            return validationResult;\n        }\n\n        var removed = await _secretStore.RemoveSecretAsync(profileId, secretName, cancellationToken).ConfigureAwait(false);\n        return removed\n            ? OperationResult.Success(\"Secret removed.\")\n            : OperationResult.Failure(\"secret_not_found\", \"Secret was not found.\");\n    }\n\n    private async Task<OperationResult> ValidateAsync(string profileId, string secretName, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            return OperationResult.Failure(\"profile_required\", \"Profile id is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(secretName)) {\n            return OperationResult.Failure(\"secret_name_required\", \"Secret name is required.\");\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return OperationResult.Failure(\"profile_not_found\", \"Profile was not found.\");\n        }\n\n        return OperationResult.Success();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Default implementation of profile lifecycle operations.\n/// </summary>\npublic sealed class MailProfileService : IMailProfileService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IMailSecretStore? _secretStore;\n\n    /// <summary>\n    /// Creates a new profile service.\n    /// </summary>\n    public MailProfileService(IMailProfileStore profileStore, IMailSecretStore? secretStore = null) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _secretStore = secretStore;\n    }\n\n    /// <inheritdoc />\n    public Task<IReadOnlyList<MailProfile>> GetProfilesAsync(CancellationToken cancellationToken = default) =>\n        _profileStore.GetAllAsync(cancellationToken);\n\n    /// <inheritdoc />\n    public Task<MailProfile?> GetProfileAsync(string profileId, CancellationToken cancellationToken = default) =>\n        _profileStore.GetByIdAsync(profileId, cancellationToken);\n\n    /// <inheritdoc />\n    public Task<MailProfileValidationResult> ValidateAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n        Task.FromResult(MailProfileValidator.Validate(profile));\n\n    /// <inheritdoc />\n    public async Task<MailProfileValidationResult> DiagnoseAsync(string profileId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            return CreateFailure(\"profile_required\", \"Profile id is required.\");\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            return CreateFailure(\"profile_not_found\", \"Profile was not found.\");\n        }\n\n        var result = MailProfileValidator.Validate(profile);\n        await AddProviderReadinessChecksAsync(profile, result, cancellationToken).ConfigureAwait(false);\n        result.Succeeded = result.Errors.Count == 0;\n        result.Code = result.Succeeded ? null : \"profile_not_ready\";\n        result.Message = result.Succeeded\n            ? (result.Warnings.Count == 0 ? \"Profile is ready.\" : \"Profile is ready with warnings.\")\n            : result.Errors[0];\n        return result;\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        var validation = MailProfileValidator.Validate(profile);\n        if (!validation.Succeeded) {\n            return validation;\n        }\n\n        await _profileStore.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Profile saved.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> DeleteAsync(string profileId, CancellationToken cancellationToken = default) {\n        var removed = await _profileStore.RemoveAsync(profileId, cancellationToken).ConfigureAwait(false);\n        if (!removed) {\n            return OperationResult.Failure(\"profile_not_found\", \"Profile was not found.\");\n        }\n\n        if (_secretStore != null) {\n            await RemoveKnownSecretsAsync(profileId, cancellationToken).ConfigureAwait(false);\n        }\n\n        return OperationResult.Success(\"Profile deleted.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SetDefaultAsync(string profileId, CancellationToken cancellationToken = default) {\n        var profiles = await _profileStore.GetAllAsync(cancellationToken).ConfigureAwait(false);\n        var profile = profiles.FirstOrDefault(p => string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase));\n        if (profile == null) {\n            return OperationResult.Failure(\"profile_not_found\", \"Profile was not found.\");\n        }\n\n        profile.IsDefault = true;\n        await _profileStore.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Default profile updated.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<ProfileCapabilities?> GetCapabilitiesAsync(string profileId, CancellationToken cancellationToken = default) {\n        var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return profile?.GetCapabilities();\n    }\n\n    private async Task AddProviderReadinessChecksAsync(MailProfile profile, MailProfileValidationResult result, CancellationToken cancellationToken) {\n        if (_secretStore == null) {\n            result.Warnings.Add(\"Secret store is unavailable, so authentication readiness could not be fully verified.\");\n            return;\n        }\n\n        switch (profile.Kind) {\n            case MailProfileKind.Graph:\n                var hasGraphAccessToken = await HasSecretAsync(profile.Id, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n                var hasGraphClientSecret = await HasSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n                var hasGraphClientId = HasSetting(profile, MailProfileSettingsKeys.ClientId);\n                var hasGraphTenantId = HasSetting(profile, MailProfileSettingsKeys.TenantId);\n                var hasGraphCertificate = HasSetting(profile, MailProfileSettingsKeys.CertificatePath);\n                if (!hasGraphAccessToken && (!hasGraphClientId || !hasGraphTenantId || (!hasGraphClientSecret && !hasGraphCertificate))) {\n                    result.Errors.Add(\"Graph profiles need an access token or a client id and tenant id with a client secret or certificate path.\");\n                }\n                break;\n            case MailProfileKind.Gmail:\n                var hasGmailAccessToken = await HasSecretAsync(profile.Id, MailSecretNames.AccessToken, cancellationToken).ConfigureAwait(false);\n                var hasGmailRefreshToken = await HasSecretAsync(profile.Id, MailSecretNames.RefreshToken, cancellationToken).ConfigureAwait(false);\n                var hasGmailClientSecret = await HasSecretAsync(profile.Id, MailSecretNames.ClientSecret, cancellationToken).ConfigureAwait(false);\n                var hasGmailClientId = HasSetting(profile, MailProfileSettingsKeys.ClientId);\n                if (!hasGmailAccessToken && (!hasGmailRefreshToken || !hasGmailClientId || !hasGmailClientSecret)) {\n                    result.Errors.Add(\"Gmail profiles need an access token or a refresh token with a client id and client secret.\");\n                }\n                break;\n        }\n    }\n\n    private async Task RemoveKnownSecretsAsync(string profileId, CancellationToken cancellationToken) {\n        var secretNames = new[] {\n            MailSecretNames.Password,\n            MailSecretNames.ClientSecret,\n            MailSecretNames.AccessToken,\n            MailSecretNames.RefreshToken,\n            MailSecretNames.CertificatePassword\n        };\n\n        foreach (var secretName in secretNames) {\n            await _secretStore!.RemoveSecretAsync(profileId, secretName, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private async Task<bool> HasSecretAsync(string profileId, string secretName, CancellationToken cancellationToken) =>\n        !string.IsNullOrWhiteSpace(await _secretStore!.GetSecretAsync(profileId, secretName, cancellationToken).ConfigureAwait(false));\n\n    private static bool HasSetting(MailProfile profile, string key) =>\n        profile.Settings.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value);\n\n    private static MailProfileValidationResult CreateFailure(string code, string message) {\n        var result = new MailProfileValidationResult {\n            Succeeded = false,\n            Code = code,\n            Message = message\n        };\n        result.Errors.Add(message);\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileSettingsKeys.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Common well-known setting keys used by profile-based adapters.\n/// </summary>\npublic static class MailProfileSettingsKeys {\n    /// <summary>User name used for authentication.</summary>\n    public const string UserName = \"userName\";\n\n    /// <summary>Server host name.</summary>\n    public const string Server = \"server\";\n\n    /// <summary>Server port number.</summary>\n    public const string Port = \"port\";\n\n    /// <summary>Default folder or folder path.</summary>\n    public const string Folder = \"folder\";\n\n    /// <summary>Tenant identifier or directory id.</summary>\n    public const string TenantId = \"tenantId\";\n\n    /// <summary>Client or application identifier.</summary>\n    public const string ClientId = \"clientId\";\n\n    /// <summary>Certificate path for certificate-based authentication.</summary>\n    public const string CertificatePath = \"certificatePath\";\n\n    /// <summary>Redirect URI used by interactive OAuth flows.</summary>\n    public const string RedirectUri = \"redirectUri\";\n\n    /// <summary>Authentication flow identifier for provider-specific login orchestration.</summary>\n    public const string AuthFlow = \"authFlow\";\n\n    /// <summary>Preferred login hint used by interactive authentication flows.</summary>\n    public const string LoginHint = \"loginHint\";\n\n    /// <summary>Access-token expiration timestamp in round-trip format.</summary>\n    public const string TokenExpiresOn = \"tokenExpiresOn\";\n\n    /// <summary>Mailbox address or user principal name.</summary>\n    public const string Mailbox = \"mailbox\";\n\n    /// <summary>Authentication mode identifier.</summary>\n    public const string AuthMode = \"authMode\";\n\n    /// <summary>Secure socket options mode.</summary>\n    public const string SecureSocketOptions = \"secureSocketOptions\";\n\n    /// <summary>Compatibility flag that requests SSL/TLS when explicit socket options are not supplied.</summary>\n    public const string UseSsl = \"useSsl\";\n\n    /// <summary>Connection timeout in milliseconds.</summary>\n    public const string Timeout = \"timeout\";\n\n    /// <summary>Retry count for transient connection failures.</summary>\n    public const string RetryCount = \"retryCount\";\n\n    /// <summary>Retry delay in milliseconds.</summary>\n    public const string RetryDelayMilliseconds = \"retryDelayMilliseconds\";\n\n    /// <summary>Retry backoff multiplier.</summary>\n    public const string RetryDelayBackoff = \"retryDelayBackoff\";\n\n    /// <summary>Whether certificate revocation checks should be skipped.</summary>\n    public const string SkipCertificateRevocation = \"skipCertificateRevocation\";\n\n    /// <summary>Whether certificate validation should be skipped.</summary>\n    public const string SkipCertificateValidation = \"skipCertificateValidation\";\n\n    /// <summary>Maximum message body size used when reading messages.</summary>\n    public const string MaxBodyBytes = \"maxBodyBytes\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileStoreOptions.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Configures where mail profiles are stored.\n/// </summary>\npublic sealed class MailProfileStoreOptions {\n    /// <summary>Directory containing the profile store file.</summary>\n    public string? DirectoryPath { get; set; }\n\n    /// <summary>File name used to persist profiles.</summary>\n    public string FileName { get; set; } = \"profiles.json\";\n\n    /// <summary>\n    /// Resolves the file path that should be used by the profile store.\n    /// </summary>\n    public string GetFilePath() {\n        var directory = string.IsNullOrWhiteSpace(DirectoryPath)\n            ? MailApplicationPaths.ResolveProfilesDirectory()\n            : Path.GetFullPath(DirectoryPath);\n        return Path.Combine(directory, FileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileValidationResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the result of validating a mail profile definition.\n/// </summary>\npublic sealed class MailProfileValidationResult : OperationResult {\n    /// <summary>Validation errors.</summary>\n    public List<string> Errors { get; } = new();\n\n    /// <summary>Validation warnings.</summary>\n    public List<string> Warnings { get; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailProfileValidator.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Validates profile definitions before they are saved or used.\n/// </summary>\npublic static class MailProfileValidator {\n    /// <summary>\n    /// Validates a profile and returns errors and warnings.\n    /// </summary>\n    public static MailProfileValidationResult Validate(MailProfile? profile) {\n        var result = new MailProfileValidationResult();\n        if (profile == null) {\n            result.Succeeded = false;\n            result.Code = \"profile_missing\";\n            result.Errors.Add(\"Profile is required.\");\n            result.Message = result.Errors[0];\n            return result;\n        }\n\n        if (string.IsNullOrWhiteSpace(profile.Id)) {\n            result.Errors.Add(\"Profile id is required.\");\n        } else if (profile.Id.Any(char.IsWhiteSpace)) {\n            result.Warnings.Add(\"Profile id contains whitespace. Hyphenated identifiers are recommended.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(profile.DisplayName)) {\n            result.Errors.Add(\"Profile display name is required.\");\n        }\n\n        if (profile.Kind == MailProfileKind.Unknown) {\n            result.Errors.Add(\"Profile kind must be specified.\");\n        }\n\n        switch (profile.Kind) {\n            case MailProfileKind.Imap:\n            case MailProfileKind.Pop3:\n            case MailProfileKind.Smtp:\n                RequireSetting(profile, MailProfileSettingsKeys.Server, result);\n                ValidateOptionalPort(profile, result);\n                break;\n            case MailProfileKind.Graph:\n                RequireOneOf(profile, result, MailProfileSettingsKeys.Mailbox, \"defaultMailbox\");\n                break;\n            case MailProfileKind.Gmail:\n                RequireOneOf(profile, result, MailProfileSettingsKeys.Mailbox, \"defaultMailbox\");\n                break;\n        }\n\n        if (profile.GetCapabilities().Supports(MailCapability.SendMessages) &&\n            string.IsNullOrWhiteSpace(profile.DefaultSender)) {\n            result.Warnings.Add(\"Send-capable profiles should define DefaultSender.\");\n        }\n\n        if (profile.GetCapabilities().Supports(MailCapability.ReadMessages) &&\n            string.IsNullOrWhiteSpace(profile.DefaultMailbox) &&\n            !profile.Settings.ContainsKey(MailProfileSettingsKeys.Mailbox)) {\n            result.Warnings.Add(\"Read-capable profiles should define DefaultMailbox or a mailbox setting.\");\n        }\n\n        result.Succeeded = result.Errors.Count == 0;\n        result.Code = result.Succeeded ? null : \"profile_invalid\";\n        result.Message = result.Succeeded\n            ? (result.Warnings.Count == 0 ? \"Profile is valid.\" : \"Profile is valid with warnings.\")\n            : result.Errors[0];\n        return result;\n    }\n\n    private static void RequireSetting(MailProfile profile, string key, MailProfileValidationResult result) {\n        if (!profile.Settings.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) {\n            result.Errors.Add($\"Profile setting '{key}' is required for {profile.Kind} profiles.\");\n        }\n    }\n\n    private static void RequireOneOf(MailProfile profile, MailProfileValidationResult result, params string[] keys) {\n        foreach (var key in keys) {\n            if (string.Equals(key, \"defaultMailbox\", StringComparison.OrdinalIgnoreCase)) {\n                if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n                    return;\n                }\n            } else if (profile.Settings.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n                return;\n            }\n        }\n\n        result.Warnings.Add($\"Profile should define one of: {string.Join(\", \", keys)}.\");\n    }\n\n    private static void ValidateOptionalPort(MailProfile profile, MailProfileValidationResult result) {\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.Port, out var value) || string.IsNullOrWhiteSpace(value)) {\n            return;\n        }\n\n        if (!int.TryParse(value, out var port) || port <= 0 || port > 65535) {\n            result.Errors.Add($\"Profile setting '{MailProfileSettingsKeys.Port}' must be a valid TCP port.\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailSearchRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for searching or listing messages.\n/// </summary>\npublic sealed class MailSearchRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Optional free-text query.</summary>\n    public string? QueryText { get; set; }\n\n    /// <summary>Optional subject filter.</summary>\n    public string? SubjectContains { get; set; }\n\n    /// <summary>Optional sender filter.</summary>\n    public string? FromContains { get; set; }\n\n    /// <summary>Optional recipient filter.</summary>\n    public string? ToContains { get; set; }\n\n    /// <summary>Only include messages with attachments.</summary>\n    public bool HasAttachments { get; set; }\n\n    /// <summary>Only include unread messages when set to <c>true</c>.</summary>\n    public bool? IsRead { get; set; }\n\n    /// <summary>Optional lower date bound.</summary>\n    public DateTimeOffset? Since { get; set; }\n\n    /// <summary>Optional upper date bound.</summary>\n    public DateTimeOffset? Before { get; set; }\n\n    /// <summary>Maximum number of results to return.</summary>\n    public int? Limit { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailSecretNames.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Common well-known secret names used by profile-based adapters.\n/// </summary>\npublic static class MailSecretNames {\n    /// <summary>Password for username/password authentication.</summary>\n    public const string Password = \"password\";\n\n    /// <summary>Client secret for confidential client auth flows.</summary>\n    public const string ClientSecret = \"clientSecret\";\n\n    /// <summary>Access token for temporary explicit sessions.</summary>\n    public const string AccessToken = \"accessToken\";\n\n    /// <summary>Refresh token when stored explicitly.</summary>\n    public const string RefreshToken = \"refreshToken\";\n\n    /// <summary>Certificate password when certificate auth is used.</summary>\n    public const string CertificatePassword = \"certificatePassword\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailSecretReferenceResolver.cs",
    "content": "namespace Mailozaurr.Application;\n\ninternal static class MailSecretReferenceResolver {\n    public static async Task<string?> ResolveAsync(\n        IMailSecretStore secretStore,\n        string targetProfileId,\n        string targetSecretName,\n        string? inlineValue,\n        string? secretReference,\n        CancellationToken cancellationToken = default) {\n        if (secretStore == null) {\n            throw new ArgumentNullException(nameof(secretStore));\n        }\n\n        var hasInline = !string.IsNullOrWhiteSpace(inlineValue);\n        var hasReference = !string.IsNullOrWhiteSpace(secretReference);\n        if (hasInline && hasReference) {\n            throw new InvalidOperationException(\n                $\"Provide either an inline secret or a secret reference for '{targetSecretName}', not both.\");\n        }\n\n        if (!hasReference) {\n            return hasInline ? inlineValue : null;\n        }\n\n        var (sourceProfileId, sourceSecretName) = Parse(secretReference!, targetProfileId);\n        var resolved = await secretStore.GetSecretAsync(sourceProfileId, sourceSecretName, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(resolved)) {\n            throw new InvalidOperationException(\n                $\"Secret reference '{secretReference}' for '{targetSecretName}' was not found.\");\n        }\n\n        return resolved;\n    }\n\n    public static (string ProfileId, string SecretName) Parse(string secretReference, string defaultProfileId) {\n        var normalized = secretReference == null ? null : secretReference.Trim();\n        if (string.IsNullOrWhiteSpace(normalized)) {\n            throw new InvalidOperationException(\"Secret reference cannot be empty.\");\n        }\n\n        var value = normalized!;\n        var colonIndex = value.IndexOf(':');\n        var slashIndex = value.IndexOf('/');\n        var separatorIndex = colonIndex >= 0 && slashIndex >= 0\n            ? Math.Min(colonIndex, slashIndex)\n            : Math.Max(colonIndex, slashIndex);\n\n        if (separatorIndex < 0) {\n            return (defaultProfileId, value);\n        }\n\n        var profileId = value.Substring(0, separatorIndex).Trim();\n        var secretName = value.Substring(separatorIndex + 1).Trim();\n        if (string.IsNullOrWhiteSpace(profileId) || string.IsNullOrWhiteSpace(secretName)) {\n            throw new InvalidOperationException(\n                $\"Secret reference '{secretReference}' must use '<profile-id>:<secret-name>' or '<secret-name>'.\");\n        }\n\n        return (profileId, secretName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailSecretStoreOptions.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Configures where protected secrets are stored.\n/// </summary>\npublic sealed class MailSecretStoreOptions {\n    /// <summary>Directory containing the secret store file.</summary>\n    public string? DirectoryPath { get; set; }\n\n    /// <summary>File name used to persist secrets.</summary>\n    public string FileName { get; set; } = \"profile-secrets.json\";\n\n    /// <summary>\n    /// Resolves the file path that should be used by the secret store.\n    /// </summary>\n    public string GetFilePath() {\n        var directory = string.IsNullOrWhiteSpace(DirectoryPath)\n            ? MailApplicationPaths.ResolveSecretsDirectory()\n            : Path.GetFullPath(DirectoryPath);\n        return Path.Combine(directory, FileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MailboxRef.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Identifies a mailbox or account container.\n/// </summary>\npublic sealed class MailboxRef {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific mailbox identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>User-facing mailbox name.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>Mailbox address or principal name when known.</summary>\n    public string? Address { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/Mailozaurr.Application.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <Company>Evotec</Company>\n        <Authors>Przemyslaw Klys</Authors>\n        <AssemblyName>Mailozaurr.Application</AssemblyName>\n        <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <LangVersion>latest</LangVersion>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Mailozaurr\\Mailozaurr.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'netstandard2.0' \">\n        <PackageReference Include=\"System.Text.Json\" Version=\"10.0.3\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <None Include=\"..\\..\\Mailozaurr.png\" Pack=\"true\" PackagePath=\"\\\" />\n        <None Include=\"..\\..\\README.MD\" Pack=\"true\" PackagePath=\"\\\" />\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionBatchExecutionItemResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Execution outcome for one normalized message action plan inside a batch.\n/// </summary>\npublic sealed class MessageActionBatchExecutionItemResult : OperationResult {\n    /// <summary>Zero-based plan index inside the batch request.</summary>\n    public int Index { get; set; }\n\n    /// <summary>Stable action name such as mark-read, archive, move, or delete.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>Normalized execution kind such as SetReadState, Move, or Delete.</summary>\n    public string ExecutionKind { get; set; } = string.Empty;\n\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Total message identifiers covered by this plan.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total message identifiers that succeeded for this plan.</summary>\n    public int SucceededCount { get; set; }\n\n    /// <summary>Total message identifiers that failed for this plan.</summary>\n    public int FailedCount { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionBatchExecutionResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate outcome for executing a batch of normalized message action plans.\n/// </summary>\npublic sealed class MessageActionBatchExecutionResult : OperationResult {\n    /// <summary>Total number of plans submitted for execution.</summary>\n    public int RequestedPlanCount { get; set; }\n\n    /// <summary>Total number of plans that were actually attempted.</summary>\n    public int AttemptedPlanCount { get; set; }\n\n    /// <summary>Total number of plans that completed successfully.</summary>\n    public int SucceededPlanCount { get; set; }\n\n    /// <summary>Total number of plans that failed.</summary>\n    public int FailedPlanCount { get; set; }\n\n    /// <summary>Total number of plans skipped after an earlier failure.</summary>\n    public int SkippedPlanCount { get; set; }\n\n    /// <summary>Per-plan execution outcomes in request order.</summary>\n    public List<MessageActionBatchExecutionItemResult> Results { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionConfirmationTokens.cs",
    "content": "using System.Security.Cryptography;\nusing System.Text;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Generates stable confirmation tokens that tie previews to destructive mailbox actions.\n/// </summary>\npublic static class MessageActionConfirmationTokens {\n    /// <summary>Creates a confirmation token for a move-like action.</summary>\n    public static string CreateMoveToken(\n        string profileId,\n        string? mailboxId,\n        string? folderId,\n        IEnumerable<string> messageIds,\n        string destinationFolderId) =>\n        CreateToken(\"move\", profileId, mailboxId, folderId, messageIds, destinationFolderId);\n\n    /// <summary>Creates a confirmation token for a delete action.</summary>\n    public static string CreateDeleteToken(\n        string profileId,\n        string? mailboxId,\n        string? folderId,\n        IEnumerable<string> messageIds) =>\n        CreateToken(\"delete\", profileId, mailboxId, folderId, messageIds, destinationFolderId: null);\n\n    /// <summary>Creates a confirmation token for a read/unread state change.</summary>\n    public static string CreateReadStateToken(\n        string profileId,\n        string? mailboxId,\n        string? folderId,\n        IEnumerable<string> messageIds,\n        bool isRead) =>\n        CreateToken(isRead ? \"read-state-read\" : \"read-state-unread\", profileId, mailboxId, folderId, messageIds, destinationFolderId: null);\n\n    /// <summary>Creates a confirmation token for a flagged/unflagged state change.</summary>\n    public static string CreateFlaggedStateToken(\n        string profileId,\n        string? mailboxId,\n        string? folderId,\n        IEnumerable<string> messageIds,\n        bool isFlagged) =>\n        CreateToken(isFlagged ? \"flagged-state-flagged\" : \"flagged-state-unflagged\", profileId, mailboxId, folderId, messageIds, destinationFolderId: null);\n\n    private static string CreateToken(\n        string action,\n        string profileId,\n        string? mailboxId,\n        string? folderId,\n        IEnumerable<string> messageIds,\n        string? destinationFolderId) {\n        if (string.IsNullOrWhiteSpace(action)) {\n            throw new ArgumentException(\"Action is required.\", nameof(action));\n        }\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n        if (messageIds == null) {\n            throw new ArgumentNullException(nameof(messageIds));\n        }\n\n        var normalizedMessageIds = messageIds\n            .Where(id => !string.IsNullOrWhiteSpace(id))\n            .Select(id => id.Trim())\n            .Distinct(StringComparer.Ordinal)\n            .OrderBy(id => id, StringComparer.Ordinal)\n            .ToArray();\n\n        var payload = string.Join(\"|\", new[] {\n            \"mail-action-confirmation-v1\",\n            action.Trim().ToLowerInvariant(),\n            profileId.Trim(),\n            mailboxId?.Trim() ?? string.Empty,\n            folderId?.Trim() ?? string.Empty,\n            destinationFolderId?.Trim() ?? string.Empty,\n            string.Join(\",\", normalizedMessageIds)\n        });\n\n        using var sha = SHA256.Create();\n        var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(payload));\n        var token = Convert.ToBase64String(bytes)\n            .TrimEnd('=')\n            .Replace('+', '-')\n            .Replace('/', '_');\n        return $\"mact_v1_{token}\";\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionExecutionPlan.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized execution plan for a selected message action.\n/// </summary>\npublic sealed class MessageActionExecutionPlan : OperationResult {\n    /// <summary>Human-readable plan name suitable for selection in stored batches.</summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>Human-readable plan summary suitable for list-style displays.</summary>\n    public string Summary { get; set; } = string.Empty;\n\n    /// <summary>Stable action name such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>Normalized execution kind such as SetReadState, SetFlaggedState, Move, or Delete.</summary>\n    public string ExecutionKind { get; set; } = string.Empty;\n\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Requested destination folder identifier or alias when relevant.</summary>\n    public string? RequestedDestinationFolderId { get; set; }\n\n    /// <summary>Resolved destination details when the action uses a destination.</summary>\n    public MailFolderTargetResolution? Destination { get; set; }\n\n    /// <summary>Desired state value when the action is a state change.</summary>\n    public bool? DesiredState { get; set; }\n\n    /// <summary>Expected confirmation token for executing this exact action plan.</summary>\n    public string? ConfirmationToken { get; set; }\n\n    /// <summary>Whether a confirmation token was provided while creating the plan.</summary>\n    public bool ConfirmationProvided { get; set; }\n\n    /// <summary>Whether the provided confirmation token matched the normalized plan.</summary>\n    public bool ConfirmationValidated { get; set; }\n\n    /// <summary>Warnings detected while building the plan.</summary>\n    public List<string> Warnings { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionExecutionPlanRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for creating a normalized execution plan from a selected message action.\n/// </summary>\npublic sealed class MessageActionExecutionPlanRequest {\n    /// <summary>Stable action name such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to normalize for execution.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Optional destination folder identifier or alias when the action requires one.</summary>\n    public string? DestinationFolderId { get; set; }\n\n    /// <summary>Optional confirmation token returned by a matching preview call.</summary>\n    public string? ConfirmationToken { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionItemResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Per-message outcome for a bulk message action.\n/// </summary>\npublic sealed class MessageActionItemResult {\n    /// <summary>Provider-specific message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Whether the action succeeded for this message.</summary>\n    public bool Succeeded { get; set; }\n\n    /// <summary>Optional normalized status/error code.</summary>\n    public string? Code { get; set; }\n\n    /// <summary>Optional human-readable action outcome.</summary>\n    public string? Message { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionPlanBatchTransformPreviewItem.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Per-plan dry-run preview entry for a transformed stored action plan.\n/// </summary>\npublic sealed class MessageActionPlanBatchTransformPreviewItem {\n    /// <summary>Zero-based plan index in the source batch.</summary>\n    public int Index { get; set; }\n\n    /// <summary>Action name such as move, delete, or mark-read.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>Normalized execution kind.</summary>\n    public string ExecutionKind { get; set; } = string.Empty;\n\n    /// <summary>Original profile identifier.</summary>\n    public string SourceProfileId { get; set; } = string.Empty;\n\n    /// <summary>Transformed profile identifier.</summary>\n    public string TargetProfileId { get; set; } = string.Empty;\n\n    /// <summary>Original mailbox identifier.</summary>\n    public string? SourceMailboxId { get; set; }\n\n    /// <summary>Transformed mailbox identifier.</summary>\n    public string? TargetMailboxId { get; set; }\n\n    /// <summary>Original source folder identifier.</summary>\n    public string? SourceFolderId { get; set; }\n\n    /// <summary>Transformed source folder identifier.</summary>\n    public string? TargetFolderId { get; set; }\n\n    /// <summary>Original requested destination folder for move-like plans.</summary>\n    public string? SourceDestinationFolderId { get; set; }\n\n    /// <summary>Transformed requested destination folder for move-like plans.</summary>\n    public string? TargetDestinationFolderId { get; set; }\n\n    /// <summary>Whether any effective plan values would change.</summary>\n    public bool WillChange { get; set; }\n\n    /// <summary>Whether the confirmation token would be regenerated.</summary>\n    public bool ConfirmationTokenWillChange { get; set; }\n\n    /// <summary>Human-readable summary of the effective transform.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionPlanBatchTransformRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Reusable transform request for cloning a stored action-plan batch into a new environment or mailbox scope.\n/// </summary>\npublic sealed class MessageActionPlanBatchTransformRequest {\n    /// <summary>Optional zero-based plan indexes to include from the source batch. When omitted, all plans are included.</summary>\n    public List<int> PlanIndexes { get; set; } = new();\n\n    /// <summary>Optional plan names to include from the source batch. When omitted, all names are included.</summary>\n    public List<string> PlanNames { get; set; } = new();\n\n    /// <summary>Optional replacement profile identifier for every transformed plan.</summary>\n    public string? ProfileId { get; set; }\n\n    /// <summary>Optional replacement mailbox identifier for every transformed plan.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional replacement source folder identifier for every transformed plan.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Optional replacement destination folder identifier for move-like plans.</summary>\n    public string? DestinationFolderId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionPreviewItem.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Reusable dry-run preview entry for a single mailbox action.\n/// </summary>\npublic sealed class MessageActionPreviewItem : OperationResult {\n    /// <summary>Stable action name such as archive, trash, move, or delete.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>Human-readable label for the action preview.</summary>\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>The requested destination folder or alias when relevant.</summary>\n    public string? RequestedDestinationFolderId { get; set; }\n\n    /// <summary>The resolved folder target when the action uses a destination.</summary>\n    public MailFolderTargetResolution? Destination { get; set; }\n\n    /// <summary>Desired state value when the action is a state change.</summary>\n    public bool? DesiredState { get; set; }\n\n    /// <summary>Optional confirmation token for executing this exact action preview.</summary>\n    public string? ConfirmationToken { get; set; }\n\n    /// <summary>Warnings detected while building this action preview.</summary>\n    public List<string> Warnings { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageActionResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate outcome for a bulk message action.\n/// </summary>\npublic sealed class MessageActionResult : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Total unique message ids requested.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total messages whose action succeeded.</summary>\n    public int SucceededCount { get; set; }\n\n    /// <summary>Total messages whose action failed.</summary>\n    public int FailedCount { get; set; }\n\n    /// <summary>Per-message outcomes in request order.</summary>\n    public List<MessageActionItemResult> Results { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageDetail.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents normalized detailed message data.\n/// </summary>\npublic sealed class MessageDetail {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Optional normalized summary for the same message.</summary>\n    public MessageSummary? Summary { get; set; }\n\n    /// <summary>Plain text body.</summary>\n    public string? TextBody { get; set; }\n\n    /// <summary>HTML body.</summary>\n    public string? HtmlBody { get; set; }\n\n    /// <summary>Attachments associated with the message.</summary>\n    public List<AttachmentSummary> Attachments { get; set; } = new();\n\n    /// <summary>Optional raw MIME or provider-native payload.</summary>\n    public string? RawContent { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageDetailCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of detailed message data for list and agent scenarios.\n/// </summary>\npublic sealed class MessageDetailCompact {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Optional normalized summary for the same message.</summary>\n    public MessageSummaryCompact? Summary { get; set; }\n\n    /// <summary>Plain-text body preview.</summary>\n    public string? TextBodyPreview { get; set; }\n\n    /// <summary>HTML body preview.</summary>\n    public string? HtmlBodyPreview { get; set; }\n\n    /// <summary>Attachments associated with the message.</summary>\n    public List<AttachmentSummary> Attachments { get; set; } = new();\n\n    /// <summary>Whether raw provider content was available.</summary>\n    public bool HasRawContent { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string SummaryText { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageRecipient.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents a normalized recipient or sender identity.\n/// </summary>\npublic sealed class MessageRecipient {\n    /// <summary>Display name of the recipient.</summary>\n    public string? Name { get; set; }\n\n    /// <summary>Email address of the recipient.</summary>\n    public string Address { get; set; } = string.Empty;\n\n    /// <inheritdoc />\n    public override string ToString() => string.IsNullOrWhiteSpace(Name) ? Address : $\"{Name} <{Address}>\";\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageStateChangePreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate dry-run result for a planned message state change such as read/unread or flag/unflag.\n/// </summary>\npublic sealed class MessageStateChangePreview : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Stable action name such as read-state or flagged-state.</summary>\n    public string Action { get; set; } = string.Empty;\n\n    /// <summary>The desired state value for the action.</summary>\n    public bool DesiredState { get; set; }\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>Total duplicate or empty message identifiers removed during normalization.</summary>\n    public int DuplicateOrEmptyCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Optional confirmation token that can be supplied when executing the action.</summary>\n    public string? ConfirmationToken { get; set; }\n\n    /// <summary>Warnings detected during preview.</summary>\n    public List<string> Warnings { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageSummary.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents normalized summary data for a message returned by search or list operations.\n/// </summary>\npublic sealed class MessageSummary {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific thread or conversation identifier.</summary>\n    public string? ThreadId { get; set; }\n\n    /// <summary>Folder identifier when known.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Message subject.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Message preview or snippet when available.</summary>\n    public string? Preview { get; set; }\n\n    /// <summary>Message sender list.</summary>\n    public List<MessageRecipient> From { get; set; } = new();\n\n    /// <summary>Primary recipients.</summary>\n    public List<MessageRecipient> To { get; set; } = new();\n\n    /// <summary>Carbon-copy recipients.</summary>\n    public List<MessageRecipient> Cc { get; set; } = new();\n\n    /// <summary>When the message was sent.</summary>\n    public DateTimeOffset? SentAt { get; set; }\n\n    /// <summary>When the message was received or observed.</summary>\n    public DateTimeOffset? ReceivedAt { get; set; }\n\n    /// <summary>Whether the message is marked read.</summary>\n    public bool? IsRead { get; set; }\n\n    /// <summary>Whether the message has attachments.</summary>\n    public bool HasAttachments { get; set; }\n\n    /// <summary>Normalized priority when available.</summary>\n    public MessagePriority? Priority { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MessageSummaryCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a message summary for list and agent scenarios.\n/// </summary>\npublic sealed class MessageSummaryCompact {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>Folder identifier when known.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Message subject.</summary>\n    public string? Subject { get; set; }\n\n    /// <summary>Message preview or snippet when available.</summary>\n    public string? Preview { get; set; }\n\n    /// <summary>First sender display value when available.</summary>\n    public string? From { get; set; }\n\n    /// <summary>When the message was received or observed.</summary>\n    public DateTimeOffset? ReceivedAt { get; set; }\n\n    /// <summary>Whether the message is marked read.</summary>\n    public bool? IsRead { get; set; }\n\n    /// <summary>Whether the message has attachments.</summary>\n    public bool HasAttachments { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MimeAttachmentStorage.cs",
    "content": "using MimeKit;\n\nnamespace Mailozaurr.Application;\n\ninternal static class MimeAttachmentStorage {\n    public static MimeEntity? ResolveAttachment(IReadOnlyList<MimeEntity> attachments, string attachmentId) {\n        if (int.TryParse(attachmentId, out var index) && index >= 0 && index < attachments.Count) {\n            return attachments[index];\n        }\n\n        return attachments.FirstOrDefault(attachment =>\n            string.Equals(GetAttachmentFileName(attachment), attachmentId, StringComparison.OrdinalIgnoreCase));\n    }\n\n    public static string ResolveDestinationPath(string requestedPath, MimeEntity attachment) {\n        var destinationPath = Path.GetFullPath(requestedPath);\n        if (Directory.Exists(destinationPath)) {\n            return Path.Combine(destinationPath, GetAttachmentFileName(attachment));\n        }\n\n        var directory = Path.GetDirectoryName(destinationPath);\n        if (!string.IsNullOrWhiteSpace(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        return destinationPath;\n    }\n\n    public static string GetAttachmentFileName(MimeEntity attachment) => attachment switch {\n        MimePart part => part.FileName ?? Path.GetRandomFileName(),\n        MessagePart messagePart => messagePart.ContentDisposition?.FileName ?? messagePart.ContentType?.Name ?? Path.GetRandomFileName(),\n        _ => Path.GetRandomFileName()\n    };\n\n    public static void SaveAttachment(MimeEntity attachment, string destinationPath) {\n        switch (attachment) {\n            case MimePart part:\n                using (var stream = File.Create(destinationPath)) {\n                    if (part.Content != null) {\n                        part.Content.DecodeTo(stream);\n                    } else {\n                        part.WriteTo(stream);\n                    }\n                }\n                break;\n            case MessagePart messagePart:\n                if (messagePart.Message != null) {\n                    messagePart.Message.WriteTo(destinationPath);\n                } else {\n                    using (var stream = File.Create(destinationPath)) {\n                        messagePart.WriteTo(stream);\n                    }\n                }\n                break;\n            default:\n                throw new InvalidOperationException(\"Unsupported attachment type.\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MoveMessagesPreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate dry-run result for a planned message move.\n/// </summary>\npublic sealed class MoveMessagesPreview : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>The originally requested destination folder value.</summary>\n    public string RequestedDestinationFolderId { get; set; } = string.Empty;\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>Total duplicate or empty message identifiers removed during normalization.</summary>\n    public int DuplicateOrEmptyCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>The resolved destination details.</summary>\n    public MailFolderTargetResolution? Destination { get; set; }\n\n    /// <summary>Optional confirmation token that can be supplied when executing the move.</summary>\n    public string? ConfirmationToken { get; set; }\n\n    /// <summary>Warnings detected during preview.</summary>\n    public List<string> Warnings { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MoveMessagesPreviewRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for previewing a message move without executing it.\n/// </summary>\npublic sealed class MoveMessagesPreviewRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to preview.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Requested destination folder identifier or provider alias.</summary>\n    public string DestinationFolderId { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/MoveMessagesRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for moving one or more messages.\n/// </summary>\npublic sealed class MoveMessagesRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to move.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Destination folder identifier or provider alias.</summary>\n    public string DestinationFolderId { get; set; } = string.Empty;\n\n    /// <summary>Optional confirmation token from a prior preview for this exact action.</summary>\n    public string? ConfirmationToken { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/OperationResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the result of an application-layer operation.\n/// </summary>\npublic class OperationResult {\n    /// <summary>Whether the operation completed successfully.</summary>\n    public bool Succeeded { get; set; }\n\n    /// <summary>Stable machine-readable code when available.</summary>\n    public string? Code { get; set; }\n\n    /// <summary>Human-readable message for logs or UI surfaces.</summary>\n    public string? Message { get; set; }\n\n    /// <summary>\n    /// Creates a successful result.\n    /// </summary>\n    public static OperationResult Success(string? message = null) => new() {\n        Succeeded = true,\n        Message = message\n    };\n\n    /// <summary>\n    /// Creates a failure result.\n    /// </summary>\n    public static OperationResult Failure(string code, string? message = null) => new() {\n        Succeeded = false,\n        Code = code,\n        Message = message\n    };\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/PendingMailQueueService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Reusable queue facade over Mailozaurr pending-message infrastructure.\n/// </summary>\npublic sealed class PendingMailQueueService : IMailQueueService {\n    private readonly IPendingMessageRepository _repository;\n    private readonly Func<IPendingMessageRepository, IPendingMessageProcessorObserver, CancellationToken, Task> _processAsync;\n\n    /// <summary>\n    /// Creates a new queue service.\n    /// </summary>\n    public PendingMailQueueService(\n        IPendingMessageRepository repository,\n        Func<IPendingMessageRepository, IPendingMessageProcessorObserver, CancellationToken, Task>? processAsync = null) {\n        _repository = repository ?? throw new ArgumentNullException(nameof(repository));\n        _processAsync = processAsync ?? DefaultProcessAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<QueuedMessageSummary>> ListAsync(CancellationToken cancellationToken = default) {\n        var messages = new List<QueuedMessageSummary>();\n        await foreach (var record in _repository.GetAllAsync(cancellationToken).ConfigureAwait(false)) {\n            if (record == null) {\n                continue;\n            }\n\n            messages.Add(Map(record));\n        }\n\n        return messages\n            .OrderBy(message => message.NextAttemptAt)\n            .ThenBy(message => message.MessageId, StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<QueuedMessageCompact>> ListCompactAsync(CancellationToken cancellationToken = default) =>\n        (await ListAsync(cancellationToken).ConfigureAwait(false))\n        .Select(ToCompact)\n        .ToArray();\n\n    /// <inheritdoc />\n    public async Task<QueuedMessageSummary?> GetAsync(string messageId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"Message id is required.\", nameof(messageId));\n        }\n\n        var record = await _repository.GetByMessageIdAsync(messageId.Trim(), cancellationToken).ConfigureAwait(false);\n        return record == null ? null : Map(record);\n    }\n\n    /// <inheritdoc />\n    public async Task<QueuedMessageCompact?> GetCompactAsync(string messageId, CancellationToken cancellationToken = default) {\n        var message = await GetAsync(messageId, cancellationToken).ConfigureAwait(false);\n        return message == null ? null : ToCompact(message);\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n        if (string.IsNullOrWhiteSpace(messageId)) {\n            throw new ArgumentException(\"Message id is required.\", nameof(messageId));\n        }\n\n        var normalizedMessageId = messageId.Trim();\n        var existing = await _repository.GetByMessageIdAsync(normalizedMessageId, cancellationToken).ConfigureAwait(false);\n        if (existing == null) {\n            return OperationResult.Failure(\"queue_message_not_found\", $\"Queued message '{normalizedMessageId}' was not found.\");\n        }\n\n        await _repository.RemoveAsync(normalizedMessageId, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success($\"Queued message '{normalizedMessageId}' removed.\");\n    }\n\n    /// <inheritdoc />\n    public async Task<QueueProcessResult> ProcessAsync(CancellationToken cancellationToken = default) {\n        var observer = new CountingObserver();\n        await _processAsync(_repository, observer, cancellationToken).ConfigureAwait(false);\n        return observer.CreateResult();\n    }\n\n    private static Task DefaultProcessAsync(\n        IPendingMessageRepository repository,\n        IPendingMessageProcessorObserver observer,\n        CancellationToken cancellationToken) {\n        var processor = new PendingMessageProcessor(repository, observer: observer);\n        return processor.ProcessAsync(cancellationToken);\n    }\n\n    private static QueuedMessageSummary Map(PendingMessageRecord record) => new() {\n        MessageId = record.MessageId,\n        Provider = record.Provider.ToString(),\n        ProfileKind = MapProfileKind(record.Provider),\n        QueuedAt = record.Timestamp,\n        NextAttemptAt = record.NextAttemptAt,\n        AttemptCount = record.AttemptCount,\n        HasProviderData = record.ProviderData.Count > 0\n    };\n\n    private static QueuedMessageCompact ToCompact(QueuedMessageSummary message) => new() {\n        MessageId = message.MessageId,\n        Provider = message.Provider,\n        ProfileKind = message.ProfileKind,\n        NextAttemptAt = message.NextAttemptAt,\n        AttemptCount = message.AttemptCount,\n        IsDue = message.NextAttemptAt <= DateTimeOffset.UtcNow,\n        HasProviderData = message.HasProviderData,\n        Summary = $\"{message.MessageId} [{message.Provider}] attempts={message.AttemptCount} next={message.NextAttemptAt:O}\"\n    };\n\n    private static MailProfileKind MapProfileKind(EmailProvider provider) => provider switch {\n        EmailProvider.Graph => MailProfileKind.Graph,\n        EmailProvider.Gmail => MailProfileKind.Gmail,\n        EmailProvider.SendGrid => MailProfileKind.SendGrid,\n        EmailProvider.Mailgun => MailProfileKind.Mailgun,\n        EmailProvider.SES => MailProfileKind.Ses,\n        _ => MailProfileKind.Smtp\n    };\n\n    private sealed class CountingObserver : IPendingMessageProcessorObserver {\n        private readonly QueueProcessResult _result = new() {\n            Succeeded = true,\n            Message = \"Queue processing completed.\"\n        };\n\n        public void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason) {\n            _result.SkippedCount++;\n        }\n\n        public void MessageAttemptStarted(PendingMessageRecord record, int attempt) {\n            _result.AttemptedCount++;\n        }\n\n        public void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration) {\n            _result.SentCount++;\n        }\n\n        public void MessageFailed(\n            PendingMessageRecord record,\n            int attempt,\n            Exception exception,\n            TimeSpan duration,\n            bool willRetry,\n            TimeSpan? retryDelay) {\n            _result.FailedCount++;\n            _result.Message = exception.Message;\n        }\n\n        public void MessageDropped(\n            PendingMessageRecord record,\n            int attempt,\n            PendingMessageDropReason reason,\n            Exception? exception) {\n            _result.DroppedCount++;\n            if (exception != null) {\n                _result.Message = exception.Message;\n            }\n        }\n\n        public QueueProcessResult CreateResult() => _result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/ProfileCapabilities.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Describes the capabilities available for a given mail profile kind.\n/// </summary>\npublic sealed class ProfileCapabilities {\n    /// <summary>\n    /// Creates a new capability description.\n    /// </summary>\n    /// <param name=\"kind\">Profile kind the capabilities apply to.</param>\n    /// <param name=\"capabilities\">Supported operations.</param>\n    public ProfileCapabilities(MailProfileKind kind, MailCapability capabilities) {\n        Kind = kind;\n        Capabilities = capabilities;\n    }\n\n    /// <summary>Profile kind the capabilities apply to.</summary>\n    public MailProfileKind Kind { get; }\n\n    /// <summary>Flags describing supported operations.</summary>\n    public MailCapability Capabilities { get; }\n\n    /// <summary>\n    /// Returns <c>true</c> when all requested capabilities are supported.\n    /// </summary>\n    public bool Supports(MailCapability capability) => (Capabilities & capability) == capability;\n\n    /// <summary>\n    /// Creates a new instance using the default capability map for <paramref name=\"kind\" />.\n    /// </summary>\n    public static ProfileCapabilities CreateDefault(MailProfileKind kind) => MailCapabilityCatalog.For(kind);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/QueueProcessResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the outcome of processing queued outbound messages.\n/// </summary>\npublic sealed class QueueProcessResult : OperationResult {\n    /// <summary>Messages skipped before delivery was attempted.</summary>\n    public int SkippedCount { get; set; }\n\n    /// <summary>Messages for which a delivery attempt started.</summary>\n    public int AttemptedCount { get; set; }\n\n    /// <summary>Messages successfully sent.</summary>\n    public int SentCount { get; set; }\n\n    /// <summary>Messages whose current attempt failed.</summary>\n    public int FailedCount { get; set; }\n\n    /// <summary>Messages removed from the queue without being delivered.</summary>\n    public int DroppedCount { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/QueuedMessageCompact.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Lightweight projection of a queued outbound message for list and agent scenarios.\n/// </summary>\npublic sealed class QueuedMessageCompact {\n    /// <summary>Stable queued message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Underlying transport/provider recorded for the message.</summary>\n    public string Provider { get; set; } = string.Empty;\n\n    /// <summary>Normalized profile kind inferred from the queued provider.</summary>\n    public MailProfileKind ProfileKind { get; set; } = MailProfileKind.Unknown;\n\n    /// <summary>When the next delivery attempt is scheduled.</summary>\n    public DateTimeOffset NextAttemptAt { get; set; }\n\n    /// <summary>Number of delivery attempts already performed.</summary>\n    public int AttemptCount { get; set; }\n\n    /// <summary>Whether the queued message is currently due for processing.</summary>\n    public bool IsDue { get; set; }\n\n    /// <summary>Whether provider-specific metadata is present.</summary>\n    public bool HasProviderData { get; set; }\n\n    /// <summary>Short human-readable summary line.</summary>\n    public string Summary { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/QueuedMessageSummary.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents normalized metadata for a queued outbound message.\n/// </summary>\npublic sealed class QueuedMessageSummary {\n    /// <summary>Stable queued message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Underlying transport/provider recorded for the message.</summary>\n    public string Provider { get; set; } = string.Empty;\n\n    /// <summary>Normalized profile kind inferred from the queued provider.</summary>\n    public MailProfileKind ProfileKind { get; set; } = MailProfileKind.Unknown;\n\n    /// <summary>When the message was first queued.</summary>\n    public DateTimeOffset QueuedAt { get; set; }\n\n    /// <summary>When the next delivery attempt is scheduled.</summary>\n    public DateTimeOffset NextAttemptAt { get; set; }\n\n    /// <summary>Number of delivery attempts already performed.</summary>\n    public int AttemptCount { get; set; }\n\n    /// <summary>Whether provider-specific metadata is present.</summary>\n    public bool HasProviderData { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/RoutedMailMessageActionService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Resolves a profile and routes message actions to the matching provider handler.\n/// </summary>\npublic sealed class RoutedMailMessageActionService : IMailMessageActionService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IMailFolderAliasService? _folderAliases;\n    private readonly IReadOnlyDictionary<MailProfileKind, IMailMessageActionHandler> _handlers;\n\n    /// <summary>\n    /// Creates a new routed message-action service.\n    /// </summary>\n    public RoutedMailMessageActionService(\n        IMailProfileStore profileStore,\n        IEnumerable<IMailMessageActionHandler> handlers,\n        IMailFolderAliasService? folderAliases = null) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        _folderAliases = folderAliases;\n        if (handlers == null) {\n            throw new ArgumentNullException(nameof(handlers));\n        }\n\n        _handlers = handlers.ToDictionary(handler => handler.Kind);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.MarkMessages);\n        var confirmationFailure = ValidateReadStateConfirmation(request);\n        if (confirmationFailure != null) {\n            confirmationFailure.ProfileId = profile.Id;\n            confirmationFailure.RequestedCount = request.MessageIds.Count;\n            confirmationFailure.FailedCount = request.MessageIds.Count;\n            return confirmationFailure;\n        }\n        return await GetHandler(profile.Kind).SetReadStateAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> SetFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.MarkMessages);\n        var confirmationFailure = ValidateFlaggedStateConfirmation(request);\n        if (confirmationFailure != null) {\n            confirmationFailure.ProfileId = profile.Id;\n            confirmationFailure.RequestedCount = request.MessageIds.Count;\n            confirmationFailure.FailedCount = request.MessageIds.Count;\n            return confirmationFailure;\n        }\n        return await GetHandler(profile.Kind).SetFlaggedStateAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> MoveAsync(MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.MoveMessages);\n        var normalizedRequest = await NormalizeMoveRequestAsync(profile, request, cancellationToken).ConfigureAwait(false);\n        var confirmationFailure = ValidateMoveConfirmation(normalizedRequest);\n        if (confirmationFailure != null) {\n            confirmationFailure.ProfileId = profile.Id;\n            confirmationFailure.RequestedCount = request.MessageIds.Count;\n            confirmationFailure.FailedCount = request.MessageIds.Count;\n            return confirmationFailure;\n        }\n        return await GetHandler(profile.Kind).MoveAsync(profile, normalizedRequest, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageActionResult> DeleteAsync(DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.DeleteMessages);\n        var confirmationFailure = ValidateDeleteConfirmation(request);\n        if (confirmationFailure != null) {\n            confirmationFailure.ProfileId = profile.Id;\n            confirmationFailure.RequestedCount = request.MessageIds.Count;\n            confirmationFailure.FailedCount = request.MessageIds.Count;\n            return confirmationFailure;\n        }\n        return await GetHandler(profile.Kind).DeleteAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<MailProfile> GetProfileAsync(string profileId, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return profile ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    private IMailMessageActionHandler GetHandler(MailProfileKind kind) =>\n        _handlers.TryGetValue(kind, out var handler)\n            ? handler\n            : throw new NotSupportedException($\"No message-action handler is registered for profile kind '{kind}'.\");\n\n    private async Task<MoveMessagesRequest> NormalizeMoveRequestAsync(\n        MailProfile profile,\n        MoveMessagesRequest request,\n        CancellationToken cancellationToken) {\n        var canonicalAlias = MailFolderAliases.Canonicalize(request.DestinationFolderId);\n        if (canonicalAlias == null) {\n            return request;\n        }\n\n        if (_folderAliases == null) {\n            if (string.Equals(request.DestinationFolderId, canonicalAlias, StringComparison.Ordinal)) {\n                return request;\n            }\n\n            return CloneMoveRequest(request, canonicalAlias);\n        }\n\n        var resolution = await _folderAliases.ResolveAsync(profile.Id, request.DestinationFolderId, request.MailboxId, cancellationToken).ConfigureAwait(false);\n        if (!resolution.IsSupported) {\n            throw new NotSupportedException($\"Profile '{profile.Id}' does not support folder alias '{resolution.Alias ?? request.DestinationFolderId}'.\");\n        }\n\n        var destinationFolderId = resolution.EffectiveFolderId;\n        if (string.Equals(request.DestinationFolderId, destinationFolderId, StringComparison.Ordinal)) {\n            return request;\n        }\n\n        return CloneMoveRequest(request, destinationFolderId);\n    }\n\n    private static MoveMessagesRequest CloneMoveRequest(MoveMessagesRequest request, string destinationFolderId) =>\n        new() {\n            ProfileId = request.ProfileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            MessageIds = request.MessageIds.ToList(),\n            DestinationFolderId = destinationFolderId,\n            ConfirmationToken = request.ConfirmationToken\n        };\n\n    private static MessageActionResult? ValidateMoveConfirmation(MoveMessagesRequest request) {\n        if (string.IsNullOrWhiteSpace(request.ConfirmationToken)) {\n            return null;\n        }\n\n        var expectedToken = MessageActionConfirmationTokens.CreateMoveToken(\n            request.ProfileId,\n            request.MailboxId,\n            request.FolderId,\n            request.MessageIds,\n            request.DestinationFolderId);\n        var providedToken = request.ConfirmationToken!;\n        providedToken = providedToken.Trim();\n        if (string.Equals(expectedToken, providedToken, StringComparison.Ordinal)) {\n            return null;\n        }\n\n        return new MessageActionResult {\n            Succeeded = false,\n            Code = \"confirmation_token_mismatch\",\n            Message = \"The supplied confirmation token does not match this move action.\"\n        };\n    }\n\n    private static MessageActionResult? ValidateDeleteConfirmation(DeleteMessagesRequest request) {\n        if (string.IsNullOrWhiteSpace(request.ConfirmationToken)) {\n            return null;\n        }\n\n        var expectedToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            request.ProfileId,\n            request.MailboxId,\n            request.FolderId,\n            request.MessageIds);\n        var providedToken = request.ConfirmationToken!;\n        providedToken = providedToken.Trim();\n        if (string.Equals(expectedToken, providedToken, StringComparison.Ordinal)) {\n            return null;\n        }\n\n        return new MessageActionResult {\n            Succeeded = false,\n            Code = \"confirmation_token_mismatch\",\n            Message = \"The supplied confirmation token does not match this delete action.\"\n        };\n    }\n\n    private static MessageActionResult? ValidateReadStateConfirmation(SetReadStateRequest request) {\n        if (string.IsNullOrWhiteSpace(request.ConfirmationToken)) {\n            return null;\n        }\n\n        var expectedToken = MessageActionConfirmationTokens.CreateReadStateToken(\n            request.ProfileId,\n            request.MailboxId,\n            request.FolderId,\n            request.MessageIds,\n            request.IsRead);\n        var providedToken = request.ConfirmationToken!;\n        providedToken = providedToken.Trim();\n        if (string.Equals(expectedToken, providedToken, StringComparison.Ordinal)) {\n            return null;\n        }\n\n        return new MessageActionResult {\n            Succeeded = false,\n            Code = \"confirmation_token_mismatch\",\n            Message = \"The supplied confirmation token does not match this read-state action.\"\n        };\n    }\n\n    private static MessageActionResult? ValidateFlaggedStateConfirmation(SetFlaggedStateRequest request) {\n        if (string.IsNullOrWhiteSpace(request.ConfirmationToken)) {\n            return null;\n        }\n\n        var expectedToken = MessageActionConfirmationTokens.CreateFlaggedStateToken(\n            request.ProfileId,\n            request.MailboxId,\n            request.FolderId,\n            request.MessageIds,\n            request.IsFlagged);\n        var providedToken = request.ConfirmationToken!;\n        providedToken = providedToken.Trim();\n        if (string.Equals(expectedToken, providedToken, StringComparison.Ordinal)) {\n            return null;\n        }\n\n        return new MessageActionResult {\n            Succeeded = false,\n            Code = \"confirmation_token_mismatch\",\n            Message = \"The supplied confirmation token does not match this flagged-state action.\"\n        };\n    }\n\n    private static void EnsureCapability(MailProfile profile, MailCapability capability) {\n        if (!profile.GetCapabilities().Supports(capability)) {\n            throw new NotSupportedException($\"Profile '{profile.Id}' does not support '{capability}'.\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/RoutedMailReadService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Resolves a profile and routes read operations to the matching provider handler.\n/// </summary>\npublic sealed class RoutedMailReadService : IMailReadService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IReadOnlyDictionary<MailProfileKind, IMailReadHandler> _handlers;\n\n    /// <summary>\n    /// Creates a new routed read service.\n    /// </summary>\n    public RoutedMailReadService(IMailProfileStore profileStore, IEnumerable<IMailReadHandler> handlers) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        if (handlers == null) {\n            throw new ArgumentNullException(nameof(handlers));\n        }\n\n        _handlers = handlers.ToDictionary(handler => handler.Kind);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n        var profile = await GetProfileAsync(query.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.ListFolders);\n        return await GetHandler(profile.Kind).GetFoldersAsync(profile, query, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n        var folders = await GetFoldersAsync(query, cancellationToken).ConfigureAwait(false);\n        return folders.Select(ToCompact).ToArray();\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default) {\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.SearchMessages);\n        return await GetHandler(profile.Kind).SearchAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default) {\n        var results = await SearchAsync(request, cancellationToken).ConfigureAwait(false);\n        return results.Select(ToCompact).ToArray();\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default) {\n        var detail = await GetMessageAsync(new GetMessageRequest {\n            ProfileId = request.ProfileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            MessageId = request.MessageId,\n            IncludeRawContent = false\n        }, cancellationToken).ConfigureAwait(false);\n\n        return detail?.Attachments?.ToArray() ?? Array.Empty<AttachmentSummary>();\n    }\n\n    /// <inheritdoc />\n    public async Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var attachments = await GetAttachmentsAsync(new ListAttachmentsRequest {\n            ProfileId = request.ProfileId,\n            MailboxId = request.MailboxId,\n            FolderId = request.FolderId,\n            MessageId = request.MessageId\n        }, cancellationToken).ConfigureAwait(false);\n\n        var selectedAttachments = FilterAttachments(attachments, request).ToArray();\n        if (selectedAttachments.Length == 0) {\n            return new SaveAttachmentsResult {\n                Succeeded = false,\n                Code = \"attachments_not_found\",\n                Message = \"No attachments matched the requested filters.\",\n                ProfileId = request.ProfileId,\n                MessageId = request.MessageId,\n                MatchedCount = 0\n            };\n        }\n\n        var result = new SaveAttachmentsResult {\n            ProfileId = request.ProfileId,\n            MessageId = request.MessageId,\n            MatchedCount = selectedAttachments.Length\n        };\n\n        foreach (var attachment in selectedAttachments) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            var saveResult = await SaveAttachmentAsync(new SaveAttachmentRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageId = request.MessageId,\n                AttachmentId = attachment.Id,\n                DestinationPath = request.DestinationPath,\n                Overwrite = request.Overwrite\n            }, cancellationToken).ConfigureAwait(false);\n\n            result.AttemptedCount++;\n            if (saveResult.Succeeded) {\n                result.SavedCount++;\n            } else {\n                result.FailedCount++;\n            }\n\n            result.Results.Add(new SavedAttachmentResult {\n                Succeeded = saveResult.Succeeded,\n                Code = saveResult.Code,\n                Message = saveResult.Message,\n                AttachmentId = attachment.Id,\n                FileName = attachment.FileName,\n                ContentType = attachment.ContentType\n            });\n        }\n\n        result.Succeeded = result.FailedCount == 0 && result.SavedCount > 0;\n        result.Code = result.Succeeded ? null : \"attachment_save_failed\";\n        result.Message = result.Succeeded\n            ? $\"Saved {result.SavedCount} attachment(s).\"\n            : $\"Saved {result.SavedCount} attachment(s); {result.FailedCount} failed.\";\n        return result;\n    }\n\n    /// <inheritdoc />\n    public async Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var messageIds = request.MessageIds\n            .Where(id => !string.IsNullOrWhiteSpace(id))\n            .Select(id => id.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n        if (messageIds.Length == 0) {\n            return new SaveAttachmentsManyResult {\n                Succeeded = false,\n                Code = \"messages_not_found\",\n                Message = \"No messages were provided for attachment export.\",\n                ProfileId = request.ProfileId,\n                RequestedMessageCount = 0\n            };\n        }\n\n        var result = new SaveAttachmentsManyResult {\n            ProfileId = request.ProfileId,\n            RequestedMessageCount = messageIds.Length\n        };\n\n        foreach (var messageId in messageIds) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            var messageResult = await SaveAttachmentsAsync(new SaveAttachmentsRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageId = messageId,\n                DestinationPath = request.DestinationPath,\n                AttachmentIds = request.AttachmentIds.ToList(),\n                FileNameContains = request.FileNameContains,\n                ContentTypeContains = request.ContentTypeContains,\n                Overwrite = request.Overwrite\n            }, cancellationToken).ConfigureAwait(false);\n\n            result.AttemptedMessageCount++;\n            if (messageResult.Succeeded) {\n                result.SucceededMessageCount++;\n            } else {\n                result.FailedMessageCount++;\n            }\n\n            result.MatchedCount += messageResult.MatchedCount;\n            result.AttemptedCount += messageResult.AttemptedCount;\n            result.SavedCount += messageResult.SavedCount;\n            result.FailedCount += messageResult.FailedCount;\n            result.MessageResults.Add(messageResult);\n        }\n\n        result.Succeeded = result.FailedMessageCount == 0 && result.SavedCount > 0;\n        result.Code = result.Succeeded ? null : \"attachment_save_failed\";\n        result.Message = result.Succeeded\n            ? $\"Saved {result.SavedCount} attachment(s) across {result.SucceededMessageCount} message(s).\"\n            : $\"Saved {result.SavedCount} attachment(s) across {result.AttemptedMessageCount} message(s); {result.FailedMessageCount} message(s) failed.\";\n        return result;\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default) {\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.ReadMessages);\n        return await GetHandler(profile.Kind).GetMessageAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var results = new List<MessageDetail>();\n        foreach (var messageId in request.MessageIds.Where(id => !string.IsNullOrWhiteSpace(id))) {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            var detail = await GetMessageAsync(new GetMessageRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageId = messageId.Trim(),\n                IncludeRawContent = request.IncludeRawContent\n            }, cancellationToken).ConfigureAwait(false);\n\n            if (detail != null) {\n                results.Add(detail);\n            }\n        }\n\n        return results;\n    }\n\n    /// <inheritdoc />\n    public async Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default) {\n        var detail = await GetMessageAsync(request, cancellationToken).ConfigureAwait(false);\n        return detail == null ? null : ToCompact(detail);\n    }\n\n    /// <inheritdoc />\n    public async Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n        var details = await GetMessagesAsync(request, cancellationToken).ConfigureAwait(false);\n        return details.Select(ToCompact).ToArray();\n    }\n\n    /// <inheritdoc />\n    public async Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n        var profile = await GetProfileAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        EnsureCapability(profile, MailCapability.SaveAttachments);\n        return await GetHandler(profile.Kind).SaveAttachmentAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<MailProfile> GetProfileAsync(string profileId, CancellationToken cancellationToken) {\n        if (string.IsNullOrWhiteSpace(profileId)) {\n            throw new ArgumentException(\"Profile id is required.\", nameof(profileId));\n        }\n\n        var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return profile ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    private IMailReadHandler GetHandler(MailProfileKind kind) =>\n        _handlers.TryGetValue(kind, out var handler)\n            ? handler\n            : throw new NotSupportedException($\"No read handler is registered for profile kind '{kind}'.\");\n\n    private static void EnsureCapability(MailProfile profile, MailCapability capability) {\n        if (!profile.GetCapabilities().Supports(capability)) {\n            throw new NotSupportedException($\"Profile '{profile.Id}' does not support '{capability}'.\");\n        }\n    }\n\n    private static MessageSummaryCompact ToCompact(MessageSummary summary) => new() {\n        ProfileId = summary.ProfileId,\n        Id = summary.Id,\n        FolderId = summary.FolderId,\n        Subject = summary.Subject,\n        Preview = summary.Preview,\n        From = summary.From.Count > 0 ? FormatRecipient(summary.From[0]) : null,\n        ReceivedAt = summary.ReceivedAt,\n        IsRead = summary.IsRead,\n        HasAttachments = summary.HasAttachments,\n        Summary = $\"{summary.Id} {summary.Subject ?? \"(no subject)\"}\"\n    };\n\n    private static MessageDetailCompact ToCompact(MessageDetail detail) => new() {\n        ProfileId = detail.ProfileId,\n        Id = detail.Id,\n        Summary = detail.Summary == null ? null : ToCompact(detail.Summary),\n        TextBodyPreview = CreatePreview(detail.TextBody),\n        HtmlBodyPreview = CreatePreview(detail.HtmlBody),\n        Attachments = detail.Attachments.Select(ToCopy).ToList(),\n        HasRawContent = !string.IsNullOrWhiteSpace(detail.RawContent),\n        SummaryText = $\"{detail.Id} {detail.Summary?.Subject ?? \"(no subject)\"}\"\n    };\n\n    private static IEnumerable<AttachmentSummary> FilterAttachments(\n        IEnumerable<AttachmentSummary> attachments,\n        SaveAttachmentsRequest request) {\n        var explicitIds = new HashSet<string>(\n            request.AttachmentIds\n                .Where(id => !string.IsNullOrWhiteSpace(id))\n                .Select(id => id!.Trim()),\n            StringComparer.OrdinalIgnoreCase);\n        var fileNameContains = request.FileNameContains?.Trim();\n        var contentTypeContains = request.ContentTypeContains?.Trim();\n        var hasFileNameFilter = !string.IsNullOrWhiteSpace(fileNameContains);\n        var hasContentTypeFilter = !string.IsNullOrWhiteSpace(contentTypeContains);\n        var requiredFileName = fileNameContains ?? string.Empty;\n        var requiredContentType = contentTypeContains ?? string.Empty;\n\n        foreach (var attachment in attachments) {\n            var attachmentFileName = attachment.FileName ?? string.Empty;\n            if (explicitIds.Count > 0 &&\n                !explicitIds.Contains(attachment.Id) &&\n                (attachmentFileName.Length == 0 || !explicitIds.Contains(attachmentFileName))) {\n                continue;\n            }\n\n            if (hasFileNameFilter) {\n                if (attachmentFileName.Length == 0 ||\n                    attachmentFileName.IndexOf(requiredFileName, StringComparison.OrdinalIgnoreCase) < 0) {\n                    continue;\n                }\n            }\n\n            if (hasContentTypeFilter) {\n                var contentType = attachment.ContentType ?? string.Empty;\n                if (contentType.Length == 0) {\n                    continue;\n                }\n\n                if (contentType.IndexOf(requiredContentType, StringComparison.OrdinalIgnoreCase) < 0) {\n                    continue;\n                }\n            }\n\n            yield return attachment;\n        }\n    }\n\n    private static FolderRefCompact ToCompact(FolderRef folder) => new() {\n        ProfileId = folder.ProfileId,\n        MailboxId = folder.MailboxId,\n        Id = folder.Id,\n        DisplayName = folder.DisplayName,\n        Path = folder.Path,\n        SpecialUse = folder.SpecialUse,\n        MessageCount = folder.MessageCount,\n        UnreadCount = folder.UnreadCount,\n        Summary = $\"{folder.Id} {folder.Path ?? folder.DisplayName}\"\n    };\n\n    private static AttachmentSummary ToCopy(AttachmentSummary attachment) => new() {\n        MessageId = attachment.MessageId,\n        Id = attachment.Id,\n        FileName = attachment.FileName,\n        ContentType = attachment.ContentType,\n        SizeInBytes = attachment.SizeInBytes,\n        IsInline = attachment.IsInline,\n        ContentId = attachment.ContentId\n    };\n\n    private static string? CreatePreview(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return value;\n        }\n\n        const int maxLength = 512;\n        var trimmed = (value ?? string.Empty).Trim();\n        return trimmed.Length <= maxLength\n            ? trimmed\n            : trimmed.Substring(0, maxLength);\n    }\n\n    private static string? FormatRecipient(MessageRecipient recipient) {\n        if (!string.IsNullOrWhiteSpace(recipient.Name) && !string.IsNullOrWhiteSpace(recipient.Address)) {\n            return $\"{recipient.Name} <{recipient.Address}>\";\n        }\n\n        if (!string.IsNullOrWhiteSpace(recipient.Name)) {\n            return recipient.Name;\n        }\n\n        return string.IsNullOrWhiteSpace(recipient.Address) ? null : recipient.Address;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/RoutedMailSendService.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Resolves a profile and routes send operations to the matching provider handler.\n/// </summary>\npublic sealed class RoutedMailSendService : IMailSendService {\n    private readonly IMailProfileStore _profileStore;\n    private readonly IReadOnlyDictionary<MailProfileKind, IMailSendHandler> _handlers;\n\n    /// <summary>\n    /// Creates a new routed send service.\n    /// </summary>\n    public RoutedMailSendService(IMailProfileStore profileStore, IEnumerable<IMailSendHandler> handlers) {\n        _profileStore = profileStore ?? throw new ArgumentNullException(nameof(profileStore));\n        if (handlers == null) {\n            throw new ArgumentNullException(nameof(handlers));\n        }\n\n        _handlers = handlers.ToDictionary(handler => handler.Kind);\n    }\n\n    /// <inheritdoc />\n    public async Task<SendResult> SendAsync(SendMessageRequest request, CancellationToken cancellationToken = default) {\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n\n        var profile = await _profileStore.GetByIdAsync(request.ProfileId, cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            throw new InvalidOperationException($\"Profile '{request.ProfileId}' was not found.\");\n        }\n\n        if (!profile.GetCapabilities().Supports(MailCapability.SendMessages)) {\n            throw new NotSupportedException($\"Profile '{profile.Id}' does not support '{MailCapability.SendMessages}'.\");\n        }\n\n        if (!_handlers.TryGetValue(profile.Kind, out var handler)) {\n            throw new NotSupportedException($\"No send handler is registered for profile kind '{profile.Kind}'.\");\n        }\n\n        return await handler.SendAsync(profile, request, cancellationToken).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SaveAttachmentRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for saving an attachment.\n/// </summary>\npublic sealed class SaveAttachmentRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Provider-specific attachment identifier.</summary>\n    public string AttachmentId { get; set; } = string.Empty;\n\n    /// <summary>Destination path.</summary>\n    public string DestinationPath { get; set; } = string.Empty;\n\n    /// <summary>Whether existing files may be overwritten.</summary>\n    public bool Overwrite { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SaveAttachmentsManyRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for saving one or more attachments across multiple messages.\n/// </summary>\npublic sealed class SaveAttachmentsManyRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to inspect.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Destination path or directory.</summary>\n    public string DestinationPath { get; set; } = string.Empty;\n\n    /// <summary>Optional explicit attachment identifiers to include.</summary>\n    public List<string> AttachmentIds { get; set; } = new();\n\n    /// <summary>Optional case-insensitive file-name filter.</summary>\n    public string? FileNameContains { get; set; }\n\n    /// <summary>Optional case-insensitive content-type filter.</summary>\n    public string? ContentTypeContains { get; set; }\n\n    /// <summary>Whether existing files may be overwritten.</summary>\n    public bool Overwrite { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SaveAttachmentsManyResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the outcome of saving attachments across multiple messages.\n/// </summary>\npublic sealed class SaveAttachmentsManyResult : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Total messages requested for inspection.</summary>\n    public int RequestedMessageCount { get; set; }\n\n    /// <summary>Total message-level save operations attempted.</summary>\n    public int AttemptedMessageCount { get; set; }\n\n    /// <summary>Total messages that completed without attachment-save failures.</summary>\n    public int SucceededMessageCount { get; set; }\n\n    /// <summary>Total messages whose attachment-save operation failed.</summary>\n    public int FailedMessageCount { get; set; }\n\n    /// <summary>Total attachments matched for processing across all messages.</summary>\n    public int MatchedCount { get; set; }\n\n    /// <summary>Total attachment save attempts across all messages.</summary>\n    public int AttemptedCount { get; set; }\n\n    /// <summary>Total attachments saved successfully across all messages.</summary>\n    public int SavedCount { get; set; }\n\n    /// <summary>Total attachments whose save attempt failed across all messages.</summary>\n    public int FailedCount { get; set; }\n\n    /// <summary>Per-message outcomes.</summary>\n    public List<SaveAttachmentsResult> MessageResults { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SaveAttachmentsRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for saving one or more attachments associated with a message.\n/// </summary>\npublic sealed class SaveAttachmentsRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Destination path or directory.</summary>\n    public string DestinationPath { get; set; } = string.Empty;\n\n    /// <summary>Optional explicit attachment identifiers to include.</summary>\n    public List<string> AttachmentIds { get; set; } = new();\n\n    /// <summary>Optional case-insensitive file-name filter.</summary>\n    public string? FileNameContains { get; set; }\n\n    /// <summary>Optional case-insensitive content-type filter.</summary>\n    public string? ContentTypeContains { get; set; }\n\n    /// <summary>Whether existing files may be overwritten.</summary>\n    public bool Overwrite { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SaveAttachmentsResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the outcome of saving multiple attachments from a single message.\n/// </summary>\npublic sealed class SaveAttachmentsResult : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning message identifier.</summary>\n    public string MessageId { get; set; } = string.Empty;\n\n    /// <summary>Total attachments matched for processing.</summary>\n    public int MatchedCount { get; set; }\n\n    /// <summary>Total save attempts performed.</summary>\n    public int AttemptedCount { get; set; }\n\n    /// <summary>Total attachments saved successfully.</summary>\n    public int SavedCount { get; set; }\n\n    /// <summary>Total attachments whose save attempt failed.</summary>\n    public int FailedCount { get; set; }\n\n    /// <summary>Per-attachment outcomes.</summary>\n    public List<SavedAttachmentResult> Results { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SavedAttachmentResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Outcome of saving a single attachment as part of a batch attachment operation.\n/// </summary>\npublic sealed class SavedAttachmentResult : OperationResult {\n    /// <summary>Provider-specific attachment identifier.</summary>\n    public string AttachmentId { get; set; } = string.Empty;\n\n    /// <summary>Attachment file name.</summary>\n    public string FileName { get; set; } = string.Empty;\n\n    /// <summary>Attachment content type when known.</summary>\n    public string? ContentType { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SendMessageRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for sending or queueing a message.\n/// </summary>\npublic sealed class SendMessageRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Draft to send or queue.</summary>\n    public DraftMessage Message { get; set; } = new();\n\n    /// <summary>Whether queueing should be preferred when supported.</summary>\n    public bool PreferQueue { get; set; } = true;\n\n    /// <summary>Whether sending immediately is required.</summary>\n    public bool RequireImmediateSend { get; set; }\n\n    /// <summary>Optional scheduled send time for queue-capable implementations.</summary>\n    public DateTimeOffset? NotBefore { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SendResult.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Represents the result of a normalized send or queue operation.\n/// </summary>\npublic sealed class SendResult : OperationResult {\n    /// <summary>Profile that was used for the send attempt.</summary>\n    public string? ProfileId { get; set; }\n\n    /// <summary>Profile kind that handled the operation.</summary>\n    public MailProfileKind ProfileKind { get; set; } = MailProfileKind.Unknown;\n\n    /// <summary>Whether the message was queued instead of sent immediately.</summary>\n    public bool Queued { get; set; }\n\n    /// <summary>Queued message identifier when the message was enqueued.</summary>\n    public string? QueueMessageId { get; set; }\n\n    /// <summary>Provider-native message identifier when available.</summary>\n    public string? ProviderMessageId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SetFlaggedStateRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for setting flagged/starred state on one or more messages.\n/// </summary>\npublic sealed class SetFlaggedStateRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to update.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Desired flagged/starred state.</summary>\n    public bool IsFlagged { get; set; }\n\n    /// <summary>Optional confirmation token from a prior preview for this exact action.</summary>\n    public string? ConfirmationToken { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SetReadStateRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for setting read/unread state on one or more messages.\n/// </summary>\npublic sealed class SetReadStateRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to update.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Desired read state.</summary>\n    public bool IsRead { get; set; }\n\n    /// <summary>Optional confirmation token from a prior preview for this exact action.</summary>\n    public string? ConfirmationToken { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SmtpMailSendHandler.cs",
    "content": "using Mailozaurr;\nusing MimeKit;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Normalized SMTP send handler backed by Mailozaurr SMTP helpers.\n/// </summary>\npublic sealed class SmtpMailSendHandler : IMailSendHandler {\n    private readonly ISmtpSessionFactory _sessionFactory;\n    private readonly IDraftMimeMessageFactory _draftMimeMessageFactory;\n    private readonly IPendingMessageRepository? _pendingMessageRepository;\n    private readonly Func<Smtp, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<SmtpResult>> _sendAsync;\n\n    /// <summary>\n    /// Creates a new SMTP send handler.\n    /// </summary>\n    public SmtpMailSendHandler(\n        ISmtpSessionFactory sessionFactory,\n        IDraftMimeMessageFactory? draftMimeMessageFactory = null,\n        IPendingMessageRepository? pendingMessageRepository = null,\n        Func<Smtp, MailProfile, SendMessageRequest, MimeMessage, CancellationToken, Task<SmtpResult>>? sendAsync = null) {\n        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));\n        _draftMimeMessageFactory = draftMimeMessageFactory ?? new DraftMimeMessageFactory();\n        _pendingMessageRepository = pendingMessageRepository;\n        _sendAsync = sendAsync ?? DefaultSendAsync;\n    }\n\n    /// <inheritdoc />\n    public MailProfileKind Kind => MailProfileKind.Smtp;\n\n    /// <inheritdoc />\n    public async Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (request == null) {\n            throw new ArgumentNullException(nameof(request));\n        }\n        if (request.NotBefore.HasValue) {\n            throw new NotSupportedException(\"Scheduled SMTP sends are not yet supported by the application send handler.\");\n        }\n\n        var session = await _sessionFactory.ConnectAsync(profile, cancellationToken).ConfigureAwait(false);\n        try {\n            var message = await _draftMimeMessageFactory.CreateAsync(profile, request.Message, cancellationToken).ConfigureAwait(false);\n            var queueEnabled = _pendingMessageRepository != null && request.PreferQueue && !request.RequireImmediateSend;\n            session.PendingMessageRepository = queueEnabled ? _pendingMessageRepository : null;\n\n            var providerResult = await _sendAsync(session, profile, request, message, cancellationToken).ConfigureAwait(false);\n            if (providerResult.Status) {\n                return new SendResult {\n                    Succeeded = true,\n                    ProfileId = profile.Id,\n                    ProfileKind = profile.Kind,\n                    ProviderMessageId = providerResult.MessageId ?? message.MessageId,\n                    QueueMessageId = providerResult.MessageId ?? message.MessageId,\n                    Message = \"Message sent successfully.\"\n                };\n            }\n\n            if (queueEnabled) {\n                var queuedId = providerResult.MessageId ?? message.MessageId;\n                if (!string.IsNullOrWhiteSpace(queuedId)) {\n                    var queued = await _pendingMessageRepository!.GetByMessageIdAsync(queuedId!, cancellationToken).ConfigureAwait(false);\n                    if (queued != null) {\n                        return new SendResult {\n                            Succeeded = true,\n                            ProfileId = profile.Id,\n                            ProfileKind = profile.Kind,\n                            Queued = true,\n                            QueueMessageId = queued.MessageId,\n                            Message = $\"Message queued after send failure: {providerResult.Error ?? providerResult.Message ?? \"SMTP send failed.\"}\"\n                        };\n                    }\n                }\n            }\n\n            return new SendResult {\n                Succeeded = false,\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind,\n                ProviderMessageId = providerResult.MessageId ?? message.MessageId,\n                Message = providerResult.Error ?? providerResult.Message ?? \"SMTP send failed.\"\n            };\n        } finally {\n            SmtpSessionService.DisposeQuietly(session);\n        }\n    }\n\n    private static Task<SmtpResult> DefaultSendAsync(\n        Smtp session,\n        MailProfile profile,\n        SendMessageRequest request,\n        MimeMessage message,\n        CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n        session.Message = message;\n        return session.SendAsync(cancellationToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/SmtpSessionFactory.cs",
    "content": "using MailKit.Security;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Application;\n\n/// <summary>\n/// Creates authenticated SMTP sessions using profile settings and stored secrets.\n/// </summary>\npublic sealed class SmtpSessionFactory : ISmtpSessionFactory {\n    private readonly IMailSecretStore? _secretStore;\n    private readonly Func<SmtpSessionRequest, CancellationToken, Task<Smtp>> _connectAsync;\n\n    /// <summary>\n    /// Creates a new SMTP session factory.\n    /// </summary>\n    public SmtpSessionFactory(\n        IMailSecretStore? secretStore = null,\n        Func<SmtpSessionRequest, CancellationToken, Task<Smtp>>? connectAsync = null) {\n        _secretStore = secretStore;\n        _connectAsync = connectAsync ?? DefaultConnectAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task<Smtp> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n        if (profile == null) {\n            throw new ArgumentNullException(nameof(profile));\n        }\n        if (profile.Kind != MailProfileKind.Smtp) {\n            throw new InvalidOperationException($\"Profile '{profile.Id}' is not an SMTP profile.\");\n        }\n\n        var request = await CreateRequestAsync(profile, cancellationToken).ConfigureAwait(false);\n        return await _connectAsync(request, cancellationToken).ConfigureAwait(false);\n    }\n\n    private async Task<SmtpSessionRequest> CreateRequestAsync(MailProfile profile, CancellationToken cancellationToken) {\n        var server = RequireSetting(profile, MailProfileSettingsKeys.Server);\n        var port = GetIntSetting(profile, MailProfileSettingsKeys.Port) ?? 587;\n        var authMode = GetAuthMode(profile);\n        var userName = ResolveUserName(profile);\n        var secret = await ResolveSecretAsync(profile, authMode, cancellationToken).ConfigureAwait(false);\n\n        return new SmtpSessionRequest {\n            Server = server,\n            Port = port,\n            SecureSocketOptions = GetSecureSocketOptions(profile),\n            UseSsl = GetBoolSetting(profile, MailProfileSettingsKeys.UseSsl) ?? false,\n            TimeoutMs = GetIntSetting(profile, MailProfileSettingsKeys.Timeout) ?? 30000,\n            RetryCount = GetIntSetting(profile, MailProfileSettingsKeys.RetryCount) ?? 3,\n            RetryDelayMilliseconds = GetIntSetting(profile, MailProfileSettingsKeys.RetryDelayMilliseconds) ?? 500,\n            RetryDelayBackoff = GetDoubleSetting(profile, MailProfileSettingsKeys.RetryDelayBackoff) ?? 2.0,\n            SkipCertificateValidation = GetBoolSetting(profile, MailProfileSettingsKeys.SkipCertificateValidation) ?? false,\n            SkipCertificateRevocation = GetBoolSetting(profile, MailProfileSettingsKeys.SkipCertificateRevocation) ?? false,\n            UserName = userName,\n            Password = secret,\n            AuthMode = authMode\n        };\n    }\n\n    private async Task<string> ResolveSecretAsync(MailProfile profile, ProtocolAuthMode authMode, CancellationToken cancellationToken) {\n        var primarySecretName = authMode == ProtocolAuthMode.OAuth2\n            ? MailSecretNames.AccessToken\n            : MailSecretNames.Password;\n        var fallbackSecretName = authMode == ProtocolAuthMode.OAuth2\n            ? MailSecretNames.Password\n            : null;\n\n        var secret = _secretStore == null\n            ? null\n            : await _secretStore.GetSecretAsync(profile.Id, primarySecretName, cancellationToken).ConfigureAwait(false);\n        if (string.IsNullOrWhiteSpace(secret) && fallbackSecretName != null && _secretStore != null) {\n            secret = await _secretStore.GetSecretAsync(profile.Id, fallbackSecretName, cancellationToken).ConfigureAwait(false);\n        }\n\n        if (string.IsNullOrWhiteSpace(secret)) {\n            throw new InvalidOperationException($\"Secret '{primarySecretName}' is required for profile '{profile.Id}'.\");\n        }\n\n        return secret!;\n    }\n\n    private static async Task<Smtp> DefaultConnectAsync(SmtpSessionRequest request, CancellationToken cancellationToken) {\n        cancellationToken.ThrowIfCancellationRequested();\n\n        var smtp = new Smtp();\n        var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request, cancellationToken).ConfigureAwait(false);\n        if (result.IsSuccess) {\n            return smtp;\n        }\n\n        SmtpSessionService.DisposeQuietly(smtp);\n        throw new InvalidOperationException($\"SMTP connection/authentication failed ({result.ErrorCode}): {result.Error}\");\n    }\n\n    private static string ResolveUserName(MailProfile profile) {\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.UserName, out var userName) && !string.IsNullOrWhiteSpace(userName)) {\n            return userName.Trim();\n        }\n        if (profile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) && !string.IsNullOrWhiteSpace(mailbox)) {\n            return mailbox.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultMailbox)) {\n            return profile.DefaultMailbox!.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(profile.DefaultSender)) {\n            return profile.DefaultSender!.Trim();\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' requires '{MailProfileSettingsKeys.UserName}' or a mailbox value.\");\n    }\n\n    private static string RequireSetting(MailProfile profile, string key) {\n        if (profile.Settings.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) {\n            return value.Trim();\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' requires setting '{key}'.\");\n    }\n\n    private static ProtocolAuthMode GetAuthMode(MailProfile profile) {\n        profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthMode, out var rawMode);\n        return ProtocolAuth.ParseMode(rawMode, ProtocolAuthMode.Basic);\n    }\n\n    private static SecureSocketOptions GetSecureSocketOptions(MailProfile profile) {\n        if (!profile.Settings.TryGetValue(MailProfileSettingsKeys.SecureSocketOptions, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return SecureSocketOptions.Auto;\n        }\n\n        if (Enum.TryParse<SecureSocketOptions>(raw, ignoreCase: true, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid secure socket option '{raw}'.\");\n    }\n\n    private static int? GetIntSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (int.TryParse(raw, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid integer setting '{key}'.\");\n    }\n\n    private static double? GetDoubleSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (double.TryParse(raw, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid numeric setting '{key}'.\");\n    }\n\n    private static bool? GetBoolSetting(MailProfile profile, string key) {\n        if (!profile.Settings.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n        if (bool.TryParse(raw, out var value)) {\n            return value;\n        }\n\n        throw new InvalidOperationException($\"Profile '{profile.Id}' has invalid boolean setting '{key}'.\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/StandardMessageActionsPreview.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Aggregate dry-run result comparing standard mailbox actions for the same message selection.\n/// </summary>\npublic sealed class StandardMessageActionsPreview : OperationResult {\n    /// <summary>Owning profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Owning mailbox identifier when relevant.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Optional custom destination folder value included in the comparison.</summary>\n    public string? RequestedDestinationFolderId { get; set; }\n\n    /// <summary>Total raw message identifiers provided in the request.</summary>\n    public int RequestedCount { get; set; }\n\n    /// <summary>Total unique, non-empty message identifiers after normalization.</summary>\n    public int UniqueMessageCount { get; set; }\n\n    /// <summary>Total duplicate or empty message identifiers removed during normalization.</summary>\n    public int DuplicateOrEmptyCount { get; set; }\n\n    /// <summary>The normalized unique message identifiers that would be acted on.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>Total number of action previews included in the comparison.</summary>\n    public int IncludedActionCount { get; set; }\n\n    /// <summary>Total number of action previews that are supported and ready.</summary>\n    public int SucceededActionCount { get; set; }\n\n    /// <summary>Total number of action previews that are unsupported or otherwise blocked.</summary>\n    public int FailedActionCount { get; set; }\n\n    /// <summary>Top-level warnings detected during preview normalization.</summary>\n    public List<string> Warnings { get; set; } = new();\n\n    /// <summary>Included action previews such as archive, trash, move, and delete.</summary>\n    public List<MessageActionPreviewItem> Actions { get; set; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Application/StandardMessageActionsPreviewRequest.cs",
    "content": "namespace Mailozaurr.Application;\n\n/// <summary>\n/// Request for previewing a standard set of mailbox actions side by side.\n/// </summary>\npublic sealed class StandardMessageActionsPreviewRequest {\n    /// <summary>Profile identifier.</summary>\n    public string ProfileId { get; set; } = string.Empty;\n\n    /// <summary>Optional mailbox identifier for multi-mailbox providers.</summary>\n    public string? MailboxId { get; set; }\n\n    /// <summary>Optional source folder identifier or folder path.</summary>\n    public string? FolderId { get; set; }\n\n    /// <summary>Provider-specific message identifiers to preview.</summary>\n    public List<string> MessageIds { get; set; } = new();\n\n    /// <summary>\n    /// Optional custom destination folder identifier or alias to include as a generic move preview.\n    /// </summary>\n    public string? DestinationFolderId { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/CliArguments.cs",
    "content": "namespace Mailozaurr.Cli;\n\ninternal sealed class CliArguments {\n    public List<string> Positionals { get; } = new();\n\n    public Dictionary<string, List<string?>> Options { get; } = new(StringComparer.OrdinalIgnoreCase);\n\n    public bool ShowHelp { get; private set; }\n\n    public bool HasFlag(string name) {\n        var value = GetOption(name);\n        return value != null && value.Equals(\"true\", StringComparison.OrdinalIgnoreCase);\n    }\n\n    public string? GetOption(string name) =>\n        Options.TryGetValue(name, out var values) && values.Count > 0\n            ? values[^1]\n            : null;\n\n    public IReadOnlyList<string?> GetOptionValues(string name) =>\n        Options.TryGetValue(name, out var values)\n            ? values.AsReadOnly()\n            : Array.Empty<string?>();\n\n    public int? GetIntOption(string name) {\n        var value = GetOption(name);\n        if (string.IsNullOrWhiteSpace(value)) {\n            return null;\n        }\n        if (int.TryParse(value, out var parsed)) {\n            return parsed;\n        }\n\n        throw new InvalidOperationException($\"Option '--{name}' must be an integer.\");\n    }\n\n    public IReadOnlyList<int> GetIntOptionValues(string name) {\n        var values = GetOptionValues(name);\n        if (values.Count == 0) {\n            return Array.Empty<int>();\n        }\n\n        var parsed = new List<int>(values.Count);\n        foreach (var value in values) {\n            if (string.IsNullOrWhiteSpace(value)) {\n                continue;\n            }\n            if (int.TryParse(value, out var item)) {\n                parsed.Add(item);\n                continue;\n            }\n\n            throw new InvalidOperationException($\"Option '--{name}' must be an integer.\");\n        }\n\n        return parsed;\n    }\n\n    public static CliArguments Parse(IReadOnlyList<string> args) {\n        var result = new CliArguments();\n        for (var i = 0; i < args.Count; i++) {\n            var arg = args[i];\n            if (string.Equals(arg, \"--help\", StringComparison.OrdinalIgnoreCase) ||\n                string.Equals(arg, \"-h\", StringComparison.OrdinalIgnoreCase)) {\n                result.ShowHelp = true;\n                continue;\n            }\n\n            if (arg.StartsWith(\"--\", StringComparison.Ordinal)) {\n                var key = arg.Substring(2);\n                string? value = null;\n                if (i + 1 < args.Count && !args[i + 1].StartsWith(\"--\", StringComparison.Ordinal)) {\n                    value = args[++i];\n                }\n                if (!result.Options.TryGetValue(key, out var values)) {\n                    values = new List<string?>();\n                    result.Options[key] = values;\n                }\n\n                values.Add(value ?? \"true\");\n                continue;\n            }\n\n            result.Positionals.Add(arg);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/CliRunner.cs",
    "content": "using System.IO;\nusing System.Text.Json;\nusing Mailozaurr.Application;\nusing Mailozaurr.Cli.Mcp;\n\nnamespace Mailozaurr.Cli;\n\npublic static class CliRunner {\n    private static readonly JsonSerializerOptions JsonOptions = new() {\n        WriteIndented = true\n    };\n\n    public static async Task<int> RunAsync(\n        string[] args,\n        TextWriter output,\n        TextWriter error,\n        Func<MailApplicationOptions, MailApplicationBuilder>? builderFactory = null,\n        TextReader? input = null) {\n        if (args == null) {\n            throw new ArgumentNullException(nameof(args));\n        }\n        if (output == null) {\n            throw new ArgumentNullException(nameof(output));\n        }\n        if (error == null) {\n            throw new ArgumentNullException(nameof(error));\n        }\n\n        var parseResult = CliArguments.Parse(args);\n        input ??= TextReader.Null;\n        if (parseResult.ShowHelp || parseResult.Positionals.Count == 0) {\n            WriteHelp(output);\n            return 0;\n        }\n\n        var options = new MailApplicationOptions();\n        var profilesDir = parseResult.GetOption(\"profiles-dir\");\n        if (!string.IsNullOrWhiteSpace(profilesDir)) {\n            options.ProfileStore.DirectoryPath = profilesDir;\n        }\n        var secretsDir = parseResult.GetOption(\"secrets-dir\");\n        if (!string.IsNullOrWhiteSpace(secretsDir)) {\n            options.SecretStore.DirectoryPath = secretsDir;\n        }\n        var draftsDir = parseResult.GetOption(\"drafts-dir\");\n        if (!string.IsNullOrWhiteSpace(draftsDir)) {\n            options.DraftStore.DirectoryPath = draftsDir;\n        }\n        var planBatchesDir = parseResult.GetOption(\"plan-batches-dir\");\n        if (!string.IsNullOrWhiteSpace(planBatchesDir)) {\n            options.ActionPlanBatchStore.DirectoryPath = planBatchesDir;\n        }\n\n        var application = (builderFactory ?? (appOptions => new MailApplicationBuilder(appOptions)))\n            .Invoke(options)\n            .Build();\n\n        try {\n            return await ExecuteAsync(application, parseResult, output, error, input).ConfigureAwait(false);\n        } catch (Exception ex) {\n            await WriteExceptionAsync(error, ex, parseResult.HasFlag(\"json\")).ConfigureAwait(false);\n            return 1;\n        }\n    }\n\n    private static async Task<int> ExecuteAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error,\n        TextReader input) {\n        var command = parseResult.Positionals[0];\n        return command switch {\n            \"profile\" => await ExecuteProfileAsync(application, parseResult, output, error, input).ConfigureAwait(false),\n            \"draft\" => await ExecuteDraftAsync(application, parseResult, output, error).ConfigureAwait(false),\n            \"mail\" => await ExecuteMailAsync(application, parseResult, output, error).ConfigureAwait(false),\n            \"mcp\" => await ExecuteMcpAsync(application, parseResult, error).ConfigureAwait(false),\n            \"send\" => await ExecuteSendAsync(application, parseResult, output, error).ConfigureAwait(false),\n            \"queue\" => await ExecuteQueueAsync(application, parseResult, output, error).ConfigureAwait(false),\n            _ => await WriteUnknownCommandAsync(command, error).ConfigureAwait(false)\n        };\n    }\n\n    private static async Task<int> ExecuteProfileAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error,\n        TextReader input) {\n        if (parseResult.Positionals.Count < 2) {\n            await error.WriteLineAsync(\"Missing profile command. Use 'profile list', 'profile create', 'profile graph-bootstrap', 'profile gmail-bootstrap', 'profile graph-login', 'profile gmail-login', 'profile refresh-auth', 'profile auth-status', 'profile test', 'profile summary', 'profile capabilities', 'profile show', 'profile validate', 'profile doctor', 'profile delete', 'profile set-default', 'profile set-secret', or 'profile remove-secret'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        var subCommand = parseResult.Positionals[1];\n        var json = parseResult.HasFlag(\"json\");\n        switch (subCommand) {\n            case \"list\":\n                if (parseResult.HasFlag(\"summary\")) {\n                    if (parseResult.HasFlag(\"compact\")) {\n                        var compactOverviews = await application.ProfileOverview.GetCompactOverviewsAsync(BuildProfileOverviewQuery(parseResult)).ConfigureAwait(false);\n                        await WriteSequenceAsync(output, compactOverviews, json, value => value.Summary).ConfigureAwait(false);\n                        return compactOverviews.All(value => value.IsReady) ? 0 : 1;\n                    }\n                    var overviews = await application.ProfileOverview.GetOverviewsAsync(BuildProfileOverviewQuery(parseResult)).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, overviews, json, value => value.Summary).ConfigureAwait(false);\n                    return overviews.All(value => value.IsReady) ? 0 : 1;\n                }\n                var profiles = await application.Profiles.GetProfilesAsync().ConfigureAwait(false);\n                await WriteSequenceAsync(output, profiles, json, profile => $\"{profile.Id} [{profile.Kind}] {profile.DisplayName}\").ConfigureAwait(false);\n                return 0;\n            case \"create\":\n                var createdProfile = BuildProfile(parseResult);\n                var createResult = await application.Profiles.SaveAsync(createdProfile).ConfigureAwait(false);\n                await WriteItemAsync(output, createResult, json, value => value.Message ?? \"Profile saved.\").ConfigureAwait(false);\n                return createResult.Succeeded ? 0 : 1;\n            case \"graph-bootstrap\":\n                var graphBootstrapResult = await application.ProfileBootstrap.SaveGraphProfileAsync(new GraphProfileBootstrapRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    DisplayName = RequireOption(parseResult, \"name\"),\n                    Description = parseResult.GetOption(\"description\"),\n                    Mailbox = RequireOption(parseResult, \"mailbox\"),\n                    DefaultSender = parseResult.GetOption(\"default-sender\"),\n                    IsDefault = parseResult.HasFlag(\"is-default\"),\n                    ClientId = parseResult.GetOption(\"client-id\"),\n                    TenantId = parseResult.GetOption(\"tenant-id\"),\n                    ClientSecret = await ResolveSensitiveOptionAsync(parseResult, \"client-secret\", input).ConfigureAwait(false),\n                    ClientSecretReference = parseResult.GetOption(\"client-secret-ref\"),\n                    AccessToken = await ResolveSensitiveOptionAsync(parseResult, \"access-token\", input).ConfigureAwait(false),\n                    AccessTokenReference = parseResult.GetOption(\"access-token-ref\"),\n                    CertificatePath = parseResult.GetOption(\"certificate-path\"),\n                    CertificatePassword = await ResolveSensitiveOptionAsync(parseResult, \"certificate-password\", input).ConfigureAwait(false),\n                    CertificatePasswordReference = parseResult.GetOption(\"certificate-password-ref\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, graphBootstrapResult, json, value => value.Message ?? \"Graph profile saved.\").ConfigureAwait(false);\n                return graphBootstrapResult.Succeeded ? 0 : 1;\n            case \"gmail-bootstrap\":\n                var gmailBootstrapResult = await application.ProfileBootstrap.SaveGmailProfileAsync(new GmailProfileBootstrapRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    DisplayName = RequireOption(parseResult, \"name\"),\n                    Description = parseResult.GetOption(\"description\"),\n                    Mailbox = parseResult.GetOption(\"mailbox\"),\n                    DefaultSender = parseResult.GetOption(\"default-sender\"),\n                    IsDefault = parseResult.HasFlag(\"is-default\"),\n                    ClientId = parseResult.GetOption(\"client-id\"),\n                    ClientSecret = await ResolveSensitiveOptionAsync(parseResult, \"client-secret\", input).ConfigureAwait(false),\n                    ClientSecretReference = parseResult.GetOption(\"client-secret-ref\"),\n                    RefreshToken = await ResolveSensitiveOptionAsync(parseResult, \"refresh-token\", input).ConfigureAwait(false),\n                    RefreshTokenReference = parseResult.GetOption(\"refresh-token-ref\"),\n                    AccessToken = await ResolveSensitiveOptionAsync(parseResult, \"access-token\", input).ConfigureAwait(false),\n                    AccessTokenReference = parseResult.GetOption(\"access-token-ref\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, gmailBootstrapResult, json, value => value.Message ?? \"Gmail profile saved.\").ConfigureAwait(false);\n                return gmailBootstrapResult.Succeeded ? 0 : 1;\n            case \"graph-login\":\n                var graphLoginResult = await application.ProfileAuth.LoginGraphAsync(new GraphProfileLoginRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    Login = parseResult.GetOption(\"login\"),\n                    Mailbox = parseResult.GetOption(\"mailbox\"),\n                    ClientId = parseResult.GetOption(\"client-id\"),\n                    TenantId = parseResult.GetOption(\"tenant-id\"),\n                    RedirectUri = parseResult.GetOption(\"redirect-uri\"),\n                    Scopes = parseResult.GetOptionValues(\"scope\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!)\n                        .ToArray()\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, graphLoginResult, json, value => value.Message ?? (value.Succeeded ? \"Graph login completed.\" : \"Graph login failed.\")).ConfigureAwait(false);\n                return graphLoginResult.Succeeded ? 0 : 1;\n            case \"gmail-login\":\n                var gmailLoginResult = await application.ProfileAuth.LoginGmailAsync(new GmailProfileLoginRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    GmailAccount = parseResult.GetOption(\"mailbox\"),\n                    ClientId = parseResult.GetOption(\"client-id\"),\n                    ClientSecret = await ResolveSensitiveOptionAsync(parseResult, \"client-secret\", input).ConfigureAwait(false),\n                    ClientSecretReference = parseResult.GetOption(\"client-secret-ref\"),\n                    Scopes = parseResult.GetOptionValues(\"scope\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!)\n                        .ToArray()\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, gmailLoginResult, json, value => value.Message ?? (value.Succeeded ? \"Gmail login completed.\" : \"Gmail login failed.\")).ConfigureAwait(false);\n                return gmailLoginResult.Succeeded ? 0 : 1;\n            case \"refresh-auth\":\n                var refreshAuthResult = await application.ProfileAuth.RefreshAsync(RequireOption(parseResult, \"profile\")).ConfigureAwait(false);\n                await WriteItemAsync(output, refreshAuthResult, json, value => value.Message ?? (value.Succeeded ? \"Profile auth refreshed.\" : \"Profile auth refresh failed.\")).ConfigureAwait(false);\n                return refreshAuthResult.Succeeded ? 0 : 1;\n            case \"auth-status\":\n                var authStatusProfileId = RequireOption(parseResult, \"profile\");\n                var authStatus = await application.ProfileAuth.GetStatusAsync(authStatusProfileId).ConfigureAwait(false);\n                if (authStatus == null) {\n                    await error.WriteLineAsync($\"Profile '{authStatusProfileId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, authStatus, json, value => value.Summary).ConfigureAwait(false);\n                return 0;\n            case \"test\":\n                var connectionResult = await application.ProfileConnections.TestAsync(\n                    RequireOption(parseResult, \"profile\"),\n                    ParseConnectionTestScope(parseResult.GetOption(\"scope\"))).ConfigureAwait(false);\n                await WriteItemAsync(output, connectionResult, json, value => value.Message ?? (value.Succeeded ? \"Profile connection succeeded.\" : \"Profile connection failed.\")).ConfigureAwait(false);\n                return connectionResult.Succeeded ? 0 : 1;\n            case \"summary\":\n                var summaryProfileId = RequireOption(parseResult, \"profile\");\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactOverview = await application.ProfileOverview.GetCompactOverviewAsync(summaryProfileId).ConfigureAwait(false);\n                    if (compactOverview == null) {\n                        await error.WriteLineAsync($\"Profile '{summaryProfileId}' was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, compactOverview, json, value => value.Summary).ConfigureAwait(false);\n                    return compactOverview.IsReady ? 0 : 1;\n                }\n                var overview = await application.ProfileOverview.GetOverviewAsync(summaryProfileId).ConfigureAwait(false);\n                if (overview == null) {\n                    await error.WriteLineAsync($\"Profile '{summaryProfileId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, overview, json, value => value.Summary).ConfigureAwait(false);\n                return overview.IsReady ? 0 : 1;\n            case \"show\":\n                var profileId = RequireOption(parseResult, \"profile\");\n                var profile = await application.Profiles.GetProfileAsync(profileId).ConfigureAwait(false);\n                if (profile == null) {\n                    await error.WriteLineAsync($\"Profile '{profileId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, profile, json, p => $\"{p.Id} [{p.Kind}] {p.DisplayName}\").ConfigureAwait(false);\n                return 0;\n            case \"capabilities\":\n                var capabilitiesProfileId = RequireOption(parseResult, \"profile\");\n                var capabilities = await application.Profiles.GetCapabilitiesAsync(capabilitiesProfileId).ConfigureAwait(false);\n                if (capabilities == null) {\n                    await error.WriteLineAsync($\"Profile '{capabilitiesProfileId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, capabilities, json, value => $\"{value.Kind}: {value.Capabilities}\").ConfigureAwait(false);\n                return 0;\n            case \"validate\":\n                var validateId = RequireOption(parseResult, \"profile\");\n                var profileToValidate = await application.Profiles.GetProfileAsync(validateId).ConfigureAwait(false);\n                if (profileToValidate == null) {\n                    await error.WriteLineAsync($\"Profile '{validateId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                var result = await application.Profiles.ValidateAsync(profileToValidate).ConfigureAwait(false);\n                await WriteItemAsync(output, result, json, r => r.Message ?? (r.Succeeded ? \"Profile is valid.\" : \"Profile is invalid.\")).ConfigureAwait(false);\n                return result.Succeeded ? 0 : 1;\n            case \"doctor\":\n                var doctorResult = await application.Profiles.DiagnoseAsync(RequireOption(parseResult, \"profile\")).ConfigureAwait(false);\n                await WriteItemAsync(output, doctorResult, json, value => value.Message ?? (value.Succeeded ? \"Profile is ready.\" : \"Profile is not ready.\")).ConfigureAwait(false);\n                return doctorResult.Succeeded ? 0 : 1;\n            case \"delete\":\n                var deleteResult = await application.Profiles.DeleteAsync(RequireOption(parseResult, \"profile\")).ConfigureAwait(false);\n                await WriteItemAsync(output, deleteResult, json, value => value.Message ?? \"Profile deleted.\").ConfigureAwait(false);\n                return deleteResult.Succeeded ? 0 : 1;\n            case \"set-default\":\n                var setDefaultResult = await application.Profiles.SetDefaultAsync(RequireOption(parseResult, \"profile\")).ConfigureAwait(false);\n                await WriteItemAsync(output, setDefaultResult, json, value => value.Message ?? \"Default profile updated.\").ConfigureAwait(false);\n                return setDefaultResult.Succeeded ? 0 : 1;\n            case \"set-secret\":\n                var secretValue = await ResolveSensitiveOptionAsync(parseResult, \"value\", input).ConfigureAwait(false);\n                var secretReference = parseResult.GetOption(\"value-ref\");\n                if (secretValue == null && string.IsNullOrWhiteSpace(secretReference)) {\n                    throw new InvalidOperationException(\n                        \"Missing required option '--value'. You can also use '--value-env <name>', '--value-stdin', or '--value-ref <profile-id:secret-name>'.\");\n                }\n                var setSecretResult = await application.ProfileSecrets.SetSecretAsync(\n                    RequireOption(parseResult, \"profile\"),\n                    RequireOption(parseResult, \"name\"),\n                    secretValue,\n                    secretReference).ConfigureAwait(false);\n                await WriteItemAsync(output, setSecretResult, json, value => value.Message ?? \"Secret saved.\").ConfigureAwait(false);\n                return setSecretResult.Succeeded ? 0 : 1;\n            case \"remove-secret\":\n                var removeSecretResult = await application.ProfileSecrets.RemoveSecretAsync(\n                    RequireOption(parseResult, \"profile\"),\n                    RequireOption(parseResult, \"name\")).ConfigureAwait(false);\n                await WriteItemAsync(output, removeSecretResult, json, value => value.Message ?? \"Secret removed.\").ConfigureAwait(false);\n                return removeSecretResult.Succeeded ? 0 : 1;\n            default:\n                await error.WriteLineAsync($\"Unknown profile command '{subCommand}'.\").ConfigureAwait(false);\n                return 1;\n        }\n    }\n\n    private static async Task<int> ExecuteMailAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error) {\n        if (parseResult.Positionals.Count < 2) {\n            await error.WriteLineAsync(\"Missing mail command. Use 'mail folders', 'mail folder-aliases', 'mail resolve-folder', 'mail list-plan-batches', 'mail show-plan-batch', 'mail import-plan-batch', 'mail export-plan-batch', 'mail create-common-plan-batch', 'mail clone-plan-batch', 'mail preview-transform-plan-batch', 'mail transform-plan-batch', 'mail add-plan-to-batch', 'mail add-plan-file-to-batch', 'mail replace-plan-in-batch', 'mail replace-plan-file-in-batch', 'mail remove-plan-from-batch', 'mail delete-plan-batch', 'mail execute-plan-batch-stored', 'mail plan-action', 'mail export-plan', 'mail show-plan', 'mail execute-plan', 'mail execute-plan-file', 'mail execute-plan-batch', 'mail preview-all', 'mail preview-mark-read', 'mail preview-flag', 'mail preview-actions', 'mail preview-move', 'mail preview-delete', 'mail search', 'mail attachments', 'mail get', 'mail get-many', 'mail mark-read', 'mail flag', 'mail archive', 'mail trash', 'mail move', 'mail delete', 'mail save-attachment', 'mail save-attachments', or 'mail save-attachments-many'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        var subCommand = parseResult.Positionals[1];\n        var json = parseResult.HasFlag(\"json\");\n        switch (subCommand) {\n            case \"folders\":\n                var folderQuery = new MailFolderQuery {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    ParentFolderId = parseResult.GetOption(\"parent-folder\"),\n                    RootOnly = parseResult.HasFlag(\"root-only\")\n                };\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactFolders = await application.Read.GetFoldersCompactAsync(folderQuery).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactFolders, json, folder =>\n                        folder.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var folders = await application.Read.GetFoldersAsync(folderQuery).ConfigureAwait(false);\n                await WriteSequenceAsync(output, folders, json, folder =>\n                    $\"{folder.Id} {folder.Path ?? folder.DisplayName}\").ConfigureAwait(false);\n                return 0;\n            case \"folder-aliases\":\n                var aliases = await application.FolderAliases.GetAliasesAsync(\n                    RequireOption(parseResult, \"profile\"),\n                    parseResult.GetOption(\"mailbox\")).ConfigureAwait(false);\n                await WriteSequenceAsync(output, aliases, json, value => value.Summary).ConfigureAwait(false);\n                return 0;\n            case \"resolve-folder\":\n                var resolution = await application.FolderAliases.ResolveAsync(\n                    RequireOption(parseResult, \"profile\"),\n                    RequireOption(parseResult, \"target-folder\"),\n                    parseResult.GetOption(\"mailbox\")).ConfigureAwait(false);\n                await WriteItemAsync(output, resolution, json, value => value.Summary).ConfigureAwait(false);\n                return resolution.IsSupported ? 0 : 1;\n            case \"list-plan-batches\":\n                var batchQuery = BuildMessageActionPlanBatchQuery(parseResult);\n                if (parseResult.HasFlag(\"summary\")) {\n                    var summaryBatches = await application.MessageActionPlanRegistry.GetBatchesSummaryAsync(batchQuery).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, summaryBatches, json, value => value.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactBatches = await application.MessageActionPlanRegistry.GetBatchesCompactAsync(batchQuery).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactBatches, json, value => value.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var batches = await application.MessageActionPlanRegistry.GetBatchesAsync(batchQuery).ConfigureAwait(false);\n                await WriteSequenceAsync(output, batches, json, value => $\"{value.Id} [{value.Plans.Count} plan(s)] {value.Name}\").ConfigureAwait(false);\n                return 0;\n            case \"show-plan-batch\":\n                var batchId = RequireOption(parseResult, \"batch\");\n                if (parseResult.HasFlag(\"summary\")) {\n                    var summaryBatch = await application.MessageActionPlanRegistry.GetBatchSummaryAsync(batchId).ConfigureAwait(false);\n                    if (summaryBatch == null) {\n                        await error.WriteLineAsync($\"Action plan batch '{batchId}' was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, summaryBatch, json, value => value.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactBatch = await application.MessageActionPlanRegistry.GetBatchCompactAsync(batchId).ConfigureAwait(false);\n                    if (compactBatch == null) {\n                        await error.WriteLineAsync($\"Action plan batch '{batchId}' was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, compactBatch, json, value => value.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var batch = await application.MessageActionPlanRegistry.GetBatchAsync(batchId).ConfigureAwait(false);\n                if (batch == null) {\n                    await error.WriteLineAsync($\"Action plan batch '{batchId}' was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, batch, json, value => $\"{value.Id} [{value.Plans.Count} plan(s)] {value.Name}\").ConfigureAwait(false);\n                return 0;\n            case \"import-plan-batch\":\n                var importBatchResult = await application.MessageActionPlanRegistry.ImportAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    RequireOption(parseResult, \"name\"),\n                    RequireOption(parseResult, \"path\"),\n                    parseResult.GetOption(\"description\")).ConfigureAwait(false);\n                await WriteItemAsync(output, importBatchResult, json, value => value.Message ?? \"Action plan batch imported.\").ConfigureAwait(false);\n                return importBatchResult.Succeeded ? 0 : 1;\n            case \"export-plan-batch\":\n                var exportBatchResult = await application.MessageActionPlanRegistry.ExportAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                await WriteItemAsync(output, exportBatchResult, json, value => value.Message ?? \"Action plan batch exported.\").ConfigureAwait(false);\n                return exportBatchResult.Succeeded ? 0 : 1;\n            case \"create-common-plan-batch\":\n                var createCommonBatchResult = await application.MessageActionPlanRegistry.CreateCommonBatchAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    RequireOption(parseResult, \"name\"),\n                    BuildCommonActionsPreviewRequest(parseResult),\n                    parseResult.GetOptionValues(\"action\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToArray(),\n                    parseResult.GetOption(\"description\")).ConfigureAwait(false);\n                await WriteItemAsync(output, createCommonBatchResult, json, value => value.Message ?? \"Action plan batch created.\").ConfigureAwait(false);\n                return createCommonBatchResult.Succeeded ? 0 : 1;\n            case \"clone-plan-batch\":\n                var cloneBatchResult = await application.MessageActionPlanRegistry.CloneAsync(\n                    RequireOption(parseResult, \"source-batch\"),\n                    RequireOption(parseResult, \"target-batch\"),\n                    RequireOption(parseResult, \"name\"),\n                    parseResult.GetOption(\"description\")).ConfigureAwait(false);\n                await WriteItemAsync(output, cloneBatchResult, json, value => value.Message ?? \"Action plan batch cloned.\").ConfigureAwait(false);\n                return cloneBatchResult.Succeeded ? 0 : 1;\n            case \"preview-transform-plan-batch\":\n                var previewTransformBatchResult = await application.MessageActionPlanRegistry.PreviewTransformCloneAsync(\n                    RequireOption(parseResult, \"source-batch\"),\n                    BuildMessageActionPlanBatchTransformRequest(parseResult)).ConfigureAwait(false);\n                await WriteItemAsync(output, previewTransformBatchResult, json, value => value.Message ?? \"Action plan batch transform preview generated.\").ConfigureAwait(false);\n                return previewTransformBatchResult.Succeeded ? 0 : 1;\n            case \"transform-plan-batch\":\n                var transformBatchResult = await application.MessageActionPlanRegistry.TransformCloneAsync(\n                    RequireOption(parseResult, \"source-batch\"),\n                    RequireOption(parseResult, \"target-batch\"),\n                    RequireOption(parseResult, \"name\"),\n                    BuildMessageActionPlanBatchTransformRequest(parseResult),\n                    parseResult.GetOption(\"description\")).ConfigureAwait(false);\n                await WriteItemAsync(output, transformBatchResult, json, value => value.Message ?? \"Action plan batch transformed and cloned.\").ConfigureAwait(false);\n                return transformBatchResult.Succeeded ? 0 : 1;\n            case \"add-plan-to-batch\":\n                var planToAppend = await application.MessageActionPlans.CreatePlanAsync(BuildMessageActionExecutionPlanRequest(parseResult)).ConfigureAwait(false);\n                var appendPlanResult = await application.MessageActionPlanRegistry.AppendPlanAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    planToAppend).ConfigureAwait(false);\n                await WriteItemAsync(output, appendPlanResult, json, value => value.Message ?? \"Action plan appended.\").ConfigureAwait(false);\n                return appendPlanResult.Succeeded ? 0 : 1;\n            case \"add-plan-file-to-batch\":\n                var appendImportedPlanResult = await application.MessageActionPlanRegistry.AppendImportedPlanAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                await WriteItemAsync(output, appendImportedPlanResult, json, value => value.Message ?? \"Imported action plan appended.\").ConfigureAwait(false);\n                return appendImportedPlanResult.Succeeded ? 0 : 1;\n            case \"replace-plan-in-batch\":\n                var replaceIndex = parseResult.GetIntOption(\"index\");\n                if (!replaceIndex.HasValue) {\n                    throw new InvalidOperationException(\"Missing required option '--index'.\");\n                }\n                var replacementPlan = await application.MessageActionPlans.CreatePlanAsync(BuildMessageActionExecutionPlanRequest(parseResult)).ConfigureAwait(false);\n                var replacePlanResult = await application.MessageActionPlanRegistry.ReplacePlanAtAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    replaceIndex.Value,\n                    replacementPlan).ConfigureAwait(false);\n                await WriteItemAsync(output, replacePlanResult, json, value => value.Message ?? \"Action plan replaced.\").ConfigureAwait(false);\n                return replacePlanResult.Succeeded ? 0 : 1;\n            case \"replace-plan-file-in-batch\":\n                var replaceImportedIndex = parseResult.GetIntOption(\"index\");\n                if (!replaceImportedIndex.HasValue) {\n                    throw new InvalidOperationException(\"Missing required option '--index'.\");\n                }\n                var replaceImportedPlanResult = await application.MessageActionPlanRegistry.ReplaceImportedPlanAtAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    replaceImportedIndex.Value,\n                    RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                await WriteItemAsync(output, replaceImportedPlanResult, json, value => value.Message ?? \"Imported action plan replaced.\").ConfigureAwait(false);\n                return replaceImportedPlanResult.Succeeded ? 0 : 1;\n            case \"remove-plan-from-batch\":\n                var index = parseResult.GetIntOption(\"index\");\n                if (!index.HasValue) {\n                    throw new InvalidOperationException(\"Missing required option '--index'.\");\n                }\n                var removePlanResult = await application.MessageActionPlanRegistry.RemovePlanAtAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    index.Value).ConfigureAwait(false);\n                await WriteItemAsync(output, removePlanResult, json, value => value.Message ?? \"Action plan removed from batch.\").ConfigureAwait(false);\n                return removePlanResult.Succeeded ? 0 : 1;\n            case \"delete-plan-batch\":\n                var deleteBatchResult = await application.MessageActionPlanRegistry.DeleteAsync(RequireOption(parseResult, \"batch\")).ConfigureAwait(false);\n                await WriteItemAsync(output, deleteBatchResult, json, value => value.Message ?? \"Action plan batch deleted.\").ConfigureAwait(false);\n                return deleteBatchResult.Succeeded ? 0 : 1;\n            case \"execute-plan-batch-stored\":\n                var storedBatchExecutionResult = await application.MessageActionPlanRegistry.ExecuteAsync(\n                    RequireOption(parseResult, \"batch\"),\n                    continueOnError: !parseResult.HasFlag(\"stop-on-error\")).ConfigureAwait(false);\n                await WriteItemAsync(output, storedBatchExecutionResult, json, value => value.Message ?? \"Stored action plan batch executed.\").ConfigureAwait(false);\n                return storedBatchExecutionResult.Succeeded ? 0 : 1;\n            case \"preview-move\":\n                var preview = await application.MessageActionPreview.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = RequireOption(parseResult, \"target-folder\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, preview, json, value => value.Message ?? \"Move preview ready.\").ConfigureAwait(false);\n                return preview.Succeeded ? 0 : 1;\n            case \"preview-actions\":\n                var standardPreview = await application.MessageActionPreview.PreviewStandardActionsAsync(new StandardMessageActionsPreviewRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = parseResult.GetOption(\"target-folder\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, standardPreview, json, value => value.Message ?? \"Action previews ready.\").ConfigureAwait(false);\n                return standardPreview.Succeeded ? 0 : 1;\n            case \"preview-all\":\n                var commonPreview = await application.MessageActionPreview.PreviewCommonActionsAsync(new CommonMessageActionsPreviewRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = parseResult.GetOption(\"target-folder\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, commonPreview, json, value => value.Message ?? \"Common action previews ready.\").ConfigureAwait(false);\n                return commonPreview.Succeeded ? 0 : 1;\n            case \"plan-action\":\n                var actionPlan = await application.MessageActionPlans.CreatePlanAsync(BuildMessageActionExecutionPlanRequest(parseResult)).ConfigureAwait(false);\n                await WriteItemAsync(output, actionPlan, json, value => value.Message ?? \"Action plan ready.\").ConfigureAwait(false);\n                return actionPlan.Succeeded ? 0 : 1;\n            case \"export-plan\":\n                var exportPlan = await application.MessageActionPlans.CreatePlanAsync(BuildMessageActionExecutionPlanRequest(parseResult)).ConfigureAwait(false);\n                await application.MessageActionPlanExchange.SaveAsync(RequireOption(parseResult, \"path\"), exportPlan).ConfigureAwait(false);\n                var exportPlanResult = OperationResult.Success(\"Action plan exported.\");\n                await WriteItemAsync(output, exportPlanResult, json, value => value.Message ?? \"Action plan exported.\").ConfigureAwait(false);\n                return exportPlan.Succeeded ? 0 : 1;\n            case \"show-plan\":\n                var loadedPlan = await application.MessageActionPlanExchange.LoadAsync(RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                await WriteItemAsync(output, loadedPlan, json, value => value.Message ?? \"Action plan loaded.\").ConfigureAwait(false);\n                return loadedPlan.Succeeded ? 0 : 1;\n            case \"execute-plan\":\n                var executionPlan = await application.MessageActionPlans.CreatePlanAsync(BuildMessageActionExecutionPlanRequest(parseResult)).ConfigureAwait(false);\n                var executePlanResult = await application.MessageActionBatch.ExecuteAsync(new[] { executionPlan }).ConfigureAwait(false);\n                await WriteItemAsync(output, executePlanResult, json, value => value.Message ?? \"Action plan executed.\").ConfigureAwait(false);\n                return executePlanResult.Succeeded ? 0 : 1;\n            case \"execute-plan-file\":\n                var storedPlan = await application.MessageActionPlanExchange.LoadAsync(RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                var executePlanFileResult = await application.MessageActionBatch.ExecuteAsync(new[] { storedPlan }).ConfigureAwait(false);\n                await WriteItemAsync(output, executePlanFileResult, json, value => value.Message ?? \"Stored action plan executed.\").ConfigureAwait(false);\n                return executePlanFileResult.Succeeded ? 0 : 1;\n            case \"execute-plan-batch\":\n                var plans = await application.MessageActionPlanExchange.LoadBatchAsync(RequireOption(parseResult, \"path\")).ConfigureAwait(false);\n                var batchExecutionResult = await application.MessageActionBatch.ExecuteAsync(\n                    plans,\n                    continueOnError: !parseResult.HasFlag(\"stop-on-error\")).ConfigureAwait(false);\n                await WriteItemAsync(output, batchExecutionResult, json, value => value.Message ?? \"Action batch executed.\").ConfigureAwait(false);\n                return batchExecutionResult.Succeeded ? 0 : 1;\n            case \"preview-delete\":\n                var deletePreview = await application.MessageActionPreview.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList()\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, deletePreview, json, value => value.Message ?? \"Delete preview ready.\").ConfigureAwait(false);\n                return deletePreview.Succeeded ? 0 : 1;\n            case \"preview-mark-read\":\n                var readPreview = await application.MessageActionPreview.PreviewReadStateAsync(new SetReadStateRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    IsRead = !parseResult.HasFlag(\"unread\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, readPreview, json, value => value.Message ?? \"Read-state preview ready.\").ConfigureAwait(false);\n                return readPreview.Succeeded ? 0 : 1;\n            case \"preview-flag\":\n                var flagPreview = await application.MessageActionPreview.PreviewFlaggedStateAsync(new SetFlaggedStateRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    IsFlagged = !parseResult.HasFlag(\"unflag\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, flagPreview, json, value => value.Message ?? \"Flag preview ready.\").ConfigureAwait(false);\n                return flagPreview.Succeeded ? 0 : 1;\n            case \"search\":\n                var searchRequest = new MailSearchRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    QueryText = parseResult.GetOption(\"query\"),\n                    SubjectContains = parseResult.GetOption(\"subject\"),\n                    FromContains = parseResult.GetOption(\"from\"),\n                    ToContains = parseResult.GetOption(\"to\"),\n                    HasAttachments = parseResult.HasFlag(\"has-attachments\"),\n                    Limit = parseResult.GetIntOption(\"limit\")\n                };\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactMessages = await application.Read.SearchCompactAsync(searchRequest).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactMessages, json, message =>\n                        message.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var messages = await application.Read.SearchAsync(searchRequest).ConfigureAwait(false);\n                await WriteSequenceAsync(output, messages, json, message =>\n                    $\"{message.Id} {message.Subject ?? \"(no subject)\"}\").ConfigureAwait(false);\n                return 0;\n            case \"get\":\n                var getRequest = new GetMessageRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageId = RequireOption(parseResult, \"message-id\"),\n                    IncludeRawContent = parseResult.HasFlag(\"include-raw\")\n                };\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactDetail = await application.Read.GetMessageCompactAsync(getRequest).ConfigureAwait(false);\n                    if (compactDetail == null) {\n                        await error.WriteLineAsync(\"Message was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, compactDetail, json, value => value.SummaryText).ConfigureAwait(false);\n                    return 0;\n                }\n                var detail = await application.Read.GetMessageAsync(getRequest).ConfigureAwait(false);\n                if (detail == null) {\n                    await error.WriteLineAsync(\"Message was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, detail, json, value => value.Summary?.Subject ?? value.Id).ConfigureAwait(false);\n                return 0;\n            case \"get-many\":\n                var getMessagesRequest = new GetMessagesRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    IncludeRawContent = parseResult.HasFlag(\"include-raw\")\n                };\n                if (getMessagesRequest.MessageIds.Count == 0) {\n                    throw new InvalidOperationException(\"Missing required option '--message-id'.\");\n                }\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactDetails = await application.Read.GetMessagesCompactAsync(getMessagesRequest).ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactDetails, json, value => value.SummaryText).ConfigureAwait(false);\n                    return 0;\n                }\n                var details = await application.Read.GetMessagesAsync(getMessagesRequest).ConfigureAwait(false);\n                await WriteSequenceAsync(output, details, json, value => value.Summary?.Subject ?? value.Id).ConfigureAwait(false);\n                return 0;\n            case \"mark-read\":\n                var markReadResult = await application.MessageActions.SetReadStateAsync(new SetReadStateRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    IsRead = !parseResult.HasFlag(\"unread\"),\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, markReadResult, json, value =>\n                    value.Message ?? $\"Updated {value.SucceededCount} message(s).\").ConfigureAwait(false);\n                return markReadResult.Succeeded ? 0 : 1;\n            case \"flag\":\n                var flagResult = await application.MessageActions.SetFlaggedStateAsync(new SetFlaggedStateRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    IsFlagged = !parseResult.HasFlag(\"unflag\"),\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, flagResult, json, value =>\n                    value.Message ?? $\"Updated {value.SucceededCount} message(s).\").ConfigureAwait(false);\n                return flagResult.Succeeded ? 0 : 1;\n            case \"move\":\n                var moveResult = await application.MessageActions.MoveAsync(new MoveMessagesRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = RequireOption(parseResult, \"target-folder\"),\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, moveResult, json, value =>\n                    value.Message ?? $\"Moved {value.SucceededCount} message(s).\").ConfigureAwait(false);\n                return moveResult.Succeeded ? 0 : 1;\n            case \"archive\":\n                var archiveResult = await application.MessageActions.MoveAsync(new MoveMessagesRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = MailFolderAliases.Archive,\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, archiveResult, json, value =>\n                    value.Message ?? $\"Archived {value.SucceededCount} message(s).\").ConfigureAwait(false);\n                return archiveResult.Succeeded ? 0 : 1;\n            case \"trash\":\n                var trashResult = await application.MessageActions.MoveAsync(new MoveMessagesRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationFolderId = MailFolderAliases.Trash,\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, trashResult, json, value =>\n                    value.Message ?? $\"Moved {value.SucceededCount} message(s) to trash.\").ConfigureAwait(false);\n                return trashResult.Succeeded ? 0 : 1;\n            case \"delete\":\n                var deleteResult = await application.MessageActions.DeleteAsync(new DeleteMessagesRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, deleteResult, json, value =>\n                    value.Message ?? $\"Deleted {value.SucceededCount} message(s).\").ConfigureAwait(false);\n                return deleteResult.Succeeded ? 0 : 1;\n            case \"attachments\":\n                var attachments = await application.Read.GetAttachmentsAsync(new ListAttachmentsRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageId = RequireOption(parseResult, \"message-id\")\n                }).ConfigureAwait(false);\n                await WriteSequenceAsync(output, attachments, json, attachment =>\n                    $\"{attachment.Id} {attachment.FileName}\").ConfigureAwait(false);\n                return 0;\n            case \"save-attachment\":\n                var saveRequest = new SaveAttachmentRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageId = RequireOption(parseResult, \"message-id\"),\n                    AttachmentId = RequireOption(parseResult, \"attachment-id\"),\n                    DestinationPath = RequireOption(parseResult, \"path\"),\n                    Overwrite = parseResult.HasFlag(\"overwrite\")\n                };\n                var saveResult = await application.Read.SaveAttachmentAsync(saveRequest).ConfigureAwait(false);\n                await WriteItemAsync(output, saveResult, json, value => value.Message ?? \"Attachment saved.\").ConfigureAwait(false);\n                return saveResult.Succeeded ? 0 : 1;\n            case \"save-attachments\":\n                var saveAttachmentsResult = await application.Read.SaveAttachmentsAsync(new SaveAttachmentsRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageId = RequireOption(parseResult, \"message-id\"),\n                    DestinationPath = RequireOption(parseResult, \"path\"),\n                    AttachmentIds = parseResult.GetOptionValues(\"attachment-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    FileNameContains = parseResult.GetOption(\"name-contains\"),\n                    ContentTypeContains = parseResult.GetOption(\"content-type\"),\n                    Overwrite = parseResult.HasFlag(\"overwrite\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, saveAttachmentsResult, json, value =>\n                    value.Message ?? $\"Saved {value.SavedCount} attachment(s).\").ConfigureAwait(false);\n                return saveAttachmentsResult.Succeeded ? 0 : 1;\n            case \"save-attachments-many\":\n                var saveAttachmentsManyResult = await application.Read.SaveAttachmentsManyAsync(new SaveAttachmentsManyRequest {\n                    ProfileId = RequireOption(parseResult, \"profile\"),\n                    MailboxId = parseResult.GetOption(\"mailbox\"),\n                    FolderId = parseResult.GetOption(\"folder\"),\n                    MessageIds = parseResult.GetOptionValues(\"message-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    DestinationPath = RequireOption(parseResult, \"path\"),\n                    AttachmentIds = parseResult.GetOptionValues(\"attachment-id\")\n                        .Where(value => !string.IsNullOrWhiteSpace(value))\n                        .Select(value => value!.Trim())\n                        .ToList(),\n                    FileNameContains = parseResult.GetOption(\"name-contains\"),\n                    ContentTypeContains = parseResult.GetOption(\"content-type\"),\n                    Overwrite = parseResult.HasFlag(\"overwrite\")\n                }).ConfigureAwait(false);\n                await WriteItemAsync(output, saveAttachmentsManyResult, json, value =>\n                    value.Message ?? $\"Saved {value.SavedCount} attachment(s) across {value.AttemptedMessageCount} message(s).\").ConfigureAwait(false);\n                return saveAttachmentsManyResult.Succeeded ? 0 : 1;\n            default:\n                await error.WriteLineAsync($\"Unknown mail command '{subCommand}'.\").ConfigureAwait(false);\n                return 1;\n        }\n    }\n\n    private static async Task<int> ExecuteDraftAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error) {\n        if (parseResult.Positionals.Count < 2) {\n            await error.WriteLineAsync(\"Missing draft command. Use 'draft list', 'draft save', 'draft get', 'draft delete', or 'draft export'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        var subCommand = parseResult.Positionals[1];\n        var json = parseResult.HasFlag(\"json\");\n        switch (subCommand) {\n            case \"list\":\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactDrafts = await application.Drafts.GetDraftsCompactAsync().ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactDrafts, json, draft =>\n                        draft.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var drafts = await application.Drafts.GetDraftsAsync().ConfigureAwait(false);\n                await WriteSequenceAsync(output, drafts, json, draft =>\n                    $\"{draft.Id} [{draft.Message.ProfileId}] {draft.Name}\").ConfigureAwait(false);\n                return 0;\n            case \"save\":\n                var draft = await BuildMailDraftAsync(application, parseResult).ConfigureAwait(false);\n                var saveResult = await application.Drafts.SaveAsync(draft).ConfigureAwait(false);\n                await WriteItemAsync(output, saveResult, json, value => value.Message ?? \"Draft saved.\").ConfigureAwait(false);\n                return saveResult.Succeeded ? 0 : 1;\n            case \"get\":\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactStoredDraft = await application.Drafts.GetDraftCompactAsync(RequireOption(parseResult, \"draft\")).ConfigureAwait(false);\n                    if (compactStoredDraft == null) {\n                        await error.WriteLineAsync(\"Draft was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, compactStoredDraft, json, value => value.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var storedDraft = await application.Drafts.GetDraftAsync(RequireOption(parseResult, \"draft\")).ConfigureAwait(false);\n                if (storedDraft == null) {\n                    await error.WriteLineAsync(\"Draft was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, storedDraft, json, value => $\"{value.Id} [{value.Message.ProfileId}] {value.Name}\").ConfigureAwait(false);\n                return 0;\n            case \"delete\":\n                var deleteResult = await application.Drafts.DeleteAsync(RequireOption(parseResult, \"draft\")).ConfigureAwait(false);\n                await WriteItemAsync(output, deleteResult, json, value => value.Message ?? \"Draft deleted.\").ConfigureAwait(false);\n                return deleteResult.Succeeded ? 0 : 1;\n            case \"export\":\n                var draftToExport = await application.Drafts.GetDraftAsync(RequireOption(parseResult, \"draft\")).ConfigureAwait(false);\n                if (draftToExport == null) {\n                    await error.WriteLineAsync(\"Draft was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await application.DraftExchange.SaveAsync(RequireOption(parseResult, \"path\"), draftToExport).ConfigureAwait(false);\n                await WriteItemAsync(output, OperationResult.Success(\"Draft exported.\"), json, value => value.Message ?? \"Draft exported.\").ConfigureAwait(false);\n                return 0;\n            default:\n                await error.WriteLineAsync($\"Unknown draft command '{subCommand}'.\").ConfigureAwait(false);\n                return 1;\n        }\n    }\n\n    private static async Task<int> ExecuteQueueAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error) {\n        if (parseResult.Positionals.Count < 2) {\n            await error.WriteLineAsync(\"Missing queue command. Use 'queue list', 'queue get', 'queue remove', or 'queue process'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        var subCommand = parseResult.Positionals[1];\n        var json = parseResult.HasFlag(\"json\");\n        switch (subCommand) {\n            case \"list\":\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactQueuedMessages = await application.Queue.ListCompactAsync().ConfigureAwait(false);\n                    await WriteSequenceAsync(output, compactQueuedMessages, json, message => message.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var queuedMessages = await application.Queue.ListAsync().ConfigureAwait(false);\n                await WriteSequenceAsync(output, queuedMessages, json, message =>\n                    $\"{message.MessageId} [{message.Provider}] attempts={message.AttemptCount} next={message.NextAttemptAt:O}\").ConfigureAwait(false);\n                return 0;\n            case \"get\":\n                if (parseResult.HasFlag(\"compact\")) {\n                    var compactQueuedMessage = await application.Queue.GetCompactAsync(RequireOption(parseResult, \"message-id\")).ConfigureAwait(false);\n                    if (compactQueuedMessage == null) {\n                        await error.WriteLineAsync(\"Queued message was not found.\").ConfigureAwait(false);\n                        return 1;\n                    }\n                    await WriteItemAsync(output, compactQueuedMessage, json, message => message.Summary).ConfigureAwait(false);\n                    return 0;\n                }\n                var queuedMessage = await application.Queue.GetAsync(RequireOption(parseResult, \"message-id\")).ConfigureAwait(false);\n                if (queuedMessage == null) {\n                    await error.WriteLineAsync(\"Queued message was not found.\").ConfigureAwait(false);\n                    return 1;\n                }\n                await WriteItemAsync(output, queuedMessage, json, message =>\n                    $\"{message.MessageId} [{message.Provider}] attempts={message.AttemptCount}\").ConfigureAwait(false);\n                return 0;\n            case \"remove\":\n                var removeResult = await application.Queue.RemoveAsync(RequireOption(parseResult, \"message-id\")).ConfigureAwait(false);\n                await WriteItemAsync(output, removeResult, json, value => value.Message ?? \"Queued message removed.\").ConfigureAwait(false);\n                return removeResult.Succeeded ? 0 : 1;\n            case \"process\":\n                var processResult = await application.Queue.ProcessAsync().ConfigureAwait(false);\n                await WriteItemAsync(output, processResult, json, value =>\n                    value.Message ?? $\"Processed queue: attempted={value.AttemptedCount}, sent={value.SentCount}, failed={value.FailedCount}.\").ConfigureAwait(false);\n                return processResult.Succeeded ? 0 : 1;\n            default:\n                await error.WriteLineAsync($\"Unknown queue command '{subCommand}'.\").ConfigureAwait(false);\n                return 1;\n        }\n    }\n\n    private static async Task<int> ExecuteSendAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter output,\n        TextWriter error) {\n        var json = parseResult.HasFlag(\"json\");\n        var request = await BuildSendRequestAsync(application, parseResult).ConfigureAwait(false);\n        var result = await application.Send.SendAsync(request).ConfigureAwait(false);\n        await WriteItemAsync(output, result, json, value => value.Message ?? (value.Queued\n            ? $\"Message queued: {value.QueueMessageId ?? \"(unknown-id)\"}\"\n            : $\"Message sent: {value.ProviderMessageId ?? \"(provider-id unavailable)\"}\")).ConfigureAwait(false);\n        return result.Succeeded ? 0 : 1;\n    }\n\n    private static async Task<int> ExecuteMcpAsync(\n        MailApplication application,\n        CliArguments parseResult,\n        TextWriter error) {\n        if (parseResult.Positionals.Count < 2) {\n            await error.WriteLineAsync(\"Missing mcp command. Use 'mcp serve'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        var subCommand = parseResult.Positionals[1];\n        if (!string.Equals(subCommand, \"serve\", StringComparison.OrdinalIgnoreCase)) {\n            await error.WriteLineAsync($\"Unknown mcp command '{subCommand}'.\").ConfigureAwait(false);\n            return 1;\n        }\n\n        await McpServerHost.RunAsync(application).ConfigureAwait(false);\n        return 0;\n    }\n\n    private static async Task<int> WriteUnknownCommandAsync(string command, TextWriter error) {\n        await error.WriteLineAsync($\"Unknown command '{command}'.\").ConfigureAwait(false);\n        return 1;\n    }\n\n    private static string RequireOption(CliArguments parseResult, string name) {\n        var value = parseResult.GetOption(name);\n        if (!string.IsNullOrWhiteSpace(value)) {\n            return value!;\n        }\n\n        throw new InvalidOperationException($\"Missing required option '--{name}'.\");\n    }\n\n    private static MailProfile BuildProfile(CliArguments parseResult) {\n        var profile = new MailProfile {\n            Id = RequireOption(parseResult, \"profile\"),\n            DisplayName = RequireOption(parseResult, \"name\"),\n            Kind = MailProfileKindParser.Parse(RequireOption(parseResult, \"kind\")),\n            Description = parseResult.GetOption(\"description\"),\n            DefaultSender = parseResult.GetOption(\"default-sender\"),\n            DefaultMailbox = parseResult.GetOption(\"default-mailbox\"),\n            IsDefault = parseResult.HasFlag(\"is-default\")\n        };\n\n        foreach (var setting in parseResult.GetOptionValues(\"setting\")) {\n            if (string.IsNullOrWhiteSpace(setting)) {\n                continue;\n            }\n\n            var separatorIndex = setting.IndexOf('=');\n            if (separatorIndex <= 0 || separatorIndex == setting.Length - 1) {\n                throw new InvalidOperationException(\"Option '--setting' must use the format key=value.\");\n            }\n\n            var key = setting[..separatorIndex].Trim();\n            var value = setting[(separatorIndex + 1)..].Trim();\n            if (key.Length == 0 || value.Length == 0) {\n                throw new InvalidOperationException(\"Option '--setting' must use the format key=value.\");\n            }\n\n            profile.Settings[key] = value;\n        }\n\n        return profile;\n    }\n\n    private static MessageActionExecutionPlanRequest BuildMessageActionExecutionPlanRequest(CliArguments parseResult) => new() {\n        Action = RequireOption(parseResult, \"action\"),\n        ProfileId = RequireOption(parseResult, \"profile\"),\n        MailboxId = parseResult.GetOption(\"mailbox\"),\n        FolderId = parseResult.GetOption(\"folder\"),\n        MessageIds = parseResult.GetOptionValues(\"message-id\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .ToList(),\n        DestinationFolderId = parseResult.GetOption(\"target-folder\"),\n        ConfirmationToken = parseResult.GetOption(\"confirm-token\")\n    };\n\n    private static CommonMessageActionsPreviewRequest BuildCommonActionsPreviewRequest(CliArguments parseResult) => new() {\n        ProfileId = RequireOption(parseResult, \"profile\"),\n        MailboxId = parseResult.GetOption(\"mailbox\"),\n        FolderId = parseResult.GetOption(\"folder\"),\n        MessageIds = parseResult.GetOptionValues(\"message-id\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .ToList(),\n        DestinationFolderId = parseResult.GetOption(\"target-folder\")\n    };\n\n    private static MessageActionPlanBatchTransformRequest BuildMessageActionPlanBatchTransformRequest(CliArguments parseResult) => new() {\n        PlanIndexes = parseResult.GetIntOptionValues(\"index\").ToList(),\n        PlanNames = parseResult.GetOptionValues(\"plan-name\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .ToList(),\n        ProfileId = parseResult.GetOption(\"target-profile\"),\n        MailboxId = parseResult.GetOption(\"mailbox\"),\n        FolderId = parseResult.GetOption(\"folder\"),\n        DestinationFolderId = parseResult.GetOption(\"target-folder\")\n    };\n\n    private static MailMessageActionPlanBatchQuery? BuildMessageActionPlanBatchQuery(CliArguments parseResult) {\n        var planNames = parseResult.GetOptionValues(\"plan-name\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var profileIds = parseResult.GetOptionValues(\"profile\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var actions = parseResult.GetOptionValues(\"action\")\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value!.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var hasExplicitSort = parseResult.GetOptionValues(\"sort\").Count > 0;\n        var sortBy = ParseBatchSortBy(parseResult.GetOption(\"sort\"));\n        var descending = parseResult.HasFlag(\"desc\");\n\n        if (planNames.Count == 0 && profileIds.Count == 0 && actions.Count == 0 && !hasExplicitSort && sortBy == MailMessageActionPlanBatchSortBy.Id && !descending) {\n            return null;\n        }\n\n        return new MailMessageActionPlanBatchQuery {\n            PlanNames = planNames,\n            ProfileIds = profileIds,\n            Actions = actions,\n            SortBy = sortBy,\n            Descending = descending\n        };\n    }\n\n    private static MailMessageActionPlanBatchSortBy ParseBatchSortBy(string? rawSortBy) {\n        if (string.IsNullOrWhiteSpace(rawSortBy)) {\n            return MailMessageActionPlanBatchSortBy.Id;\n        }\n\n        return rawSortBy.Trim().ToLowerInvariant() switch {\n            \"id\" => MailMessageActionPlanBatchSortBy.Id,\n            \"name\" => MailMessageActionPlanBatchSortBy.Name,\n            \"plans\" or \"plan-count\" => MailMessageActionPlanBatchSortBy.PlanCount,\n            \"ready\" or \"ready-count\" => MailMessageActionPlanBatchSortBy.ReadyPlanCount,\n            \"updated\" or \"updated-at\" => MailMessageActionPlanBatchSortBy.UpdatedAt,\n            \"actions\" or \"action-types\" => MailMessageActionPlanBatchSortBy.ActionTypeCount,\n            _ => throw new InvalidOperationException($\"Unsupported batch sort '{rawSortBy}'.\")\n        };\n    }\n\n    private static async Task<SendMessageRequest> BuildSendRequestAsync(MailApplication application, CliArguments parseResult) {\n        if (application == null) {\n            throw new ArgumentNullException(nameof(application));\n        }\n\n        var existingDraftId = parseResult.GetOption(\"draft\");\n        var draftFilePath = parseResult.GetOption(\"file\");\n        if (!string.IsNullOrWhiteSpace(existingDraftId) && !string.IsNullOrWhiteSpace(draftFilePath)) {\n            throw new InvalidOperationException(\"Options '--draft' and '--file' cannot be used together.\");\n        }\n\n        var sendNow = parseResult.HasFlag(\"send-now\");\n        if (!string.IsNullOrWhiteSpace(draftFilePath)) {\n            var importedDraft = await application.DraftExchange.LoadAsync(draftFilePath!).ConfigureAwait(false);\n            return CreateSendRequestFromDraft(importedDraft.Message, sendNow);\n        }\n\n        if (!string.IsNullOrWhiteSpace(existingDraftId)) {\n            var existingDraft = await application.Drafts.GetDraftAsync(existingDraftId!).ConfigureAwait(false);\n            if (existingDraft == null) {\n                throw new InvalidOperationException($\"Draft '{existingDraftId}' was not found.\");\n            }\n\n            return CreateSendRequestFromDraft(existingDraft.Message, sendNow);\n        }\n\n        var profileId = RequireOption(parseResult, \"profile\");\n        var draft = BuildDraftMessage(parseResult, profileId);\n\n        return new SendMessageRequest {\n            ProfileId = profileId,\n            Message = draft,\n            PreferQueue = !sendNow,\n            RequireImmediateSend = sendNow\n        };\n    }\n\n    private static async Task<MailDraft> BuildMailDraftAsync(MailApplication application, CliArguments parseResult) {\n        if (application == null) {\n            throw new ArgumentNullException(nameof(application));\n        }\n\n        var filePath = parseResult.GetOption(\"file\");\n        if (!string.IsNullOrWhiteSpace(filePath)) {\n            var importedDraft = await application.DraftExchange.LoadAsync(filePath!).ConfigureAwait(false);\n            var overrideDraftId = parseResult.GetOption(\"draft\");\n            var overrideName = parseResult.GetOption(\"name\");\n            if (!string.IsNullOrWhiteSpace(overrideDraftId)) {\n                importedDraft.Id = overrideDraftId!.Trim();\n            }\n            if (!string.IsNullOrWhiteSpace(overrideName)) {\n                importedDraft.Name = overrideName!.Trim();\n            }\n\n            return importedDraft;\n        }\n\n        var draftId = RequireOption(parseResult, \"draft\");\n        var draftName = RequireOption(parseResult, \"name\");\n\n        return new MailDraft {\n            Id = draftId,\n            Name = draftName,\n            Message = BuildDraftMessage(parseResult, RequireOption(parseResult, \"profile\"))\n        };\n    }\n\n    private static SendMessageRequest CreateSendRequestFromDraft(DraftMessage draft, bool sendNow) =>\n        new() {\n            ProfileId = draft.ProfileId,\n            Message = CloneDraftMessage(draft),\n            PreferQueue = !sendNow,\n            RequireImmediateSend = sendNow\n        };\n\n    private static MessageRecipient ToRecipient(string value) => new() {\n        Address = value.Trim()\n    };\n\n    private static DraftMessage BuildDraftMessage(CliArguments parseResult, string profileId) {\n        var draft = new DraftMessage {\n            ProfileId = profileId,\n            Subject = parseResult.GetOption(\"subject\"),\n            TextBody = parseResult.GetOption(\"text\"),\n            HtmlBody = parseResult.GetOption(\"html\")\n        };\n\n        var from = parseResult.GetOption(\"from\");\n        if (!string.IsNullOrWhiteSpace(from)) {\n            draft.From = ToRecipient(from!);\n        }\n\n        draft.To.AddRange(parseResult.GetOptionValues(\"to\").Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => ToRecipient(value!)));\n        draft.Cc.AddRange(parseResult.GetOptionValues(\"cc\").Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => ToRecipient(value!)));\n        draft.Bcc.AddRange(parseResult.GetOptionValues(\"bcc\").Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => ToRecipient(value!)));\n        draft.ReplyTo.AddRange(parseResult.GetOptionValues(\"reply-to\").Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => ToRecipient(value!)));\n\n        foreach (var header in parseResult.GetOptionValues(\"header\")) {\n            if (string.IsNullOrWhiteSpace(header)) {\n                continue;\n            }\n\n            var separatorIndex = header.IndexOf('=');\n            if (separatorIndex <= 0 || separatorIndex == header.Length - 1) {\n                throw new InvalidOperationException(\"Option '--header' must use the format key=value.\");\n            }\n\n            draft.Headers[header[..separatorIndex].Trim()] = header[(separatorIndex + 1)..].Trim();\n        }\n\n        foreach (var attachmentPath in parseResult.GetOptionValues(\"attachment\")) {\n            if (string.IsNullOrWhiteSpace(attachmentPath)) {\n                continue;\n            }\n\n            draft.Attachments.Add(new DraftAttachment {\n                Path = attachmentPath!.Trim()\n            });\n        }\n\n        return draft;\n    }\n\n    private static DraftMessage CloneDraftMessage(DraftMessage draft) => new() {\n        ProfileId = draft.ProfileId,\n        From = draft.From == null ? null : new MessageRecipient {\n            Name = draft.From.Name,\n            Address = draft.From.Address\n        },\n        To = draft.To.Select(ToRecipientCopy).ToList(),\n        Cc = draft.Cc.Select(ToRecipientCopy).ToList(),\n        Bcc = draft.Bcc.Select(ToRecipientCopy).ToList(),\n        ReplyTo = draft.ReplyTo.Select(ToRecipientCopy).ToList(),\n        Subject = draft.Subject,\n        TextBody = draft.TextBody,\n        HtmlBody = draft.HtmlBody,\n        Headers = new Dictionary<string, string>(draft.Headers, StringComparer.OrdinalIgnoreCase),\n        Attachments = draft.Attachments.Select(attachment => new DraftAttachment {\n            Path = attachment.Path,\n            FileName = attachment.FileName,\n            ContentType = attachment.ContentType,\n            IsInline = attachment.IsInline,\n            ContentId = attachment.ContentId\n        }).ToList()\n    };\n\n    private static MessageRecipient ToRecipientCopy(MessageRecipient recipient) => new() {\n        Name = recipient.Name,\n        Address = recipient.Address\n    };\n\n    private static async Task WriteItemAsync<T>(\n        TextWriter output,\n        T value,\n        bool json,\n        Func<T, string> formatter) {\n        if (json) {\n            await output.WriteLineAsync(JsonSerializer.Serialize(value, JsonOptions)).ConfigureAwait(false);\n            return;\n        }\n\n        await output.WriteLineAsync(formatter(value)).ConfigureAwait(false);\n    }\n\n    private static async Task WriteSequenceAsync<T>(\n        TextWriter output,\n        IReadOnlyList<T> values,\n        bool json,\n        Func<T, string> formatter) {\n        if (json) {\n            await output.WriteLineAsync(JsonSerializer.Serialize(values, JsonOptions)).ConfigureAwait(false);\n            return;\n        }\n\n        foreach (var value in values) {\n            await output.WriteLineAsync(formatter(value)).ConfigureAwait(false);\n        }\n    }\n\n    private static async Task<string?> ResolveSensitiveOptionAsync(\n        CliArguments parseResult,\n        string optionName,\n        TextReader input,\n        bool required = false) {\n        var directValue = parseResult.GetOption(optionName);\n        var envName = parseResult.GetOption($\"{optionName}-env\");\n        var stdinRequested = parseResult.HasFlag($\"{optionName}-stdin\");\n\n        var sourceCount = 0;\n        if (directValue != null) {\n            sourceCount++;\n        }\n        if (!string.IsNullOrWhiteSpace(envName)) {\n            sourceCount++;\n        }\n        if (stdinRequested) {\n            sourceCount++;\n        }\n\n        if (sourceCount > 1) {\n            throw new InvalidOperationException($\"Option '--{optionName}' accepts only one secret source at a time.\");\n        }\n\n        string? resolvedValue = directValue;\n        if (!string.IsNullOrWhiteSpace(envName)) {\n            resolvedValue = Environment.GetEnvironmentVariable(envName!);\n            if (resolvedValue == null) {\n                throw new InvalidOperationException($\"Environment variable '{envName}' was not found for '--{optionName}-env'.\");\n            }\n        } else if (stdinRequested) {\n            resolvedValue = await input.ReadToEndAsync().ConfigureAwait(false);\n            resolvedValue = resolvedValue.TrimEnd('\\r', '\\n');\n        }\n\n        if (required && resolvedValue == null) {\n            throw new InvalidOperationException(\n                $\"Missing required option '--{optionName}'. You can also use '--{optionName}-env <name>' or '--{optionName}-stdin'.\");\n        }\n\n        return resolvedValue;\n    }\n\n    private static async Task WriteExceptionAsync(TextWriter error, Exception exception, bool json) {\n        if (json) {\n            var payload = new CliErrorEnvelope {\n                Error = new CliError {\n                    Type = exception.GetType().Name,\n                    Message = exception.Message\n                }\n            };\n            await error.WriteLineAsync(JsonSerializer.Serialize(payload, JsonOptions)).ConfigureAwait(false);\n            return;\n        }\n\n        await error.WriteLineAsync(exception.Message).ConfigureAwait(false);\n    }\n\n    private static void WriteHelp(TextWriter output) {\n        output.WriteLine(\"Mailozaurr CLI\");\n        output.WriteLine();\n        output.WriteLine(\"Commands:\");\n        output.WriteLine(\"  profile list [--summary] [--compact] [--kind <kind>] [--ready-only] [--can-read] [--can-send] [--default-only] [--sort <id|kind|readiness>] [--desc] [--json]\");\n        output.WriteLine(\"  profile create --profile <id> --kind <kind> --name <display-name> [--description <text>] [--default-sender <email>] [--default-mailbox <value>] [--is-default] [--setting <key=value>] [--json]\");\n        output.WriteLine(\"  profile graph-bootstrap --profile <id> --name <display-name> --mailbox <address> [--description <text>] [--default-sender <email>] [--is-default] [--client-id <id>] [--tenant-id <id>] [--client-secret <secret>|--client-secret-env <name>|--client-secret-stdin|--client-secret-ref <profile-id:secret-name>] [--access-token <token>|--access-token-env <name>|--access-token-stdin|--access-token-ref <profile-id:secret-name>] [--certificate-path <path>] [--certificate-password <secret>|--certificate-password-env <name>|--certificate-password-stdin|--certificate-password-ref <profile-id:secret-name>] [--json]\");\n        output.WriteLine(\"  profile gmail-bootstrap --profile <id> --name <display-name> [--mailbox <address|me>] [--description <text>] [--default-sender <email>] [--is-default] [--client-id <id>] [--client-secret <secret>|--client-secret-env <name>|--client-secret-stdin|--client-secret-ref <profile-id:secret-name>] [--refresh-token <token>|--refresh-token-env <name>|--refresh-token-stdin|--refresh-token-ref <profile-id:secret-name>] [--access-token <token>|--access-token-env <name>|--access-token-stdin|--access-token-ref <profile-id:secret-name>] [--json]\");\n        output.WriteLine(\"  profile graph-login --profile <id> [--login <upn>] [--mailbox <address>] [--client-id <id>] [--tenant-id <id>] [--redirect-uri <uri>] [--scope <value>] [--scope <value>] [--json]\");\n        output.WriteLine(\"  profile gmail-login --profile <id> [--mailbox <address>] [--client-id <id>] [--client-secret <secret>|--client-secret-env <name>|--client-secret-stdin|--client-secret-ref <profile-id:secret-name>] [--scope <value>] [--scope <value>] [--json]\");\n        output.WriteLine(\"  profile refresh-auth --profile <id> [--json]\");\n        output.WriteLine(\"  profile auth-status --profile <id> [--json]\");\n        output.WriteLine(\"  profile test --profile <id> [--scope <auto|auth|mailbox|send>] [--json]\");\n        output.WriteLine(\"  profile summary --profile <id> [--compact] [--json]\");\n        output.WriteLine(\"  profile capabilities --profile <id> [--json]\");\n        output.WriteLine(\"  profile show --profile <id> [--json]\");\n        output.WriteLine(\"  profile validate --profile <id> [--json]\");\n        output.WriteLine(\"  profile doctor --profile <id> [--json]\");\n        output.WriteLine(\"  profile delete --profile <id> [--json]\");\n        output.WriteLine(\"  profile set-default --profile <id> [--json]\");\n        output.WriteLine(\"  profile set-secret --profile <id> --name <secret-name> [--value <secret-value>|--value-env <name>|--value-stdin|--value-ref <profile-id:secret-name>] [--json]\");\n        output.WriteLine(\"  profile remove-secret --profile <id> --name <secret-name> [--json]\");\n        output.WriteLine(\"  draft list [--compact] [--json]\");\n        output.WriteLine(\"  draft save --file <path> [--draft <id>] [--name <display-name>] [--json]\");\n        output.WriteLine(\"  draft save --draft <id> --name <display-name> --profile <id> --to <address> [--to <address>] [--cc <address>] [--bcc <address>] [--reply-to <address>] [--from <address>] [--subject <text>] [--text <text>] [--html <html>] [--attachment <path>] [--header <key=value>] [--json]\");\n        output.WriteLine(\"  draft get --draft <id> [--compact] [--json]\");\n        output.WriteLine(\"  draft delete --draft <id> [--json]\");\n        output.WriteLine(\"  draft export --draft <id> --path <file> [--json]\");\n        output.WriteLine(\"  mail folders --profile <id> [--mailbox <id>] [--parent-folder <id>] [--root-only] [--compact] [--json]\");\n        output.WriteLine(\"  mail folder-aliases --profile <id> [--mailbox <id>] [--json]\");\n        output.WriteLine(\"  mail resolve-folder --profile <id> --target-folder <id> [--mailbox <id>] [--json]\");\n        output.WriteLine(\"  mail list-plan-batches [--summary|--compact] [--plan-name <name>] [--plan-name <name>] [--profile <id>] [--profile <id>] [--action <name>] [--action <name>] [--sort <id|name|plans|ready|updated|actions>] [--desc] [--json]\");\n        output.WriteLine(\"  mail show-plan-batch --batch <id> [--summary|--compact] [--json]\");\n        output.WriteLine(\"  mail import-plan-batch --batch <id> --name <display-name> --path <file> [--description <text>] [--json]\");\n        output.WriteLine(\"  mail export-plan-batch --batch <id> --path <file> [--json]\");\n        output.WriteLine(\"  mail create-common-plan-batch --batch <id> --name <display-name> --profile <id> --message-id <id> [--message-id <id>] [--action <name>] [--action <name>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--description <text>] [--json]\");\n        output.WriteLine(\"  mail clone-plan-batch --source-batch <id> --target-batch <id> --name <display-name> [--description <text>] [--json]\");\n        output.WriteLine(\"  mail preview-transform-plan-batch --source-batch <id> [--index <n>] [--index <n>] [--plan-name <name>] [--plan-name <name>] [--target-profile <id>] [--mailbox <id>] [--folder <name>] [--target-folder <id>] [--json]\");\n        output.WriteLine(\"  mail transform-plan-batch --source-batch <id> --target-batch <id> --name <display-name> [--index <n>] [--index <n>] [--plan-name <name>] [--plan-name <name>] [--target-profile <id>] [--mailbox <id>] [--folder <name>] [--target-folder <id>] [--description <text>] [--json]\");\n        output.WriteLine(\"  mail add-plan-to-batch --batch <id> --action <mark-read|mark-unread|flag|unflag|archive|trash|move|delete> --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail add-plan-file-to-batch --batch <id> --path <file> [--json]\");\n        output.WriteLine(\"  mail replace-plan-in-batch --batch <id> --index <n> --action <mark-read|mark-unread|flag|unflag|archive|trash|move|delete> --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail replace-plan-file-in-batch --batch <id> --index <n> --path <file> [--json]\");\n        output.WriteLine(\"  mail remove-plan-from-batch --batch <id> --index <n> [--json]\");\n        output.WriteLine(\"  mail delete-plan-batch --batch <id> [--json]\");\n        output.WriteLine(\"  mail execute-plan-batch-stored --batch <id> [--stop-on-error] [--json]\");\n        output.WriteLine(\"  mail plan-action --action <mark-read|mark-unread|flag|unflag|archive|trash|move|delete> --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail export-plan --action <mark-read|mark-unread|flag|unflag|archive|trash|move|delete> --profile <id> --message-id <id> [--message-id <id>] --path <file> [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail show-plan --path <file> [--json]\");\n        output.WriteLine(\"  mail execute-plan --action <mark-read|mark-unread|flag|unflag|archive|trash|move|delete> --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail execute-plan-file --path <file> [--json]\");\n        output.WriteLine(\"  mail execute-plan-batch --path <file> [--stop-on-error] [--json]\");\n        output.WriteLine(\"  mail preview-all --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--json]\");\n        output.WriteLine(\"  mail preview-mark-read --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--unread] [--json]\");\n        output.WriteLine(\"  mail preview-flag --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--unflag] [--json]\");\n        output.WriteLine(\"  mail preview-actions --profile <id> --message-id <id> [--message-id <id>] [--target-folder <id>] [--mailbox <id>] [--folder <name>] [--json]\");\n        output.WriteLine(\"  mail preview-move --profile <id> --message-id <id> [--message-id <id>] --target-folder <id> [--mailbox <id>] [--folder <name>] [--json]\");\n        output.WriteLine(\"  mail preview-delete --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--json]\");\n        output.WriteLine(\"  mail search --profile <id> [--mailbox <id>] [--folder <name>] [--query <text>] [--subject <text>] [--from <text>] [--to <text>] [--limit <n>] [--compact] [--json]\");\n        output.WriteLine(\"  mail get --profile <id> --message-id <id> [--mailbox <id>] [--folder <name>] [--include-raw] [--compact] [--json]\");\n        output.WriteLine(\"  mail get-many --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--include-raw] [--compact] [--json]\");\n        output.WriteLine(\"  mail mark-read --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--unread] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail flag --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--unflag] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail archive --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail trash --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail move --profile <id> --message-id <id> [--message-id <id>] --target-folder <id> [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail delete --profile <id> --message-id <id> [--message-id <id>] [--mailbox <id>] [--folder <name>] [--confirm-token <token>] [--json]\");\n        output.WriteLine(\"  mail attachments --profile <id> --message-id <id> [--mailbox <id>] [--folder <name>] [--json]\");\n        output.WriteLine(\"  mail save-attachment --profile <id> --message-id <id> --attachment-id <id> --path <destination> [--mailbox <id>] [--folder <name>] [--overwrite] [--json]\");\n        output.WriteLine(\"  mail save-attachments --profile <id> --message-id <id> --path <destination> [--mailbox <id>] [--folder <name>] [--attachment-id <id>] [--attachment-id <id>] [--name-contains <text>] [--content-type <text>] [--overwrite] [--json]\");\n        output.WriteLine(\"  mail save-attachments-many --profile <id> --message-id <id> [--message-id <id>] --path <destination> [--mailbox <id>] [--folder <name>] [--attachment-id <id>] [--attachment-id <id>] [--name-contains <text>] [--content-type <text>] [--overwrite] [--json]\");\n        output.WriteLine(\"  mcp serve\");\n        output.WriteLine(\"  send --draft <id> [--send-now] [--json]\");\n        output.WriteLine(\"  send --file <path> [--send-now] [--json]\");\n        output.WriteLine(\"  send --profile <id> --to <address> [--to <address>] [--cc <address>] [--bcc <address>] [--reply-to <address>] [--from <address>] [--subject <text>] [--text <text>] [--html <html>] [--attachment <path>] [--header <key=value>] [--send-now] [--json]\");\n        output.WriteLine(\"  queue list [--compact] [--json]\");\n        output.WriteLine(\"  queue get --message-id <id> [--compact] [--json]\");\n        output.WriteLine(\"  queue remove --message-id <id> [--json]\");\n        output.WriteLine(\"  queue process [--json]\");\n        output.WriteLine();\n        output.WriteLine(\"Global options:\");\n        output.WriteLine(\"  --profiles-dir <path>\");\n        output.WriteLine(\"  --secrets-dir <path>\");\n        output.WriteLine(\"  --drafts-dir <path>\");\n        output.WriteLine(\"  --plan-batches-dir <path>\");\n        output.WriteLine(\"  --help\");\n    }\n\n    private static MailProfileConnectionTestScope ParseConnectionTestScope(string? rawScope) {\n        if (string.IsNullOrWhiteSpace(rawScope)) {\n            return MailProfileConnectionTestScope.Auto;\n        }\n\n        if (Enum.TryParse<MailProfileConnectionTestScope>(rawScope.Trim(), ignoreCase: true, out var scope)) {\n            return scope;\n        }\n\n        throw new InvalidOperationException($\"Unsupported connection test scope '{rawScope}'.\");\n    }\n\n    private static MailProfileOverviewQuery BuildProfileOverviewQuery(CliArguments parseResult) {\n        var query = new MailProfileOverviewQuery {\n            Descending = parseResult.HasFlag(\"desc\"),\n            ReadyOnly = parseResult.HasFlag(\"ready-only\"),\n            CanReadOnly = parseResult.HasFlag(\"can-read\"),\n            CanSendOnly = parseResult.HasFlag(\"can-send\"),\n            DefaultOnly = parseResult.HasFlag(\"default-only\")\n        };\n\n        var kind = parseResult.GetOption(\"kind\");\n        if (!string.IsNullOrWhiteSpace(kind)) {\n            query.Kind = MailProfileKindParser.Parse(kind);\n        }\n\n        var sort = parseResult.GetOption(\"sort\");\n        if (!string.IsNullOrWhiteSpace(sort)) {\n            query.SortBy = ParseProfileOverviewSortBy(sort);\n        }\n\n        return query;\n    }\n\n    private static MailProfileOverviewSortBy ParseProfileOverviewSortBy(string rawSort) {\n        if (Enum.TryParse<MailProfileOverviewSortBy>(rawSort.Trim(), ignoreCase: true, out var sortBy)) {\n            return sortBy;\n        }\n\n        throw new InvalidOperationException($\"Unsupported profile overview sort '{rawSort}'.\");\n    }\n\n    private sealed class CliErrorEnvelope {\n        public required CliError Error { get; init; }\n    }\n\n    private sealed class CliError {\n        public required string Type { get; init; }\n\n        public required string Message { get; init; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/Mailozaurr.Cli.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <OutputType>Exe</OutputType>\n        <TargetFramework>net8.0</TargetFramework>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <LangVersion>latest</LangVersion>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Mailozaurr.Application\\Mailozaurr.Application.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n      <PackageReference Include=\"Microsoft.Extensions.Hosting\" Version=\"10.0.5\" />\n      <PackageReference Include=\"ModelContextProtocol\" Version=\"1.1.0\" />\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/Mcp/MailMcpTools.cs",
    "content": "using System.ComponentModel;\nusing Mailozaurr.Application;\nusing ModelContextProtocol.Server;\n\nnamespace Mailozaurr.Cli.Mcp;\n\n[McpServerToolType]\npublic sealed class MailMcpTools {\n    private readonly MailApplication _application;\n\n    public MailMcpTools(MailApplication application) {\n        _application = application ?? throw new ArgumentNullException(nameof(application));\n    }\n\n    [McpServerTool]\n    [Description(\"Lists configured Mailozaurr profiles that can be used for mailbox and send operations.\")]\n    public Task<IReadOnlyList<MailProfile>> mail_profiles_list(CancellationToken cancellationToken = default) =>\n        _application.Profiles.GetProfilesAsync(cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists higher-level summaries for all configured Mailozaurr profiles, including kind, capabilities, auth posture, and readiness.\")]\n    public Task<IReadOnlyList<MailProfileOverview>> mail_profiles_summary_list(\n        [Description(\"Optional provider kind filter, such as imap, graph, gmail, smtp, or pop3.\")] string? kind = null,\n        [Description(\"When true, only returns ready profiles.\")] bool readyOnly = false,\n        [Description(\"When true, only returns profiles that support reading.\")] bool canReadOnly = false,\n        [Description(\"When true, only returns profiles that support sending.\")] bool canSendOnly = false,\n        [Description(\"When true, only returns profiles marked as default.\")] bool defaultOnly = false,\n        [Description(\"Optional sort key: id, kind, or readiness.\")] string? sortBy = null,\n        [Description(\"When true, reverses the selected sort order.\")] bool descending = false,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileOverview.GetOverviewsAsync(new MailProfileOverviewQuery {\n            Kind = string.IsNullOrWhiteSpace(kind) ? null : MailProfileKindParser.Parse(kind),\n            SortBy = string.IsNullOrWhiteSpace(sortBy)\n                ? MailProfileOverviewSortBy.Id\n                : Enum.Parse<MailProfileOverviewSortBy>(sortBy, ignoreCase: true),\n            Descending = descending,\n            ReadyOnly = readyOnly,\n            CanReadOnly = canReadOnly,\n            CanSendOnly = canSendOnly,\n            DefaultOnly = defaultOnly\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists lightweight profile summaries for all configured Mailozaurr profiles.\")]\n    public Task<IReadOnlyList<MailProfileOverviewCompact>> mail_profiles_summary_compact_list(\n        [Description(\"Optional provider kind filter, such as imap, graph, gmail, smtp, or pop3.\")] string? kind = null,\n        [Description(\"When true, only returns ready profiles.\")] bool readyOnly = false,\n        [Description(\"When true, only returns profiles that support reading.\")] bool canReadOnly = false,\n        [Description(\"When true, only returns profiles that support sending.\")] bool canSendOnly = false,\n        [Description(\"When true, only returns profiles marked as default.\")] bool defaultOnly = false,\n        [Description(\"Optional sort key: id, kind, or readiness.\")] string? sortBy = null,\n        [Description(\"When true, reverses the selected sort order.\")] bool descending = false,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileOverview.GetCompactOverviewsAsync(new MailProfileOverviewQuery {\n            Kind = string.IsNullOrWhiteSpace(kind) ? null : MailProfileKindParser.Parse(kind),\n            SortBy = string.IsNullOrWhiteSpace(sortBy)\n                ? MailProfileOverviewSortBy.Id\n                : Enum.Parse<MailProfileOverviewSortBy>(sortBy, ignoreCase: true),\n            Descending = descending,\n            ReadyOnly = readyOnly,\n            CanReadOnly = canReadOnly,\n            CanSendOnly = canSendOnly,\n            DefaultOnly = defaultOnly\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Returns the effective capabilities for a configured Mailozaurr profile.\")]\n    public async Task<ProfileCapabilities> mail_capabilities_get(\n        [Description(\"The profile identifier to inspect.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var capabilities = await _application.Profiles.GetCapabilitiesAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return capabilities ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets a configured Mailozaurr profile by identifier.\")]\n    public async Task<MailProfile> mail_profile_get(\n        [Description(\"The profile identifier to retrieve.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var profile = await _application.Profiles.GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return profile ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Inspects whether a configured Mailozaurr profile is ready to use, including provider-specific auth prerequisites.\")]\n    public Task<MailProfileValidationResult> mail_profile_doctor(\n        [Description(\"The profile identifier to inspect.\")] string profileId,\n        CancellationToken cancellationToken = default) =>\n        _application.Profiles.DiagnoseAsync(profileId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Returns a higher-level summary for a configured Mailozaurr profile, combining kind, capabilities, auth posture, and readiness.\")]\n    public async Task<MailProfileOverview> mail_profile_summary(\n        [Description(\"The profile identifier to summarize.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var overview = await _application.ProfileOverview.GetOverviewAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return overview ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Returns a lightweight summary for a configured Mailozaurr profile.\")]\n    public async Task<MailProfileOverviewCompact> mail_profile_summary_compact(\n        [Description(\"The profile identifier to summarize.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var overview = await _application.ProfileOverview.GetCompactOverviewAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return overview ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Runs structural validation for a configured Mailozaurr profile without provider-specific readiness checks.\")]\n    public async Task<MailProfileValidationResult> mail_profile_validate(\n        [Description(\"The profile identifier to validate.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var profile = await _application.Profiles.GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);\n        if (profile == null) {\n            throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n        }\n\n        return await _application.Profiles.ValidateAsync(profile, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Creates or updates a Mailozaurr profile using shared profile storage.\")]\n    public async Task<MailProfile> mail_profile_save(\n        [Description(\"The stable profile identifier to create or update.\")] string profileId,\n        [Description(\"The provider kind, such as imap, graph, gmail, smtp, or pop3.\")] string kind,\n        [Description(\"The human-readable display name for the profile.\")] string displayName,\n        [Description(\"Optional description for operators.\")] string? description = null,\n        [Description(\"Optional default sender email address.\")] string? defaultSender = null,\n        [Description(\"Optional default mailbox identifier or address.\")] string? defaultMailbox = null,\n        [Description(\"When true, marks this profile as the default profile.\")] bool isDefault = false,\n        [Description(\"Optional non-secret settings to persist with the profile.\")] Dictionary<string, string>? settings = null,\n        CancellationToken cancellationToken = default) {\n        var profile = new MailProfile {\n            Id = profileId,\n            DisplayName = displayName,\n            Kind = MailProfileKindParser.Parse(kind),\n            Description = description,\n            DefaultSender = defaultSender,\n            DefaultMailbox = defaultMailbox,\n            IsDefault = isDefault,\n            Settings = settings == null\n                ? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n                : new Dictionary<string, string>(settings, StringComparer.OrdinalIgnoreCase)\n        };\n\n        var result = await _application.Profiles.SaveAsync(profile, cancellationToken).ConfigureAwait(false);\n        if (!result.Succeeded) {\n            throw new InvalidOperationException(result.Message ?? $\"Profile '{profileId}' could not be saved.\");\n        }\n\n        return await mail_profile_get(profileId, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Creates or updates a Microsoft Graph profile using the shared Graph bootstrap workflow.\")]\n    public async Task<MailProfile> mail_profile_graph_bootstrap(\n        [Description(\"The stable profile identifier to create or update.\")] string profileId,\n        [Description(\"The human-readable display name for the profile.\")] string displayName,\n        [Description(\"The mailbox address or user principal name for Graph operations.\")] string mailbox,\n        [Description(\"Optional description for operators.\")] string? description = null,\n        [Description(\"Optional default sender email address. Defaults to the mailbox when omitted.\")] string? defaultSender = null,\n        [Description(\"When true, marks this profile as the default profile.\")] bool isDefault = false,\n        [Description(\"Optional Graph client/application identifier.\")] string? clientId = null,\n        [Description(\"Optional Graph tenant/directory identifier.\")] string? tenantId = null,\n        [Description(\"Optional confidential client secret to store securely. Prefer using clientSecretReference for MCP hosts.\")] string? clientSecret = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Graph client secret.\")] string? clientSecretReference = null,\n        [Description(\"Optional explicit access token to store securely. Prefer using accessTokenReference for MCP hosts.\")] string? accessToken = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Graph access token.\")] string? accessTokenReference = null,\n        [Description(\"Optional certificate path for certificate-based auth.\")] string? certificatePath = null,\n        [Description(\"Optional certificate password to store securely. Prefer using certificatePasswordReference for MCP hosts.\")] string? certificatePassword = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the certificate password.\")] string? certificatePasswordReference = null,\n        CancellationToken cancellationToken = default) {\n        var result = await _application.ProfileBootstrap.SaveGraphProfileAsync(new GraphProfileBootstrapRequest {\n            ProfileId = profileId,\n            DisplayName = displayName,\n            Description = description,\n            Mailbox = mailbox,\n            DefaultSender = defaultSender,\n            IsDefault = isDefault,\n            ClientId = clientId,\n            TenantId = tenantId,\n            ClientSecret = clientSecret,\n            ClientSecretReference = clientSecretReference,\n            AccessToken = accessToken,\n            AccessTokenReference = accessTokenReference,\n            CertificatePath = certificatePath,\n            CertificatePassword = certificatePassword,\n            CertificatePasswordReference = certificatePasswordReference\n        }, cancellationToken).ConfigureAwait(false);\n        if (!result.Succeeded) {\n            throw new InvalidOperationException(result.Message ?? $\"Graph profile '{profileId}' could not be saved.\");\n        }\n\n        return await mail_profile_get(profileId, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Creates or updates a Gmail profile using the shared Gmail bootstrap workflow.\")]\n    public async Task<MailProfile> mail_profile_gmail_bootstrap(\n        [Description(\"The stable profile identifier to create or update.\")] string profileId,\n        [Description(\"The human-readable display name for the profile.\")] string displayName,\n        [Description(\"Optional Gmail mailbox address or user id. Defaults to 'me' when omitted.\")] string? mailbox = null,\n        [Description(\"Optional description for operators.\")] string? description = null,\n        [Description(\"Optional default sender email address. Defaults to the mailbox when appropriate.\")] string? defaultSender = null,\n        [Description(\"When true, marks this profile as the default profile.\")] bool isDefault = false,\n        [Description(\"Optional Google OAuth client identifier.\")] string? clientId = null,\n        [Description(\"Optional Google OAuth client secret to store securely. Prefer using clientSecretReference for MCP hosts.\")] string? clientSecret = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Gmail client secret.\")] string? clientSecretReference = null,\n        [Description(\"Optional Google OAuth refresh token to store securely. Prefer using refreshTokenReference for MCP hosts.\")] string? refreshToken = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Gmail refresh token.\")] string? refreshTokenReference = null,\n        [Description(\"Optional explicit access token to store securely. Prefer using accessTokenReference for MCP hosts.\")] string? accessToken = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Gmail access token.\")] string? accessTokenReference = null,\n        CancellationToken cancellationToken = default) {\n        var result = await _application.ProfileBootstrap.SaveGmailProfileAsync(new GmailProfileBootstrapRequest {\n            ProfileId = profileId,\n            DisplayName = displayName,\n            Description = description,\n            Mailbox = mailbox,\n            DefaultSender = defaultSender,\n            IsDefault = isDefault,\n            ClientId = clientId,\n            ClientSecret = clientSecret,\n            ClientSecretReference = clientSecretReference,\n            RefreshToken = refreshToken,\n            RefreshTokenReference = refreshTokenReference,\n            AccessToken = accessToken,\n            AccessTokenReference = accessTokenReference\n        }, cancellationToken).ConfigureAwait(false);\n        if (!result.Succeeded) {\n            throw new InvalidOperationException(result.Message ?? $\"Gmail profile '{profileId}' could not be saved.\");\n        }\n\n        return await mail_profile_get(profileId, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Authenticates a saved Microsoft Graph profile using the shared interactive login workflow and persists the resulting token.\")]\n    public Task<MailProfileAuthenticationResult> mail_profile_graph_login(\n        [Description(\"The saved Graph profile identifier to authenticate.\")] string profileId,\n        [Description(\"Optional login hint for the interactive flow.\")] string? login = null,\n        [Description(\"Optional mailbox override to persist on the profile.\")] string? mailbox = null,\n        [Description(\"Optional client/application identifier override.\")] string? clientId = null,\n        [Description(\"Optional tenant/directory identifier override.\")] string? tenantId = null,\n        [Description(\"Optional redirect URI override for the interactive flow.\")] string? redirectUri = null,\n        [Description(\"Optional scopes override.\")] string[]? scopes = null,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileAuth.LoginGraphAsync(new GraphProfileLoginRequest {\n            ProfileId = profileId,\n            Login = login,\n            Mailbox = mailbox,\n            ClientId = clientId,\n            TenantId = tenantId,\n            RedirectUri = redirectUri,\n            Scopes = scopes\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Authenticates a saved Gmail profile using the shared interactive login workflow and persists the resulting tokens.\")]\n    public Task<MailProfileAuthenticationResult> mail_profile_gmail_login(\n        [Description(\"The saved Gmail profile identifier to authenticate.\")] string profileId,\n        [Description(\"Optional Gmail account override used for the login flow.\")] string? mailbox = null,\n        [Description(\"Optional OAuth client identifier override.\")] string? clientId = null,\n        [Description(\"Optional OAuth client secret override. Prefer using clientSecretReference for MCP hosts.\")] string? clientSecret = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' for the Gmail client secret override.\")] string? clientSecretReference = null,\n        [Description(\"Optional scopes override.\")] string[]? scopes = null,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileAuth.LoginGmailAsync(new GmailProfileLoginRequest {\n            ProfileId = profileId,\n            GmailAccount = mailbox,\n            ClientId = clientId,\n            ClientSecret = clientSecret,\n            ClientSecretReference = clientSecretReference,\n            Scopes = scopes\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Refreshes or reauthenticates a saved profile using its persisted Mailozaurr auth metadata.\")]\n    public Task<MailProfileAuthenticationResult> mail_profile_refresh_auth(\n        [Description(\"The saved profile identifier to refresh.\")] string profileId,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileAuth.RefreshAsync(profileId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Returns the persisted authentication status for a saved Mailozaurr profile, including auth mode, token presence, and refreshability.\")]\n    public async Task<MailProfileAuthStatus> mail_profile_auth_status(\n        [Description(\"The saved profile identifier to inspect.\")] string profileId,\n        CancellationToken cancellationToken = default) {\n        var status = await _application.ProfileAuth.GetStatusAsync(profileId, cancellationToken).ConfigureAwait(false);\n        return status ?? throw new InvalidOperationException($\"Profile '{profileId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Runs a live provider connection test for a saved Mailozaurr profile.\")]\n    public Task<MailProfileConnectionTestResult> mail_profile_test(\n        [Description(\"The saved profile identifier to test.\")] string profileId,\n        [Description(\"Optional test scope: auto, auth, mailbox, or send.\")] string? scope = null,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileConnections.TestAsync(profileId, ParseConnectionTestScope(scope), cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Deletes a Mailozaurr profile from shared profile storage.\")]\n    public Task<OperationResult> mail_profile_delete(\n        [Description(\"The profile identifier to delete.\")] string profileId,\n        CancellationToken cancellationToken = default) =>\n        _application.Profiles.DeleteAsync(profileId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Marks a Mailozaurr profile as the shared default profile.\")]\n    public Task<OperationResult> mail_profile_set_default(\n        [Description(\"The profile identifier to mark as default.\")] string profileId,\n        CancellationToken cancellationToken = default) =>\n        _application.Profiles.SetDefaultAsync(profileId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Stores or replaces a secret for an existing Mailozaurr profile.\")]\n    public Task<OperationResult> mail_profile_secret_set(\n        [Description(\"The profile identifier that owns the secret.\")] string profileId,\n        [Description(\"The stable secret name, such as password, client-secret, or refresh-token.\")] string secretName,\n        [Description(\"The secret value to store. Prefer using secretReference for MCP hosts when the secret already exists in the shared store.\")] string? secretValue = null,\n        [Description(\"Optional secret reference in the form '<profile-id>:<secret-name>' or '<secret-name>' to copy without re-exposing the secret value.\")] string? secretReference = null,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileSecrets.SetSecretAsync(\n            profileId,\n            secretName,\n            secretValue,\n            secretReference,\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Removes a secret associated with an existing Mailozaurr profile.\")]\n    public Task<OperationResult> mail_profile_secret_remove(\n        [Description(\"The profile identifier that owns the secret.\")] string profileId,\n        [Description(\"The stable secret name to remove.\")] string secretName,\n        CancellationToken cancellationToken = default) =>\n        _application.ProfileSecrets.RemoveSecretAsync(profileId, secretName, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists folders or folder-like mailbox containers for a profile.\")]\n    public Task<IReadOnlyList<FolderRef>> mail_folders_list(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional parent folder identifier to scope the listing.\")] string? parentFolderId = null,\n        [Description(\"When true, limits the result to root-level folders only.\")] bool rootOnly = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.GetFoldersAsync(new MailFolderQuery {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            ParentFolderId = parentFolderId,\n            RootOnly = rootOnly\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists folders or folder-like mailbox containers for a profile using a lightweight projection.\")]\n    public Task<IReadOnlyList<FolderRefCompact>> mail_folders_compact_list(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional parent folder identifier to scope the listing.\")] string? parentFolderId = null,\n        [Description(\"When true, limits the result to root-level folders only.\")] bool rootOnly = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.GetFoldersCompactAsync(new MailFolderQuery {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            ParentFolderId = parentFolderId,\n            RootOnly = rootOnly\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists provider-neutral folder aliases for a profile, resolving them to provider folders when possible.\")]\n    public Task<IReadOnlyList<MailFolderAliasSummary>> mail_folder_aliases_list(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.FolderAliases.GetAliasesAsync(profileId, mailboxId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Resolves a requested folder target to a provider-neutral alias or an effective provider folder destination.\")]\n    public Task<MailFolderTargetResolution> mail_folder_resolve(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The requested target folder identifier or alias.\")] string targetFolderId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.FolderAliases.ResolveAsync(profileId, targetFolderId, mailboxId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Creates a normalized execution plan for a selected message action, with optional preview-token validation and resolved destinations.\")]\n    public Task<MessageActionExecutionPlan> mail_action_plan(\n        [Description(\"The selected action, such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.\")] string action,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to normalize.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias when the action is move.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlans.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n            Action = action,\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists persisted reusable message action plan batches.\")]\n    public Task<IReadOnlyList<MailMessageActionPlanBatch>> mail_action_batch_store_list(\n        [Description(\"Optional human-readable plan names that must exist in the returned batch.\")] IReadOnlyList<string>? planNames = null,\n        [Description(\"Optional profile identifiers that must be referenced by the returned batch.\")] IReadOnlyList<string>? profileIds = null,\n        [Description(\"Optional normalized action names that must be referenced by the returned batch.\")] IReadOnlyList<string>? actions = null,\n        [Description(\"Optional sort key: id, name, plans, ready, updated, or actions.\")] string? sortBy = null,\n        [Description(\"When true, reverses the selected sort order.\")] bool descending = false,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.GetBatchesAsync(BuildBatchQuery(planNames, profileIds, actions, sortBy, descending), cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists persisted reusable message action plan batches using a lightweight projection.\")]\n    public Task<IReadOnlyList<MailMessageActionPlanBatchCompact>> mail_action_batch_store_compact_list(\n        [Description(\"Optional human-readable plan names that must exist in the returned batch.\")] IReadOnlyList<string>? planNames = null,\n        [Description(\"Optional profile identifiers that must be referenced by the returned batch.\")] IReadOnlyList<string>? profileIds = null,\n        [Description(\"Optional normalized action names that must be referenced by the returned batch.\")] IReadOnlyList<string>? actions = null,\n        [Description(\"Optional sort key: id, name, plans, ready, updated, or actions.\")] string? sortBy = null,\n        [Description(\"When true, reverses the selected sort order.\")] bool descending = false,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.GetBatchesCompactAsync(BuildBatchQuery(planNames, profileIds, actions, sortBy, descending), cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists persisted reusable message action plan batches using a richer summary projection.\")]\n    public Task<IReadOnlyList<MailMessageActionPlanBatchSummary>> mail_action_batch_store_summary_list(\n        [Description(\"Optional human-readable plan names that must exist in the returned batch.\")] IReadOnlyList<string>? planNames = null,\n        [Description(\"Optional profile identifiers that must be referenced by the returned batch.\")] IReadOnlyList<string>? profileIds = null,\n        [Description(\"Optional normalized action names that must be referenced by the returned batch.\")] IReadOnlyList<string>? actions = null,\n        [Description(\"Optional sort key: id, name, plans, ready, updated, or actions.\")] string? sortBy = null,\n        [Description(\"When true, reverses the selected sort order.\")] bool descending = false,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.GetBatchesSummaryAsync(BuildBatchQuery(planNames, profileIds, actions, sortBy, descending), cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Gets one persisted reusable message action plan batch by identifier.\")]\n    public async Task<MailMessageActionPlanBatch> mail_action_batch_store_get(\n        [Description(\"The persisted batch identifier to retrieve.\")] string batchId,\n        CancellationToken cancellationToken = default) {\n        var batch = await _application.MessageActionPlanRegistry.GetBatchAsync(batchId, cancellationToken).ConfigureAwait(false);\n        return batch ?? throw new InvalidOperationException($\"Action plan batch '{batchId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets one persisted reusable message action plan batch by identifier using a lightweight projection.\")]\n    public async Task<MailMessageActionPlanBatchCompact> mail_action_batch_store_compact_get(\n        [Description(\"The persisted batch identifier to retrieve.\")] string batchId,\n        CancellationToken cancellationToken = default) {\n        var batch = await _application.MessageActionPlanRegistry.GetBatchCompactAsync(batchId, cancellationToken).ConfigureAwait(false);\n        return batch ?? throw new InvalidOperationException($\"Action plan batch '{batchId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets one persisted reusable message action plan batch by identifier using a richer summary projection.\")]\n    public async Task<MailMessageActionPlanBatchSummary> mail_action_batch_store_summary_get(\n        [Description(\"The persisted batch identifier to retrieve.\")] string batchId,\n        CancellationToken cancellationToken = default) {\n        var batch = await _application.MessageActionPlanRegistry.GetBatchSummaryAsync(batchId, cancellationToken).ConfigureAwait(false);\n        return batch ?? throw new InvalidOperationException($\"Action plan batch '{batchId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Imports a persisted reusable message action plan batch from an external batch file.\")]\n    public Task<OperationResult> mail_action_batch_store_import(\n        [Description(\"The persisted batch identifier to create or update.\")] string batchId,\n        [Description(\"The human-readable batch name.\")] string name,\n        [Description(\"The source batch file path on the server filesystem.\")] string path,\n        [Description(\"Optional operator-facing description.\")] string? description = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.ImportAsync(batchId, name, path, description, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Exports a persisted reusable message action plan batch to an external batch file.\")]\n    public Task<OperationResult> mail_action_batch_store_export(\n        [Description(\"The persisted batch identifier to export.\")] string batchId,\n        [Description(\"The destination batch file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.ExportAsync(batchId, path, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds and stores a reusable action plan batch from a common message selection and selected common actions.\")]\n    public Task<OperationResult> mail_action_batch_store_create_common(\n        [Description(\"The persisted batch identifier to create or update.\")] string batchId,\n        [Description(\"The human-readable batch name.\")] string name,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to include.\")] string[] messageIds,\n        [Description(\"Optional subset of common actions to include, such as mark-read, archive, delete, or move. When omitted, all supported common actions are considered.\")] string[]? actions = null,\n        [Description(\"Optional custom destination folder identifier or alias when a generic move action should be included.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional operator-facing description.\")] string? description = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.CreateCommonBatchAsync(\n            batchId,\n            name,\n            new CommonMessageActionsPreviewRequest {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                FolderId = folderId,\n                MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n                DestinationFolderId = destinationFolderId\n            },\n            actions?.Where(action => !string.IsNullOrWhiteSpace(action)).Select(action => action.Trim()).ToArray(),\n            description,\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds and stores a reusable action plan batch from an existing common action preview bundle and optional selected previewed actions.\")]\n    public Task<OperationResult> mail_action_batch_store_create_from_preview(\n        [Description(\"The persisted batch identifier to create or update.\")] string batchId,\n        [Description(\"The human-readable batch name.\")] string name,\n        [Description(\"The previously generated common action preview bundle.\")] CommonMessageActionsPreview preview,\n        [Description(\"Optional subset of previewed actions to persist. When omitted, all supported previewed actions are used.\")] string[]? actions = null,\n        [Description(\"Optional operator-facing description.\")] string? description = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.CreateCommonBatchFromPreviewAsync(\n            batchId,\n            name,\n            preview,\n            actions?.Where(action => !string.IsNullOrWhiteSpace(action)).Select(action => action.Trim()).ToArray(),\n            description,\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Clones an existing persisted reusable message action plan batch to a new identifier and name.\")]\n    public Task<OperationResult> mail_action_batch_store_clone(\n        [Description(\"The persisted source batch identifier to clone.\")] string sourceBatchId,\n        [Description(\"The persisted target batch identifier to create.\")] string targetBatchId,\n        [Description(\"The human-readable name for the cloned batch.\")] string name,\n        [Description(\"Optional operator-facing description.\")] string? description = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.CloneAsync(sourceBatchId, targetBatchId, name, description, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Clones an existing persisted reusable message action plan batch while applying shared profile, mailbox, folder, or destination transforms.\")]\n    public Task<OperationResult> mail_action_batch_store_transform_clone(\n        [Description(\"The persisted source batch identifier to clone.\")] string sourceBatchId,\n        [Description(\"The persisted target batch identifier to create.\")] string targetBatchId,\n        [Description(\"The human-readable name for the transformed clone.\")] string name,\n        [Description(\"Optional zero-based plan indexes to include from the source batch. When omitted, all plans are included.\")] int[]? indexes = null,\n        [Description(\"Optional stored plan names to include from the source batch. When omitted, all names are included.\")] string[]? planNames = null,\n        [Description(\"Optional replacement profile identifier for every transformed plan.\")] string? profileId = null,\n        [Description(\"Optional replacement mailbox identifier for every transformed plan.\")] string? mailboxId = null,\n        [Description(\"Optional replacement source folder identifier for every transformed plan.\")] string? folderId = null,\n        [Description(\"Optional replacement destination folder identifier for move-like plans.\")] string? destinationFolderId = null,\n        [Description(\"Optional operator-facing description.\")] string? description = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.TransformCloneAsync(\n            sourceBatchId,\n            targetBatchId,\n            name,\n            new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = indexes?.Distinct().ToList() ?? new List<int>(),\n                PlanNames = planNames?.Where(nameValue => !string.IsNullOrWhiteSpace(nameValue)).Select(nameValue => nameValue.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList() ?? new List<string>(),\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                FolderId = folderId,\n                DestinationFolderId = destinationFolderId\n            },\n            description,\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Previews a stored action plan batch transform before cloning and saving it.\")]\n    public Task<MailMessageActionPlanBatchTransformPreview> mail_action_batch_store_transform_preview(\n        [Description(\"The persisted source batch identifier to preview.\")] string sourceBatchId,\n        [Description(\"Optional zero-based plan indexes to include from the source batch. When omitted, all plans are included.\")] int[]? indexes = null,\n        [Description(\"Optional stored plan names to include from the source batch. When omitted, all names are included.\")] string[]? planNames = null,\n        [Description(\"Optional replacement profile identifier for every transformed plan.\")] string? profileId = null,\n        [Description(\"Optional replacement mailbox identifier for every transformed plan.\")] string? mailboxId = null,\n        [Description(\"Optional replacement source folder identifier for every transformed plan.\")] string? folderId = null,\n        [Description(\"Optional replacement destination folder identifier for move-like plans.\")] string? destinationFolderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.PreviewTransformCloneAsync(\n            sourceBatchId,\n            new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = indexes?.Distinct().ToList() ?? new List<int>(),\n                PlanNames = planNames?.Where(nameValue => !string.IsNullOrWhiteSpace(nameValue)).Select(nameValue => nameValue.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList() ?? new List<string>(),\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                FolderId = folderId,\n                DestinationFolderId = destinationFolderId\n            },\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Appends one newly planned normalized action to an existing persisted batch.\")]\n    public async Task<OperationResult> mail_action_batch_store_append_plan(\n        [Description(\"The persisted batch identifier to update.\")] string batchId,\n        [Description(\"The selected action, such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.\")] string action,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to normalize.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias when the action is move.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) {\n        var plan = await mail_action_plan(\n            action,\n            profileId,\n            messageIds,\n            destinationFolderId,\n            mailboxId,\n            folderId,\n            confirmationToken,\n            cancellationToken).ConfigureAwait(false);\n\n        return await _application.MessageActionPlanRegistry.AppendPlanAsync(batchId, plan, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Loads one normalized action plan from a file and appends it to an existing persisted batch.\")]\n    public Task<OperationResult> mail_action_batch_store_append_imported_plan(\n        [Description(\"The persisted batch identifier to update.\")] string batchId,\n        [Description(\"The source plan file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.AppendImportedPlanAsync(batchId, path, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Replaces one stored plan in an existing persisted batch using a newly planned normalized action.\")]\n    public async Task<OperationResult> mail_action_batch_store_replace_plan(\n        [Description(\"The persisted batch identifier to update.\")] string batchId,\n        [Description(\"The zero-based plan index to replace.\")] int index,\n        [Description(\"The selected action, such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.\")] string action,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to normalize.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias when the action is move.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) {\n        var plan = await mail_action_plan(\n            action,\n            profileId,\n            messageIds,\n            destinationFolderId,\n            mailboxId,\n            folderId,\n            confirmationToken,\n            cancellationToken).ConfigureAwait(false);\n\n        return await _application.MessageActionPlanRegistry.ReplacePlanAtAsync(batchId, index, plan, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Loads one normalized action plan from a file and replaces a stored plan by zero-based index.\")]\n    public Task<OperationResult> mail_action_batch_store_replace_imported_plan(\n        [Description(\"The persisted batch identifier to update.\")] string batchId,\n        [Description(\"The zero-based plan index to replace.\")] int index,\n        [Description(\"The source plan file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.ReplaceImportedPlanAtAsync(batchId, index, path, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Removes one plan from an existing persisted batch by zero-based index.\")]\n    public Task<OperationResult> mail_action_batch_store_remove_plan(\n        [Description(\"The persisted batch identifier to update.\")] string batchId,\n        [Description(\"The zero-based plan index to remove.\")] int index,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.RemovePlanAtAsync(batchId, index, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Deletes a persisted reusable message action plan batch.\")]\n    public Task<OperationResult> mail_action_batch_store_delete(\n        [Description(\"The persisted batch identifier to delete.\")] string batchId,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.DeleteAsync(batchId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Executes a persisted reusable message action plan batch through the shared batch-execution service.\")]\n    public Task<MessageActionBatchExecutionResult> mail_action_batch_store_execute(\n        [Description(\"The persisted batch identifier to execute.\")] string batchId,\n        [Description(\"When true, continues after failures. When false, later plans are skipped after the first failure.\")] bool continueOnError = true,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanRegistry.ExecuteAsync(batchId, continueOnError, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Creates a normalized action plan and exports it to a file through the shared Mailozaurr plan-exchange service.\")]\n    public async Task<OperationResult> mail_action_plan_export(\n        [Description(\"The selected action, such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.\")] string action,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to normalize.\")] string[] messageIds,\n        [Description(\"The destination file path on the server filesystem.\")] string path,\n        [Description(\"Optional custom destination folder identifier or alias when the action is move.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) {\n        var plan = await mail_action_plan(\n            action,\n            profileId,\n            messageIds,\n            destinationFolderId,\n            mailboxId,\n            folderId,\n            confirmationToken,\n            cancellationToken).ConfigureAwait(false);\n\n        await _application.MessageActionPlanExchange.SaveAsync(path, plan, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Action plan exported.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Loads one normalized action plan from a file through the shared Mailozaurr plan-exchange service.\")]\n    public Task<MessageActionExecutionPlan> mail_action_plan_import(\n        [Description(\"The source file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanExchange.LoadAsync(path, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Exports a batch of normalized action plans to a file through the shared Mailozaurr plan-exchange service.\")]\n    public async Task<OperationResult> mail_action_batch_export(\n        [Description(\"The destination file path on the server filesystem.\")] string path,\n        [Description(\"The normalized action plans to export.\")] MessageActionExecutionPlan[] plans,\n        CancellationToken cancellationToken = default) {\n        await _application.MessageActionPlanExchange.SaveBatchAsync(\n            path,\n            plans?.ToList() ?? new List<MessageActionExecutionPlan>(),\n            cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Action plan batch exported.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Loads a batch of normalized action plans from a file through the shared Mailozaurr plan-exchange service.\")]\n    public Task<IReadOnlyList<MessageActionExecutionPlan>> mail_action_batch_import(\n        [Description(\"The source file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPlanExchange.LoadBatchAsync(path, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Creates and executes one normalized message action plan through the shared Mailozaurr planning and batch-execution services.\")]\n    public async Task<MessageActionBatchExecutionResult> mail_action_execute(\n        [Description(\"The selected action, such as mark-read, mark-unread, flag, unflag, archive, trash, move, or delete.\")] string action,\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to normalize and execute.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias when the action is move.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) {\n        var plan = await _application.MessageActionPlans.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n            Action = action,\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken).ConfigureAwait(false);\n\n        return await _application.MessageActionBatch.ExecuteAsync(new[] { plan }, cancellationToken: cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Executes a batch of prebuilt normalized message action plans through the shared Mailozaurr batch-execution service.\")]\n    public Task<MessageActionBatchExecutionResult> mail_action_batch_execute(\n        [Description(\"The normalized action plans to execute.\")] MessageActionExecutionPlan[] plans,\n        [Description(\"When true, continues after failures. When false, later plans are skipped after the first failure.\")] bool continueOnError = true,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionBatch.ExecuteAsync(\n            plans?.ToList() ?? new List<MessageActionExecutionPlan>(),\n            continueOnError,\n            cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run bundle of common message actions, including read/unread, flag/unflag, archive, trash, delete, and an optional custom move target.\")]\n    public Task<CommonMessageActionsPreview> mail_actions_bundle_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias to include as a generic move preview.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewCommonActionsAsync(new CommonMessageActionsPreviewRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run comparison for standard message actions such as archive, trash, delete, and an optional custom move target.\")]\n    public Task<StandardMessageActionsPreview> mail_actions_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"Optional custom destination folder identifier or alias to include as a generic move preview.\")] string? destinationFolderId = null,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewStandardActionsAsync(new StandardMessageActionsPreviewRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run preview for moving messages, including normalized message ids and the effective destination folder target.\")]\n    public Task<MoveMessagesPreview> mail_move_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"The requested destination folder identifier or alias.\")] string destinationFolderId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run preview for deleting messages, including normalized message ids before execution.\")]\n    public Task<DeleteMessagesPreview> mail_delete_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>()\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Searches messages in a mailbox using normalized Mailozaurr filters.\")]\n    public Task<IReadOnlyList<MessageSummary>> mail_search(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier to scope the search.\")] string? folderId = null,\n        [Description(\"Optional free-text query to match across provider-specific searchable content.\")] string? queryText = null,\n        [Description(\"Optional subject text filter.\")] string? subjectContains = null,\n        [Description(\"Optional sender text filter.\")] string? fromContains = null,\n        [Description(\"Optional recipient text filter.\")] string? toContains = null,\n        [Description(\"When true, only returns messages with attachments.\")] bool hasAttachments = false,\n        [Description(\"Optional maximum number of messages to return.\")] int? limit = null,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.SearchAsync(new MailSearchRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            QueryText = queryText,\n            SubjectContains = subjectContains,\n            FromContains = fromContains,\n            ToContains = toContains,\n            HasAttachments = hasAttachments,\n            Limit = limit\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Searches messages in a mailbox using a lightweight Mailozaurr message projection.\")]\n    public Task<IReadOnlyList<MessageSummaryCompact>> mail_search_compact(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier to scope the search.\")] string? folderId = null,\n        [Description(\"Optional free-text query to match across provider-specific searchable content.\")] string? queryText = null,\n        [Description(\"Optional subject text filter.\")] string? subjectContains = null,\n        [Description(\"Optional sender text filter.\")] string? fromContains = null,\n        [Description(\"Optional recipient text filter.\")] string? toContains = null,\n        [Description(\"When true, only returns messages with attachments.\")] bool hasAttachments = false,\n        [Description(\"Optional maximum number of messages to return.\")] int? limit = null,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.SearchCompactAsync(new MailSearchRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            QueryText = queryText,\n            SubjectContains = subjectContains,\n            FromContains = fromContains,\n            ToContains = toContains,\n            HasAttachments = hasAttachments,\n            Limit = limit\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Gets a detailed message view for a specific message identifier.\")]\n    public async Task<MessageDetail> mail_get(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifier to retrieve.\")] string messageId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"When true, includes raw provider content where supported.\")] bool includeRawContent = false,\n        CancellationToken cancellationToken = default) {\n        var message = await _application.Read.GetMessageAsync(new GetMessageRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageId = messageId,\n            IncludeRawContent = includeRawContent\n        }, cancellationToken).ConfigureAwait(false);\n\n        return message ?? throw new InvalidOperationException($\"Message '{messageId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets a lightweight detailed message view for a specific message identifier.\")]\n    public async Task<MessageDetailCompact> mail_get_compact(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifier to retrieve.\")] string messageId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"When true, allows the provider to include raw-content presence in the projection.\")] bool includeRawContent = false,\n        CancellationToken cancellationToken = default) {\n        var message = await _application.Read.GetMessageCompactAsync(new GetMessageRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageId = messageId,\n            IncludeRawContent = includeRawContent\n        }, cancellationToken).ConfigureAwait(false);\n\n        return message ?? throw new InvalidOperationException($\"Message '{messageId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets detailed message views for multiple specific message identifiers.\")]\n    public Task<IReadOnlyList<MessageDetail>> mail_get_many(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to retrieve.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"When true, includes raw provider content where supported.\")] bool includeRawContent = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.GetMessagesAsync(new GetMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IncludeRawContent = includeRawContent\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Gets lightweight detailed message views for multiple specific message identifiers.\")]\n    public Task<IReadOnlyList<MessageDetailCompact>> mail_get_many_compact(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to retrieve.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"When true, includes raw-content presence where supported.\")] bool includeRawContent = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.GetMessagesCompactAsync(new GetMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IncludeRawContent = includeRawContent\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run preview for changing messages to read or unread, including normalized message ids and a reusable confirmation token.\")]\n    public Task<MessageStateChangePreview> mail_mark_read_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"Desired read state. True marks as read; false marks as unread.\")] bool isRead = true,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewReadStateAsync(new SetReadStateRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IsRead = isRead\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Marks one or more messages as read or unread using the shared Mailozaurr message-action service.\")]\n    public Task<MessageActionResult> mail_mark_read(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to update.\")] string[] messageIds,\n        [Description(\"Desired read state. True marks as read; false marks as unread.\")] bool isRead = true,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.SetReadStateAsync(new SetReadStateRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IsRead = isRead,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Builds a dry-run preview for flagging or unflagging messages, including normalized message ids and a reusable confirmation token.\")]\n    public Task<MessageStateChangePreview> mail_flag_preview(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to preview.\")] string[] messageIds,\n        [Description(\"Desired flagged state. True flags/star-marks; false unflags.\")] bool isFlagged = true,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActionPreview.PreviewFlaggedStateAsync(new SetFlaggedStateRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IsFlagged = isFlagged\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Flags or unflags one or more messages using the shared Mailozaurr message-action service.\")]\n    public Task<MessageActionResult> mail_flag(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to update.\")] string[] messageIds,\n        [Description(\"Desired flagged state. True flags/star-marks; false unflags.\")] bool isFlagged = true,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.SetFlaggedStateAsync(new SetFlaggedStateRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            IsFlagged = isFlagged,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Archives one or more messages using the shared Mailozaurr message-action service and a provider-neutral Archive alias.\")]\n    public Task<MessageActionResult> mail_archive(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to archive.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.MoveAsync(new MoveMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = MailFolderAliases.Archive,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Moves one or more messages to trash using the shared Mailozaurr message-action service and a provider-neutral Trash alias.\")]\n    public Task<MessageActionResult> mail_trash(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to trash.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.MoveAsync(new MoveMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = MailFolderAliases.Trash,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Moves one or more messages to a destination folder using the shared Mailozaurr message-action service.\")]\n    public Task<MessageActionResult> mail_move(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to move.\")] string[] messageIds,\n        [Description(\"Destination folder identifier or provider alias.\")] string destinationFolderId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional source folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.MoveAsync(new MoveMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationFolderId = destinationFolderId,\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Deletes one or more messages using the shared Mailozaurr message-action service.\")]\n    public Task<MessageActionResult> mail_delete(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers to delete.\")] string[] messageIds,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional confirmation token returned by a matching preview call.\")] string? confirmationToken = null,\n        CancellationToken cancellationToken = default) =>\n        _application.MessageActions.DeleteAsync(new DeleteMessagesRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            ConfirmationToken = confirmationToken\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists attachment metadata for a specific message without returning the full message body.\")]\n    public Task<IReadOnlyList<AttachmentSummary>> mail_attachments_list(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifier that owns the attachments.\")] string messageId,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.GetAttachmentsAsync(new ListAttachmentsRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageId = messageId\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Saves a message attachment to a local path that the Mailozaurr server can access.\")]\n    public Task<OperationResult> mail_attachment_save(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifier that owns the attachment.\")] string messageId,\n        [Description(\"The provider-specific attachment identifier to save.\")] string attachmentId,\n        [Description(\"The destination file path on the server filesystem.\")] string destinationPath,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"When true, allows an existing destination file to be overwritten.\")] bool overwrite = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.SaveAttachmentAsync(new SaveAttachmentRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageId = messageId,\n            AttachmentId = attachmentId,\n            DestinationPath = destinationPath,\n            Overwrite = overwrite\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Saves one or more attachments from a message using shared Mailozaurr filtering and batching logic.\")]\n    public Task<SaveAttachmentsResult> mail_attachments_save(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifier that owns the attachments.\")] string messageId,\n        [Description(\"The destination path or directory on the server filesystem.\")] string destinationPath,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional explicit attachment identifiers to save.\")] string[]? attachmentIds = null,\n        [Description(\"Optional case-insensitive file-name filter.\")] string? fileNameContains = null,\n        [Description(\"Optional case-insensitive content-type filter.\")] string? contentTypeContains = null,\n        [Description(\"When true, allows existing destination files to be overwritten.\")] bool overwrite = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.SaveAttachmentsAsync(new SaveAttachmentsRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageId = messageId,\n            DestinationPath = destinationPath,\n            AttachmentIds = attachmentIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            FileNameContains = fileNameContains,\n            ContentTypeContains = contentTypeContains,\n            Overwrite = overwrite\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Saves attachments from multiple messages using shared Mailozaurr filtering and batching logic.\")]\n    public Task<SaveAttachmentsManyResult> mail_attachments_save_many(\n        [Description(\"The profile identifier to query.\")] string profileId,\n        [Description(\"The provider-specific message identifiers that own the attachments.\")] string[] messageIds,\n        [Description(\"The destination path or directory on the server filesystem.\")] string destinationPath,\n        [Description(\"Optional mailbox identifier for providers that support multiple mailboxes.\")] string? mailboxId = null,\n        [Description(\"Optional folder identifier when the provider requires folder scoping.\")] string? folderId = null,\n        [Description(\"Optional explicit attachment identifiers to save.\")] string[]? attachmentIds = null,\n        [Description(\"Optional case-insensitive file-name filter.\")] string? fileNameContains = null,\n        [Description(\"Optional case-insensitive content-type filter.\")] string? contentTypeContains = null,\n        [Description(\"When true, allows existing destination files to be overwritten.\")] bool overwrite = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Read.SaveAttachmentsManyAsync(new SaveAttachmentsManyRequest {\n            ProfileId = profileId,\n            MailboxId = mailboxId,\n            FolderId = folderId,\n            MessageIds = messageIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            DestinationPath = destinationPath,\n            AttachmentIds = attachmentIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()).ToList() ?? new List<string>(),\n            FileNameContains = fileNameContains,\n            ContentTypeContains = contentTypeContains,\n            Overwrite = overwrite\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Sends or queues a message using a configured Mailozaurr profile. Queueing is the default unless sendNow is true.\")]\n    public Task<SendResult> mail_send(\n        [Description(\"The profile identifier to use for sending.\")] string profileId,\n        [Description(\"Primary recipient email addresses.\")] string[] to,\n        [Description(\"Optional subject line.\")] string? subject = null,\n        [Description(\"Optional plain text body.\")] string? textBody = null,\n        [Description(\"Optional HTML body.\")] string? htmlBody = null,\n        [Description(\"Optional CC recipient email addresses.\")] string[]? cc = null,\n        [Description(\"Optional BCC recipient email addresses.\")] string[]? bcc = null,\n        [Description(\"Optional Reply-To recipient email addresses.\")] string[]? replyTo = null,\n        [Description(\"Optional From email address override.\")] string? from = null,\n        [Description(\"Optional attachment file paths on the server filesystem.\")] string[]? attachmentPaths = null,\n        [Description(\"When true, sends immediately instead of preferring the queue.\")] bool sendNow = false,\n        CancellationToken cancellationToken = default) =>\n        _application.Send.SendAsync(new SendMessageRequest {\n            ProfileId = profileId,\n            PreferQueue = !sendNow,\n            RequireImmediateSend = sendNow,\n            Message = BuildDraftMessage(profileId, to, subject, textBody, htmlBody, cc, bcc, replyTo, from, attachmentPaths)\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists reusable drafts stored by Mailozaurr.\")]\n    public Task<IReadOnlyList<MailDraft>> mail_draft_list(CancellationToken cancellationToken = default) =>\n        _application.Drafts.GetDraftsAsync(cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists reusable drafts stored by Mailozaurr using a lightweight projection.\")]\n    public Task<IReadOnlyList<MailDraftCompact>> mail_draft_compact_list(CancellationToken cancellationToken = default) =>\n        _application.Drafts.GetDraftsCompactAsync(cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Gets a reusable draft by identifier.\")]\n    public async Task<MailDraft> mail_draft_get(\n        [Description(\"The stored draft identifier to retrieve.\")] string draftId,\n        CancellationToken cancellationToken = default) {\n        var draft = await _application.Drafts.GetDraftAsync(draftId, cancellationToken).ConfigureAwait(false);\n        return draft ?? throw new InvalidOperationException($\"Draft '{draftId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets a reusable draft by identifier using a lightweight projection.\")]\n    public async Task<MailDraftCompact> mail_draft_compact_get(\n        [Description(\"The stored draft identifier to retrieve.\")] string draftId,\n        CancellationToken cancellationToken = default) {\n        var draft = await _application.Drafts.GetDraftCompactAsync(draftId, cancellationToken).ConfigureAwait(false);\n        return draft ?? throw new InvalidOperationException($\"Draft '{draftId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Creates or updates a reusable draft using the shared Mailozaurr draft store.\")]\n    public Task<OperationResult> mail_draft_save(\n        [Description(\"The stable draft identifier to create or update.\")] string draftId,\n        [Description(\"A human-readable draft name.\")] string name,\n        [Description(\"The profile identifier that should eventually send this draft.\")] string profileId,\n        [Description(\"Primary recipient email addresses.\")] string[] to,\n        [Description(\"Optional subject line.\")] string? subject = null,\n        [Description(\"Optional plain text body.\")] string? textBody = null,\n        [Description(\"Optional HTML body.\")] string? htmlBody = null,\n        [Description(\"Optional CC recipient email addresses.\")] string[]? cc = null,\n        [Description(\"Optional BCC recipient email addresses.\")] string[]? bcc = null,\n        [Description(\"Optional Reply-To recipient email addresses.\")] string[]? replyTo = null,\n        [Description(\"Optional From email address override.\")] string? from = null,\n        [Description(\"Optional attachment file paths on the server filesystem.\")] string[]? attachmentPaths = null,\n        CancellationToken cancellationToken = default) =>\n        _application.Drafts.SaveAsync(new MailDraft {\n            Id = draftId,\n            Name = name,\n            Message = BuildDraftMessage(profileId, to, subject, textBody, htmlBody, cc, bcc, replyTo, from, attachmentPaths)\n        }, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Deletes a reusable draft from the shared Mailozaurr draft store.\")]\n    public Task<OperationResult> mail_draft_delete(\n        [Description(\"The stored draft identifier to delete.\")] string draftId,\n        CancellationToken cancellationToken = default) =>\n        _application.Drafts.DeleteAsync(draftId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Imports a draft JSON file into the shared Mailozaurr draft store.\")]\n    public async Task<MailDraft> mail_draft_import(\n        [Description(\"The draft file path on the server filesystem.\")] string path,\n        [Description(\"Optional replacement draft identifier to use after import.\")] string? draftId = null,\n        [Description(\"Optional replacement draft name to use after import.\")] string? name = null,\n        CancellationToken cancellationToken = default) {\n        var draft = await _application.DraftExchange.LoadAsync(path, cancellationToken).ConfigureAwait(false);\n        if (!string.IsNullOrWhiteSpace(draftId)) {\n            draft.Id = draftId.Trim();\n        }\n        if (!string.IsNullOrWhiteSpace(name)) {\n            draft.Name = name.Trim();\n        }\n\n        var result = await _application.Drafts.SaveAsync(draft, cancellationToken).ConfigureAwait(false);\n        if (!result.Succeeded) {\n            throw new InvalidOperationException(result.Message ?? $\"Draft '{draft.Id}' could not be imported.\");\n        }\n\n        return draft;\n    }\n\n    [McpServerTool]\n    [Description(\"Exports a stored Mailozaurr draft to a draft JSON file.\")]\n    public async Task<OperationResult> mail_draft_export(\n        [Description(\"The stored draft identifier to export.\")] string draftId,\n        [Description(\"The destination draft file path on the server filesystem.\")] string path,\n        CancellationToken cancellationToken = default) {\n        var draft = await _application.Drafts.GetDraftAsync(draftId, cancellationToken).ConfigureAwait(false);\n        if (draft == null) {\n            throw new InvalidOperationException($\"Draft '{draftId}' was not found.\");\n        }\n\n        await _application.DraftExchange.SaveAsync(path, draft, cancellationToken).ConfigureAwait(false);\n        return OperationResult.Success(\"Draft exported.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Sends or queues a previously stored Mailozaurr draft. Queueing is the default unless sendNow is true.\")]\n    public async Task<SendResult> mail_draft_send(\n        [Description(\"The stored draft identifier to send.\")] string draftId,\n        [Description(\"When true, sends immediately instead of preferring the queue.\")] bool sendNow = false,\n        CancellationToken cancellationToken = default) {\n        var draft = await _application.Drafts.GetDraftAsync(draftId, cancellationToken).ConfigureAwait(false);\n        if (draft == null) {\n            throw new InvalidOperationException($\"Draft '{draftId}' was not found.\");\n        }\n\n        return await _application.Send.SendAsync(new SendMessageRequest {\n            ProfileId = draft.Message.ProfileId,\n            PreferQueue = !sendNow,\n            RequireImmediateSend = sendNow,\n            Message = CloneDraftMessage(draft.Message)\n        }, cancellationToken).ConfigureAwait(false);\n    }\n\n    [McpServerTool]\n    [Description(\"Lists outbound messages currently waiting in Mailozaurr's pending queue.\")]\n    public Task<IReadOnlyList<QueuedMessageSummary>> mail_queue_list(CancellationToken cancellationToken = default) =>\n        _application.Queue.ListAsync(cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Lists outbound messages currently waiting in Mailozaurr's pending queue using a lightweight projection.\")]\n    public Task<IReadOnlyList<QueuedMessageCompact>> mail_queue_compact_list(CancellationToken cancellationToken = default) =>\n        _application.Queue.ListCompactAsync(cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Gets a queued outbound message by identifier.\")]\n    public async Task<QueuedMessageSummary> mail_queue_get(\n        [Description(\"The queued message identifier to inspect.\")] string messageId,\n        CancellationToken cancellationToken = default) {\n        var message = await _application.Queue.GetAsync(messageId, cancellationToken).ConfigureAwait(false);\n        return message ?? throw new InvalidOperationException($\"Queued message '{messageId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Gets a queued outbound message by identifier using a lightweight projection.\")]\n    public async Task<QueuedMessageCompact> mail_queue_compact_get(\n        [Description(\"The queued message identifier to inspect.\")] string messageId,\n        CancellationToken cancellationToken = default) {\n        var message = await _application.Queue.GetCompactAsync(messageId, cancellationToken).ConfigureAwait(false);\n        return message ?? throw new InvalidOperationException($\"Queued message '{messageId}' was not found.\");\n    }\n\n    [McpServerTool]\n    [Description(\"Removes a queued outbound message without sending it.\")]\n    public Task<OperationResult> mail_queue_remove(\n        [Description(\"The queued message identifier to remove.\")] string messageId,\n        CancellationToken cancellationToken = default) =>\n        _application.Queue.RemoveAsync(messageId, cancellationToken);\n\n    [McpServerTool]\n    [Description(\"Processes all due queued outbound messages.\")]\n    public Task<QueueProcessResult> mail_queue_process(CancellationToken cancellationToken = default) =>\n        _application.Queue.ProcessAsync(cancellationToken);\n\n    private static DraftMessage BuildDraftMessage(\n        string profileId,\n        IEnumerable<string> to,\n        string? subject,\n        string? textBody,\n        string? htmlBody,\n        IEnumerable<string>? cc,\n        IEnumerable<string>? bcc,\n        IEnumerable<string>? replyTo,\n        string? from,\n        IEnumerable<string>? attachmentPaths) {\n        var draft = new DraftMessage {\n            ProfileId = profileId,\n            Subject = subject,\n            TextBody = textBody,\n            HtmlBody = htmlBody\n        };\n\n        if (!string.IsNullOrWhiteSpace(from)) {\n            draft.From = new MessageRecipient {\n                Address = from.Trim()\n            };\n        }\n\n        AddRecipients(draft.To, to);\n        AddRecipients(draft.Cc, cc);\n        AddRecipients(draft.Bcc, bcc);\n        AddRecipients(draft.ReplyTo, replyTo);\n\n        if (attachmentPaths != null) {\n            foreach (var attachmentPath in attachmentPaths.Where(path => !string.IsNullOrWhiteSpace(path))) {\n                draft.Attachments.Add(new DraftAttachment {\n                    Path = attachmentPath.Trim()\n                });\n            }\n        }\n\n        return draft;\n    }\n\n    private static DraftMessage CloneDraftMessage(DraftMessage draft) => new() {\n        ProfileId = draft.ProfileId,\n        From = draft.From == null ? null : new MessageRecipient {\n            Name = draft.From.Name,\n            Address = draft.From.Address\n        },\n        To = draft.To.Select(ToRecipientCopy).ToList(),\n        Cc = draft.Cc.Select(ToRecipientCopy).ToList(),\n        Bcc = draft.Bcc.Select(ToRecipientCopy).ToList(),\n        ReplyTo = draft.ReplyTo.Select(ToRecipientCopy).ToList(),\n        Subject = draft.Subject,\n        TextBody = draft.TextBody,\n        HtmlBody = draft.HtmlBody,\n        Headers = new Dictionary<string, string>(draft.Headers, StringComparer.OrdinalIgnoreCase),\n        Attachments = draft.Attachments.Select(attachment => new DraftAttachment {\n            Path = attachment.Path,\n            FileName = attachment.FileName,\n            ContentType = attachment.ContentType,\n            IsInline = attachment.IsInline,\n            ContentId = attachment.ContentId\n        }).ToList()\n    };\n\n    private static void AddRecipients(ICollection<MessageRecipient> destination, IEnumerable<string>? addresses) {\n        if (addresses == null) {\n            return;\n        }\n\n        foreach (var address in addresses.Where(value => !string.IsNullOrWhiteSpace(value))) {\n            destination.Add(new MessageRecipient {\n                Address = address.Trim()\n            });\n        }\n    }\n\n    private static MessageRecipient ToRecipientCopy(MessageRecipient recipient) => new() {\n        Name = recipient.Name,\n        Address = recipient.Address\n    };\n\n    private static MailMessageActionPlanBatchQuery? BuildBatchQuery(IReadOnlyList<string>? planNames, IReadOnlyList<string>? profileIds, IReadOnlyList<string>? actions, string? sortBy, bool descending) {\n        var normalizedPlanNames = (planNames ?? Array.Empty<string>())\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var normalizedProfileIds = (profileIds ?? Array.Empty<string>())\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var normalizedActions = (actions ?? Array.Empty<string>())\n            .Where(value => !string.IsNullOrWhiteSpace(value))\n            .Select(value => value.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToList();\n        var hasExplicitSort = !string.IsNullOrWhiteSpace(sortBy);\n        var parsedSortBy = ParseBatchSortBy(sortBy);\n\n        if (normalizedPlanNames.Count == 0 && normalizedProfileIds.Count == 0 && normalizedActions.Count == 0 && !hasExplicitSort && parsedSortBy == MailMessageActionPlanBatchSortBy.Id && !descending) {\n            return null;\n        }\n\n        return new MailMessageActionPlanBatchQuery {\n            PlanNames = normalizedPlanNames,\n            ProfileIds = normalizedProfileIds,\n            Actions = normalizedActions,\n            SortBy = parsedSortBy,\n            Descending = descending\n        };\n    }\n\n    private static MailMessageActionPlanBatchSortBy ParseBatchSortBy(string? rawSortBy) {\n        if (string.IsNullOrWhiteSpace(rawSortBy)) {\n            return MailMessageActionPlanBatchSortBy.Id;\n        }\n\n        return rawSortBy.Trim().ToLowerInvariant() switch {\n            \"id\" => MailMessageActionPlanBatchSortBy.Id,\n            \"name\" => MailMessageActionPlanBatchSortBy.Name,\n            \"plans\" or \"plan-count\" => MailMessageActionPlanBatchSortBy.PlanCount,\n            \"ready\" or \"ready-count\" => MailMessageActionPlanBatchSortBy.ReadyPlanCount,\n            \"updated\" or \"updated-at\" => MailMessageActionPlanBatchSortBy.UpdatedAt,\n            \"actions\" or \"action-types\" => MailMessageActionPlanBatchSortBy.ActionTypeCount,\n            _ => throw new InvalidOperationException($\"Unsupported batch sort '{rawSortBy}'.\")\n        };\n    }\n\n    private static MailProfileConnectionTestScope ParseConnectionTestScope(string? rawScope) {\n        if (string.IsNullOrWhiteSpace(rawScope)) {\n            return MailProfileConnectionTestScope.Auto;\n        }\n\n        if (Enum.TryParse<MailProfileConnectionTestScope>(rawScope.Trim(), ignoreCase: true, out var scope)) {\n            return scope;\n        }\n\n        throw new InvalidOperationException($\"Unsupported connection test scope '{rawScope}'.\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/Mcp/McpServerHost.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Cli.Mcp;\n\ninternal static class McpServerHost {\n    public static async Task RunAsync(MailApplication application, CancellationToken cancellationToken = default) {\n        ArgumentNullException.ThrowIfNull(application);\n\n        var builder = Host.CreateEmptyApplicationBuilder(settings: null);\n        builder.Services.AddSingleton(application);\n        builder.Services\n            .AddMcpServer()\n            .WithStdioServerTransport()\n            .WithToolsFromAssembly(typeof(MailMcpTools).Assembly);\n\n        using var host = builder.Build();\n        await host.RunAsync(cancellationToken).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Cli/Program.cs",
    "content": "using Mailozaurr.Application;\nusing Mailozaurr.Cli;\n\nreturn await CliRunner.RunAsync(\n    args,\n    Console.Out,\n    Console.Error,\n    builderFactory: options => new MailApplicationBuilder(options),\n    input: Console.In);\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/AcquireGoogleTokenInteractive.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Obtains an OAuth token for Gmail using the interactive flow.\n/// </summary>\npublic static class AcquireGoogleTokenInteractive {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string gmailAccount = \"user@gmail.com\";\n        string clientId = \"your-client-id\";\n        string clientSecret = \"your-client-secret\";\n        string[] scopes = { \"https://mail.google.com/\" };\n\n        // === EXAMPLE ===\n        try {\n            var cred = await OAuthHelpers.AcquireGoogleTokenInteractiveAsync(\n                gmailAccount,\n                clientId,\n                clientSecret,\n                scopes);\n            Console.WriteLine($\"Token acquired for {cred.UserName}: {cred.AccessToken.Substring(0,5)}...\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"AcquireGoogleTokenInteractive Example Error: {ex.Message}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/AcquireO365TokenDeviceCode.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Obtains an OAuth token for Office 365 using the device code flow.\n/// </summary>\npublic static class AcquireO365TokenDeviceCode {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        string clientId = \"your-client-id\";\n        string tenantId = \"your-tenant-id\";\n        string[] scopes = { \"Mail.ReadWrite\", \"Mail.Send\" };\n\n        try {\n            var cred = await OAuthHelpers.AcquireO365TokenDeviceCodeAsync(\n                clientId,\n                tenantId,\n                scopes,\n                result => {\n                    Console.WriteLine(result.Message);\n                    return Task.CompletedTask;\n                });\n            Console.WriteLine($\"Token acquired for {cred.UserName}: {cred.AccessToken.Substring(0,5)}...\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"AcquireO365TokenDeviceCode Example Error: {ex.Message}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/AcquireO365TokenInteractive.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Obtains an OAuth token for Office 365 using the interactive flow.\n/// </summary>\npublic static class AcquireO365TokenInteractive {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string? login = \"user@example.com\";\n        string clientId = \"your-client-id\";\n        string tenantId = \"your-tenant-id\";\n        string redirectUri = \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n        string[] scopes = {\n            \"email\",\n            \"offline_access\",\n            \"https://outlook.office.com/IMAP.AccessAsUser.All\",\n            \"https://outlook.office.com/POP.AccessAsUser.All\",\n            \"https://outlook.office.com/SMTP.Send\"\n        };\n\n        // === EXAMPLE ===\n        try {\n            var cred = await OAuthHelpers.AcquireO365TokenInteractiveAsync(\n                login,\n                clientId,\n                tenantId,\n                redirectUri,\n                scopes);\n            Console.WriteLine($\"Token acquired for {cred.UserName}: {cred.AccessToken.Substring(0,5)}...\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"AcquireO365TokenInteractive Example Error: {ex.Message}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/BuildGraphUriExample.cs",
    "content": "using System;\n\n/// <summary>\n/// Example demonstrating usage of <see cref=\"GraphEndpoint\"/> with MicrosoftGraphUtils.\n/// </summary>\npublic static class BuildGraphUriExample {\n    public static void Run() {\n        string uri = Mailozaurr.MicrosoftGraphUtils.BuildGraphUri(Mailozaurr.GraphEndpoint.V1, \"/users/me/messages\");\n        Console.WriteLine(uri);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/DetectNonDeliveryReportExample.cs",
    "content": "using Mailozaurr;\nusing MimeKit;\nusing System;\nusing System.IO;\nusing System.Text;\n\n/// <summary>\n/// Demonstrates detecting a Non-Delivery Report from a MIME message.\n/// </summary>\npublic static class DetectNonDeliveryReportExample {\n    /// <summary>Runs the example.</summary>\n    public static void Run() {\n        const string raw = \"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXXX\\\"\\n\\n--XXXX\\nContent-Type: text/plain; charset=utf-8\\n\\nThis is the mail system at example.com\\n\\n--XXXX\\nContent-Type: message/delivery-status\\n\\nOriginal-Recipient: rfc822; orig@example.com\\nFinal-Recipient: rfc822; final@example.com\\nReporting-MTA: dns; mx.example.com\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\nStatus: 5.1.1\\nArrival-Date: Wed, 24 Jul 2024 10:00:00 +0000\\n\\n--XXXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        if (reports.Count > 0) {\n            foreach (var report in reports) {\n                Console.WriteLine($\"Detected NDR: {report.Type} with status {report.Status}\");\n            }\n        } else {\n            Console.WriteLine(\"No NDR detected.\");\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Examples/FetchGmailMessages.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example demonstrating how to list messages using the Gmail API.\n/// </summary>\npublic static class FetchGmailMessages {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string gmailAccount = \"user@gmail.com\";\n        string accessToken = \"ya29.your_token\"; // OAuth access token\n        int maxResults = 5; // limit number of messages\n\n        // === EXAMPLE ===\n        try {\n            var cred = new OAuthCredential {\n                UserName = gmailAccount,\n                AccessToken = accessToken,\n                ExpiresOn = DateTimeOffset.MaxValue\n            };\n            var client = new GmailApiClient(cred);\n            var messages = await client.ListAsync(\"me\", maxResults: maxResults);\n            Console.WriteLine($\"Gmail API: fetched {messages.Count} messages\");\n        } catch (GmailAuthenticationException ex) {\n            Console.WriteLine($\"Gmail API Authentication Error: {ex.Message}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"Gmail API Example Error: {ex.Message}\");\n        }\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/FetchImapMessages.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MailKit.Security;\nusing MimeKit;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example demonstrating how to search and fetch messages using IMAP.\n/// </summary>\npublic static class FetchImapMessages {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string server = \"imap.example.com\";\n        string username = \"user@example.com\";\n        string password = \"Pa55w0rd\";\n\n        // === EXAMPLE ===\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, 993, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n        var folder = client.GetFolder(\"Inbox/Reports\");\n        await folder.OpenAsync(FolderAccess.ReadWrite);\n        var query = SearchQuery.FromContains(\"microsoft.com\").And(SearchQuery.HeaderContains(\"Importance\", \"High\"));\n        var uids = await folder.SearchAsync(query);\n        foreach (var uid in uids) {\n            MimeMessage msg = await folder.GetMessageAsync(uid);\n            Console.WriteLine($\"{msg.Date.LocalDateTime}: {msg.Subject}\");\n            await folder.AddFlagsAsync(uid, MessageFlags.Deleted, true);\n        }\n        if (uids.Count > 0) {\n            await folder.ExpungeAsync();\n        }\n        await client.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/FetchPopMessages.cs",
    "content": "using MailKit.Net.Pop3;\nusing MailKit.Security;\nusing MimeKit;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example demonstrating how to fetch messages from a POP3 server.\n/// </summary>\npublic static class FetchPopMessages {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string server = \"pop.example.com\";\n        string username = \"user@example.com\";\n        string password = \"Pa55w0rd\";\n\n        // === EXAMPLE ===\n        using var client = new Pop3Client();\n        await client.ConnectAsync(server, 995, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n        for (int i = 0; i < client.Count; i++) {\n            MimeMessage msg = await client.GetMessageAsync(i);\n            Console.WriteLine($\"{msg.Date.LocalDateTime}: {msg.Subject}\");\n            await client.DeleteMessageAsync(i);\n        }\n        await client.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/GenerateTemporaryMailCrypto.cs",
    "content": "using System;\nusing Mailozaurr;\n\n/// <summary>\n/// Example showing how to create temporary cryptographic material.\n/// </summary>\npublic static class GenerateTemporaryMailCrypto\n{\n    /// <summary>Runs the example.</summary>\n    public static void Run()\n    {\n        using var keys = TemporaryPgpKeyPair.Create(outputDirectory: \"pgp\", deleteOnDispose: false);\n        Console.WriteLine($\"PGP public key: {keys.PublicKeyPath}\");\n        using var cert = TemporarySmimeCertificate.CreateSelfSigned(outputPath: \"cert.pfx\");\n        Console.WriteLine($\"S/MIME certificate: {cert.Subject}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/GenerateTemporaryPgpKeyPair.cs",
    "content": "using System;\nusing Mailozaurr;\n\n/// <summary>\n/// Example showing how to create a temporary PGP key pair.\n/// </summary>\npublic static class GenerateTemporaryPgpKeyPair\n{\n    /// <summary>Runs the example.</summary>\n    public static void Run()\n    {\n        using var keys = TemporaryPgpKeyPair.Create();\n        Console.WriteLine($\"Public key saved to: {keys.PublicKeyPath}\");\n        Console.WriteLine($\"Private key saved to: {keys.PrivateKeyPath}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/GenerateTemporarySmimeCertificate.cs",
    "content": "using System;\nusing Mailozaurr;\n\n/// <summary>\n/// Example showing how to create a temporary S/MIME certificate.\n/// </summary>\npublic static class GenerateTemporarySmimeCertificate\n{\n    /// <summary>Runs the example.</summary>\n    public static void Run()\n    {\n        using var cert = TemporarySmimeCertificate.CreateSelfSigned();\n        Console.WriteLine($\"Temporary certificate created: {cert.Subject}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/ImapIdleListenerExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Demonstrates how to use <see cref=\"Mailozaurr.ImapIdleListener\"/> to\n/// receive IMAP messages as they arrive.\n/// </summary>\npublic static class ImapIdleListenerExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        const string server = \"imap.example.com\";\n        const int port = 993;\n        const string username = \"user@example.com\";\n        const string password = \"Pa55w0rd\";\n\n        // === EXAMPLE ===\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, port, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n\n        var listener = new Mailozaurr.ImapIdleListener(client);\n        listener.MessageArrived += (s, msg) =>\n            Console.WriteLine($\"New message: {msg.Message.Subject}\");\n\n        await listener.StartAsync();\n\n        Console.WriteLine(\"Press any key to stop...\");\n        Console.ReadKey();\n\n        listener.Stop();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/ImapIdleListenerFilteredExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing MailKit.Search;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Demonstrates filtering when using <see cref=\"Mailozaurr.ImapIdleListener\"/>.\n/// </summary>\npublic static class ImapIdleListenerFilteredExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        const string server = \"imap.example.com\";\n        const int port = 993;\n        const string username = \"user@example.com\";\n        const string password = \"Pa55w0rd\";\n\n        // === EXAMPLE ===\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, port, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n\n        // Listen only for messages from a specific sender\n        var query = SearchQuery.FromContains(\"alice@example.com\");\n        var listener = new Mailozaurr.ImapIdleListener(client, searchQuery: query);\n        listener.MessageArrived += (s, msg) =>\n            Console.WriteLine($\"New message from Alice: {msg.Message.Subject}\");\n\n        await listener.StartAsync();\n\n        Console.WriteLine(\"Press any key to stop...\");\n        Console.ReadKey();\n\n        listener.Stop();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/Mailozaurr.Examples.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<OutputType>Exe</OutputType>\n\t\t<TargetFramework>net8.0</TargetFramework>\n\t\t<ImplicitUsings>enable</ImplicitUsings>\n\t\t<Nullable>enable</Nullable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\Mailozaurr\\Mailozaurr.csproj\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/NonDeliveryReportServiceExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Demonstrates using the non-delivery report service with IMAP.\n/// </summary>\npublic static class NonDeliveryReportServiceExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        string server = \"imap.example.com\";\n        string username = \"user@example.com\";\n        string password = \"Pa55w0rd\";\n\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, 993, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n\n        var repository = new FileSentMessageRepository(\"sent.json\");\n        var resolver = new SendLogResolver(repository);\n        var service = new ImapNonDeliveryReportService(client, resolver);\n        var results = await service.SearchAsync(DateTime.UtcNow.AddDays(-7));\n        foreach (NonDeliveryReportResult result in results) {\n            Console.WriteLine($\"NDR for {result.Report.FinalRecipient}: {result.Report.Type}\");\n        }\n        await client.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/ParseNonDeliveryReportExample.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Collections.Generic;\n\n/// <summary>\n/// Demonstrates parsing of a Non-Delivery Report from DSN headers.\n/// </summary>\npublic static class ParseNonDeliveryReportExample {\n    /// <summary>Runs the example.</summary>\n    public static void Run() {\n        var headers = new Dictionary<string, string> {\n            [\"Original-Recipient\"] = \"rfc822; user@example.com\",\n            [\"Final-Recipient\"] = \"rfc822; user@example.com\",\n            [\"Reporting-MTA\"] = \"dns; mx.example.com\",\n            [\"Diagnostic-Code\"] = \"smtp; 550 5.1.1 User unknown\",\n            [\"Status\"] = \"5.1.1\",\n            [\"Arrival-Date\"] = DateTimeOffset.UtcNow.ToString(\"R\"),\n            [\"Last-Attempt-Date\"] = DateTimeOffset.UtcNow.ToString(\"R\")\n        };\n\n        var ndr = NonDeliveryReport.FromHeaders(headers);\n        Console.WriteLine($\"NDR Type: {ndr.Type}, Last Attempt: {ndr.LastAttemptDate?.ToString() ?? \"n/a\"}, Status: {ndr.Status}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/PendingMessageRepositoryExample.cs",
    "content": "using MimeKit;\nusing System.IO;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\n/// <summary>\n/// Example demonstrating how to persist and retrieve pending messages.\n/// </summary>\npublic static class PendingMessageRepositoryExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        var dir = Path.Combine(Path.GetTempPath(), \"pending\");\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir };\n        var repository = new FilePendingMessageRepository(options);\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"Pending\";\n        message.Body = new TextPart(\"plain\") { Text = \"Hello\" };\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n        var record = new PendingMessageRecord {\n            MessageId = message.MessageId ?? MimeKit.Utils.MimeUtils.GenerateMessageId(),\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            Timestamp = DateTimeOffset.UtcNow,\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(5)\n        };\n        await repository.SaveAsync(record);\n\n        await foreach (var r in repository.GetAllAsync()) {\n            Console.WriteLine($\"Pending: {r.MessageId}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/Program.cs",
    "content": "using System.Threading.Tasks;\n\nclass Program {\n    static async Task Main(string[] args) {\n        // Uncomment the example you want to run:\n\n        // SendEmailGmail.Run();\n        // await AcquireGoogleTokenInteractive.RunAsync();\n        // await AcquireO365TokenInteractive.RunAsync();\n        await SendEmailGraphClientSecret.RunAsync();\n        // await SendEmailGraphCertificate.RunAsync();\n        await SendEmailMailgun.RunAsync();\n        // await SendEmailSmtpAsync.RunAsync();\n        // await FetchImapMessages.RunAsync();\n        // await FetchPopMessages.RunAsync();\n    }\n}\n\n// See individual example files for usage of Mailozaurr.\n// Examples:\n//   - SmtpGmailExample.cs\n//   - GraphClientSecretExample.cs\n//   - GraphCertificateExample.cs\n//   - FetchImapMessages.cs\n//   - FetchPopMessages.cs\n//   - SendEmailGraphClientSecret.cs\n//   - SendEmailGraphCertificate.cs\n//   - AcquireGoogleTokenInteractive.cs\n//   - AcquireO365TokenInteractive.cs\n//   - SendEmailMailgun.cs\n//   - SendEmailSmtpAsync.cs\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/RetrieveAndCorrelateNonDeliveryReportsExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Demonstrates retrieving non-delivery reports and correlating them with sent messages.\n/// </summary>\npublic static class RetrieveAndCorrelateNonDeliveryReportsExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        string server = \"imap.example.com\";\n        string username = \"user@example.com\";\n        string password = \"Pa55w0rd\";\n\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, 993, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n\n        var repository = new FileSentMessageRepository(\"sent.json\");\n        var resolver = new SendLogResolver(repository);\n        IList<NonDeliveryReport> reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            since: DateTime.UtcNow.AddDays(-7));\n        foreach (var report in reports) {\n            var match = await resolver.ResolveAsync(report);\n            if (match != null) {\n                Console.WriteLine($\"NDR for {report.FinalRecipient} matches message '{match.Subject}'\");\n            }\n        }\n\n        await client.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/RetrieveDmarcReportsExample.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.DmarcReports;\nusing MailKit.Net.Imap;\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Examples;\n\npublic static class RetrieveDmarcReportsExample {\n    public static async Task RunAsync(ImapClient client) {\n        var reports = await MailboxSearcher.SearchDmarcReportsAsync(client, \"INBOX\", since: DateTime.UtcNow.AddDays(-7), domain: \"example.com\");\n        foreach (var report in reports) {\n            foreach (var att in report.Attachments) {\n                using (att) {\n                    DomainDetective.Process(att.Content, att.Name);\n                }\n            }\n        }\n    }\n}\n\npublic static class DomainDetective {\n    public static void Process(Stream zip, string name) {\n        // Placeholder for integration with Domain Detective analysis\n        Console.WriteLine($\"Processing {name}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SearchBodyContainsExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\n/// <summary>\n/// Example demonstrating how to search message bodies.\n/// </summary>\npublic static class SearchBodyContainsExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        string user = \"user@example.com\";\n        string pass = \"Pa55w0rd\";\n\n        using var imap = new ImapClient();\n        await imap.ConnectAsync(\"imap.example.com\", 993, SecureSocketOptions.SslOnConnect);\n        await imap.AuthenticateAsync(user, pass);\n        var imapMessages = await MailboxSearcher.SearchImapAsync(imap, bodyContains: \"invoice\");\n        foreach (var m in imapMessages) Console.WriteLine($\"IMAP: {m.Message.Subject}\");\n        await imap.DisconnectAsync(true);\n\n        using var pop = new Pop3Client();\n        await pop.ConnectAsync(\"pop.example.com\", 995, SecureSocketOptions.SslOnConnect);\n        await pop.AuthenticateAsync(user, pass);\n        var popMessages = await MailboxSearcher.SearchPop3Async(pop, bodyContains: \"invoice\");\n        foreach (var m in popMessages) Console.WriteLine($\"POP3: {m.Message.Subject}\");\n        await pop.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SearchNonDeliveryReportsExample.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Demonstrates searching for Non-Delivery Reports in an IMAP mailbox.\n/// </summary>\npublic static class SearchNonDeliveryReportsExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string server = \"imap.example.com\";\n        string username = \"user@example.com\";\n        string password = \"Pa55w0rd\";\n\n        // === EXAMPLE ===\n        using var client = new ImapClient();\n        await client.ConnectAsync(server, 993, SecureSocketOptions.SslOnConnect);\n        await client.AuthenticateAsync(username, password);\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            folder: null,\n            since: DateTime.UtcNow.AddDays(-7),\n            recipientContains: \"user@example.com\");\n        foreach (NonDeliveryReport report in reports) {\n            Console.WriteLine($\"NDR for {report.FinalRecipient}: {report.Type}\");\n        }\n        await client.DisconnectAsync(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailAttachments.cs",
    "content": "using Mailozaurr;\n\nusing MimeKit;\nusing Mailozaurr.Definitions;\n\npublic static class SendEmailAttachments\n\n{\n\n    public static void Run()\n\n    {\n\n        var smtp = new Smtp();\n\n        smtp.From = \"sender@example.com\";\n\n        smtp.To = new[] { \"recipient@example.com\" };\n\n        smtp.Subject = \"Attachment Demo\";\n\n        smtp.TextBody = \"Check attachments\";\n\n        smtp.Attachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(\"C:\\\\Temp\\\\report.pdf\") };\n\n        var part = new MimePart(\"text/plain\")\n\n        {\n\n            Content = new MimeContent(new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(\"Hello from memory\"))),\n\n            FileName = \"Memory.txt\"\n\n        };\n\n        smtp.Attachments.Add(new MimeEntityAttachmentDescriptor(part));\n\n        smtp.InlineAttachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(\"C:\\\\Temp\\\\logo.png\") };\n\n        smtp.Connect(\"smtp.example.com\", 25);\n\n        smtp.Send();\n\n        smtp.Disconnect();\n\n    }\n\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailGmail.cs",
    "content": "using Mailozaurr;\nusing MailKit.Security;\nusing System;\n\n/// <summary>\n/// Sends an email using Gmail SMTP and an app password.\n/// </summary>\npublic static class SendEmailGmail {\n    /// <summary>Runs the example.</summary>\n    public static void Run() {\n        // === CONFIGURATION ===\n        string gmailAddress = \"youraddress@gmail.com\";\n        string gmailAppPassword = \"your_app_password\"; // Use Gmail App Password\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            var smtp = new Smtp();\n            smtp.From = gmailAddress;\n            smtp.To = new[] { recipient };\n            smtp.Subject = \"Test Email via Gmail SMTP\";\n            smtp.HtmlBody = \"<b>Hello from Mailozaurr via Gmail SMTP!</b>\";\n            var connectResult = smtp.Connect(\"smtp.gmail.com\", 587, SecureSocketOptions.StartTls);\n            if (!connectResult.Status)\n                throw new Exception($\"SMTP Connect failed: {connectResult.Error}\");\n            var authResult = smtp.Authenticate(gmailAddress, gmailAppPassword, false);\n            if (!authResult.Status)\n                throw new Exception($\"SMTP Auth failed: {authResult.Error}\");\n            var sendResult = smtp.Send();\n            Console.WriteLine(sendResult.Status ? \"Gmail SMTP: Email sent!\" : $\"Gmail SMTP: Failed: {sendResult.Error}\");\n            smtp.Disconnect();\n        } catch (Exception ex) {\n            Console.WriteLine($\"Gmail SMTP Example Error: {ex.Message}\");\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailGmailApi.cs",
    "content": "using Mailozaurr;\nusing MimeKit;\nusing System;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Sends a simple message using the Gmail API.\n/// </summary>\npublic static class SendEmailGmailApi {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string gmailAccount = \"user@gmail.com\";\n        string accessToken = \"ya29.your_token\"; // OAuth access token\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            var cred = new OAuthCredential {\n                UserName = gmailAccount,\n                AccessToken = accessToken,\n                ExpiresOn = DateTimeOffset.MaxValue\n            };\n            var client = new GmailApiClient(cred);\n            var message = new MimeMessage();\n            message.From.Add(MailboxAddress.Parse(gmailAccount));\n            message.To.Add(MailboxAddress.Parse(recipient));\n            message.Subject = \"Gmail API Test\";\n            message.Body = new TextPart(\"plain\") { Text = \"Hello from Mailozaurr via Gmail API!\" };\n            var sent = await client.SendAsync(\"me\", message);\n            Console.WriteLine($\"Gmail API: sent id {sent.Id}\");\n        } catch (GmailAuthenticationException ex) {\n            Console.WriteLine($\"Gmail API Authentication Error: {ex.Message}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"Gmail API Example Error: {ex.Message}\");\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailGraphCertificate.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Threading.Tasks;\n\npublic static class SendEmailGraphCertificate {\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string clientId = \"your-client-id\";\n        string tenantId = \"your-tenant-id\";\n        string certificatePath = \"path-to-your.pfx\";\n        string certificatePassword = \"your-cert-password\";\n        string sender = \"sender@yourtenant.onmicrosoft.com\";\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            var token = await OAuthHelpers.AcquireGraphCertificateTokenAsync(\n                clientId,\n                tenantId,\n                certificatePath,\n                certificatePassword);\n\n            using var graph = new Graph();\n            graph.From = sender;\n            graph.To = new[] { recipient };\n            graph.Subject = \"Test Email via Microsoft Graph (Certificate)\";\n            graph.HTML = \"<p>Hello from Mailozaurr via Microsoft Graph (Certificate)!</p>\";\n            graph.AccessToken = token.AccessToken;\n            graph.TokenType = token.TokenType;\n\n            var sendResult = await graph.SendMessageAsync();\n            Console.WriteLine(sendResult.Status\n                ? \"Graph (Certificate): Email sent!\"\n                : $\"Graph (Certificate): Failed: {sendResult.Error}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"Graph (Certificate) Example Error: {ex.Message}\");\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailGraphClientSecret.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Net;\nusing System.Threading.Tasks;\n\npublic static class SendEmailGraphClientSecret {\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string clientId = \"your-client-id\";\n        string tenantId = \"your-tenant-id\";\n        string clientSecret = \"your-client-secret\";\n        string sender = \"sender@yourtenant.onmicrosoft.com\";\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            using var graph = new Graph();\n            graph.From = sender;\n            graph.To = new[] { recipient };\n            graph.Subject = \"Test Email via Microsoft Graph (Client Secret)\";\n            graph.HTML = \"<p>Hello from Mailozaurr via Microsoft Graph (Client Secret)!</p>\";\n            // Authenticate: username = clientid@tenantid, password = client secret\n            var credential = new NetworkCredential($\"{clientId}@{tenantId}\", clientSecret);\n            graph.Authenticate(credential);\n            var connectResult = await graph.ConnectO365GraphAsync();\n            if (!connectResult.Status)\n                throw new Exception($\"Graph Connect failed: {connectResult.Error}\");\n            var sendResult = await graph.SendMessageAsync();\n            Console.WriteLine(sendResult.Status ? \"Graph (Client Secret): Email sent!\" : $\"Graph (Client Secret): Failed: {sendResult.Error}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"Graph (Client Secret) Example Error: {ex.Message}\");\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailGraphWithPolicy.cs",
    "content": "using System;\nusing System.Net;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Examples;\n\npublic static class SendEmailGraphWithPolicy\n{\n    public static async Task RunAsync()\n    {\n        var policy = new GraphSendPolicy\n        {\n            MaxConcurrency = 2,\n            MaxRetries = 4,\n            BaseDelayMs = 1000,\n            MaxDelayMs = 30000,\n            JitterMs = 500,\n            RetryOnTransient = true,\n            EnableSmtpFallback = true\n        };\n\n        using var graph = new Graph()\n            .WithSendPolicy(policy)\n            .WithSmtpFallback(() =>\n            {\n                var smtp = new Smtp();\n                smtp.Connect(\"smtp.office365.com\", 587);\n                smtp.Authenticate(new NetworkCredential(\"user@example.com\", \"password\"));\n                return smtp;\n            });\n\n        graph.From = \"sender@example.com\";\n        graph.To = new object[] { \"recipient@example.com\" };\n        graph.Subject = \"Graph policy demo\";\n        graph.HTML = \"<b>Hello</b>\";\n        graph.Authenticate(new NetworkCredential(\"clientid@tenant.onmicrosoft.com\", \"client-secret\"));\n\n        var connect = await graph.ConnectO365GraphAsync();\n        if (!connect.Status)\n        {\n            Console.WriteLine($\"Connect failed: {connect.Error}\");\n            return;\n        }\n        var send = await graph.SendMessageAsync();\n        Console.WriteLine($\"Status: {send.Status}, Message: {send.Message ?? send.Error}\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailHeadersGraph.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\n\npublic static class SendEmailHeadersGraph\n{\n    public static async Task RunAsync()\n    {\n        using var graph = new Graph();\n        graph.From = \"sender@example.com\";\n        graph.To = new[] { \"recipient@example.com\" };\n        graph.Subject = \"Graph Headers\";\n        graph.HTML = \"<p>Hello</p>\";\n        graph.Headers = new Dictionary<string, string>\n        {\n            [\"X-Tracking-ID\"] = \"abc123\",\n            [\"X-Source\"] = \"Mailozaurr\"\n        };\n        var cred = new NetworkCredential(\"clientid@tenant\", \"secret\");\n        graph.Authenticate(cred);\n        var connect = await graph.ConnectO365GraphAsync();\n        if (connect.Status)\n        {\n            await graph.SendMessageAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailHeadersSendGrid.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\n\npublic static class SendEmailHeadersSendGrid\n{\n    public static async Task RunAsync()\n    {\n        var sendGrid = new SendGridClient();\n        sendGrid.From = \"sender@example.com\";\n        sendGrid.To = new List<object> { \"recipient@example.com\" };\n        sendGrid.Subject = \"SendGrid Headers\";\n        sendGrid.Html = \"<p>Hello</p>\";\n        sendGrid.Headers = new Dictionary<string, string>\n        {\n            [\"X-Tracking-ID\"] = \"abc123\",\n            [\"X-Source\"] = \"Mailozaurr\"\n        };\n        sendGrid.Credentials = new NetworkCredential(\"apikey\", \"sg-key\");\n        await sendGrid.SendEmailAsync();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailHeadersSmtp.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Collections.Generic;\n\npublic static class SendEmailHeadersSmtp\n{\n    public static void Run()\n    {\n        var smtp = new Smtp();\n        smtp.From = \"sender@example.com\";\n        smtp.To = new[] { \"recipient@example.com\" };\n        smtp.Subject = \"SMTP Headers\";\n        smtp.TextBody = \"Hello\";\n        smtp.Headers = new Dictionary<string, string>\n        {\n            [\"X-Tracking-ID\"] = \"abc123\",\n            [\"X-Source\"] = \"Mailozaurr\"\n        };\n        smtp.Connect(\"smtp.example.com\", 25);\n        smtp.Send();\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailMailgun.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example sending an email using the <see cref=\"MailgunClient\"/>.\n/// </summary>\npublic static class SendEmailMailgun {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string apiKey = \"your-mailgun-api-key\";\n        string sender = \"sender@yourdomain.com\";\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            using var mailgun = new MailgunClient();\n            mailgun.From = sender;\n            mailgun.To = new[] { recipient }.ToList<object>();\n            mailgun.Subject = \"Test Email via Mailgun\";\n            mailgun.Text = \"Hello from Mailozaurr via Mailgun!\";\n            mailgun.Html = \"<p>Hello from Mailozaurr via Mailgun!</p>\";\n            mailgun.Credentials = new NetworkCredential(\"api\", apiKey);\n            using var cts = new CancellationTokenSource();\n            var result = await mailgun.SendEmailAsync(cts.Token);\n            Console.WriteLine(result.Status ? \"Mailgun: Email sent!\" : $\"Mailgun: Failed: {result.Error}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"Mailgun Example Error: {ex.Message}\");\n        }\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailPgp.cs",
    "content": "using System;\nusing Mailozaurr;\n\n/// <summary>\n/// Example demonstrating how to sign and encrypt email using PGP.\n/// </summary>\npublic static class SendEmailPgp {\n    /// <summary>Runs the example.</summary>\n    public static void Run() {\n        var smtp = new Smtp();\n        smtp.From = \"mimekit@example.com\";\n        smtp.To = new[] { \"mimekit@example.com\" };\n        smtp.Subject = \"PGP Test\";\n        smtp.TextBody = \"Hello\";\n        smtp.Connect(\"smtp.example.com\", 25);\n        smtp.CreateMessage();\n        smtp.PgpSignAndEncrypt(\"Examples/PGPKeys/mimekit.gpg.pub\", \"Examples/PGPKeys/mimekit.gpg.sec\", \"no.secret\", false);\n        smtp.Send();\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailRemoteImages.cs",
    "content": "using Mailozaurr;\n\n/// <summary>\n/// Example showing how to embed remote images automatically.\n/// </summary>\npublic static class SendEmailRemoteImages\n{\n    public static void Run()\n    {\n        var smtp = new Smtp { AutoEmbedRemoteImages = true };\n        smtp.From = \"sender@example.com\";\n        smtp.To = new[] { \"recipient@example.com\" };\n        smtp.Subject = \"Remote Images\";\n        smtp.HtmlBody = \"<img src=\\\"https://example.com/logo.png\\\">\";\n        smtp.Connect(\"smtp.example.com\", 25);\n        smtp.Send();\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailSasl.cs",
    "content": "using MailKit.Security;\nusing Mailozaurr;\n\n/// <summary>\n/// Example showing how to authenticate using SASL mechanisms.\n/// </summary>\npublic static class SendEmailSasl {\n    /// <summary>Runs the example.</summary>\n    public static void Run() {\n        var smtp = new Smtp();\n        smtp.From = \"sender@example.com\";\n        smtp.To = new[] { \"recipient@example.com\" };\n        smtp.Subject = \"Test with CRAM-MD5\";\n        smtp.HtmlBody = \"<b>Hello</b>\";\n        smtp.Connect(\"smtp.example.com\", 587, SecureSocketOptions.StartTls);\n        smtp.Authenticate(\"user\", \"pass\", false, AuthenticationMechanism.CramMd5);\n        smtp.Send();\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailSmtpAsync.cs",
    "content": "using MailKit.Security;\nusing Mailozaurr;\nusing System;\nusing System.Net;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example demonstrating asynchronous SMTP usage.\n/// </summary>\npublic static class SendEmailSmtpAsync {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        var smtp = new Smtp();\n        smtp.From = \"sender@example.com\";\n        smtp.To = new[] { \"recipient@example.com\" };\n        smtp.Subject = \"Async SMTP\";\n        smtp.TextBody = \"Hello from async SMTP\";\n        var connectResult = await smtp.ConnectAsync(\"smtp.example.com\", 25);\n        if (!connectResult.Status)\n            throw new Exception($\"SMTP Connect failed: {connectResult.Error}\");\n        var authResult = await smtp.AuthenticateAsync(new NetworkCredential(\"user\", \"pass\"));\n        if (!authResult.Status)\n            throw new Exception($\"SMTP Auth failed: {authResult.Error}\");\n        var sendResult = await smtp.SendAsync();\n        Console.WriteLine(sendResult.Status ? \"SMTP async: Email sent!\" : $\"SMTP async: Failed: {sendResult.Error}\");\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendEmailVerifyAttachments.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.Definitions;\n\npublic static class SendEmailVerifyAttachments\n{\n    public static void Run()\n    {\n        var smtp = new Smtp\n        {\n            From = \"sender@example.com\",\n            To = new[] { \"recipient@example.com\" },\n            Subject = \"Verify attachments demo\",\n            TextBody = \"body\",\n            Attachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(\"missing-file.txt\") }\n        };\n\n        smtp.Connect(\"smtp.example.com\", 25);\n        smtp.Send();\n        smtp.Disconnect();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SendTemplatedEmailSes.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// Example sending a templated email using <see cref=\"SesClient\"/>.\n/// </summary>\npublic static class SendTemplatedEmailSes {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        // === CONFIGURATION ===\n        string accessKey = \"your-access-key\";\n        string secretKey = \"your-secret-key\";\n        string sender = \"sender@example.com\";\n        string recipient = \"recipient@example.com\";\n\n        // === EXAMPLE ===\n        try {\n            using var ses = new SesClient();\n            ses.Credentials = new NetworkCredential(accessKey, secretKey);\n            ses.From = sender;\n            ses.To = new List<object> { recipient };\n            ses.TemplateName = \"MyTemplate\";\n            ses.TemplateData = new Dictionary<string, string> { [\"Name\"] = \"John\" };\n            using var cts = new CancellationTokenSource();\n            var result = await ses.SendTemplatedEmailAsync(cts.Token);\n            Console.WriteLine(result.Status ? \"SES: Email sent!\" : $\"SES: Failed: {result.Error}\");\n        } catch (Exception ex) {\n            Console.WriteLine($\"SES Example Error: {ex.Message}\");\n        }\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SmtpPendingMessageExample.cs",
    "content": "using Mailozaurr;\nusing System.IO;\n\n/// <summary>\n/// Example showing how failed sends are persisted for later retry.\n/// </summary>\npublic static class SmtpPendingMessageExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        var path = Path.Combine(Path.GetTempPath(), \"pending\");\n        var smtp = new Smtp { PendingMessagesPath = path };\n        smtp.From = \"sender@example.com\";\n        smtp.To = new[] { \"recipient@example.com\" };\n        smtp.Subject = \"Pending\";\n        smtp.TextBody = \"Hello\";\n        smtp.CreateMessage();\n\n        // Without a configured server this send will fail and the message will be queued\n        var result = await smtp.SendAsync();\n        Console.WriteLine($\"Queued message id: {result.MessageId}\");\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Examples/SmtpProcessPendingMessagesExample.cs",
    "content": "using Mailozaurr;\nusing System.IO;\n\n/// <summary>\n/// Example showing how to replay messages from a pending repository.\n/// </summary>\npublic static class SmtpProcessPendingMessagesExample {\n    /// <summary>Runs the example.</summary>\n    public static async Task RunAsync() {\n        var pendingPath = Path.Combine(Path.GetTempPath(), \"pending\");\n        var sentPath = Path.Combine(Path.GetTempPath(), \"sent.log\");\n        var sentRepo = new FileSentMessageRepository(sentPath);\n\n        var smtp = new Smtp {\n            PendingMessagesPath = pendingPath,\n            SentMessageRepository = sentRepo\n        };\n\n        await smtp.ProcessPendingMessagesAsync();\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/Definitions/EmlConversionResult.cs",
    "content": "﻿namespace Mailozaurr;\n\n/// <summary>\n/// Result information returned when converting an EML file to MSG.\n/// </summary>\n/// <remarks>\n/// Used by <see cref=\"EmailMessage.ConvertEmlToMsg(string[], string, bool)\"/> to report\n/// the path of the generated message and whether the operation succeeded.\n/// </remarks>\npublic class EmlConversionResult {\n    /// <summary>\n    /// Eml file path to file that was converted\n    /// </summary>\n    public string EmlFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Msg file path to file that was created\n    /// </summary>\n    public string MsgFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Status of the conversion\n    /// </summary>\n    public bool Status { get; set; }\n    /// <summary>\n    /// Error message if conversion failed\n    /// </summary>\n    public string? Error { get; set; }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Msg/Definitions/MsgConversionResult.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>\n/// Result information returned when converting a MSG file to EML.\n/// </summary>\n/// <remarks>\n/// Used by <see cref=\"EmailMessage.ConvertMsgToEml(string[], string, bool)\"/> to expose\n/// the source and target paths along with status information.\n/// </remarks>\npublic class MsgConversionResult {\n    /// <summary>\n    /// Msg file path to file that was converted\n    /// </summary>\n    public string MsgFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Eml file path to file that was created\n    /// </summary>\n    public string EmlFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Status of the conversion\n    /// </summary>\n    public bool Status { get; set; }\n    /// <summary>\n    /// Error message if conversion failed\n    /// </summary>\n    public string? Error { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/EmailMessage.cs",
    "content": "﻿using MsgKit;\nusing System.IO;\n\nnamespace Mailozaurr;\n\n/// <summary>\n/// Provides helper methods for converting EML messages to MSG format.\n/// </summary>\npublic static class EmailMessage {\n    /// <summary>\n    /// Converts one or more EML files to MSG format.\n    /// </summary>\n    /// <param name=\"emlFile\">Paths to the EML files to convert.</param>\n    /// <param name=\"outputFolder\">The folder where MSG files should be saved.</param>\n    /// <param name=\"force\">If set to <c>true</c>, existing MSG files will be overwritten.</param>\n    /// <returns>A collection of conversion results for each processed file.</returns>\n    public static IEnumerable<EmlConversionResult> ConvertEmlToMsg(string[] emlFile, string outputFolder, bool force) {\n        LoggingMessages.Logger.WriteVerbose($\"Converting {emlFile.Length} EML file(s) to MSG file(s)...\");\n        return ConvertFiles(emlFile, outputFolder, \".msg\", ConvertEmlToMsg, force);\n    }\n\n    /// <summary>\n    /// Converts a single EML file to MSG format.\n    /// </summary>\n    /// <param name=\"emlFile\">The input EML file.</param>\n    /// <param name=\"msgFile\">The target MSG file.</param>\n    /// <param name=\"force\">If set to <c>true</c>, an existing MSG file will be overwritten.</param>\n    /// <returns>The result of the conversion.</returns>\n    public static EmlConversionResult ConvertEmlToMsg(FileInfo emlFile, FileInfo msgFile, bool force) {\n        if (File.Exists(emlFile.FullName)) {\n            LoggingMessages.Logger.WriteVerbose(\"Processing EML file: {0}\", emlFile);\n            var dir = msgFile.DirectoryName;\n            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) {\n                Directory.CreateDirectory(dir);\n            }\n            var msgFilePath = msgFile.FullName;\n            var tempFile = CreateTempOutputPath(msgFile);\n            try {\n                Converter.ConvertEmlToMsg(emlFile.FullName, tempFile);\n                if (TryFinalizeConvertedFile(tempFile, msgFilePath, force, \"MSG file already exists\", out var finalizeError)) {\n                    return new EmlConversionResult() { EmlFile = emlFile.FullName, MsgFile = msgFilePath, Status = true };\n                }\n\n                return new EmlConversionResult() { EmlFile = emlFile.FullName, MsgFile = msgFilePath, Status = false, Error = finalizeError };\n            } catch (IOException ex) {\n                LoggingMessages.Logger.WriteWarning(\"Error converting EML to MSG: {0}\", ex.Message);\n                return new EmlConversionResult() { EmlFile = emlFile.FullName, MsgFile = msgFilePath, Status = false, Error = ex.Message };\n            } finally {\n                SafeDelete(tempFile);\n            }\n        }\n        return new EmlConversionResult() { EmlFile = emlFile.FullName, MsgFile = msgFile.FullName, Status = false, Error = \"EML file does not exist\" };\n    }\n\n    /// <summary>\n    /// Converts one or more MSG files to EML format.\n    /// </summary>\n    /// <param name=\"msgFile\">Paths to the MSG files to convert.</param>\n    /// <param name=\"outputFolder\">The folder where EML files should be saved.</param>\n    /// <param name=\"force\">If set to <c>true</c>, existing EML files will be overwritten.</param>\n    /// <returns>A collection of conversion results for each processed file.</returns>\n    public static IEnumerable<MsgConversionResult> ConvertMsgToEml(string[] msgFile, string outputFolder, bool force) {\n        LoggingMessages.Logger.WriteVerbose($\"Converting {msgFile.Length} MSG file(s) to EML file(s)...\");\n        return ConvertFiles(msgFile, outputFolder, \".eml\", ConvertMsgToEml, force);\n    }\n\n    /// <summary>\n    /// Converts a single MSG file to EML format.\n    /// </summary>\n    /// <param name=\"msgFile\">The input MSG file.</param>\n    /// <param name=\"emlFile\">The target EML file.</param>\n    /// <param name=\"force\">If set to <c>true</c>, an existing EML file will be overwritten.</param>\n    /// <returns>The result of the conversion.</returns>\n    public static MsgConversionResult ConvertMsgToEml(FileInfo msgFile, FileInfo emlFile, bool force) {\n        if (File.Exists(msgFile.FullName)) {\n            LoggingMessages.Logger.WriteVerbose(\"Processing MSG file: {0}\", msgFile);\n            var dir = emlFile.DirectoryName;\n            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) {\n                Directory.CreateDirectory(dir);\n            }\n            var emlFilePath = emlFile.FullName;\n            var tempFile = CreateTempOutputPath(emlFile);\n            try {\n                Converter.ConvertMsgToEml(msgFile.FullName, tempFile);\n                if (TryFinalizeConvertedFile(tempFile, emlFilePath, force, \"EML file already exists\", out var finalizeError)) {\n                    return new MsgConversionResult() { MsgFile = msgFile.FullName, EmlFile = emlFilePath, Status = true };\n                }\n\n                return new MsgConversionResult() { MsgFile = msgFile.FullName, EmlFile = emlFilePath, Status = false, Error = finalizeError };\n            } catch (IOException ex) {\n                LoggingMessages.Logger.WriteWarning(\"Error converting MSG to EML: {0}\", ex.Message);\n                return new MsgConversionResult() { MsgFile = msgFile.FullName, EmlFile = emlFilePath, Status = false, Error = ex.Message };\n            } finally {\n                SafeDelete(tempFile);\n            }\n        }\n        return new MsgConversionResult() { MsgFile = msgFile.FullName, EmlFile = emlFile.FullName, Status = false, Error = \"MSG file does not exist\" };\n    }\n\n    private static IEnumerable<TResult> ConvertFiles<TResult>(string[] inputFiles, string outputFolder, string targetExtension, Func<FileInfo, FileInfo, bool, TResult> converter, bool force) {\n        if (!Directory.Exists(outputFolder)) {\n            Directory.CreateDirectory(outputFolder);\n        }\n        foreach (var file in inputFiles) {\n            var fileName = Path.GetFileNameWithoutExtension(file);\n            var targetFile = Path.Combine(outputFolder, $\"{fileName}{targetExtension}\");\n            yield return converter(new FileInfo(file), new FileInfo(targetFile), force);\n        }\n    }\n\n    private static string CreateTempOutputPath(FileInfo targetFile) {\n        var directory = targetFile.DirectoryName ?? Path.GetTempPath();\n        var baseName = Path.GetFileNameWithoutExtension(targetFile.Name);\n        var extension = targetFile.Extension;\n        var tempName = $\"{baseName}.{Guid.NewGuid():N}{extension}.tmp\";\n        return Path.Combine(directory, tempName);\n    }\n\n    private static bool TryFinalizeConvertedFile(string tempFile, string targetFile, bool force, string existingFileError, out string? error) {\n        error = null;\n        if (!force) {\n            try {\n                File.Move(tempFile, targetFile);\n                return true;\n            } catch (IOException ex) {\n                error = File.Exists(targetFile) ? existingFileError : ex.Message;\n                return false;\n            } catch (UnauthorizedAccessException ex) {\n                error = File.Exists(targetFile) ? existingFileError : ex.Message;\n                return false;\n            }\n        }\n\n        return TryReplaceFile(tempFile, targetFile, out error);\n    }\n\n    private static bool TryReplaceFile(string sourceFile, string destinationFile, out string? error) {\n        error = null;\n        try {\n            File.Replace(sourceFile, destinationFile, null);\n            return true;\n        } catch (PlatformNotSupportedException) {\n            // Fall through to delete + move.\n        } catch (IOException) {\n            // Fall through to delete + move.\n        } catch (UnauthorizedAccessException) {\n            // Fall through to delete + move.\n        }\n\n        try {\n            if (File.Exists(destinationFile)) {\n                File.Delete(destinationFile);\n            }\n            File.Move(sourceFile, destinationFile);\n            return true;\n        } catch (Exception ex) {\n            error = ex.Message;\n            return false;\n        }\n    }\n\n    private static void SafeDelete(string path) {\n        try {\n            if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) {\n                File.Delete(path);\n            }\n        } catch (IOException) {\n            // Best effort cleanup only.\n        } catch (UnauthorizedAccessException) {\n            // Best effort cleanup only.\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/MailFileModels.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>Supported mail file formats.</summary>\npublic enum MailFileFormat {\n    /// <summary>Outlook MSG format.</summary>\n    Msg,\n    /// <summary>RFC 822 EML format.</summary>\n    Eml\n}\n\n/// <summary>Recipient classification.</summary>\npublic enum MailFileRecipientType {\n    /// <summary>Unknown or unspecified recipient type.</summary>\n    Unknown,\n    /// <summary>Primary recipient.</summary>\n    To,\n    /// <summary>Carbon copy recipient.</summary>\n    Cc,\n    /// <summary>Blind carbon copy recipient.</summary>\n    Bcc,\n    /// <summary>Resource recipient.</summary>\n    Resource,\n    /// <summary>Room recipient.</summary>\n    Room\n}\n\n/// <summary>Represents an address from a mail file.</summary>\npublic sealed class MailFileAddress {\n    /// <summary>Creates an address with optional display name and raw value.</summary>\n    /// <param name=\"address\">Email address value.</param>\n    /// <param name=\"displayName\">Display name value.</param>\n    /// <param name=\"raw\">Raw header value.</param>\n    public MailFileAddress(string? address, string? displayName = null, string? raw = null) {\n        Address = address;\n        DisplayName = displayName;\n        Raw = raw;\n    }\n\n    /// <summary>Email address value.</summary>\n    public string? Address { get; }\n    /// <summary>Display name value.</summary>\n    public string? DisplayName { get; }\n    /// <summary>Raw header value.</summary>\n    public string? Raw { get; }\n\n    /// <summary>Returns a formatted display string.</summary>\n    public override string ToString() {\n        if (!string.IsNullOrWhiteSpace(DisplayName) && !string.IsNullOrWhiteSpace(Address)) {\n            return $\"{DisplayName} <{Address}>\";\n        }\n        if (!string.IsNullOrWhiteSpace(DisplayName)) {\n            return DisplayName!;\n        }\n        return Address ?? string.Empty;\n    }\n}\n\n/// <summary>Represents a recipient entry with type and address.</summary>\npublic sealed class MailFileRecipient {\n    /// <summary>Creates a recipient entry.</summary>\n    /// <param name=\"type\">Recipient type.</param>\n    /// <param name=\"address\">Recipient address.</param>\n    public MailFileRecipient(MailFileRecipientType type, MailFileAddress address) {\n        Type = type;\n        Address = address ?? throw new ArgumentNullException(nameof(address));\n    }\n\n    /// <summary>Recipient type.</summary>\n    public MailFileRecipientType Type { get; }\n    /// <summary>Recipient address.</summary>\n    public MailFileAddress Address { get; }\n}\n\n/// <summary>Represents an attachment from a mail file.</summary>\npublic sealed class MailFileAttachment {\n    /// <summary>Attachment file name.</summary>\n    public string? FileName { get; set; }\n    /// <summary>Attachment content type.</summary>\n    public string? ContentType { get; set; }\n    /// <summary>Attachment content id.</summary>\n    public string? ContentId { get; set; }\n    /// <summary>Indicates whether the attachment is inline.</summary>\n    public bool IsInline { get; set; }\n    /// <summary>Attachment size in bytes.</summary>\n    public long? Size { get; set; }\n    /// <summary>Attachment content bytes.</summary>\n    public byte[]? Content { get; set; }\n}\n\n/// <summary>Represents a mail file with metadata and content.</summary>\npublic sealed class MailFileMessage {\n    /// <summary>Mail file format.</summary>\n    public MailFileFormat Format { get; set; }\n    /// <summary>Full file path.</summary>\n    public string FilePath { get; set; } = string.Empty;\n    /// <summary>Message subject.</summary>\n    public string? Subject { get; set; }\n    /// <summary>Sender address.</summary>\n    public MailFileAddress? From { get; set; }\n    /// <summary>To recipients.</summary>\n    public IReadOnlyList<MailFileAddress> To { get; set; } = Array.Empty<MailFileAddress>();\n    /// <summary>Cc recipients.</summary>\n    public IReadOnlyList<MailFileAddress> Cc { get; set; } = Array.Empty<MailFileAddress>();\n    /// <summary>Bcc recipients.</summary>\n    public IReadOnlyList<MailFileAddress> Bcc { get; set; } = Array.Empty<MailFileAddress>();\n    /// <summary>All recipients with types.</summary>\n    public IReadOnlyList<MailFileRecipient> Recipients { get; set; } = Array.Empty<MailFileRecipient>();\n    /// <summary>Sent date.</summary>\n    public DateTimeOffset? SentOn { get; set; }\n    /// <summary>Received date.</summary>\n    public DateTimeOffset? ReceivedOn { get; set; }\n    /// <summary>Plain text body.</summary>\n    public string? BodyText { get; set; }\n    /// <summary>HTML body.</summary>\n    public string? BodyHtml { get; set; }\n    /// <summary>Attachments list.</summary>\n    public IReadOnlyList<MailFileAttachment> Attachments { get; set; } = Array.Empty<MailFileAttachment>();\n    /// <summary>Message id value.</summary>\n    public string? MessageId { get; set; }\n    /// <summary>Indicates whether a signature is valid.</summary>\n    public bool? SignatureIsValid { get; set; }\n    /// <summary>Signer identity, if available.</summary>\n    public string? SignedBy { get; set; }\n    /// <summary>Signature timestamp.</summary>\n    public DateTimeOffset? SignedOn { get; set; }\n    /// <summary>Raw headers merged into a single dictionary.</summary>\n    public IReadOnlyDictionary<string, string>? Headers { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/MailFileReader.cs",
    "content": "using System.Collections.Specialized;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing MsgReader.Mime;\nusing MsgReader.Mime.Header;\nusing MsgReader.Outlook;\nusing OutlookMessage = MsgReader.Outlook.Storage.Message;\nusing OutlookAttachment = MsgReader.Outlook.Storage.Attachment;\n\nnamespace Mailozaurr;\n\n/// <summary>Reads MSG and EML files into MailFileMessage models.</summary>\npublic static class MailFileReader {\n    /// <summary>Reads a mail file from a path.</summary>\n    /// <param name=\"path\">Path to the mail file.</param>\n    /// <param name=\"options\">Reader options.</param>\n    /// <returns>Parsed mail file message.</returns>\n    public static MailFileMessage Read(string path, MailFileReaderOptions? options = null) {\n        if (string.IsNullOrWhiteSpace(path)) {\n            throw new ArgumentException(\"Value cannot be null or whitespace.\", nameof(path));\n        }\n\n        return Read(new FileInfo(path), options);\n    }\n\n    /// <summary>Reads a mail file from a FileInfo instance.</summary>\n    /// <param name=\"fileInfo\">Mail file info.</param>\n    /// <param name=\"options\">Reader options.</param>\n    /// <returns>Parsed mail file message.</returns>\n    public static MailFileMessage Read(FileInfo fileInfo, MailFileReaderOptions? options = null) {\n        if (fileInfo == null) {\n            throw new ArgumentNullException(nameof(fileInfo));\n        }\n\n        if (!fileInfo.Exists) {\n            throw new FileNotFoundException(\"Mail file not found.\", fileInfo.FullName);\n        }\n\n        options ??= new MailFileReaderOptions();\n\n        if (fileInfo.Extension.Equals(\".msg\", StringComparison.OrdinalIgnoreCase)) {\n            return ReadMsg(fileInfo, options);\n        }\n\n        if (fileInfo.Extension.Equals(\".eml\", StringComparison.OrdinalIgnoreCase)) {\n            return ReadEml(fileInfo, options);\n        }\n\n        throw new NotSupportedException($\"Unsupported mail file extension '{fileInfo.Extension}'.\");\n    }\n\n    /// <summary>Attempts to read a mail file and returns an error message on failure.</summary>\n    /// <param name=\"path\">Path to the mail file.</param>\n    /// <param name=\"message\">Parsed mail file message when successful.</param>\n    /// <param name=\"error\">Error message when reading fails.</param>\n    /// <param name=\"options\">Reader options.</param>\n    /// <returns>True when reading succeeds; otherwise false.</returns>\n    public static bool TryRead(string path, out MailFileMessage? message, out string? error, MailFileReaderOptions? options = null) {\n        message = null;\n        error = null;\n\n        if (string.IsNullOrWhiteSpace(path)) {\n            error = \"File path is empty.\";\n            return false;\n        }\n\n        if (!File.Exists(path)) {\n            error = $\"File {path} doesn't exist.\";\n            return false;\n        }\n\n        try {\n            message = Read(path, options);\n            return true;\n        } catch (NotSupportedException) {\n            error = $\"File {path} is not a .msg or .eml file.\";\n            return false;\n        } catch (Exception ex) {\n            error = $\"File {path} is not a .msg or .eml file or another error occurred. Error: {ex.Message}\";\n            return false;\n        }\n    }\n\n    private static MailFileMessage ReadMsg(FileInfo fileInfo, MailFileReaderOptions options) {\n        using var message = new OutlookMessage(fileInfo.FullName);\n\n        var recipients = BuildRecipients(message.Recipients);\n        var to = recipients.Where(r => r.Type == MailFileRecipientType.To).Select(r => r.Address).ToList();\n        var cc = recipients.Where(r => r.Type == MailFileRecipientType.Cc).Select(r => r.Address).ToList();\n        var bcc = recipients.Where(r => r.Type == MailFileRecipientType.Bcc).Select(r => r.Address).ToList();\n\n        MailFileAddress? from = null;\n        if (message.Sender != null) {\n            from = CreateAddress(message.Sender.Email, message.Sender.DisplayName, message.Sender.Raw);\n        }\n\n        var headers = options.IncludeHeaders ? ExtractHeaders(message.Headers) : null;\n        var attachments = options.IncludeAttachments ? BuildMsgAttachments(message.Attachments, options.IncludeAttachmentContent) : Array.Empty<MailFileAttachment>();\n        var headerMessageId = message.Headers?.MessageId;\n        var messageId = !string.IsNullOrWhiteSpace(headerMessageId) ? headerMessageId : message.Id;\n\n        return new MailFileMessage {\n            Format = MailFileFormat.Msg,\n            FilePath = fileInfo.FullName,\n            Subject = message.Subject,\n            From = from,\n            To = to,\n            Cc = cc,\n            Bcc = bcc,\n            Recipients = recipients,\n            SentOn = message.SentOn,\n            ReceivedOn = message.ReceivedOn,\n            BodyText = message.BodyText,\n            BodyHtml = message.BodyHtml,\n            Attachments = attachments,\n            MessageId = messageId,\n            SignatureIsValid = message.SignatureIsValid,\n            SignedBy = message.SignedBy,\n            SignedOn = message.SignedOn,\n            Headers = headers\n        };\n    }\n\n    private static MailFileMessage ReadEml(FileInfo fileInfo, MailFileReaderOptions options) {\n        var message = Message.Load(fileInfo);\n        var headers = message.Headers;\n\n        var recipients = new List<MailFileRecipient>();\n        var to = BuildRecipients(headers?.To, MailFileRecipientType.To, recipients);\n        var cc = BuildRecipients(headers?.Cc, MailFileRecipientType.Cc, recipients);\n        var bcc = BuildRecipients(headers?.Bcc, MailFileRecipientType.Bcc, recipients);\n\n        var sentOn = headers != null && headers.DateSent != DateTimeOffset.MinValue\n            ? headers.DateSent\n            : (DateTimeOffset?)null;\n\n        var from = CreateAddress(headers?.From);\n        var attachments = options.IncludeAttachments ? BuildEmlAttachments(message.Attachments, options.IncludeAttachmentContent) : Array.Empty<MailFileAttachment>();\n\n        return new MailFileMessage {\n            Format = MailFileFormat.Eml,\n            FilePath = fileInfo.FullName,\n            Subject = headers?.Subject,\n            From = from,\n            To = to,\n            Cc = cc,\n            Bcc = bcc,\n            Recipients = recipients,\n            SentOn = sentOn,\n            BodyText = DecodeBody(message.TextBody),\n            BodyHtml = DecodeBody(message.HtmlBody),\n            Attachments = attachments,\n            MessageId = headers?.MessageId,\n            SignatureIsValid = message.SignatureIsValid,\n            SignedBy = message.SignedBy,\n            SignedOn = message.SignedOn,\n            Headers = options.IncludeHeaders ? ExtractHeaders(headers) : null\n        };\n    }\n\n    private static List<MailFileRecipient> BuildRecipients(List<MsgReader.Outlook.Storage.Recipient>? recipients) {\n        var result = new List<MailFileRecipient>();\n        if (recipients == null || recipients.Count == 0) {\n            return result;\n        }\n\n        foreach (var recipient in recipients) {\n            var address = CreateAddress(recipient.Email, recipient.DisplayName, recipient.Raw);\n            if (address == null) {\n                continue;\n            }\n\n            var type = recipient.Type.HasValue ? MapRecipientType(recipient.Type.Value) : MailFileRecipientType.Unknown;\n            result.Add(new MailFileRecipient(type, address));\n        }\n\n        return result;\n    }\n\n    private static List<MailFileAddress> BuildRecipients(IEnumerable<RfcMailAddress>? addresses, MailFileRecipientType type, List<MailFileRecipient> recipients) {\n        var list = new List<MailFileAddress>();\n        if (addresses == null) {\n            return list;\n        }\n\n        foreach (var address in addresses) {\n            var mapped = CreateAddress(address);\n            if (mapped == null) {\n                continue;\n            }\n\n            list.Add(mapped);\n            recipients.Add(new MailFileRecipient(type, mapped));\n        }\n\n        return list;\n    }\n\n    private static MailFileRecipientType MapRecipientType(MsgReader.Outlook.RecipientType type) {\n        return type switch {\n            MsgReader.Outlook.RecipientType.To => MailFileRecipientType.To,\n            MsgReader.Outlook.RecipientType.Cc => MailFileRecipientType.Cc,\n            MsgReader.Outlook.RecipientType.Bcc => MailFileRecipientType.Bcc,\n            MsgReader.Outlook.RecipientType.Resource => MailFileRecipientType.Resource,\n            MsgReader.Outlook.RecipientType.Room => MailFileRecipientType.Room,\n            _ => MailFileRecipientType.Unknown\n        };\n    }\n\n    private static MailFileAddress? CreateAddress(RfcMailAddress? address) {\n        if (address == null) {\n            return null;\n        }\n\n        return CreateAddress(address.Address, address.DisplayName, address.Raw);\n    }\n\n    private static MailFileAddress? CreateAddress(string? address, string? displayName, string? raw) {\n        if (string.IsNullOrWhiteSpace(address) && string.IsNullOrWhiteSpace(displayName) && string.IsNullOrWhiteSpace(raw)) {\n            return null;\n        }\n\n        return new MailFileAddress(address, displayName, raw);\n    }\n\n    private static string? DecodeBody(MessagePart? part) {\n        if (part == null || part.Body == null || part.Body.Length == 0) {\n            return null;\n        }\n\n        var encoding = part.BodyEncoding ?? Encoding.UTF8;\n        try {\n            return encoding.GetString(part.Body);\n        } catch (DecoderFallbackException ex) {\n            LoggingMessages.Logger.WriteWarning(\n                \"MailFileReader - Failed to decode body using {0}. Falling back to UTF-8. Error: {1}\",\n                encoding.WebName,\n                ex.Message);\n            return Encoding.UTF8.GetString(part.Body);\n        } catch (ArgumentException ex) {\n            LoggingMessages.Logger.WriteWarning(\n                \"MailFileReader - Failed to decode body using {0}. Falling back to UTF-8. Error: {1}\",\n                encoding.WebName,\n                ex.Message);\n            return Encoding.UTF8.GetString(part.Body);\n        }\n    }\n\n    private static IReadOnlyList<MailFileAttachment> BuildMsgAttachments(List<object>? attachments, bool includeContent) {\n        if (attachments == null || attachments.Count == 0) {\n            return Array.Empty<MailFileAttachment>();\n        }\n\n        var results = new List<MailFileAttachment>();\n        foreach (var attachmentObj in attachments) {\n            if (attachmentObj is OutlookAttachment attachment) {\n                byte[]? content = includeContent ? attachment.Data : null;\n                results.Add(new MailFileAttachment {\n                    FileName = attachment.FileName,\n                    ContentType = attachment.MimeType,\n                    ContentId = attachment.ContentId,\n                    IsInline = attachment.IsInline,\n                    Size = attachment.Data?.LongLength,\n                    Content = content\n                });\n                continue;\n            }\n\n            // Ignore unknown attachment types to stay AOT-friendly (no reflection).\n        }\n\n        return results;\n    }\n\n    private static IReadOnlyList<MailFileAttachment> BuildEmlAttachments(IEnumerable<MessagePart>? attachments, bool includeContent) {\n        if (attachments == null) {\n            return Array.Empty<MailFileAttachment>();\n        }\n\n        var results = new List<MailFileAttachment>();\n        foreach (var attachment in attachments) {\n            var body = includeContent ? attachment.Body : null;\n            results.Add(new MailFileAttachment {\n                FileName = ResolveAttachmentFileName(attachment),\n                ContentType = attachment.ContentType?.ToString(),\n                ContentId = attachment.ContentId,\n                IsInline = attachment.IsInline,\n                Size = attachment.Body?.LongLength,\n                Content = body\n            });\n        }\n\n        return results;\n    }\n\n    private static string? ResolveAttachmentFileName(MessagePart attachment) {\n        if (!string.IsNullOrWhiteSpace(attachment.FileName)) {\n            return attachment.FileName;\n        }\n\n        var contentDispositionFileName = attachment.ContentDisposition?.FileName;\n        if (!string.IsNullOrWhiteSpace(contentDispositionFileName)) {\n            return contentDispositionFileName;\n        }\n\n        var contentTypeName = attachment.ContentType?.Name;\n        if (!string.IsNullOrWhiteSpace(contentTypeName)) {\n            return contentTypeName;\n        }\n\n        return null;\n    }\n\n    private static IReadOnlyDictionary<string, string>? ExtractHeaders(MessageHeader? headers) {\n        if (headers == null) {\n            return null;\n        }\n\n        return MergeHeaders(headers.RawHeaders, headers.UnknownHeaders);\n    }\n\n    private static IReadOnlyDictionary<string, string> MergeHeaders(NameValueCollection? headers, NameValueCollection? unknownHeaders) {\n        var count = (headers?.Count ?? 0) + (unknownHeaders?.Count ?? 0);\n        var result = count > 0\n            ? new Dictionary<string, string>(count, StringComparer.OrdinalIgnoreCase)\n            : new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n        AddHeaders(result, headers);\n        AddHeaders(result, unknownHeaders);\n        return result;\n    }\n\n    private static void AddHeaders(IDictionary<string, string> destination, NameValueCollection? headers) {\n        if (headers == null) {\n            return;\n        }\n\n        foreach (var key in headers.AllKeys) {\n            if (string.IsNullOrWhiteSpace(key)) {\n                continue;\n            }\n\n            var value = headers[key];\n            if (string.IsNullOrEmpty(value)) {\n                continue;\n            }\n\n            if (destination.TryGetValue(key, out var existing)) {\n                destination[key] = string.Concat(existing, \", \", value);\n            } else {\n                destination[key] = value;\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/MailFileReaderOptions.cs",
    "content": "namespace Mailozaurr;\n\n/// <summary>Options for reading mail files.</summary>\npublic sealed class MailFileReaderOptions {\n    /// <summary>Includes attachment metadata when true.</summary>\n    public bool IncludeAttachments { get; set; } = true;\n    /// <summary>Includes attachment content bytes when true.</summary>\n    public bool IncludeAttachmentContent { get; set; } = true;\n    /// <summary>Includes raw headers when true.</summary>\n    public bool IncludeHeaders { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Msg/Mailozaurr.Msg.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n    <PropertyGroup>\n        <Company>Evotec</Company>\n        <Authors>Przemyslaw Klys</Authors>\n        <VersionPrefix>2.0.7</VersionPrefix>\n        <TargetFrameworks>net472;netstandard2.0;net8.0</TargetFrameworks>\n        <AssemblyName>Mailozaurr.Msg</AssemblyName>\n        <Copyright>(c) 2011 - 2024 Przemyslaw Klys @ Evotec. All rights reserved.</Copyright>\n        <LangVersion>latest</LangVersion>\n        <PackageIcon>Mailozaurr.png</PackageIcon>\n        <PackageReadmeFile>README.MD</PackageReadmeFile>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <RepositoryUrl>https://github.com/EvotecIT/Mailozaurr</RepositoryUrl>\n        <PackageTags>\n            Windows;MacOS;Linux;Mail;Email;MSG;EML;MsgReader;MsgKit\n        </PackageTags>\n    </PropertyGroup>\n\n    <PropertyGroup>\n        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Mailozaurr\\Mailozaurr.csproj\" />\n        <PackageReference Include=\"MsgKit\" Version=\"3.0.3\" />\n        <PackageReference Include=\"MsgReader\" Version=\"6.0.9\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <None Include=\"..\\..\\Mailozaurr.png\" Pack=\"true\" PackagePath=\"\\\" />\n        <None Include=\"..\\..\\README.MD\" Pack=\"true\" PackagePath=\"\\\" />\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletAddGraphMailboxPermission.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Adds mailbox permissions via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Add, \"GraphMailboxPermission\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic class CmdletAddGraphMailboxPermission : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name of the mailbox owner.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Permission definitions when using the Graph parameter set.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNullOrEmpty]\n    public Hashtable[]? Permission { get; set; }\n\n    /// <summary>\n    /// Mailbox permission objects provided via the pipeline.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Object\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphMailboxPermission[]? MailboxPermission { get; set; }\n\n    /// <summary>\n    /// Path to a CSV file containing permission definitions.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Csv\")]\n    [ValidateNotNullOrEmpty]\n    public string? CsvPath { get; set; }\n\n    /// <summary>\n    /// Graph connection information.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [Parameter(ParameterSetName = \"Object\", ValueFromPipeline = true)]\n    [Parameter(ParameterSetName = \"Csv\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Indicates that the raw Microsoft Graph request should be returned.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Timeout for Graph requests in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            ProcessMgGraph();\n            return;\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Add-GraphMailboxPermission - Connection not provided and no default session available.\");\n            return;\n        }\n\n        if (ParameterSetName == \"Csv\") {\n            await ProcessCsvAsync(conn.Credential);\n            return;\n        }\n\n        if (ParameterSetName == \"Object\") {\n            foreach (var perm in MailboxPermission!) {\n                if (perm.UserPrincipalName == null)\n                    perm.UserPrincipalName = UserPrincipalName;\n                await ProcessGraphAsync(conn.Credential, perm);\n            }\n            return;\n        }\n\n        foreach (var ht in Permission!)\n            await ProcessGraphAsync(conn.Credential, ht);\n        return;\n    }\n\n    private async Task ProcessCsvAsync(GraphCredential cred) {\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Import-Csv\").AddParameter(\"Path\", CsvPath);\n        var rows = ps.Invoke();\n        foreach (var row in rows.OfType<PSObject>()) {\n            var dict = row.Properties.ToDictionary(p => p.Name, p => (object?)p.Value!);\n            await ProcessGraphAsync(cred, new Hashtable(dict));\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred, object permission) {\n        var dryRun = !ShouldProcess(UserPrincipalName!, \"Adding mailbox permission\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        string body = permission switch {\n            Hashtable ht => JsonSerializer.Serialize(ht.Cast<DictionaryEntry>().ToDictionary(e => (string)e.Key, e => (object?)e.Value!), MailozaurrJsonContext.Default.DictionaryStringObject),\n            GraphMailboxPermission p => JsonSerializer.Serialize(p.ToDictionary(), MailozaurrJsonContext.Default.DictionaryStringObject),\n            _ => JsonSerializer.Serialize(permission, MailozaurrJsonContext.Default.Object)\n        };\n        if (dryRun) {\n            await MicrosoftGraphUtils.AddMailboxPermissionAsync(cred, UserPrincipalName!, body, dryRun: true);\n            return;\n        }\n        do {\n            try {\n                await MicrosoftGraphUtils.AddMailboxPermissionAsync(cred, UserPrincipalName!, body, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Add-GraphMailboxPermission - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex)\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null)\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(UserPrincipalName!, \"Adding mailbox permission\")) return;\n        string body;\n        if (CsvPath != null) {\n            var psCsv = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n            psCsv.AddCommand(\"Import-Csv\").AddParameter(\"Path\", CsvPath);\n            var rows = psCsv.Invoke();\n            foreach (var row in rows.OfType<PSObject>()) {\n                var dict = row.Properties.ToDictionary(p => p.Name, p => (object?)p.Value!);\n                body = JsonSerializer.Serialize(dict, MailozaurrJsonContext.Default.DictionaryStringObject);\n                InvokeMgGraph(body);\n            }\n        } else if (MailboxPermission != null) {\n            foreach (var perm in MailboxPermission) {\n                body = JsonSerializer.Serialize(perm.ToDictionary(), MailozaurrJsonContext.Default.DictionaryStringObject);\n                InvokeMgGraph(body);\n            }\n        } else if (Permission != null) {\n            foreach (var ht in Permission) {\n                var conv = ht.Cast<DictionaryEntry>().ToDictionary(e => (string)e.Key, e => (object?)e.Value!);\n                body = JsonSerializer.Serialize(conv, MailozaurrJsonContext.Default.DictionaryStringObject);\n                InvokeMgGraph(body);\n            }\n        }\n    }\n\n    private void InvokeMgGraph(string body) {\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/permissions\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"POST\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletClearGraphJunk.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing System.Threading.Tasks;\nusing System.Linq;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Clears the Junk Email folder via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Clear, \"GraphJunk\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletClearGraphJunk : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name of the mailbox to clean.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Connection information for Microsoft Graph.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Returns the Microsoft Graph request payload instead of sending it.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Additional message properties to retrieve.\n    /// </summary>\n    [Parameter]\n    public string[]? Property { get; set; }\n\n    /// <summary>\n    /// When present, only outputs the messages that would be removed.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Preview { get; set; }\n\n    /// <summary>\n    /// Message identifiers that should not be removed.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipId { get; set; }\n\n    /// <summary>\n    /// Sender addresses to exclude from deletion.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipFrom { get; set; }\n\n    /// <summary>\n    /// Recipient addresses to exclude from deletion.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipTo { get; set; }\n\n    /// <summary>\n    /// Skips messages when the subject contains any of these strings.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipSubjectContains { get; set; }\n\n    /// <summary>\n    /// Skip messages that have attachments.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter SkipHasAttachment { get; set; }\n\n    /// <summary>\n    /// Attachment file extensions to exclude.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipAttachmentExtension { get; set; }\n\n    /// <summary>\n    /// Timeout for Graph requests in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Executes the cmdlet logic based on the provided parameter set.\n    /// </summary>\n    /// <returns>The asynchronous task representing the operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Clear-GraphJunk - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                if (Preview.IsPresent) {\n                    return ProcessPreviewGraphAsync(conn.Credential);\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                if (Preview.IsPresent) {\n                    ProcessPreviewMgGraph();\n                } else {\n                    ProcessMgGraph();\n                }\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(UserPrincipalName!, \"Clearing Graph junk\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.ClearJunkMailAsync(\n                cred,\n                UserPrincipalName!,\n                true,\n                SkipId,\n                SkipFrom,\n                SkipTo,\n                SkipSubjectContains,\n                SkipHasAttachment.IsPresent,\n                SkipAttachmentExtension);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.ClearJunkMailAsync(\n                    cred,\n                    UserPrincipalName!,\n                    false,\n                    SkipId,\n                    SkipFrom,\n                    SkipTo,\n                    SkipSubjectContains,\n                    SkipHasAttachment.IsPresent,\n                    SkipAttachmentExtension);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Clear-GraphJunk - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private async Task ProcessPreviewGraphAsync(GraphCredential cred) {\n        var props = new List<string>();\n        if (Property != null && Property.Length > 0) props.AddRange(Property);\n        if (!props.Contains(\"id\")) props.Add(\"id\");\n        if (!props.Contains(\"bodyPreview\")) props.Add(\"bodyPreview\");\n        if (SkipFrom != null && !props.Contains(\"from\")) props.Add(\"from\");\n        if (SkipTo != null && !props.Contains(\"toRecipients\")) props.Add(\"toRecipients\");\n        if (SkipSubjectContains != null && !props.Contains(\"subject\")) props.Add(\"subject\");\n        if ((SkipHasAttachment.IsPresent || SkipAttachmentExtension != null) && !props.Contains(\"hasAttachments\")) props.Add(\"hasAttachments\");\n\n        var messages = await MicrosoftGraphUtils.GetJunkMailMessagesAsync(\n            cred,\n            UserPrincipalName!,\n            props,\n            SkipId,\n            SkipFrom,\n            SkipTo,\n            SkipSubjectContains,\n            SkipHasAttachment.IsPresent,\n            SkipAttachmentExtension);\n\n        foreach (var msg in messages) {\n            WriteObject(PSObject.AsPSObject(msg));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(UserPrincipalName!, \"Clearing Graph junk\")) return;\n        var select = new List<string> { \"id\" };\n        if (SkipSubjectContains != null) select.Add(\"subject\");\n        if (SkipFrom != null) select.Add(\"from\");\n        if (SkipTo != null) select.Add(\"toRecipients\");\n        if (SkipHasAttachment.IsPresent || SkipAttachmentExtension != null) select.Add(\"hasAttachments\");\n        var listUri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/junkemail/messages\",\n            new Dictionary<string, object> { [\"$select\"] = string.Join(\",\", select) });\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"GET\")\n            .AddParameter(\"Uri\", listUri);\n        var results = ps.Invoke();\n        var dict = results\n            .OfType<PSObject>()\n            .Select(o => o.Properties.ToDictionary(p => p.Name, p => p.Value))\n            .ToList();\n        var filtered = MicrosoftGraphUtils.FilterJunkMessages(dict, SkipId, SkipFrom, SkipTo, SkipSubjectContains, SkipHasAttachment.IsPresent);\n        if (SkipAttachmentExtension != null && SkipAttachmentExtension.Length > 0) {\n            var final = new List<Dictionary<string, object>>();\n            foreach (var msg in filtered) {\n                if (!msg.TryGetValue(\"id\", out var idObj) || idObj is not string id) continue;\n                if (msg.TryGetValue(\"hasAttachments\", out var hasObj) && hasObj is bool ha && ha) {\n                    var attPs = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n                    attPs.AddCommand(\"Invoke-MgGraphRequest\")\n                        .AddParameter(\"Method\", \"GET\")\n                        .AddParameter(\n                            \"Uri\",\n                            MicrosoftGraphUtils.JoinUriQuery(\n                                GraphEndpoint.V1,\n                                $\"/users/{UserPrincipalName}/messages/{id}/attachments\",\n                                new Dictionary<string, object> { [\"$select\"] = \"name\" }));\n                    var attRes = attPs.Invoke();\n                    var names = attRes\n                        .OfType<PSObject>()\n                        .SelectMany<PSObject, PSObject>(o =>\n                            ((System.Collections.IEnumerable?)o.Properties[\"value\"].Value)?.OfType<PSObject>() ?? System.Array.Empty<PSObject>())\n                        .Select(a => a.Properties[\"name\"].Value as string)\n                        .Where(n => n != null)\n                        .ToList();\n                    if (names.Any(n => SkipAttachmentExtension.Contains(System.IO.Path.GetExtension(n!).TrimStart('.'), StringComparer.OrdinalIgnoreCase))) {\n                        continue;\n                    }\n                }\n                final.Add(msg);\n            }\n            filtered = final;\n        }\n        foreach (var msg in filtered) {\n            var id = msg[\"id\"] as string;\n            if (string.IsNullOrWhiteSpace(id)) continue;\n            var delPs = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n            delPs.AddCommand(\"Invoke-MgGraphRequest\")\n                .AddParameter(\"Method\", \"DELETE\")\n                .AddParameter(\n                    \"Uri\",\n                    MicrosoftGraphUtils.BuildGraphUri(\n                        GraphEndpoint.V1,\n                        $\"/users/{UserPrincipalName}/messages/{id}\"))\n                .AddParameter(\"ContentType\", \"application/json\");\n            delPs.Invoke();\n        }\n    }\n\n    private void ProcessPreviewMgGraph() {\n        var props = new List<string>();\n        if (Property != null && Property.Length > 0) props.AddRange(Property);\n        if (!props.Contains(\"id\")) props.Add(\"id\");\n        if (SkipFrom != null && !props.Contains(\"from\")) props.Add(\"from\");\n        if (SkipTo != null && !props.Contains(\"toRecipients\")) props.Add(\"toRecipients\");\n        if (SkipSubjectContains != null && !props.Contains(\"subject\")) props.Add(\"subject\");\n        if ((SkipHasAttachment.IsPresent || SkipAttachmentExtension != null) && !props.Contains(\"hasAttachments\")) props.Add(\"hasAttachments\");\n        var listUri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/junkemail/messages\",\n            new Dictionary<string, object> { [\"$select\"] = string.Join(\",\", props) });\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"GET\")\n            .AddParameter(\"Uri\", listUri);\n        var results = ps.Invoke();\n        var dict = results\n            .OfType<PSObject>()\n            .Select(o => o.Properties.ToDictionary(p => p.Name, p => p.Value))\n            .ToList();\n        var filtered = MicrosoftGraphUtils.FilterJunkMessages(dict, SkipId, SkipFrom, SkipTo, SkipSubjectContains, SkipHasAttachment.IsPresent);\n        if (SkipAttachmentExtension != null && SkipAttachmentExtension.Length > 0) {\n            var final = new List<Dictionary<string, object>>();\n            foreach (var msg in filtered) {\n                if (!msg.TryGetValue(\"id\", out var idObj) || idObj is not string id) continue;\n                if (msg.TryGetValue(\"hasAttachments\", out var hasObj) && hasObj is bool ha && ha) {\n                    var attPs = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n                    attPs.AddCommand(\"Invoke-MgGraphRequest\")\n                        .AddParameter(\"Method\", \"GET\")\n                        .AddParameter(\n                            \"Uri\",\n                            MicrosoftGraphUtils.JoinUriQuery(\n                                GraphEndpoint.V1,\n                                $\"/users/{UserPrincipalName}/messages/{id}/attachments\",\n                                new Dictionary<string, object> { [\"$select\"] = \"name\" }));\n                    var attRes = attPs.Invoke();\n                    var names = attRes\n                        .OfType<PSObject>()\n                        .SelectMany<PSObject, PSObject>(o =>\n                            ((System.Collections.IEnumerable?)o.Properties[\"value\"].Value)?.OfType<PSObject>() ?? System.Array.Empty<PSObject>())\n                        .Select(a => a.Properties[\"name\"].Value as string)\n                        .Where(n => n != null)\n                        .ToList();\n                    if (names.Any(n => SkipAttachmentExtension.Contains(System.IO.Path.GetExtension(n!).TrimStart('.'), StringComparer.OrdinalIgnoreCase))) {\n                        continue;\n                    }\n                }\n                final.Add(msg);\n            }\n            filtered = final;\n        }\n        foreach (var msg in filtered) {\n            WriteObject(PSObject.AsPSObject(msg));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletClearIMAPJunk.cs",
    "content": "using System.Management.Automation;\nusing MailKit.Net.Imap;\nusing System.Threading.Tasks;\nusing System.Linq;\nusing MailKit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Clears messages from an IMAP junk folder.\n/// </summary>\n[Cmdlet(VerbsCommon.Clear, \"IMAPJunk\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletClearIMAPJunk : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>Junk folder name.</summary>\n    [Parameter(Position = 1)]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// When set, lists messages that would be removed without deleting them.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Preview { get; set; }\n\n    /// <summary>\n    /// Sender addresses to exclude from deletion.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipFrom { get; set; }\n\n    /// <summary>\n    /// Recipient addresses to exclude from deletion.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipTo { get; set; }\n\n    /// <summary>\n    /// Skips messages when the subject contains these strings.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipSubjectContains { get; set; }\n\n    /// <summary>\n    /// Message-Id headers to exclude.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipMessageId { get; set; }\n\n    /// <summary>\n    /// IMAP UIDs to exclude from deletion.\n    /// </summary>\n    [Parameter]\n    public uint[]? SkipUid { get; set; }\n\n    /// <summary>\n    /// Skips messages that contain attachments.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter SkipHasAttachment { get; set; }\n\n    /// <summary>\n    /// Attachment file extensions to exclude.\n    /// </summary>\n    [Parameter]\n    public string[]? SkipAttachmentExtension { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var folder = Folder ?? \"Junk\";\n            if (Preview.IsPresent) {\n                await foreach (var msg in JunkCleaner.GetImapJunkAsync(\n                    conn.Data,\n                    folder,\n                    SkipFrom,\n                    SkipTo,\n                    SkipSubjectContains,\n                    SkipMessageId,\n                    SkipUid,\n                    SkipHasAttachment.IsPresent,\n                    SkipAttachmentExtension,\n                    CancelToken)) {\n                    WriteObject(msg);\n                }\n                return;\n            }\n            var dryRun = !ShouldProcess(folder, \"Clearing IMAP junk\");\n            await JunkCleaner.ClearImapJunkAsync(\n                conn.Data,\n                folder,\n                SkipFrom,\n                SkipTo,\n                SkipSubjectContains,\n                SkipMessageId,\n                SkipUid,\n                SkipHasAttachment.IsPresent,\n                SkipAttachmentExtension,\n                dryRun,\n                CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Clear-IMAPJunk - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletClearSmtpConnectionPool.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Clears all cached SMTP connections used for connection pooling.</para>\n/// <para type=\"description\">The <c>Clear-SmtpConnectionPool</c> cmdlet removes any\n/// pooled SMTP connections maintained by the <see cref=\"Smtp\"/> class. Use this\n/// when you want to force new connections, for example after changing credentials\n/// or server settings.</para>\n/// <example>\n///   <summary>Reset the SMTP connection pool</summary>\n///   <code>Clear-SmtpConnectionPool</code>\n/// </example>\n/// </summary>\n[Cmdlet(VerbsCommon.Clear, \"SmtpConnectionPool\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]\npublic sealed class CmdletClearSmtpConnectionPool : AsyncPSCmdlet {\n    /// <summary>Clears all connections from the pool.</summary>\n    protected override Task ProcessRecordAsync() {\n        if (!ShouldProcess(\"SmtpConnectionPool\", \"Clearing SMTP connection pool\")) {\n            return Task.CompletedTask;\n        }\n        SmtpConnectionPool.ClearConnectionPool();\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConnectEmailGraph.cs",
    "content": "using System.Management.Automation;\nusing System.Security;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Connects to Microsoft Graph using application credentials, certificates, device code, or on-behalf-of authentication.</para>\n/// <para type=\"description\">The <c>Connect-EmailGraph</c> cmdlet creates a Microsoft Graph connection for Mailozaurr cmdlets. It supports client secret, certificate, device code, and on-behalf-of flows, and can authenticate directly or from a prebuilt <see cref=\"PSCredential\"/>.</para>\n/// <para type=\"description\">For new scripts, prefer the <c>SecureString</c> or <c>SecretManagement</c> parameter sets instead of passing secrets and tokens in plain text.</para>\n/// <example>\n///   <summary>Connect to Microsoft Graph using a vault-backed client secret</summary>\n///   <code>Connect-EmailGraph -ClientId \"id\" -DirectoryId \"tenant\" -ClientSecretSecretName \"graph-client-secret\" -ClientSecretVaultName \"MailSecrets\"</code>\n/// </example>\n/// <example>\n///   <summary>Connect to Microsoft Graph using a certificate password stored as a SecureString</summary>\n///   <code>$certificatePassword = Read-Host \"Certificate password\" -AsSecureString\n/// Connect-EmailGraph -ClientId \"id\" -DirectoryId \"tenant\" -CertificatePath \"cert.pfx\" -CertificatePasswordSecureString $certificatePassword</code>\n/// </example>\n/// <remarks>\n/// The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// </summary>\n[Cmdlet(VerbsCommunications.Connect, \"EmailGraph\")]\n[OutputType(typeof(GraphConnectionInfo))]\npublic sealed class CmdletConnectEmailGraph : AsyncPSCmdlet {\n    /// <summary>\n    /// Credential object containing client ID and secret.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Credential\")]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Client (application) identifier.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Plain\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Certificate\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytes\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificatePem\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"DeviceCode\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOf\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientId { get; set; }\n\n    /// <summary>\n    /// Secret associated with the application (for app-only auth).\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Plain\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOf\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecret { get; set; }\n\n    /// <summary>\n    /// Secret associated with the application (for app-only auth) as a SecureString.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecureString\")]\n    [ValidateNotNull]\n    public SecureString? ClientSecretSecureString { get; set; }\n\n    /// <summary>\n    /// Secret name used with Get-Secret for the application client secret.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecretSecretName { get; set; }\n\n    /// <summary>\n    /// Optional vault name used with Get-Secret for the application client secret.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"PlainSecretManagement\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    public string? ClientSecretVaultName { get; set; }\n\n    /// <summary>\n    /// Directory (tenant) identifier.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Plain\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Certificate\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytes\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificatePem\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"DeviceCode\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOf\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? DirectoryId { get; set; }\n\n    /// <summary>\n    /// Path to a PFX certificate used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Certificate\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePath { get; set; }\n\n    /// <summary>\n    /// Raw bytes of a PFX certificate used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytes\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecretManagement\")]\n    [ValidateNotNull]\n    public byte[]? CertificateBytes { get; set; }\n\n    /// <summary>\n    /// Path to a PEM encoded certificate used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificatePem\")]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePemPath { get; set; }\n\n    /// <summary>\n    /// Password used to decrypt the certificate file.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Certificate\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytes\")]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePassword { get; set; }\n\n    /// <summary>\n    /// Password used to decrypt the certificate file as a SecureString.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecureString\")]\n    [ValidateNotNull]\n    public SecureString? CertificatePasswordSecureString { get; set; }\n\n    /// <summary>\n    /// Secret name used with Get-Secret for the certificate password.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateSecretManagement\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"CertificateBytesSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePasswordSecretName { get; set; }\n\n    /// <summary>\n    /// Optional vault name used with Get-Secret for the certificate password.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"CertificateSecretManagement\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"CertificateBytesSecretManagement\")]\n    public string? CertificatePasswordVaultName { get; set; }\n\n    /// <summary>\n    /// Use the device code flow for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"DeviceCode\")]\n    public SwitchParameter DeviceCode { get; set; }\n\n    /// <summary>\n    /// Access token to use for on-behalf-of authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOf\")]\n    [ValidateNotNullOrEmpty]\n    public string? OnBehalfOfToken { get; set; }\n\n    /// <summary>\n    /// Access token to use for on-behalf-of authentication as a SecureString.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecureString\")]\n    [ValidateNotNull]\n    public SecureString? OnBehalfOfTokenSecureString { get; set; }\n\n    /// <summary>\n    /// Secret name used with Get-Secret for the on-behalf-of token.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? OnBehalfOfTokenSecretName { get; set; }\n\n    /// <summary>\n    /// Optional vault name used with Get-Secret for the on-behalf-of token.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    public string? OnBehalfOfTokenVaultName { get; set; }\n\n    /// <summary>\n    /// Microsoft Graph permission scopes to request.\n    /// </summary>\n    [Parameter(ParameterSetName = \"DeviceCode\")]\n    [Parameter(ParameterSetName = \"OnBehalfOf\")]\n    [Parameter(ParameterSetName = \"OnBehalfOfSecureString\")]\n    [Parameter(ParameterSetName = \"OnBehalfOfSecretManagement\")]\n    public string[] Scopes { get; set; } = new[] { \"https://graph.microsoft.com/.default\" };\n\n    /// <summary>\n    /// <para type=\"description\">Number of connection retry attempts.</para>\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Delay in milliseconds between retries.</para>\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Multiplier for increasing retry delay.</para>\n    /// </summary>\n    [Parameter]\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>\n    /// Request timeout for Microsoft Graph operations in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Microsoft Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Connects to Microsoft Graph using the provided authentication details.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        GraphCredential cred;\n        OAuthCredential? oauth = null;\n        var clientSecret = ParameterSetName == \"PlainSecretManagement\" || ParameterSetName == \"OnBehalfOfSecretManagement\"\n            ? CredentialHelpers.ToPlainText(CredentialHelpers.ResolveSecretFromVault(ClientSecretSecretName!, ClientSecretVaultName))\n            : CredentialHelpers.ToPlainText(ClientSecretSecureString);\n        var certificatePassword = ParameterSetName == \"CertificateSecretManagement\" || ParameterSetName == \"CertificateBytesSecretManagement\"\n            ? CredentialHelpers.ToPlainText(CredentialHelpers.ResolveSecretFromVault(CertificatePasswordSecretName!, CertificatePasswordVaultName))\n            : CredentialHelpers.ToPlainText(CertificatePasswordSecureString);\n        var onBehalfOfToken = ParameterSetName == \"OnBehalfOfSecretManagement\"\n            ? CredentialHelpers.ToPlainText(CredentialHelpers.ResolveSecretFromVault(OnBehalfOfTokenSecretName!, OnBehalfOfTokenVaultName))\n            : CredentialHelpers.ToPlainText(OnBehalfOfTokenSecureString);\n        if (ParameterSetName == \"Credential\") {\n            cred = MicrosoftGraphUtils.ConvertFromGraphCredential(\n                Credential!.UserName,\n                Credential.GetNetworkCredential().Password);\n        } else if (ParameterSetName == \"Certificate\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificatePath = CertificatePath!,\n                CertificatePassword = CertificatePassword!\n            };\n        } else if (ParameterSetName == \"CertificateSecureString\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificatePath = CertificatePath!,\n                CertificatePassword = certificatePassword\n            };\n        } else if (ParameterSetName == \"CertificateSecretManagement\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificatePath = CertificatePath!,\n                CertificatePassword = certificatePassword\n            };\n        } else if (ParameterSetName == \"CertificateBytes\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificateBytes = CertificateBytes!,\n                CertificatePassword = CertificatePassword!\n            };\n        } else if (ParameterSetName == \"CertificateBytesSecureString\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificateBytes = CertificateBytes!,\n                CertificatePassword = certificatePassword\n            };\n        } else if (ParameterSetName == \"CertificateBytesSecretManagement\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificateBytes = CertificateBytes!,\n                CertificatePassword = certificatePassword\n            };\n        } else if (ParameterSetName == \"CertificatePem\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!,\n                CertificatePemPath = CertificatePemPath!\n            };\n        } else if (ParameterSetName == \"DeviceCode\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                DirectoryId = DirectoryId!\n            };\n            oauth = await OAuthHelpers.AcquireO365TokenDeviceCodeAsync(\n                ClientId!,\n                DirectoryId!,\n                Scopes);\n            cred.AccessToken = oauth.AccessToken;\n        } else if (ParameterSetName == \"OnBehalfOf\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = ClientSecret!,\n                DirectoryId = DirectoryId!\n            };\n            oauth = await OAuthHelpers.AcquireO365TokenOnBehalfOfAsync(\n                ClientId!,\n                DirectoryId!,\n                ClientSecret!,\n                OnBehalfOfToken!,\n                Scopes);\n            cred.AccessToken = oauth.AccessToken;\n        } else if (ParameterSetName == \"OnBehalfOfSecureString\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = clientSecret,\n                DirectoryId = DirectoryId!\n            };\n            oauth = await OAuthHelpers.AcquireO365TokenOnBehalfOfAsync(\n                ClientId!,\n                DirectoryId!,\n                clientSecret,\n                onBehalfOfToken,\n                Scopes);\n            cred.AccessToken = oauth.AccessToken;\n        } else if (ParameterSetName == \"OnBehalfOfSecretManagement\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = clientSecret,\n                DirectoryId = DirectoryId!\n            };\n            oauth = await OAuthHelpers.AcquireO365TokenOnBehalfOfAsync(\n                ClientId!,\n                DirectoryId!,\n                clientSecret,\n                onBehalfOfToken,\n                Scopes);\n            cred.AccessToken = oauth.AccessToken;\n        } else if (ParameterSetName == \"PlainSecureString\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = clientSecret,\n                DirectoryId = DirectoryId!\n            };\n        } else if (ParameterSetName == \"PlainSecretManagement\") {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = clientSecret,\n                DirectoryId = DirectoryId!\n            };\n        } else {\n            cred = new GraphCredential {\n                ClientId = ClientId!,\n                ClientSecret = ClientSecret!,\n                DirectoryId = DirectoryId!\n            };\n        }\n\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n\n        bool connected = false;\n        if (ParameterSetName == \"DeviceCode\" || ParameterSetName == \"OnBehalfOf\" || ParameterSetName == \"OnBehalfOfSecureString\" || ParameterSetName == \"OnBehalfOfSecretManagement\") {\n            connected = !string.IsNullOrWhiteSpace(cred.AccessToken);\n        } else {\n            try {\n                var token = await MicrosoftGraphUtils.ConnectO365GraphWithRetryAsync(\n                    cred,\n                    cred.DirectoryId!,\n                    RetryCount,\n                    RetryDelayMilliseconds,\n                    RetryDelayBackoff,\n                    \"https://graph.microsoft.com\");\n                connected = !string.IsNullOrWhiteSpace(token);\n            } catch (GraphApiException ex) {\n                WriteError(new ErrorRecord(ex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n            }\n        }\n\n        var info = new GraphConnectionInfo { Credential = cred, IsConnected = connected, OAuthCredential = oauth };\n        DefaultSessions.GraphSession = info;\n        WriteObject(info);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConnectIMAP.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing Mailozaurr;\nusing System.Security;\nusing System.Security.Authentication;\nusing System.Threading;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Connects to an IMAP server and authenticates using credentials, OAuth2, or clear text.</para>\n/// <para type=\"description\">The <c>Connect-IMAP</c> cmdlet establishes a connection to an IMAP server using MailKit. It supports multiple authentication methods, including OAuth2, PSCredential, and clear text username/password. The cmdlet returns an <see cref=\"ImapConnectionInfo\"/> object containing connection details and the authenticated client for further use in subsequent cmdlets.</para>\n/// <para type=\"description\">Supports advanced options such as certificate validation skipping, custom timeouts, and secure socket options. Designed for secure, flexible, and scriptable IMAP connectivity in PowerShell automation scenarios.</para>\n/// <example>\n///   <summary>Connect to an IMAP server using credentials</summary>\n///   <code>Connect-IMAP -Server \"imap.example.com\" -Credential (Get-Credential)</code>\n/// </example>\n/// <example>\n///   <summary>Connect to Gmail IMAP using OAuth2</summary>\n///   <code>$clientSecret = Read-Host \"Google client secret\" -AsSecureString\n/// $cred = Connect-OAuthGoogle -GmailAccount \"user@gmail.com\" -ClientID \"id\" -ClientSecretSecureString $clientSecret\n/// Connect-IMAP -Server \"imap.gmail.com\" -Credential $cred -OAuth2</code>\n/// </example>\n/// <example>\n///   <summary>Connect to an IMAP server with clear text username and password</summary>\n///   <code>Connect-IMAP -Server \"imap.example.com\" -UserName \"user\" -Password \"pass\"</code>\n/// </example>\n/// <remarks>\n/// For OAuth2, use the <c>Connect-OAuthGoogle</c> or <c>Connect-OAuthO365</c> cmdlets to obtain a credential object.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// <seealso cref=\"CmdletDisconnectIMAP\"/>\n/// <seealso cref=\"CmdletGetIMAPFolder\"/>\n/// <seealso cref=\"CmdletGetIMAPMessage\"/>\n/// </summary>\n[Cmdlet(VerbsCommunications.Connect, \"IMAP\")]\npublic sealed class CmdletConnectIMAP : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the IMAP server hostname or IP address to connect to.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string Server { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the port to use for the IMAP connection. Default is 993 (IMAPS).</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int Port { get; set; } = 993;\n\n    /// <summary>\n    /// <para type=\"description\">Skips certificate revocation checks during the connection. Useful for environments with limited certificate infrastructure.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SwitchParameter SkipCertificateRevocation { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Skips certificate validation. Use with caution; only for trusted/test environments or self-signed certificates.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SwitchParameter SkipCertificateValidation { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the username for clear text authentication. Required for the ClearText parameter set.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"ClearText\", Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string UserName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the password for clear text authentication. Required for the ClearText parameter set.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"ClearText\", Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string Password { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies a PSCredential object for authentication. Used for OAuth2 or standard credential-based authentication.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the secure socket options for the IMAP connection. Default is Auto. Options: None, Auto, SslOnConnect, StartTls, StartTlsWhenAvailable.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SecureSocketOptions Options { get; set; } = SecureSocketOptions.Auto;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the connection timeout in milliseconds. Default is 120000 (2 minutes).</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int TimeOut { get; set; } = 120000;\n\n    /// <summary>\n    /// <para type=\"description\">Number of connection retry attempts.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Delay in milliseconds between retries.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Multiplier for increasing retry delay.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>\n    /// <para type=\"description\">Enables OAuth2 authentication. Use with a PSCredential object containing the access token as the password.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    public SwitchParameter OAuth2 { get; set; }\n\n    /// <summary>\n    /// Connects to the IMAP server and returns an <see cref=\"ImapConnectionInfo\"/> object with connection and client details.\n    /// </summary>\n    /// <remarks>\n    /// Use the returned object with <c>Disconnect-IMAP</c>, <c>Get-IMAPFolder</c>, or <c>Get-IMAPMessage</c> for further operations.\n    /// </remarks>\n    protected override async Task ProcessRecordAsync() {\n        async Task Authenticate(ImapClient c, CancellationToken ct) {\n            if (ParameterSetName == \"OAuth2\" && OAuth2.IsPresent) {\n                if (Credential == null) {\n                    throw new System.Security.Authentication.AuthenticationException(\"Credential is required for OAuth2 authentication.\");\n                }\n                var username = Credential.UserName;\n                var token = new System.Net.NetworkCredential(string.Empty, Credential.Password).Password;\n                var sasl = new MailKit.Security.SaslMechanismOAuth2(username, token);\n                await c.AuthenticateAsync(sasl, ct);\n            } else if (ParameterSetName == \"ClearText\" && !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) {\n                await c.AuthenticateAsync(UserName, Password, ct);\n            } else if (Credential != null) {\n                var username = Credential.UserName;\n                var password = Credential.Password is SecureString ss ? new System.Net.NetworkCredential(string.Empty, ss).Password : Credential.GetNetworkCredential().Password;\n                await c.AuthenticateAsync(username, password, ct);\n            } else {\n                throw new System.Security.Authentication.AuthenticationException(\"No valid authentication method provided.\");\n            }\n        }\n\n        ImapClient client;\n        try {\n            client = await ImapConnector.ConnectAsync(\n                Server,\n                Port,\n                Options,\n                TimeOut,\n                SkipCertificateRevocation.IsPresent,\n                SkipCertificateValidation.IsPresent,\n                Authenticate,\n                RetryCount,\n                RetryDelayMilliseconds,\n                RetryDelayBackoff,\n                CancelToken);\n        } catch (Exception ex) {\n            WriteWarning($\"Connect-IMAP - {ex.Message}\");\n            return;\n        }\n\n        if (client.IsAuthenticated) {\n            // Open the inbox once and cache folder reference\n            try {\n                _ = client.GetCachedFolder(null, MailKit.FolderAccess.ReadOnly);\n            } catch (ImapCommandException ex) {\n                var responseText = ex.ResponseText;\n                if (!string.IsNullOrWhiteSpace(responseText)) {\n                    LoggingMessages.Logger.WriteWarning($\"Connect-IMAP - Failed to open inbox: {ex.Message} | Server response: {responseText}\");\n                } else {\n                    LoggingMessages.Logger.WriteWarning($\"Connect-IMAP - Failed to open inbox: {ex.Message}\");\n                }\n            }\n            var folder = client.GetCachedFolder(null, MailKit.FolderAccess.ReadOnly);\n            if (folder is not ImapFolder inbox) {\n                WriteWarning(\"Connect-IMAP - Inbox folder not found.\");\n                await client.DisconnectAsync(true);\n                return;\n            }\n            var info = new ImapConnectionInfo {\n                Uri = $\"imaps://{Server}:{Port}/\",\n                AuthenticationMechanisms = client.AuthenticationMechanisms,\n                Capabilities = client.Capabilities,\n                Stream = null, // Not exposed\n                State = null, // Not exposed\n                IsConnected = client.IsConnected,\n                ApopToken = null, // Not applicable for IMAP\n                ExpirePolicy = null, // Not applicable for IMAP\n                Implementation = null, // Not directly available\n                LoginDelay = null, // Not directly available\n                IsAuthenticated = client.IsAuthenticated,\n                IsSecure = client.IsSecure,\n                Data = client,\n                Count = inbox.Count,\n                Messages = inbox,\n                Recent = inbox.Recent,\n                Folder = inbox\n            };\n            info.Folders[inbox.FullName] = inbox;\n            DefaultSessions.ImapSession = info;\n            WriteObject(info);\n        } else {\n            WriteWarning(\"Connect-IMAP - Authentication failed.\");\n            await client.DisconnectAsync(true);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConnectOAuthGoogle.cs",
    "content": "using System.Management.Automation;\nusing System.Security;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Obtains an OAuth2 access token for a Google (Gmail) account for use with IMAP, SMTP, or other Google APIs.</para>\n/// <para type=\"description\">The <c>Connect-OAuthGoogle</c> cmdlet initiates an interactive OAuth2 authentication flow for a Gmail account, returning a <see cref=\"PSCredential\"/> object containing the access token. This credential can be used with other cmdlets (such as <c>Connect-IMAP</c>) that support OAuth2 authentication.</para>\n/// <para type=\"description\">For new scripts, prefer <c>-ClientSecretSecureString</c> or <c>-ClientSecretSecretName</c> rather than passing the client secret as plain text.</para>\n/// <example>\n///   <summary>Obtain an OAuth2 credential for Gmail using a SecureString secret</summary>\n///   <code>$clientSecret = Read-Host \"Google client secret\" -AsSecureString\n/// Connect-OAuthGoogle -GmailAccount \"user@gmail.com\" -ClientID \"id\" -ClientSecretSecureString $clientSecret</code>\n/// </example>\n/// <example>\n///   <summary>Obtain an OAuth2 credential for Gmail using a vault secret</summary>\n///   <code>Connect-OAuthGoogle -GmailAccount \"user@gmail.com\" -ClientID \"id\" -ClientSecretSecretName \"gmail-client-secret\" -ClientSecretVaultName \"MailSecrets\"</code>\n/// </example>\n/// <remarks>\n/// The returned PSCredential contains the access token as the password. Use with IMAP/SMTP cmdlets that support OAuth2. The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommunications.Connect, \"OAuthGoogle\", DefaultParameterSetName = \"PlainText\")]\n[OutputType(typeof(PSCredential))]\npublic sealed class CmdletConnectOAuthGoogle : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the Gmail account (email address) to authenticate.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 client ID from the Google Developer Console.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? ClientID { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 client secret from the Google Developer Console.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainText\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecret { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 client secret from the Google Developer Console as a SecureString.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? ClientSecretSecureString { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the name of a secret to resolve using Get-Secret for the OAuth2 client secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecretSecretName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional vault name used with Get-Secret for the OAuth2 client secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? ClientSecretVaultName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 scopes to request. Default is \"https://mail.google.com/\".</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string[]? Scope { get; set; } = new[] { \"https://mail.google.com/\" };\n\n    /// <summary>\n    /// Performs the interactive OAuth2 authentication and returns a PSCredential with the access token.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var clientSecret = this.ParameterSetName == \"SecureString\"\n            ? CredentialHelpers.ToPlainText(ClientSecretSecureString)\n            : this.ParameterSetName == \"SecretManagement\"\n                ? CredentialHelpers.ToPlainText(CredentialHelpers.ResolveSecretFromVault(ClientSecretSecretName!, ClientSecretVaultName))\n                : ClientSecret;\n\n        if (GmailAccount is null || ClientID is null || string.IsNullOrWhiteSpace(clientSecret) || Scope is null) {\n            WriteError(new ErrorRecord(new PSArgumentNullException(\"GmailAccount\"), \"OAuthGoogleInvalidParameters\", ErrorCategory.InvalidArgument, null));\n            return;\n        }\n\n        OAuthCredential? cred = null;\n        try {\n            cred = await Mailozaurr.OAuthHelpers\n                .AcquireGoogleTokenCachedAsync(GmailAccount, ClientID, clientSecret!, Scope);\n        } catch (System.Exception ex) {\n            WriteError(new ErrorRecord(ex, \"OAuthGoogleAuthFailed\", ErrorCategory.AuthenticationError, null));\n            return;\n        }\n        if (cred != null) {\n            var secure = CredentialHelpers.ToSecureString(cred.AccessToken);\n            var psCred = new PSCredential(cred.UserName, secure);\n            WriteObject(psCred);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConnectOAuthO365.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Obtains an OAuth2 access token for an Office 365 (Microsoft 365) account for use with IMAP, SMTP, or Microsoft Graph.</para>\n/// <para type=\"description\">The <c>Connect-OAuthO365</c> cmdlet initiates an interactive OAuth2 authentication flow for an Office 365 account, returning a <see cref=\"PSCredential\"/> object containing the access token. This credential can be used with other cmdlets (such as <c>Connect-IMAP</c> or <c>Send-EmailMessage</c>) that support OAuth2 authentication.</para>\n/// <example>\n///   <summary>Obtain an OAuth2 credential for Office 365</summary>\n///   <code>Connect-OAuthO365 -Login \"user@tenant.onmicrosoft.com\" -ClientID \"id\" -TenantID \"tenant\"</code>\n/// </example>\n/// <remarks>\n/// The returned PSCredential contains the access token as the password. Use with IMAP/SMTP/Graph cmdlets that support OAuth2.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommunications.Connect, \"OAuthO365\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConnectOAuthO365 : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the login (user principal name) for the Office 365 account. Optional; if not provided, interactive login is used.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? Login { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 client ID from Azure AD App Registration.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? ClientID { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the Azure AD tenant ID (Directory ID).</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? TenantID { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the redirect URI for the OAuth2 flow. Default is the recommended Microsoft URI.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? RedirectUri { get; set; } = \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 scopes to request. Default includes IMAP, POP, and SMTP permissions.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string[]? Scopes { get; set; } = new[] {\n        \"email\",\n        \"offline_access\",\n        \"https://outlook.office.com/IMAP.AccessAsUser.All\",\n        \"https://outlook.office.com/POP.AccessAsUser.All\",\n        \"https://outlook.office.com/SMTP.Send\"\n    };\n\n    /// <summary>\n    /// Performs the interactive OAuth2 authentication and returns a PSCredential with the access token.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        if (ClientID is null || TenantID is null || RedirectUri is null || Scopes is null) {\n            WriteError(new ErrorRecord(new PSArgumentNullException(\"ClientID\"), \"OAuthO365InvalidParameters\", ErrorCategory.InvalidArgument, null));\n            return;\n        }\n\n        Mailozaurr.OAuthCredential? cred = null;\n        try {\n            cred = await Mailozaurr.OAuthHelpers.AcquireO365TokenCachedAsync(Login, ClientID, TenantID, RedirectUri, Scopes);\n        } catch (System.Exception ex) {\n            WriteError(new ErrorRecord(ex, \"OAuthO365AuthFailed\", ErrorCategory.AuthenticationError, null));\n            return;\n        }\n\n        if (cred != null) {\n            var secure = CredentialHelpers.ToSecureString(cred.AccessToken);\n            var psCred = new PSCredential(cred.UserName, secure);\n            WriteObject(psCred);\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConnectPOP3.cs",
    "content": "using MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System.Security;\nusing System.Security.Authentication;\nusing System.Threading;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Connects to a POP3 server and authenticates using credentials, OAuth2, or clear text.</para>\n/// <para type=\"description\">The <c>Connect-POP3</c> cmdlet establishes a connection to a POP3 server using MailKit. It supports multiple authentication methods, including OAuth2, PSCredential, and clear text username/password. The cmdlet returns a <see cref=\"PopConnectionInfo\"/> object containing connection details and the authenticated client for further use in subsequent cmdlets.</para>\n/// <para type=\"description\">Supports advanced options such as certificate validation skipping, custom timeouts, and secure socket options. Designed for secure, flexible, and scriptable POP3 connectivity in PowerShell automation scenarios.</para>\n/// <example>\n///   <summary>Connect to a POP3 server using credentials</summary>\n///   <code>Connect-POP3 -Server \"pop.example.com\" -Credential (Get-Credential)</code>\n/// </example>\n/// <example>\n///   <summary>Connect to Gmail POP3 using OAuth2</summary>\n///   <code>$clientSecret = Read-Host \"Google client secret\" -AsSecureString\n/// $cred = Connect-OAuthGoogle -GmailAccount \"user@gmail.com\" -ClientID \"id\" -ClientSecretSecureString $clientSecret\n/// Connect-POP3 -Server \"pop.gmail.com\" -Credential $cred -OAuth2</code>\n/// </example>\n/// <example>\n///   <summary>Connect to a POP3 server with clear text username and password</summary>\n///   <code>Connect-POP3 -Server \"pop.example.com\" -UserName \"user\" -Password \"pass\"</code>\n/// </example>\n/// <remarks>\n/// For OAuth2, use the <c>Connect-OAuthGoogle</c> or <c>Connect-OAuthO365</c> cmdlets to obtain a credential object.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// <seealso cref=\"CmdletDisconnectPOP3\"/>\n/// <seealso cref=\"CmdletGetPOP3Message\"/>\n/// </summary>\n[Cmdlet(VerbsCommunications.Connect, \"POP3\")]\npublic sealed class CmdletConnectPOP3 : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the POP3 server hostname or IP address to connect to.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string Server { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the port to use for the POP3 connection. Default is 995 (POPS).</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int Port { get; set; } = 995;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the username for clear text authentication. Required for the ClearText parameter set.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"ClearText\", Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string UserName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the password for clear text authentication. Required for the ClearText parameter set.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"ClearText\", Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string Password { get; set; } = string.Empty;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies a PSCredential object for authentication. Used for OAuth2 or standard credential-based authentication.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Skips certificate revocation checks during the connection. Useful for environments with limited certificate infrastructure.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SwitchParameter SkipCertificateRevocation { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Skips certificate validation. Use with caution; only for trusted/test environments or self-signed certificates.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SwitchParameter SkipCertificateValidation { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the secure socket options for the POP3 connection. Default is Auto. Options: None, Auto, SslOnConnect, StartTls, StartTlsWhenAvailable.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SecureSocketOptions Options { get; set; } = SecureSocketOptions.Auto;\n\n    /// <summary>\n    /// <para type=\"description\">When <see cref=\"Options\"/> remains <c>Auto</c>, this switch forces the use of <c>StartTls</c>.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public SwitchParameter EnableExplicit { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the connection timeout in milliseconds. Default is 120000 (2 minutes).</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int TimeOut { get; set; } = 120000;\n\n    /// <summary>\n    /// <para type=\"description\">Number of connection retry attempts.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Delay in milliseconds between retries.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para type=\"description\">Multiplier for increasing retry delay.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    [Parameter(ParameterSetName = \"Credential\")]\n    [Parameter(ParameterSetName = \"ClearText\")]\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>\n    /// <para type=\"description\">Enables OAuth2 authentication. Use with a PSCredential object containing the access token as the password.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"OAuth2\")]\n    public SwitchParameter OAuth2 { get; set; }\n\n    /// <summary>\n    /// Connects to the POP3 server and returns a <see cref=\"PopConnectionInfo\"/> object with connection and client details.\n    /// </summary>\n    /// <remarks>\n    /// Use the returned object with <c>Disconnect-POP3</c> or <c>Get-POP3Message</c> for further operations.\n    /// </remarks>\n    protected override async Task ProcessRecordAsync() {\n        async Task Authenticate(Pop3Client c, CancellationToken ct) {\n            if (ParameterSetName == \"OAuth2\" && OAuth2.IsPresent) {\n                if (Credential == null) {\n                    throw new System.Security.Authentication.AuthenticationException(\"Credential is required for OAuth2 authentication.\");\n                }\n                var username = Credential.UserName;\n                var token = new System.Net.NetworkCredential(string.Empty, Credential.Password).Password;\n                var sasl = new MailKit.Security.SaslMechanismOAuth2(username, token);\n                await c.AuthenticateAsync(sasl, ct);\n            } else if (ParameterSetName == \"ClearText\" && !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) {\n                await c.AuthenticateAsync(UserName, Password, ct);\n            } else if (Credential != null) {\n                var username = Credential.UserName;\n                var password = Credential.Password is SecureString ss ? new System.Net.NetworkCredential(string.Empty, ss).Password : Credential.GetNetworkCredential().Password;\n                await c.AuthenticateAsync(username, password, ct);\n            } else {\n                throw new System.Security.Authentication.AuthenticationException(\"No valid authentication method provided.\");\n            }\n        }\n\n        Pop3Client client;\n        var opts = Options;\n        if (EnableExplicit.IsPresent && opts == SecureSocketOptions.Auto) {\n            opts = SecureSocketOptions.StartTls;\n        }\n        try {\n            client = await Pop3Connector.ConnectAsync(\n                Server,\n                Port,\n                opts,\n                TimeOut,\n                SkipCertificateRevocation.IsPresent,\n                SkipCertificateValidation.IsPresent,\n                Authenticate,\n                RetryCount,\n                RetryDelayMilliseconds,\n                RetryDelayBackoff,\n                CancelToken);\n        } catch (Exception ex) {\n            WriteWarning($\"Connect-POP3 - {ex.Message}\");\n            return;\n        }\n\n        if (client.IsAuthenticated) {\n            var info = new PopConnectionInfo {\n                Uri = $\"pops://{Server}:{Port}/\",\n                AuthenticationMechanisms = client.AuthenticationMechanisms,\n                Capabilities = client.Capabilities,\n                Stream = null, // Not exposed\n                State = null,\n                IsConnected = client.IsConnected,\n                ApopToken = null, // Not directly available\n                ExpirePolicy = null, // Not directly available\n                Implementation = null, // Not directly available\n                LoginDelay = null, // Not directly available\n                IsAuthenticated = client.IsAuthenticated,\n                IsSecure = client.IsSecure,\n                Data = client,\n                Count = client.Count,\n                Messages = null, // Not directly available\n                Recent = 0 // Not directly available\n            };\n            DefaultSessions.Pop3Session = info;\n            WriteObject(info);\n        } else {\n            WriteWarning(\"Connect-POP3 - Authentication failed.\");\n            await client.DisconnectAsync(true);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertFromEmlToMsg.cs",
    "content": "﻿namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Converts EML files to MSG format for compatibility with Microsoft Outlook and other clients.</para>\n/// <para type=\"description\">The <c>ConvertFrom-EmlToMsg</c> cmdlet converts one or more EML files to MSG format. Specify the input EML file paths and the output folder. The cmdlet processes each EML file and saves the converted MSG file in the specified output folder. Supports overwriting existing files with the <c>-Force</c> parameter.</para>\n/// <example>\n///   <summary>Convert multiple EML files to MSG format</summary>\n///   <code>ConvertFrom-EmlToMsg -InputPath \"C:\\Mail\\mail1.eml\",\"C:\\Mail\\mail2.eml\" -OutputFolder \"C:\\Converted\"</code>\n/// </example>\n/// <example>\n///   <summary>Convert EML files and overwrite existing MSG files</summary>\n///   <code>ConvertFrom-EmlToMsg -InputPath \"C:\\Mail\\*.eml\" -OutputFolder \"C:\\Converted\" -Force</code>\n/// </example>\n/// <remarks>\n/// MSG format is commonly used by Microsoft Outlook. Use this cmdlet to migrate or archive EML messages for Outlook compatibility.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.ConvertFrom, \"EmlToMsg\")]\npublic sealed class CmdletConvertFromEmlToMsg : AsyncPSCmdlet {\n    private InternalLogger? _logger;\n    private InternalLoggerPowerShell? _listener;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the paths to the EML files to convert. Accepts an array of strings. This parameter is mandatory.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]\n    [ValidateNotNullOrEmpty]\n    public string[]? InputPath { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the folder where the converted MSG files will be saved. This parameter is mandatory.</para>\n    /// </summary>\n    [Alias(\"OutputPath\")]\n    [Parameter(Mandatory = true, Position = 1)]\n    [ValidateNotNullOrEmpty]\n    public string? OutputFolder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, the cmdlet will overwrite existing MSG files without prompting.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, Position = 2)]\n    public SwitchParameter Force { get; set; }\n\n    /// <summary>\n    /// Initializes logging for the conversion process.\n    /// </summary>\n    protected override Task BeginProcessingAsync() {\n        // Initialize the logger to be able to see verbose, warning, debug, error, progress, and information messages.\n        _logger = new InternalLogger();\n        _listener = new InternalLoggerPowerShell(_logger, this.WriteVerbose, this.WriteWarning, this.WriteDebug, this.WriteError, this.WriteProgress, this.WriteInformation);\n        LoggingMessages.Logger = _logger;\n        return Task.CompletedTask;\n    }\n    /// <summary>\n    /// Converts the specified EML files to MSG format and writes the results to the output folder.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (InputPath is null || OutputFolder is null) {\n            return Task.CompletedTask;\n        }\n\n        var outputMessage = EmailMessage.ConvertEmlToMsg(InputPath, OutputFolder, Force);\n        foreach (var obj in outputMessage) {\n            WriteObject(obj);\n        }\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Releases logging resources.\n    /// </summary>\n    protected override Task EndProcessingAsync() {\n        _listener?.Dispose();\n        _listener = null;\n        return Task.CompletedTask;\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertFromMsgToEml.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Converts MSG files to EML format for interoperability with other clients.</para>\n/// <para type=\"description\">The <c>ConvertFrom-MsgToEml</c> cmdlet converts one or more MSG files to EML format. Provide input MSG file paths and the destination folder. Existing files can be overwritten with the <c>-Force</c> switch.</para>\n/// <example>\n///   <summary>Convert multiple MSG files</summary>\n///   <code>ConvertFrom-MsgToEml -InputPath \"C:\\Mail\\mail1.msg\",\"C:\\Mail\\mail2.msg\" -OutputFolder \"C:\\Converted\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to archive or migrate Outlook messages to the portable EML format.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.ConvertFrom, \"MsgToEml\")]\npublic sealed class CmdletConvertFromMsgToEml : AsyncPSCmdlet {\n    private InternalLogger? _logger;\n    private InternalLoggerPowerShell? _listener;\n    /// <summary>\n    /// <para type=\"description\">Paths to the MSG files to convert.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]\n    [ValidateNotNullOrEmpty]\n    public string[]? InputPath { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Destination folder for the converted EML files.</para>\n    /// </summary>\n    [Alias(\"OutputPath\")]\n    [Parameter(Mandatory = true, Position = 1)]\n    [ValidateNotNullOrEmpty]\n    public string? OutputFolder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Overwrite existing files.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, Position = 2)]\n    public SwitchParameter Force { get; set; }\n\n    /// <summary>\n    /// Initializes logging for the conversion process.\n    /// </summary>\n    protected override Task BeginProcessingAsync() {\n        _logger = new InternalLogger();\n        _listener = new InternalLoggerPowerShell(_logger, this.WriteVerbose, this.WriteWarning, this.WriteDebug, this.WriteError, this.WriteProgress, this.WriteInformation);\n        LoggingMessages.Logger = _logger;\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Converts the specified MSG files to EML format.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (InputPath is null || OutputFolder is null) {\n            return Task.CompletedTask;\n        }\n\n        var outputMessage = EmailMessage.ConvertMsgToEml(InputPath, OutputFolder, Force);\n        foreach (var obj in outputMessage) {\n            WriteObject(obj);\n        }\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Cleans up logging resources.\n    /// </summary>\n    protected override Task EndProcessingAsync() {\n        _listener?.Dispose();\n        _listener = null;\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertFromOAuth2Credential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\n\n/// <summary>\n/// <para type=\"synopsis\">Extracts username and token from an OAuth2 PSCredential.</para>\n/// <para type=\"description\">The <c>ConvertFrom-OAuth2Credential</c> cmdlet converts a <see cref=\"PSCredential\"/> containing an OAuth2 access token into a PSCustomObject with <c>UserName</c> and <c>Token</c> properties.</para>\n/// <example>\n///   <summary>Convert credential back to plain token</summary>\n///   <code>$info = ConvertFrom-OAuth2Credential -Credential $OAuthCred</code>\n/// </example>\n/// </summary>\n[Cmdlet(VerbsData.ConvertFrom, \"OAuth2Credential\")]\n[OutputType(typeof(PSObject))]\npublic class CmdletConvertFromOAuth2Credential : PSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the PSCredential containing the OAuth2 token.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Processes the record by extracting the user name and token from the credential.\n    /// </summary>\n    protected override void ProcessRecord() {\n        if (Credential is null) {\n            return;\n        }\n        var network = Credential.GetNetworkCredential();\n        var (userName, token) = Helpers.ConvertFromOAuth2Credential(network);\n        var obj = new PSObject();\n        obj.Properties.Add(new PSNoteProperty(\"UserName\", userName));\n        obj.Properties.Add(new PSNoteProperty(\"Token\", token));\n        WriteObject(obj);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertToGraphCertificateCredential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// <para type=\"synopsis\">Creates a PSCredential containing a Microsoft Graph access token obtained using certificate authentication.</para>\n/// <para type=\"description\">The <c>ConvertTo-GraphCertificateCredential</c> cmdlet authenticates with Microsoft Graph using a client certificate and returns a <see cref=\"PSCredential\"/> with the access token. Use the resulting credential with cmdlets that accept Graph tokens.</para>\n/// <para type=\"description\">For new scripts, prefer <c>-CertificatePasswordSecureString</c> or <c>-SecretName</c> rather than passing the certificate password as plain text.</para>\n/// <example>\n///   <summary>Acquire a token using a certificate and SecureString password</summary>\n///   <code>$certificatePassword = Read-Host \"Certificate password\" -AsSecureString\n/// ConvertTo-GraphCertificateCredential -ClientId \"id\" -TenantId \"tenant\" -CertificatePath \"cert.pfx\" -CertificatePasswordSecureString $certificatePassword</code>\n/// </example>\n/// <example>\n///   <summary>Acquire a token using a certificate password from a vault secret</summary>\n///   <code>ConvertTo-GraphCertificateCredential -ClientId \"id\" -TenantId \"tenant\" -CertificatePath \"cert.pfx\" -SecretName \"graph-certificate-password\" -VaultName \"MailSecrets\"</code>\n/// </example>\n/// <remarks>\n/// This cmdlet simplifies obtaining an app-only token for Microsoft Graph when using certificate authentication. The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// </summary>\n[Cmdlet(VerbsData.ConvertTo, \"GraphCertificateCredential\", DefaultParameterSetName = \"PlainText\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConvertToGraphCertificateCredential : AsyncPSCmdlet {\n    /// <summary>\n    /// Azure AD application (client) identifier.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? ClientId { get; set; }\n\n    /// <summary>\n    /// Azure AD tenant identifier.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? TenantId { get; set; }\n\n    /// <summary>\n    /// Path to the client certificate (PFX).\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePath { get; set; }\n\n    /// <summary>\n    /// Password for the client certificate.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainText\")]\n    [ValidateNotNullOrEmpty]\n    public string? CertificatePassword { get; set; }\n\n    /// <summary>\n    /// Password for the client certificate as a SecureString.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? CertificatePasswordSecureString { get; set; }\n\n    /// <summary>\n    /// Name of the certificate password secret resolved via Get-Secret.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? SecretName { get; set; }\n\n    /// <summary>\n    /// Optional vault name used with Get-Secret.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? VaultName { get; set; }\n\n    /// <summary>\n    /// Optional scopes to request.\n    /// </summary>\n    [Parameter]\n    public string[]? Scopes { get; set; }\n\n    /// <summary>\n    /// Acquires an app-only access token using certificate authentication and returns it as a credential.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var certificatePassword = this.ParameterSetName == \"SecureString\"\n            ? CredentialHelpers.ToPlainText(CertificatePasswordSecureString)\n            : this.ParameterSetName == \"SecretManagement\"\n                ? CredentialHelpers.ToPlainText(CredentialHelpers.ResolveSecretFromVault(SecretName!, VaultName))\n            : CertificatePassword;\n\n        GraphAuthorization token;\n        try {\n            token = await Mailozaurr.OAuthHelpers.AcquireGraphCertificateTokenAsync(\n                    ClientId!,\n                    TenantId!,\n                    CertificatePath!,\n                    certificatePassword!,\n                    Scopes);\n        } catch (System.Exception ex) {\n            WriteError(new ErrorRecord(ex, \"GraphCertificateAuthFailed\", ErrorCategory.AuthenticationError, null));\n            return;\n        }\n        SecureString secure = new();\n        foreach (var ch in token.AccessToken) secure.AppendChar(ch);\n        var username = $\"{ClientId}@{TenantId}\";\n        var credential = new PSCredential(username, secure);\n        WriteObject(credential);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertToGraphCredential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security;\n\n/// <summary>\n/// <para type=\"synopsis\">Creates a PSCredential object for Microsoft Graph authentication from client ID, secret, and directory ID.</para>\n/// <para type=\"description\">The <c>ConvertTo-GraphCredential</c> cmdlet creates a <see cref=\"PSCredential\"/> object suitable for Microsoft Graph authentication, using the provided client ID, client secret, and directory (tenant) ID. The resulting credential can be used with cmdlets that require Graph authentication.</para>\n/// <para type=\"description\">For new scripts, prefer <c>-ClientSecretSecureString</c> or <c>-SecretName</c> instead of passing the client secret in plain text.</para>\n/// <example>\n///   <summary>Create a Graph credential from a SecureString secret</summary>\n///   <code>$secret = Read-Host \"Graph client secret\" -AsSecureString\n/// ConvertTo-GraphCredential -ClientId \"id\" -ClientSecretSecureString $secret -DirectoryId \"tenant\"</code>\n/// </example>\n/// <example>\n///   <summary>Create a Graph credential from a vault secret</summary>\n///   <code>ConvertTo-GraphCredential -ClientId \"id\" -SecretName \"graph-client-secret\" -VaultName \"MailSecrets\" -DirectoryId \"tenant\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to prepare credentials for use with Microsoft Graph cmdlets in automation scenarios. The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.ConvertTo, \"GraphCredential\", DefaultParameterSetName = \"ClearText\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConvertToGraphCredential: PSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the client ID for Microsoft Graph authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"ClearText\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Encrypted\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientId { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the client secret in clear text. Use only with the ClearText parameter set.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"ClearText\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecret { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the client secret in encrypted form. Use only with the Encrypted parameter set.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Encrypted\")]\n    [ValidateNotNullOrEmpty]\n    public string? ClientSecretEncrypted { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the client secret as a SecureString. Use only with the SecureString parameter set.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? ClientSecretSecureString { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the name of a secret to resolve using Get-Secret. Use only with the SecretManagement parameter set.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? SecretName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional vault name used with Get-Secret. Use only with the SecretManagement parameter set.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? VaultName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the directory (tenant) ID for Microsoft Graph authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"ClearText\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Encrypted\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? DirectoryId { get; set; }\n\n    /// <summary>\n    /// Creates a PSCredential object for Microsoft Graph authentication.\n    /// </summary>\n    protected override void ProcessRecord() {\n        SecureString secret = this.ParameterSetName == \"Encrypted\"\n            ? CredentialHelpers.ToSecureString(ClientSecretEncrypted)\n            : this.ParameterSetName == \"SecureString\"\n                ? ClientSecretSecureString!\n                : this.ParameterSetName == \"SecretManagement\"\n                    ? CredentialHelpers.ResolveSecretFromVault(SecretName!, VaultName)\n                : CredentialHelpers.ToSecureString(ClientSecret);\n        var username = $\"{ClientId}@{DirectoryId}\";\n        var credential = new PSCredential(username, secret);\n        WriteObject(credential);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertToMailgunCredential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security;\n\n/// <summary>\n/// <para type=\"synopsis\">Creates a PSCredential object for Mailgun API authentication from an API key.</para>\n/// <para type=\"description\">Use <c>ConvertTo-MailgunCredential</c> to generate a <see cref=\"PSCredential\"/> that can be passed to <c>Send-EmailMessage</c> when using the Mailgun provider.</para>\n/// <para type=\"description\">For new scripts, prefer <c>-ApiKeySecureString</c> or <c>-SecretName</c> instead of passing the API key as plain text.</para>\n/// <example>\n///   <summary>Create a Mailgun credential from a SecureString API key</summary>\n///   <code>$apiKey = Read-Host \"Mailgun API key\" -AsSecureString\n/// ConvertTo-MailgunCredential -ApiKeySecureString $apiKey</code>\n/// </example>\n/// <example>\n///   <summary>Create a Mailgun credential from a vault secret</summary>\n///   <code>ConvertTo-MailgunCredential -SecretName \"mailgun-api-key\" -VaultName \"MailSecrets\"</code>\n/// </example>\n/// </summary>\n[Cmdlet(VerbsData.ConvertTo, \"MailgunCredential\", DefaultParameterSetName = \"PlainText\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConvertToMailgunCredential : PSCmdlet {\n    /// <summary>\n    /// Mailgun API key used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainText\")]\n    [ValidateNotNullOrEmpty]\n    public string? ApiKey { get; set; }\n\n    /// <summary>\n    /// Mailgun API key used for authentication as a SecureString.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? ApiKeySecureString { get; set; }\n\n    /// <summary>\n    /// Mailgun secret name used with Get-Secret.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? SecretName { get; set; }\n\n    /// <summary>\n    /// Optional vault name used with Get-Secret.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? VaultName { get; set; }\n\n    /// <summary>\n    /// Converts the provided API key into a PSCredential for Mailgun.\n    /// </summary>\n    protected override void ProcessRecord() {\n        SecureString secret = this.ParameterSetName == \"SecureString\"\n            ? ApiKeySecureString!\n            : this.ParameterSetName == \"SecretManagement\"\n                ? CredentialHelpers.ResolveSecretFromVault(SecretName!, VaultName)\n            : CredentialHelpers.ToSecureString(ApiKey);\n        var credential = new PSCredential(\"Mailgun\", secret);\n        WriteObject(credential);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertToOAuth2Credential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security;\n\n/// <summary>\n/// <para type=\"synopsis\">Creates a PSCredential object for OAuth2 authentication from a username and token.</para>\n/// <para type=\"description\">The <c>ConvertTo-OAuth2Credential</c> cmdlet creates a <see cref=\"PSCredential\"/> object suitable for OAuth2 authentication, using the provided username and access token. The resulting credential can be used with cmdlets that require OAuth2 authentication (such as IMAP, SMTP, or POP3 with OAuth2).</para>\n/// <para type=\"description\">For automation and interactive use, prefer <c>-TokenSecureString</c> or <c>-SecretName</c> over passing the token as plain text.</para>\n/// <example>\n///   <summary>Create an OAuth2 credential from a SecureString token</summary>\n///   <code>$token = Read-Host \"OAuth token\" -AsSecureString\n/// ConvertTo-OAuth2Credential -UserName \"user@example.com\" -TokenSecureString $token</code>\n/// </example>\n/// <example>\n///   <summary>Create an OAuth2 credential from a vault secret</summary>\n///   <code>ConvertTo-OAuth2Credential -UserName \"user@example.com\" -SecretName \"gmail-access-token\" -VaultName \"MailSecrets\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to prepare credentials for use with OAuth2-enabled cmdlets in automation scenarios. The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.ConvertTo, \"OAuth2Credential\", DefaultParameterSetName = \"PlainText\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConvertToOAuth2Credential : PSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the username for OAuth2 authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserName { get; set; }\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 access token.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainText\")]\n    [ValidateNotNullOrEmpty]\n    public string? Token { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the OAuth2 access token as a SecureString.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? TokenSecureString { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the name of a secret to resolve using Get-Secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? SecretName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional vault name used with Get-Secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? VaultName { get; set; }\n\n    /// <summary>\n    /// Creates a PSCredential object for OAuth2 authentication.\n    /// </summary>\n    protected override void ProcessRecord() {\n        var secret = this.ParameterSetName switch {\n            \"SecureString\" => TokenSecureString!,\n            \"SecretManagement\" => CredentialHelpers.ResolveSecretFromVault(SecretName!, VaultName),\n            _ => CredentialHelpers.ToSecureString(Token)\n        };\n        var credential = new PSCredential(UserName, secret);\n        WriteObject(credential);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletConvertToSendGridCredential.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security;\n\n/// <summary>\n/// <para type=\"synopsis\">Creates a PSCredential object for SendGrid API authentication from an API key.</para>\n/// <para type=\"description\">The <c>ConvertTo-SendGridCredential</c> cmdlet creates a <see cref=\"PSCredential\"/> object suitable for SendGrid API authentication, using the provided API key. The resulting credential can be used with cmdlets that require SendGrid authentication (such as <c>Send-EmailMessage</c> with the <c>-SendGrid</c> switch).</para>\n/// <para type=\"description\">For new scripts, prefer <c>-ApiKeySecureString</c> or <c>-SecretName</c> instead of passing the API key as plain text.</para>\n/// <example>\n///   <summary>Create a SendGrid credential from a SecureString API key</summary>\n///   <code>$apiKey = Read-Host \"SendGrid API key\" -AsSecureString\n/// ConvertTo-SendGridCredential -ApiKeySecureString $apiKey</code>\n/// </example>\n/// <example>\n///   <summary>Create a SendGrid credential from a vault secret</summary>\n///   <code>ConvertTo-SendGridCredential -SecretName \"sendgrid-api-key\" -VaultName \"MailSecrets\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to prepare credentials for use with SendGrid-enabled cmdlets in automation scenarios. The vault example requires a <c>Get-Secret</c> implementation such as Microsoft.PowerShell.SecretManagement.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.ConvertTo, \"SendGridCredential\", DefaultParameterSetName = \"PlainText\")]\n[OutputType(typeof(PSCredential))]\npublic class CmdletConvertToSendGridCredential : PSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the SendGrid API key.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"PlainText\")]\n    [ValidateNotNullOrEmpty]\n    public string? ApiKey { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the SendGrid API key as a SecureString.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [ValidateNotNull]\n    public SecureString? ApiKeySecureString { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the name of a secret to resolve using Get-Secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecretManagement\")]\n    [ValidateNotNullOrEmpty]\n    public string? SecretName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional vault name used with Get-Secret.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecretManagement\")]\n    public string? VaultName { get; set; }\n\n    /// <summary>\n    /// Creates a PSCredential object for SendGrid API authentication.\n    /// </summary>\n    protected override void ProcessRecord() {\n        var secret = this.ParameterSetName == \"SecureString\"\n            ? ApiKeySecureString!\n            : this.ParameterSetName == \"SecretManagement\"\n                ? CredentialHelpers.ResolveSecretFromVault(SecretName!, VaultName)\n                : CredentialHelpers.ToSecureString(ApiKey);\n        var credential = new PSCredential(\"SendGrid\", secret);\n        WriteObject(credential);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletDisconnectEmailGraph.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Clears Microsoft Graph credentials created by Connect-EmailGraph.</para>\n/// <para type=\"description\">The <c>Disconnect-EmailGraph</c> cmdlet removes sensitive\n/// information from a <see cref=\"GraphConnectionInfo\"/> object returned by\n/// <c>Connect-EmailGraph</c>. Use it when you no longer need the connection to\n/// ensure credentials are disposed and not kept in memory.</para>\n/// <example>\n///   <summary>Disconnect from Microsoft Graph</summary>\n///   <code>$graph = Connect-EmailGraph ...; Disconnect-EmailGraph -Connection $graph</code>\n/// </example>\n/// <remarks>\n/// This cmdlet does not close any network connections but clears the stored\n/// client secret and marks the connection as disconnected.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectEmailGraph\"/>\n/// </summary>\n[Cmdlet(VerbsCommunications.Disconnect, \"EmailGraph\")]\npublic sealed class CmdletDisconnectEmailGraph : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"GraphConnectionInfo\"/> object to clear.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Clears the credential stored in the connection object.</summary>\n    protected override Task ProcessRecordAsync() {\n        if (Connection != null) {\n            var cred = Connection.Credential;\n            if (cred != null) {\n                cred.ClientSecret = string.Empty;\n            }\n            Connection.IsConnected = false;\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletDisconnectIMAP.cs",
    "content": "using Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Disconnects an active IMAP connection previously established with Connect-IMAP.</para>\n/// <para type=\"description\">The <c>Disconnect-IMAP</c> cmdlet disconnects an active MailKit IMAP client session. Pass the <see cref=\"ImapConnectionInfo\"/> object returned by <c>Connect-IMAP</c> to this cmdlet to safely close the connection and release resources.</para>\n/// <example>\n///   <summary>Disconnect an IMAP client</summary>\n///   <code>$client = Connect-IMAP ...; Disconnect-IMAP -Client $client</code>\n/// </example>\n/// <remarks>\n/// Always disconnect IMAP sessions to avoid resource leaks and server-side session limits.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommunications.Disconnect, \"IMAP\")]\npublic sealed class CmdletDisconnectIMAP : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object containing the MailKit IMAP client instance to disconnect. This is the object returned by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// Disconnects the provided IMAP client. Writes a warning if disconnection fails.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (Client != null) {\n            var data = Client.Data;\n            if (data != null) {\n                try {\n                    data.Disconnect(true);\n                    data.ClearFolderCache();\n                } catch (System.Exception ex) {\n                    WriteWarning($\"Disconnect-IMAP - Unable to disconnect: {ex.Message}\");\n                } finally {\n                    data.ServerCertificateValidationCallback = null;\n                }\n            } else {\n                WriteWarning(\"Disconnect-IMAP - The provided object does not contain a valid Data property of type ImapClient.\");\n            }\n        }\n        return Task.CompletedTask;\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletDisconnectPOP3.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit.Net.Pop3;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Disconnects an active POP3 connection previously established with Connect-POP3.</para>\n/// <para type=\"description\">The <c>Disconnect-POP3</c> cmdlet disconnects an active MailKit POP3 client session. Pass the <see cref=\"PopConnectionInfo\"/> object returned by <c>Connect-POP3</c> to this cmdlet to safely close the connection and release resources.</para>\n/// <example>\n///   <summary>Disconnect a POP3 client</summary>\n///   <code>$client = Connect-POP3 ...; Disconnect-POP3 -Client $client</code>\n/// </example>\n/// <remarks>\n/// Always disconnect POP3 sessions to avoid resource leaks and server-side session limits.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectPOP3\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommunications.Disconnect, \"POP3\")]\npublic sealed class CmdletDisconnectPOP3 : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"PopConnectionInfo\"/> object containing the MailKit POP3 client instance to disconnect. This is the object returned by <c>Connect-POP3</c>.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// Disconnects the provided POP3 client. Writes a warning if disconnection fails.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (Client != null) {\n            var data = Client.Data;\n            if (data != null) {\n                try {\n                    data.Disconnect(true);\n                } catch (System.Exception ex) {\n                    WriteWarning($\"Disconnect-POP3 - Unable to disconnect: {ex.Message}\");\n                } finally {\n                    data.ServerCertificateValidationCallback = null;\n                }\n            } else {\n                WriteWarning(\"Disconnect-POP3 - The provided object does not contain a valid Data property of type Pop3Client.\");\n            }\n        }\n        return Task.CompletedTask;\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetDmarcReport.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr.DmarcReports;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Searches for DMARC aggregate reports in a mailbox.</para>\n/// <para type=\"description\">The <c>Get-DmarcReport</c> cmdlet queries IMAP, POP3, Microsoft Graph, or Gmail API to find DMARC aggregate reports and expose zipped XML attachments.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"DmarcReport\")]\n[OutputType(typeof(DmarcReport))]\npublic sealed class CmdletGetDmarcReport : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Mail protocol to use.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public EmailProtocol Protocol { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional domain filter.</para>\n    /// </summary>\n    [Parameter]\n    public string? Domain { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports since this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports before this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Maximum number of reports to return. Default is unlimited.</para>\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int Count { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">IMAP folder to search.</para>\n    /// </summary>\n    [Parameter]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">User principal name when using Microsoft Graph.</para>\n    /// </summary>\n    [Parameter]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Gmail account address when using Gmail API.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"GmailApi\")]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">OAuth credential used for Gmail API authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"GmailApi\")]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Maximum concurrent MIME downloads; set to 1 to disable parallelism.</para>\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int ParallelDownloadLimit { get; set; } = 4;\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        int max = Count > 0 ? Count : int.MaxValue;\n        switch (Protocol) {\n            case EmailProtocol.Imap: {\n                var conn = DefaultSessions.ImapSession;\n                if (conn != null && conn.Data != null) {\n                    var reports = await MailboxSearcher.SearchDmarcReportsAsync(\n                        conn.Data,\n                        Folder,\n                        Since,\n                        Before,\n                        Domain,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports) WriteObject(report);\n                } else {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-DmarcReport - IMAP client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n                break;\n            }\n            case EmailProtocol.Pop3: {\n                var conn = DefaultSessions.Pop3Session;\n                if (conn != null && conn.Data != null) {\n                    var reports = await MailboxSearcher.SearchDmarcReportsAsync(\n                        conn.Data,\n                        Since,\n                        Before,\n                        Domain,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports) WriteObject(report);\n                } else {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-DmarcReport - POP3 client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n                break;\n            }\n            case EmailProtocol.Graph: {\n                var conn = DefaultSessions.GraphSession;\n                if (conn != null && conn.Credential != null && !string.IsNullOrWhiteSpace(UserPrincipalName)) {\n                    var reports = await MailboxSearcher.SearchDmarcReportsAsync(\n                        conn.Credential,\n                        UserPrincipalName!,\n                        Since,\n                        Before,\n                        Domain,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports) WriteObject(report);\n                } else {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-DmarcReport - Graph connection or UserPrincipalName missing.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n                break;\n            }\n            case EmailProtocol.GmailApi: {\n                if (Credential != null && !string.IsNullOrWhiteSpace(GmailAccount)) {\n                    var net = Credential.GetNetworkCredential();\n                    var oauth = new OAuthCredential { UserName = net.UserName, AccessToken = net.Password, ExpiresOn = DateTimeOffset.MaxValue };\n                    var client = new GmailApiClient(oauth);\n                    var reports = await MailboxSearcher.SearchDmarcReportsAsync(\n                        client,\n                        GmailAccount!,\n                        Since,\n                        Before,\n                        Domain,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports) WriteObject(report);\n                } else {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-DmarcReport - Gmail account or Credential missing.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailDeliveryMatch.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Searches for non-delivery reports and matches them to sent messages.</para>\n/// <para type=\"description\">The <c>Get-EmailDeliveryMatch</c> cmdlet searches for non-delivery reports and uses a <see cref=\"SendLogResolver\"/> to correlate them with sent messages.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailDeliveryMatch\")]\n[OutputType(typeof(NonDeliveryReportResult))]\npublic sealed class CmdletGetEmailDeliveryMatch : AsyncPSCmdlet\n{\n    /// <summary>\n    /// <para type=\"description\">Mail protocol to use.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public EmailProtocol Protocol { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Resolver used to correlate NDRs with sent messages.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public SendLogResolver? Resolver { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports for recipients containing this value are returned.</para>\n    /// </summary>\n    [Parameter]\n    public string? Recipient { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports matching this message ID are returned.</para>\n    /// </summary>\n    [Parameter]\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports since this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports before this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Maximum number of reports to return. Default is unlimited.</para>\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int Count { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">IMAP folder to search.</para>\n    /// </summary>\n    [Parameter]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">User principal name when using Microsoft Graph.</para>\n    /// </summary>\n    [Parameter]\n    public string? UserPrincipalName { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync()\n    {\n        if (Resolver == null)\n        {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Get-EmailDeliveryMatch - Resolver is required.\"),\n                \"ResolverMissing\",\n                ErrorCategory.InvalidArgument,\n                null));\n            return;\n        }\n\n        int max = Count > 0 ? Count : int.MaxValue;\n        switch (Protocol)\n        {\n            case EmailProtocol.Imap:\n            {\n                var conn = DefaultSessions.ImapSession;\n                if (conn != null && conn.Data != null)\n                {\n                    var service = new ImapNonDeliveryReportService(conn.Data, Resolver, Folder);\n                    var results = await service.SearchAsync(Since, Before, Recipient, MessageId, max, CancelToken);\n                    foreach (var result in results)\n                    {\n                        WriteObject(result);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryMatch - IMAP client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n            case EmailProtocol.Pop3:\n            {\n                var conn = DefaultSessions.Pop3Session;\n                if (conn != null && conn.Data != null)\n                {\n                    var service = new Pop3NonDeliveryReportService(conn.Data, Resolver);\n                    var results = await service.SearchAsync(Since, Before, Recipient, MessageId, max, CancelToken);\n                    foreach (var result in results)\n                    {\n                        WriteObject(result);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryMatch - POP3 client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n            case EmailProtocol.Graph:\n            {\n                var conn = DefaultSessions.GraphSession;\n                if (conn != null && conn.Credential != null && !string.IsNullOrWhiteSpace(UserPrincipalName))\n                {\n                    var service = new GraphNonDeliveryReportService(conn.Credential, UserPrincipalName!, Resolver);\n                    var results = await service.SearchAsync(Since, Before, Recipient, MessageId, max, CancelToken);\n                    foreach (var result in results)\n                    {\n                        WriteObject(result);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryMatch - Graph connection or UserPrincipalName missing.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailDeliveryStatus.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Searches for non-delivery reports in a mailbox.</para>\n/// <para type=\"description\">The <c>Get-EmailDeliveryStatus</c> cmdlet queries IMAP, POP3, Microsoft Graph, or Gmail API to find non-delivery reports using optional filters.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailDeliveryStatus\")]\n[OutputType(typeof(NonDeliveryReport))]\npublic sealed class CmdletGetEmailDeliveryStatus : AsyncPSCmdlet\n{\n    /// <summary>\n    /// <para type=\"description\">Mail protocol to use.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public EmailProtocol Protocol { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports for recipients containing this value are returned.</para>\n    /// </summary>\n    [Parameter]\n    public string? Recipient { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports matching this message ID are returned.</para>\n    /// </summary>\n    [Parameter]\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports since this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only reports before this time are returned.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Maximum number of reports to return. Default is unlimited.</para>\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int Count { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">IMAP folder to search.</para>\n    /// </summary>\n    [Parameter]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">User principal name when using Microsoft Graph.</para>\n    /// </summary>\n    [Parameter]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Gmail account address when using Gmail API.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"GmailApi\")]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">OAuth credential used for Gmail API authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"GmailApi\")]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Maximum concurrent MIME downloads; set to 1 to disable parallelism.</para>\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int ParallelDownloadLimit { get; set; } = 4;\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync()\n    {\n        int max = Count > 0 ? Count : int.MaxValue;\n        switch (Protocol)\n        {\n            case EmailProtocol.Imap:\n            {\n                var conn = DefaultSessions.ImapSession;\n                if (conn != null && conn.Data != null)\n                {\n                    var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                        conn.Data,\n                        Folder,\n                        Since,\n                        Before,\n                        Recipient,\n                        MessageId,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports)\n                    {\n                        WriteObject(report);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryStatus - IMAP client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n            case EmailProtocol.Pop3:\n            {\n                var conn = DefaultSessions.Pop3Session;\n                if (conn != null && conn.Data != null)\n                {\n                    var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                        conn.Data,\n                        Since,\n                        Before,\n                        Recipient,\n                        MessageId,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports)\n                    {\n                        WriteObject(report);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryStatus - POP3 client not provided or not connected.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n            case EmailProtocol.Graph:\n            {\n                var conn = DefaultSessions.GraphSession;\n                if (conn != null && conn.Credential != null && !string.IsNullOrWhiteSpace(UserPrincipalName))\n                {\n                    var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                        conn.Credential,\n                        UserPrincipalName!,\n                        Since,\n                        Before,\n                        Recipient,\n                        MessageId,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports)\n                    {\n                        WriteObject(report);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryStatus - Graph connection or UserPrincipalName missing.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n            case EmailProtocol.GmailApi:\n            {\n                if (Credential != null && !string.IsNullOrWhiteSpace(GmailAccount))\n                {\n                    var net = Credential.GetNetworkCredential();\n                    var oauth = new OAuthCredential { UserName = net.UserName, AccessToken = net.Password, ExpiresOn = DateTimeOffset.MaxValue };\n                    var client = new GmailApiClient(oauth);\n                    var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                        client,\n                        GmailAccount!,\n                        Since,\n                        Before,\n                        Recipient,\n                        MessageId,\n                        max,\n                        parallelDownloadLimit: ParallelDownloadLimit,\n                        cancellationToken: CancelToken);\n                    foreach (var report in reports)\n                    {\n                        WriteObject(report);\n                    }\n                }\n                else\n                {\n                    ThrowTerminatingError(new ErrorRecord(\n                        new InvalidOperationException(\"Get-EmailDeliveryStatus - Gmail account or Credential missing.\"),\n                        \"ClientNotConnected\",\n                        ErrorCategory.InvalidOperation,\n                        null));\n                }\n\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailGraphFolder.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves mail folders for a user via Microsoft Graph API.</para>\n/// <para type=\"description\">The <c>Get-EmailGraphFolder</c> cmdlet retrieves mail folders for the specified user principal name using Microsoft Graph API. Provide a <see cref=\"GraphConnectionInfo\"/> object created with <c>Connect-EmailGraph</c> or authenticate via <c>Connect-MgGraph</c>.</para>\n/// <example>\n///   <summary>Get mail folders using application permissions</summary>\n///   <code>$graph = Connect-EmailGraph -ClientId \"id\" -DirectoryId \"tenant\" -ClientSecretSecretName \"graph-client-secret\" -ClientSecretVaultName \"MailSecrets\"\n///   Get-EmailGraphFolder -UserPrincipalName \"user@domain.com\" -Connection $graph</code>\n/// </example>\n/// <example>\n///   <summary>Get mail folders using Connect-MgGraph</summary>\n///   <code>Connect-MgGraph -Scopes Mail.Read -NoWelcome\n///   Get-EmailGraphFolder -UserPrincipalName \"user@domain.com\" -MgGraphRequest</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to enumerate mail folders for mailbox management, reporting, or migration scenarios.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailGraphFolder\")]\n[OutputType(typeof(object))]\npublic class CmdletGetEmailGraphFolder : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the user principal name (email address) whose mail folders will be retrieved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Graph connection context to use when performing the request.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Switch indicating that Invoke-MgGraphRequest should be used.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum parallel Microsoft Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts when requests fail.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retry attempts in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Retrieves mail folders for the specified user via Microsoft Graph API.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            return ProcessMgGraph();\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-EmailGraphFolder - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                var folders = await MicrosoftGraphUtils.GetMailFoldersAsync(cred, UserPrincipalName!);\n                foreach (var folder in folders) {\n                    WriteObject(folder);\n                }\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Get-EmailGraphFolder - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private Task ProcessMgGraph() {\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"GET\")\n            .AddParameter(\"Uri\", uri);\n        var results = ps.Invoke();\n        foreach (var res in results) WriteObject(res);\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailGraphMessage.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves mail messages for a user via Microsoft Graph.</para>\n/// <para type=\"description\">The <c>Get-EmailGraphMessage</c> cmdlet fetches messages for the specified user principal name using Microsoft Graph. It supports optional filters like subject, sender, recipient, priority and date range. Results can be limited and optionally deleted.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailGraphMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\n[OutputType(typeof(GraphMessageInfo))]\npublic sealed class CmdletGetEmailGraphMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name whose mailbox is queried.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    [ValidateNotNullOrEmpty]\n    public string UserPrincipalName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Graph connection information.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Message properties to select.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [Parameter(ParameterSetName = \"MgGraphRequest\")]\n    public string[]? Property { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Raw OData filter passed directly to Microsoft Graph.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [Parameter(ParameterSetName = \"MgGraphRequest\")]\n    [Alias(\"ODataFilter\")]\n    public string? Filter { get; set; }\n\n    /// <summary>\n    /// Limits the number of returned messages.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [Parameter(ParameterSetName = \"MgGraphRequest\")]\n    public int? Limit { get; set; }\n\n    /// <summary>\n    /// Filters messages by subject text.\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Filters messages where the sender contains this value.\n    /// </summary>\n    [Parameter]\n    public string? FromContains { get; set; }\n\n    /// <summary>\n    /// Filters messages where the recipient contains this value.\n    /// </summary>\n    [Parameter]\n    public string? ToContains { get; set; }\n\n    /// <summary>\n    /// Filters messages by importance.\n    /// </summary>\n    [Parameter]\n    public MessagePriority? Priority { get; set; }\n\n    /// <summary>\n    /// Retrieves messages received since this date.\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// Retrieves messages received before this date.\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// Filters messages that have attachments.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter HasAttachment { get; set; }\n\n    /// <summary>\n    /// When present, retrieves all messages ignoring limit.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter All { get; set; }\n\n    /// <summary>\n    /// Deletes messages after retrieval when set.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// When specified, uses Invoke-MgGraphRequest for the operation.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Retrieves messages using either built-in logic or Invoke-MgGraphRequest.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var upn = UserPrincipalName;\n\n        if (ParameterSetName == \"MgGraphRequest\") {\n            return ProcessMgGraph(upn);\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-EmailGraphMessage - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential, upn);\n    }\n\n    /// <summary>\n    /// Executes the Graph API calls to fetch messages.\n    /// </summary>\n    private async Task ProcessGraphAsync(GraphCredential cred, string userPrincipalName) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                var filter = BuildFilter(Filter);\n                var delete = Delete.IsPresent;\n                if (delete && !ShouldProcess(userPrincipalName, \"Deleting Graph messages\")) {\n                    delete = false;\n                }\n                var messages = await MicrosoftGraphUtils.GetMailMessagesAsync(\n                    cred,\n                    userPrincipalName,\n                    Property,\n                    filter,\n                    Limit);\n\n                foreach (var msg in messages) {\n                    var info = new GraphMessageInfo(msg, userPrincipalName);\n                    WriteObject(info);\n                    if (delete && msg.TryGetValue(\"id\", out var idObj) && idObj is string id) {\n                        await MicrosoftGraphUtils.DeleteMailMessageAsync(cred, userPrincipalName, id);\n                    }\n                }\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Get-EmailGraphMessage - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    /// <summary>\n    /// Uses Invoke-MgGraphRequest to retrieve messages.\n    /// </summary>\n    private Task ProcessMgGraph(string userPrincipalName) {\n        var filter = BuildFilter(Filter);\n        var query = new Dictionary<string, object>();\n        if (!string.IsNullOrWhiteSpace(filter)) query[\"$filter\"] = filter;\n        if (Property != null && Property.Length > 0) query[\"$select\"] = string.Join(\",\", Property);\n        if (Limit.HasValue) query[\"$top\"] = Limit.Value.ToString();\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{userPrincipalName}/messages\",\n            query);\n\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        var parameters = new Hashtable { { \"Method\", \"GET\" }, { \"Uri\", uri } };\n        ps.AddCommand(\"Invoke-MgGraphRequest\");\n        ps.AddParameters(parameters);\n        var results = ps.Invoke();\n        foreach (var res in results) {\n            WriteObject(res);\n        }\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Combines built-in conditions with a raw OData filter.\n    /// </summary>\n    private string BuildFilter(string? rawFilter) {\n        var filters = new List<string>();\n        if (!All.IsPresent) {\n            if (!string.IsNullOrWhiteSpace(Subject)) {\n                filters.Add($\"contains(subject,'{Subject!.Replace(\"'\", \"''\")}')\");\n            }\n            if (!string.IsNullOrWhiteSpace(FromContains)) {\n                filters.Add($\"contains(from/emailAddress/address,'{FromContains!.Replace(\"'\", \"''\")}')\");\n            }\n            if (!string.IsNullOrWhiteSpace(ToContains)) {\n                filters.Add($\"toRecipients/any(r:contains(r/emailAddress/address,'{ToContains!.Replace(\"'\", \"''\")}'))\");\n            }\n            if (Since.HasValue) {\n                filters.Add($\"receivedDateTime ge {Since.Value.ToUniversalTime():yyyy-MM-ddTHH:mm:ssZ}\");\n            }\n            if (Before.HasValue) {\n                filters.Add($\"receivedDateTime le {Before.Value.ToUniversalTime():yyyy-MM-ddTHH:mm:ssZ}\");\n            }\n        }\n        if (HasAttachment.IsPresent) {\n            filters.Add(\"hasAttachments eq true\");\n        }\n        var filter = string.Join(\" and \", filters);\n        if (!string.IsNullOrWhiteSpace(rawFilter)) {\n            filter = string.IsNullOrWhiteSpace(filter) ? rawFilter! : $\"{filter} and {rawFilter}\";\n        }\n        return filter;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailGraphMessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation.Runspaces;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves attachments for a specific mail message via Microsoft Graph API.</para>\n/// <para type=\"description\">The <c>Get-EmailGraphMessageAttachment</c> cmdlet retrieves attachments for the specified mail message ID and user principal name using Microsoft Graph API. Provide a <see cref=\"GraphConnectionInfo\"/> object created with <c>Connect-EmailGraph</c> or authenticate via <c>Connect-MgGraph</c>.</para>\n/// <example>\n///   <summary>Get attachments for a mail message</summary>\n///   <code>$graph = Connect-EmailGraph -ClientId \"id\" -DirectoryId \"tenant\" -ClientSecretSecretName \"graph-client-secret\" -ClientSecretVaultName \"MailSecrets\"\n///   Get-EmailGraphMessageAttachment -UserPrincipalName \"user@domain.com\" -MessageId \"AAMk...\" -Connection $graph</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to enumerate attachments for mailbox management, reporting, or migration scenarios.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailGraphMessageAttachment\")]\n[OutputType(typeof(Attachment))]\npublic class CmdletGetEmailGraphMessageAttachment : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the user principal name (email address) whose mail message attachments will be retrieved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n    /// <summary>\n    /// <para type=\"description\">Specifies the message ID for which attachments will be retrieved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? MessageId { get; set; }\n    /// <summary>\n    /// Graph connection information.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n\n    /// <summary>\n    /// Executes the request via Invoke-MgGraphRequest when set.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n    /// <summary>\n    /// <para type=\"description\">Specifies the properties to retrieve for each attachment.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [Parameter(ParameterSetName = \"MgGraphRequest\")]\n    public string[]? Property { get; set; }\n\n    /// <summary>\n    /// Retrieves attachments for the specified mail message via Microsoft Graph API.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        if (ParameterSetName == \"Graph\") {\n            var conn = Connection ?? DefaultSessions.GraphSession;\n            if (conn == null) {\n                WriteWarning(\"Get-EmailGraphMessageAttachment - Connection not provided and no default session available.\");\n                return;\n            }\n            MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n            MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n            int attempts = 0;\n            Exception? lastException = null;\n            do {\n                try {\n                    var attachments = await MicrosoftGraphUtils.GetMailMessageAttachmentsAsync(conn.Credential, UserPrincipalName!, MessageId!, Property);\n                    foreach (var att in attachments) {\n                        WriteObject(att);\n                    }\n                    return;\n                } catch (Exception ex) {\n                    lastException = ex;\n                    WriteWarning($\"Get-EmailGraphMessageAttachment - {ex.Message}\");\n                    if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                        if (ex is GraphApiException gex) {\n                            WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                        } else {\n                            WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                        }\n                        return;\n                    }\n                    if (RetryDelayMilliseconds > 0) {\n                        await Task.Delay(RetryDelayMilliseconds);\n                    }\n                }\n                attempts++;\n            } while (attempts <= RetryCount);\n            if (lastException is not null) {\n                WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n            }\n        } else {\n            var query = new Dictionary<string, object>();\n            if (Property != null && Property.Length > 0) query[\"$select\"] = string.Join(\",\", Property);\n            var uri = MicrosoftGraphUtils.JoinUriQuery(\n                GraphEndpoint.V1,\n                $\"/users/{UserPrincipalName}/messages/{MessageId}/attachments\",\n                query);\n            var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n            ps.AddCommand(\"Invoke-MgGraphRequest\").AddParameter(\"Method\", \"GET\").AddParameter(\"Uri\", uri);\n            var results = ps.Invoke();\n            foreach (var res in results) WriteObject(res);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailGraphMessageMime.cs",
    "content": "using System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves a MIME representation of a Graph mail message.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailGraphMessageMime\")]\n[OutputType(typeof(GraphEmailMessage))]\npublic sealed class CmdletGetEmailGraphMessageMime : AsyncPSCmdlet {\n    /// <summary>Message info from Get-EmailGraphMessage.</summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = \"Info\")]\n    public GraphMessageInfo? MessageInfo { get; set; }\n\n    /// <summary>User principal name owning the message.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"ById\")]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Identifier of the message.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"ById\")]\n    public string? MessageId { get; set; }\n\n    /// <summary>Graph connection.</summary>\n    [Parameter]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-EmailGraphMessageMime - Connection not provided and no default session available.\");\n            return;\n        }\n\n        var upn = ParameterSetName == \"Info\" ? MessageInfo?.UserPrincipalName : UserPrincipalName;\n        var id = ParameterSetName == \"Info\" ? MessageInfo?.Id : MessageId;\n        if (string.IsNullOrEmpty(upn) || string.IsNullOrEmpty(id)) {\n            WriteWarning(\"Get-EmailGraphMessageMime - Message id or user principal name missing.\");\n            return;\n        }\n\n        var mime = await MicrosoftGraphUtils.GetMailMessageMimeAsync(conn.Credential, upn!, id!);\n        WriteObject(new GraphEmailMessage(id!, mime));\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetEmailPendingMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves pending email messages from a file based repository.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"EmailPendingMessage\")]\n[OutputType(typeof(PendingMessageRecord))]\npublic sealed class CmdletGetEmailPendingMessage : AsyncPSCmdlet {\n    /// <summary>Directory containing pending message log file.</summary>\n    [Parameter(Mandatory = true)]\n    [Alias(\"PendingPath\")]\n    public string? PendingMessagesPath { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = PendingMessagesPath! };\n        var repo = new FilePendingMessageRepository(options);\n        await foreach (var record in repo.GetAllAsync(CancelToken)) {\n            WriteObject(record);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGmailMessage.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves messages using the Gmail API.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GmailMessage\")]\n[OutputType(typeof(GmailMessage))]\npublic sealed class CmdletGetGmailMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// Gmail account address to operate on.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// OAuth credential used to authenticate to Gmail.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Search query used when listing messages.\n    /// </summary>\n    [Parameter(ParameterSetName = \"List\")]\n    public string? Query { get; set; }\n\n    /// <summary>\n    /// Maximum number of messages to return when listing.\n    /// </summary>\n    [Parameter(ParameterSetName = \"List\")]\n    public int? MaxResults { get; set; }\n\n    /// <summary>\n    /// Identifier of a specific Gmail message.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Id\")]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Retrieves Gmail messages based on the specified parameters.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var net = Credential!.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n        var client = new GmailApiClient(oauth);\n        if (ParameterSetName == \"Id\") {\n            var msg = await client.GetAsync(GmailAccount!, Id!, CancelToken);\n            WriteObject(msg);\n            return;\n        }\n        IList<GmailMessage> list = await client.ListAsync(GmailAccount!, Query, MaxResults, CancelToken);\n        foreach (var m in list) {\n            WriteObject(m);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGmailThread.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves Gmail threads using the Gmail API.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GmailThread\")]\n[OutputType(typeof(GmailThread))]\n[OutputType(typeof(GmailThreadInfo), ParameterSetName = new[] { \"List\" })]\npublic sealed class CmdletGetGmailThread : AsyncPSCmdlet {\n    /// <summary>\n    /// Gmail account address to operate on.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// OAuth credential used to authenticate to Gmail.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Search query used when listing threads.\n    /// </summary>\n    [Parameter(ParameterSetName = \"List\")]\n    public string? Query { get; set; }\n\n    /// <summary>\n    /// Maximum number of threads to return when listing.\n    /// </summary>\n    [Parameter(ParameterSetName = \"List\")]\n    public int? MaxResults { get; set; }\n\n    /// <summary>\n    /// Identifier of a specific Gmail thread.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Id\")]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Retrieves Gmail threads based on the specified parameters.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var net = Credential!.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n        var client = new GmailApiClient(oauth);\n        if (ParameterSetName == \"Id\") {\n            var thread = await client.GetThreadAsync(GmailAccount!, Id!, CancelToken);\n            WriteObject(thread);\n            return;\n        }\n        IList<GmailThreadInfo> list = await client.ListThreadsAsync(GmailAccount!, Query, MaxResults, CancelToken);\n        foreach (var t in list) {\n            WriteObject(t);\n        }\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGraphEvent.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves calendar events using Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GraphEvent\")]\n[OutputType(typeof(Dictionary<string, object>))]\npublic sealed class CmdletGetGraphEvent : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name whose calendar events are retrieved.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Graph connection information to use for the request.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Additional event properties to select.\n    /// </summary>\n    [Parameter]\n    public string[]? Property { get; set; }\n\n    /// <summary>\n    /// OData filter string applied to the query.\n    /// </summary>\n    [Parameter]\n    public string? Filter { get; set; }\n\n    /// <summary>\n    /// Maximum number of events to return.\n    /// </summary>\n    [Parameter]\n    public int? Limit { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on transient errors.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retry attempts in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Retrieves events for the specified user.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-GraphEvent - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? last = null;\n        do {\n            try {\n                var events = await MicrosoftGraphUtils.GetEventsAsync(cred, UserPrincipalName!, Property, Filter, Limit);\n                foreach (var ev in events) WriteObject(ev);\n                return;\n            } catch (Exception ex) {\n                last = ex;\n                WriteWarning($\"Get-GraphEvent - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (last != null) WriteError(new ErrorRecord(last, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGraphInboxRule.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves inbox rules for a mailbox via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GraphInboxRule\")]\n[OutputType(typeof(GraphInboxRule))]\npublic sealed class CmdletGetGraphInboxRule : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name whose inbox rules are retrieved.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Connection information for Microsoft Graph.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Optional OData filter string.\n    /// </summary>\n    [Parameter]\n    public string? Filter { get; set; }\n\n    /// <summary>\n    /// Indicates the use of Invoke-MgGraphRequest for this operation.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Retrieves inbox rules using Graph or Invoke-MgGraphRequest.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            return ProcessMgGraph();\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-GraphInboxRule - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                var rules = await MicrosoftGraphUtils.GetRulesAsync(cred, UserPrincipalName!, Filter);\n                foreach (var r in rules) WriteObject(r);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Get-GraphInboxRule - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private Task ProcessMgGraph() {\n        var qp = string.IsNullOrWhiteSpace(Filter) ? null : new Dictionary<string, object> { [\"$filter\"] = Filter! };\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/inbox/messageRules\",\n            qp);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"GET\")\n            .AddParameter(\"Uri\", uri);\n        var results = ps.Invoke();\n        foreach (var res in results) WriteObject(res);\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGraphMailboxPermission.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves mailbox permissions for a user via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GraphMailboxPermission\")]\n[OutputType(typeof(GraphMailboxPermission))]\npublic class CmdletGetGraphMailboxPermission : AsyncPSCmdlet {\n    /// <summary>User principal name owning the mailbox.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Connection to Microsoft Graph.</summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Use Invoke-MgGraphRequest instead of built-in logic.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retries on transient failures.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retry attempts in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <inheritdoc />\n    protected override Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            ProcessMgGraph();\n            return Task.CompletedTask;\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-GraphMailboxPermission - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                var perms = await MicrosoftGraphUtils.GetMailboxPermissionsAsync(cred, UserPrincipalName!);\n                foreach (var p in perms) WriteObject(p);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Get-GraphMailboxPermission - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex)\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null)\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n\n    private void ProcessMgGraph() {\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/permissions\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"GET\")\n            .AddParameter(\"Uri\", uri);\n        var results = ps.Invoke();\n        foreach (var res in results) WriteObject(res);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetGraphMailboxStatistics.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves detailed mailbox statistics via Microsoft Graph, including\n/// message and folder counts as well as attachment sizes.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"GraphMailboxStatistics\")]\n[OutputType(typeof(GraphMailboxStatistics))]\npublic class CmdletGetGraphMailboxStatistics : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name for the mailbox being queried.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Existing Graph connection to use for requests.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Retrieves mailbox statistics for the specified user.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Get-GraphMailboxStatistics - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                var stats = await MicrosoftGraphUtils.GetMailboxStatisticsAsync(cred, UserPrincipalName!);\n                WriteObject(stats);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Get-GraphMailboxStatistics - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetIMAPFolder.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves the IMAP inbox folder and updates message counts for an active IMAP connection.</para>\n/// <para type=\"description\">The <c>Get-IMAPFolder</c> cmdlet opens the inbox folder for the provided <see cref=\"ImapConnectionInfo\"/> object (from <c>Connect-IMAP</c>), updates message and recent counts, and returns the updated connection info. Use this to refresh folder state or after connecting to an IMAP server.</para>\n/// <example>\n///   <summary>Get the inbox folder and message counts</summary>\n///   <code>$client = Connect-IMAP ...; Get-IMAPFolder -Client $client</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to refresh the folder state after connecting or to update message counts.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"IMAPFolder\")]\npublic sealed class CmdletGetIMAPFolder : AsyncPSCmdlet {\n    private const string OpenParameterSet = \"Open\";\n    private const string RootParameterSet = \"Root\";\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object representing the active IMAP connection. This is the object returned by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">When specified, lists only the top-level folders instead of opening one.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = RootParameterSet)]\n    public SwitchParameter Root { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Folder path to open. Defaults to the inbox when not provided.</para>\n    /// </summary>\n    [Parameter(ParameterSetName = OpenParameterSet)]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the folder access mode (ReadOnly or ReadWrite). Default is ReadOnly.</para>\n    /// </summary>\n    [Parameter(Position = 1, ParameterSetName = OpenParameterSet)]\n    public FolderAccess FolderAccess { get; set; } = FolderAccess.ReadOnly;\n\n    /// <summary>\n    /// Opens the inbox folder, updates message counts, and returns the updated connection info.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            if (Root) {\n                await foreach (var folder in ImapRootFolderEnumerator.EnumerateAsync(conn.Data, CancelToken)) {\n                    WriteObject(folder);\n                }\n            } else {\n                var name = string.IsNullOrWhiteSpace(Path) ? conn.Folder?.FullName : Path;\n                var folder = (ImapFolder)conn.Data.GetCachedFolder(name, FolderAccess);\n                WriteVerbose($\"Get-IMAPFolder - Total messages {folder.Count}, Recent messages {folder.Recent}\");\n                conn.Messages = folder;\n                conn.Count = folder.Count;\n                conn.Recent = folder.Recent;\n                conn.Folder = folder;\n                conn.Folders[folder.FullName] = folder;\n                DefaultSessions.ImapSession = conn;\n                WriteObject(conn);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Get-IMAPFolder - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetIMAPMessage.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Search;\nusing MimeKit;\nusing Mailozaurr.PowerShell;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves messages from an IMAP folder using optional filters.</para>\n/// <para type=\"description\">The <c>Get-IMAPMessage</c> cmdlet fetches messages from the current IMAP folder associated with the provided <see cref=\"ImapConnectionInfo\"/> object. You can filter by subject, sender, recipients, priority, date range and attachment presence. Messages can also be deleted after retrieval.</para>\n/// <example>\n///   <summary>Get all messages from the current folder</summary>\n///   <code>$client = Connect-IMAP ...; Get-IMAPMessage -Client $client -All</code>\n/// </example>\n/// <example>\n///   <summary>Get messages with a subject filter</summary>\n///   <code>$client = Connect-IMAP ...; Get-IMAPMessage -Client $client -Subject 'Report'</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to retrieve and optionally delete messages from an IMAP server.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(\n    VerbsCommon.Get,\n    \"IMAPMessage\",\n    DefaultParameterSetName = SequenceParameterSet,\n    SupportsShouldProcess = true,\n    ConfirmImpact = ConfirmImpact.High)]\n[OutputType(typeof(ImapMessageInfo))]\npublic sealed class CmdletGetIMAPMessage : AsyncPSCmdlet {\n    private const string SequenceParameterSet = \"Sequence\";\n    private const string UidParameterSet = \"Uid\";\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object representing the active IMAP connection. This is the object returned by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the folder access mode (ReadOnly or ReadWrite). Default is ReadOnly.</para>\n    /// </summary>\n    [Parameter(Position = 1)]\n    public FolderAccess FolderAccess { get; set; } = FolderAccess.ReadOnly;\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the starting sequence number of messages to retrieve.</para>\n    /// </summary>\n    [Parameter(Position = 2, ParameterSetName = SequenceParameterSet)]\n    public int? SequenceStart { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the ending sequence number of messages to retrieve. If not provided, only <c>SequenceStart</c> is fetched.</para>\n    /// </summary>\n    [Parameter(Position = 3, ParameterSetName = SequenceParameterSet)]\n    public int? SequenceEnd { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the starting UID of messages to retrieve.</para>\n    /// </summary>\n    [Parameter(Position = 2, ParameterSetName = UidParameterSet)]\n    public uint? UidStart { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the ending UID of messages to retrieve. If not provided, only <c>UidStart</c> is fetched.</para>\n    /// </summary>\n    [Parameter(Position = 3, ParameterSetName = UidParameterSet)]\n    public uint? UidEnd { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Search queries used to match messages to retrieve.</para>\n    /// </summary>\n    [Parameter]\n    public SearchQuery[]? SearchQuery { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages containing this text in the subject.</para>\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages sent from addresses matching this value.</para>\n    /// </summary>\n    [Parameter]\n    public string? FromContains { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages sent to addresses matching this value.</para>\n    /// </summary>\n    [Parameter]\n    public string? ToContains { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages with the specified priority.</para>\n    /// </summary>\n    [Parameter]\n    public MessagePriority? Priority { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages that contain attachments.</para>\n    /// </summary>\n    [Parameter]\n    public SwitchParameter HasAttachment { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, retrieves all messages ignoring other filters.</para>\n    /// </summary>\n    [Parameter]\n    public SwitchParameter All { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, deletes the retrieved messages.</para>\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Return messages delivered on or after this date.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Return messages delivered on or before this date.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// Opens the inbox folder and retrieves messages if message parameters are specified.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var folder = conn.Folder?.FullName;\n\n            var del = Delete.IsPresent;\n            if (del && !ShouldProcess(folder ?? \"INBOX\", \"Deleting IMAP messages\")) {\n                del = false;\n            }\n\n            await foreach (var message in MessageFetcher.Fetch(\n                conn.Data,\n                folder,\n                Subject,\n                FromContains,\n                ToContains,\n                Priority,\n                Since,\n                Before,\n                All.IsPresent,\n                del,\n                HasAttachment.IsPresent,\n                SearchQuery,\n                CancelToken)) {\n                WriteObject(new ImapMessageInfo(message));\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Get-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetMimeMessageContent.cs",
    "content": "using System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Retrieves text and HTML bodies from a MIME message.\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"MimeMessageContent\")]\n[OutputType(typeof(MimeMessageContent))]\npublic sealed class CmdletGetMimeMessageContent : PSCmdlet {\n    /// <summary>Message to extract content from.</summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [Alias(\"Message\")]\n    public object? InputObject { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (InputObject == null) return;\n        MimeMessage? message = InputObject switch {\n            MimeMessage m => m,\n            ImapMessageInfo info => info.Raw.Message,\n            ImapEmailMessage imap => imap.Message,\n            Pop3MessageInfo pinfo => pinfo.Raw.Message,\n            Pop3EmailMessage pop => pop.Message,\n            GraphEmailMessage g => g.Message,\n            _ => null\n        };\n        if (message != null) {\n            WriteObject(new MimeMessageContent(message));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetPOP3Message.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr.PowerShell;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves messages from a POP3 mailbox with optional filters.</para>\n/// <para type=\"description\">The <c>Get-POP3Message</c> cmdlet fetches messages from a POP3 mailbox using the provided <see cref=\"PopConnectionInfo\"/> object. It supports filtering by subject, sender, recipients, priority, date range and attachment presence. Messages can be removed after retrieval using <c>-Delete</c>.</para>\n/// <example>\n///   <summary>Get messages from a POP3 mailbox with a subject filter</summary>\n///   <code>$client = Connect-POP3 ...; Get-POP3Message -Client $client -Subject 'Report'</code>\n/// </example>\n/// <example>\n///   <summary>Get all messages from a POP3 mailbox and delete them</summary>\n///   <code>$client = Connect-POP3 ...; Get-POP3Message -Client $client -All -Delete</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to enumerate or download messages from a POP3 mailbox for backup, migration, or processing.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectPOP3\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"POP3Message\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\n[OutputType(typeof(Pop3MessageInfo))]\npublic sealed class CmdletGetPOP3Message : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"PopConnectionInfo\"/> object representing the active POP3 connection. This is the object returned by <c>Connect-POP3</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the index of the first message to retrieve. Default is 0 (the first message).</para>\n    /// </summary>\n    [Parameter(Position = 1)]\n    public int Index { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the number of messages to retrieve starting from <c>Index</c>. Default is 1.</para>\n    /// </summary>\n    [Parameter(Position = 2)]\n    public int Count { get; set; } = 1;\n\n    /// <summary>\n    /// <para type=\"description\">If set, retrieves all messages from the POP3 mailbox.</para>\n    /// </summary>\n    [Parameter()]\n    public SwitchParameter All { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages containing this text in the subject.</para>\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages sent from addresses matching this value.</para>\n    /// </summary>\n    [Parameter]\n    public string? FromContains { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages sent to addresses matching this value.</para>\n    /// </summary>\n    [Parameter]\n    public string? ToContains { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages with the specified priority.</para>\n    /// </summary>\n    [Parameter]\n    public MessagePriority? Priority { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Return messages delivered on or after this date.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Return messages delivered on or before this date.</para>\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Only return messages that contain attachments.</para>\n    /// </summary>\n    [Parameter]\n    public SwitchParameter HasAttachment { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, deletes the retrieved messages.</para>\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// Retrieves one or more messages from the POP3 mailbox.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            if (!conn.IsConnected) {\n                WriteWarning(\"Get-POP3Message - Client is not connected.\");\n                return;\n            }\n\n            var delete = Delete.IsPresent;\n            if (delete && !ShouldProcess(string.IsNullOrWhiteSpace(conn.Uri) ? \"POP3 mailbox\" : conn.Uri, \"Deleting POP3 messages\")) {\n                delete = false;\n            }\n\n            await foreach (var message in MessageFetcher.Fetch(\n                conn.Data,\n                Subject,\n                FromContains,\n                ToContains,\n                Priority,\n                Since,\n                Before,\n                All.IsPresent,\n                delete,\n                HasAttachment.IsPresent,\n                CancelToken)) {\n                WriteObject(new Pop3MessageInfo(message));\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Get-POP3Message - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletGetSmtpConnectionPool.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Retrieves information about the SMTP connection pool.</para>\n/// <para type=\"description\">The <c>Get-SmtpConnectionPool</c> cmdlet returns a snapshot of\n/// pooled SMTP connections or, when used with <c>-Watch</c>, continuously emits\n/// updates as the pool changes. An optional <c>-Action</c> script block can be\n/// executed for each update.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Get, \"SmtpConnectionPool\")]\n[OutputType(typeof(SmtpConnectionPoolSnapshot))]\npublic sealed class CmdletGetSmtpConnectionPool : AsyncPSCmdlet {\n    /// <summary>Continuously watch the pool for changes.</summary>\n    [Parameter]\n    public SwitchParameter Watch { get; set; }\n\n    /// <summary>Script block executed for each snapshot when watching.</summary>\n    [Parameter]\n    public ScriptBlock? Action { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        void Emit() {\n            var snapshot = SmtpConnectionPool.GetSnapshot();\n            if (Action != null) {\n                try {\n                    Action.Invoke(snapshot);\n                } catch (RuntimeException ex) {\n                    WriteError(ex.ErrorRecord);\n                }\n            } else {\n                WriteObject(snapshot);\n            }\n        }\n\n        if (Watch) {\n            void Handler(int _) => Emit();\n            SmtpConnectionPool.PoolSizeChanged += Handler;\n            Emit();\n            try {\n                await Task.Delay(-1, CancelToken);\n            } catch (TaskCanceledException) {\n            } finally {\n                SmtpConnectionPool.PoolSizeChanged -= Handler;\n            }\n        } else {\n            Emit();\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletImportMailFile.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Imports a .msg or .eml mail file and returns its contents as a message object.</para>\n/// <para type=\"description\">The <c>Import-MailFile</c> cmdlet loads a .msg (Outlook) or .eml (RFC822) file from disk and returns a <see cref=\"MailFileMessage\"/> for further processing, inspection, or conversion. Supports both file types and provides warnings for unsupported files or errors.</para>\n/// <example>\n///   <summary>Import a .msg file</summary>\n///   <code>Import-MailFile -InputPath \"C:\\Mail\\message.msg\"</code>\n/// </example>\n/// <example>\n///   <summary>Import a .eml file</summary>\n///   <code>Import-MailFile -InputPath \"C:\\Mail\\message.eml\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to inspect, convert, or process mail files in automation or \n/// migration scenarios.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.Import, \"MailFile\")]\npublic sealed class CmdletImportMailFile : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the path to the .msg or .eml file to \n    /// import. Accepts aliases FilePath and Path.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0)]\n    [Alias(\"FilePath\", \"Path\")]\n    [ValidateNotNullOrEmpty]\n    public string? InputPath { get; set; }\n\n    /// <summary>\n    /// Imports the specified mail file and returns its contents as a message object.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var inputPath = InputPath;\n        if (string.IsNullOrWhiteSpace(inputPath)) {\n            WriteWarning(\"Import-MailFile - File path is empty.\");\n            return Task.CompletedTask;\n        }\n\n        if (MailFileReader.TryRead(inputPath!, out var message, out var error)) {\n            WriteObject(message);\n        } else {\n            WriteWarning($\"Import-MailFile - {error}\");\n        }\n\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletMoveGraphFolder.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Moves a Microsoft Graph mail folder.\n/// </summary>\n[Cmdlet(VerbsCommon.Move, \"GraphFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic class CmdletMoveGraphFolder : AsyncPSCmdlet {\n    private const string ParentParameterSet = \"Parent\";\n    private const string RootParameterSet = \"Root\";\n    /// <summary>User principal name owning the folder.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Identifier of the folder to move.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? FolderId { get; set; }\n\n    /// <summary>Identifier of the destination folder.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = ParentParameterSet)]\n    [ValidateNotNullOrEmpty]\n    public string? DestinationFolderId { get; set; }\n\n    /// <summary>Move folder to the root.</summary>\n    [Parameter(ParameterSetName = RootParameterSet)]\n    public SwitchParameter Root { get; set; }\n\n    /// <summary>Connection information for Microsoft Graph.</summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Use <c>Invoke-MgGraphRequest</c> instead of built-in logic.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>Request timeout in seconds.</summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent requests during the move operation.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>Number of retries on transient errors.</summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>Delay between retries in milliseconds.</summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Moves a folder within a mailbox using Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case ParentParameterSet:\n            case RootParameterSet:\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Move-GraphFolder - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(FolderId!, \"Moving Graph folder\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        var dest = ParameterSetName == RootParameterSet ? \"msgfolderroot\" : DestinationFolderId!;\n        if (dryRun) {\n            await MicrosoftGraphUtils.MoveFolderAsync(cred, UserPrincipalName!, FolderId!, dest, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.MoveFolderAsync(cred, UserPrincipalName!, FolderId!, dest, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Move-GraphFolder - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(FolderId!, \"Moving Graph folder\")) {\n            return;\n        }\n        var dest = ParameterSetName == RootParameterSet ? \"msgfolderroot\" : DestinationFolderId;\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/{FolderId}/move\");\n        var body = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = dest }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"POST\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletMoveGraphMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Moves a Microsoft Graph message to a different folder.\n/// </summary>\n[Cmdlet(VerbsCommon.Move, \"GraphMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic class CmdletMoveGraphMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// UPN of the mailbox owner containing the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the message to move.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// Target folder identifier where the message will be moved.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? DestinationFolderId { get; set; }\n\n    /// <summary>\n    /// Optional Graph connection used when moving the message.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n\n    /// <summary>\n    /// Indicates that the message should be moved using Invoke-MgGraphRequest.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts when moving fails.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retry attempts in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Processes the cmdlet invocation.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Move-GraphMessage - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    /// <summary>\n    /// Executes the move operation using the Graph SDK.\n    /// </summary>\n    /// <param name=\"cred\">Credential used to access Graph.</param>\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(MessageId!, \"Moving Graph message\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.MoveMailMessageAsync(cred, UserPrincipalName!, MessageId!, DestinationFolderId!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.MoveMailMessageAsync(cred, UserPrincipalName!, MessageId!, DestinationFolderId!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Move-GraphMessage - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    /// <summary>\n    /// Executes the move operation using the <c>Invoke-MgGraphRequest</c> cmdlet.\n    /// </summary>\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(MessageId!, \"Moving Graph message\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/messages/{MessageId}/move\");\n        var body = JsonSerializer.Serialize(new GraphDestinationRequest { DestinationId = DestinationFolderId }, MailozaurrJsonContext.Default.GraphDestinationRequest);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"POST\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletMoveIMAPFolder.cs",
    "content": "using System.Management.Automation;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Moves an IMAP folder to a new location.\n/// </summary>\n[Cmdlet(VerbsCommon.Move, \"IMAPFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletMoveIMAPFolder : AsyncPSCmdlet {\n    private const string ParentParameterSet = \"Parent\";\n    private const string RootParameterSet = \"Root\";\n    /// <summary>Active IMAP connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>Name of the folder to move.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    [ValidateNotNullOrEmpty]\n    public string? Folder { get; set; }\n\n    /// <summary>Destination folder name.</summary>\n    [Parameter(Mandatory = true, Position = 2, ParameterSetName = ParentParameterSet)]\n    [ValidateNotNullOrEmpty]\n    public string? DestinationFolder { get; set; }\n\n    /// <summary>Move folder to the root.</summary>\n    [Parameter(ParameterSetName = RootParameterSet)]\n    public SwitchParameter Root { get; set; }\n\n    /// <summary>\n    /// Moves an IMAP folder to the specified destination.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var dryRun = !ShouldProcess(Folder!, \"Moving IMAP folder\");\n            var dest = ParameterSetName == RootParameterSet ? null : DestinationFolder;\n            await FolderOperations.MoveFolderAsync(conn.Data, Folder!, dest, dryRun, CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Move-IMAPFolder - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletMoveIMAPMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Moves an IMAP message to another folder.</para>\n/// <para type=\"description\">The <c>Move-IMAPMessage</c> cmdlet moves a message identified by its UID from the current folder to the specified destination folder.</para>\n/// <example>\n///   <summary>Move a message to Archive</summary>\n///   <code>$client = Connect-IMAP ...; Move-IMAPMessage -Client $client -Uid 10 -DestinationFolder \"Archive\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to organize messages on an IMAP server.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsCommon.Move, \"IMAPMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletMoveIMAPMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object representing the active IMAP connection. This is the object returned by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">UID of the message to move.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public uint Uid { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional source folder of the message. Defaults to Inbox.</para>\n    /// </summary>\n    [Parameter(Position = 2)]\n    public string? SourceFolder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Destination folder where the message should be moved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 3)]\n    [ValidateNotNullOrEmpty]\n    public string? DestinationFolder { get; set; }\n\n    /// <summary>\n    /// Moves the specified message to the destination folder.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            if (!ShouldProcess(Uid.ToString(), $\"Moving IMAP message to {DestinationFolder}\")) {\n                return Task.CompletedTask;\n            }\n            var uid = new UniqueId(Uid);\n            var source = conn.Data.GetCachedFolder(SourceFolder ?? conn.Folder?.FullName, FolderAccess.ReadWrite);\n            var dest = conn.Data.GetCachedFolder(DestinationFolder!, FolderAccess.ReadWrite);\n            conn.Folders[source.FullName] = (ImapFolder)source;\n            conn.Folders[dest.FullName] = (ImapFolder)dest;\n            source.MoveTo(uid, dest);\n            if (source.IsOpen)\n                source.Close(false);\n            if (dest.IsOpen)\n                dest.Close(false);\n            conn.Data.ClearFolderCache();\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Move-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphEvent.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a new calendar event via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphEvent\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\n[OutputType(typeof(GraphEvent))]\npublic sealed class CmdletNewGraphEvent : AsyncPSCmdlet\n{\n    /// <summary>\n    /// User principal name owning the calendar.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Event object to create.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Event\")]\n    [ValidateNotNull]\n    public GraphEvent? Event { get; set; }\n\n    /// <summary>\n    /// Builder used to construct the event.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Builder\")]\n    [ValidateNotNull]\n    public GraphEventBuilder? EventBuilder { get; set; }\n\n    /// <summary>\n    /// Graph connection context for the request.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Creates the event using Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"New-GraphEvent - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(UserPrincipalName!, \"Creating Graph event\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? last = null;\n        GraphEvent toCreate = ParameterSetName == \"Builder\" ? EventBuilder! : Event!;\n        if (dryRun) {\n            await MicrosoftGraphUtils.NewEventAsync(cred, UserPrincipalName!, toCreate, dryRun: true);\n            return;\n        }\n        do {\n            try {\n                var result = await MicrosoftGraphUtils.NewEventAsync(cred, UserPrincipalName!, toCreate, dryRun: false);\n                WriteObject(result);\n                return;\n            } catch (Exception ex) {\n                last = ex;\n                WriteWarning($\"New-GraphEvent - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (last != null) WriteError(new ErrorRecord(last, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphEventBuilder.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a <see cref=\"GraphEventBuilder\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphEventBuilder\")]\n[OutputType(typeof(GraphEventBuilder))]\npublic sealed class CmdletNewGraphEventBuilder : PSCmdlet\n{\n    /// <summary>\n    /// Subject of the new event.\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Start time of the event.\n    /// </summary>\n    [Parameter]\n    public DateTime? Start { get; set; }\n\n    /// <summary>\n    /// Time zone for the start time.\n    /// </summary>\n    [Parameter]\n    public string StartTimeZone { get; set; } = \"UTC\";\n\n    /// <summary>\n    /// End time of the event.\n    /// </summary>\n    [Parameter]\n    public DateTime? End { get; set; }\n\n    /// <summary>\n    /// Time zone for the end time.\n    /// </summary>\n    [Parameter]\n    public string EndTimeZone { get; set; } = \"UTC\";\n\n    /// <summary>\n    /// Body content of the event.\n    /// </summary>\n    [Parameter]\n    public string? Body { get; set; }\n\n    /// <summary>\n    /// Type of the body content: HTML or Text.\n    /// </summary>\n    [Parameter]\n    public string BodyType { get; set; } = \"HTML\";\n\n    /// <summary>\n    /// List of attendee email addresses.\n    /// </summary>\n    [Parameter]\n    public string[]? Attendees { get; set; }\n\n    /// <summary>\n    /// Builds the <see cref=\"GraphEventBuilder\"/> object from provided parameters.\n    /// </summary>\n    protected override void ProcessRecord()\n    {\n        var builder = new GraphEventBuilder();\n        if (!string.IsNullOrEmpty(Subject))\n            builder.Subject(Subject!);\n        if (Start.HasValue)\n            builder.Start(Start.Value, StartTimeZone);\n        if (End.HasValue)\n            builder.End(End.Value, EndTimeZone);\n        if (!string.IsNullOrEmpty(Body))\n            builder.Body(Body!, BodyType);\n        if (Attendees != null)\n        {\n            foreach (var addr in Attendees)\n            {\n                builder.Attendee(addr, addr);\n            }\n        }\n        WriteObject(builder);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphInboxRule.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a new inbox rule via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphInboxRule\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\n[OutputType(typeof(GraphInboxRule))]\npublic sealed class CmdletNewGraphInboxRule : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name owning the mailbox.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Hashtable definition of the rule.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public Hashtable? Rule { get; set; }\n\n    /// <summary>\n    /// Rule object describing the inbox rule.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphInboxRule? RuleObject { get; set; }\n\n    /// <summary>\n    /// Builder used to create a rule object.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphInboxRuleBuilder? RuleBuilder { get; set; }\n\n    /// <summary>\n    /// Display name for the new rule.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Params\")]\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Order in which the rule is processed.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public int Sequence { get; set; }\n\n    /// <summary>\n    /// Determines if the rule is enabled.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public SwitchParameter Enabled { get; set; }\n\n    /// <summary>\n    /// Destination folder to move messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? MoveToFolder { get; set; }\n\n    /// <summary>\n    /// Destination folder to copy messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? CopyToFolder { get; set; }\n\n    /// <summary>\n    /// Deletes messages matching the rule.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// Addresses to forward matching messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? ForwardTo { get; set; }\n\n    /// <summary>\n    /// Stops processing additional rules when this rule matches.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public new SwitchParameter StopProcessing { get; set; }\n\n    /// <summary>\n    /// Sender addresses that trigger the rule.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? SenderContains { get; set; }\n\n    /// <summary>\n    /// Recipients that trigger the rule.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? RecipientContains { get; set; }\n\n    /// <summary>\n    /// Strings that must appear in the subject.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? SubjectContains { get; set; }\n\n    /// <summary>\n    /// Strings that must appear in the body.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? BodyContains { get; set; }\n\n    /// <summary>\n    /// Message importance level to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? Importance { get; set; }\n\n    /// <summary>\n    /// Connection information for Microsoft Graph.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [Parameter(ParameterSetName = \"Params\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Use <c>Invoke-MgGraphRequest</c> for sending requests.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            ProcessMgGraph();\n            return Task.CompletedTask;\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"New-GraphInboxRule - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(UserPrincipalName!, \"Creating inbox rule via Graph\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        GraphInboxRule obj;\n        if (ParameterSetName == \"Params\") {\n            obj = BuildFromParams();\n        } else if (RuleBuilder != null) {\n            obj = RuleBuilder.Build();\n        } else if (RuleObject != null) {\n            obj = RuleObject;\n        } else {\n            var dict = Rule!.Cast<DictionaryEntry>().ToDictionary(d => (string)d.Key, d => d.Value!);\n            var json = JsonSerializer.Serialize(dict, MailozaurrJsonContext.Default.DictionaryStringObject);\n            obj = JsonSerializer.Deserialize<GraphInboxRule>(json)!;\n        }\n        if (dryRun) {\n            await MicrosoftGraphUtils.NewRuleAsync(cred, UserPrincipalName!, obj, dryRun: true);\n            return;\n        }\n        do {\n            try {\n                var res = await MicrosoftGraphUtils.NewRuleAsync(cred, UserPrincipalName!, obj, dryRun: false);\n                WriteObject(res);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"New-GraphInboxRule - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(UserPrincipalName!, \"Creating inbox rule via Graph\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/inbox/messageRules\");\n        var rulePayload = Rule ?? throw new PSArgumentNullException(nameof(Rule), \"Rule has to be provided or built.\");\n        var bodyObj = RuleBuilder != null\n            ? RuleBuilder.Build()\n            : RuleObject ?? JsonSerializer.Deserialize(JsonSerializer.Serialize(rulePayload, MailozaurrJsonContext.Default.Object), MailozaurrJsonContext.Default.GraphInboxRule);\n\n        if (bodyObj is null) {\n            throw new PSArgumentException(\"Graph inbox rule definition cannot be null.\");\n        }\n\n        var body = JsonSerializer.Serialize(bodyObj, MailozaurrJsonContext.Default.GraphInboxRule);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"POST\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        var results = ps.Invoke();\n        foreach (var res in results) WriteObject(res);\n    }\n\n    private GraphInboxRule BuildFromParams() {\n        var rule = new GraphInboxRule {\n            DisplayName = DisplayName,\n            Sequence = Sequence,\n            IsEnabled = Enabled.IsPresent\n        };\n\n        if (SenderContains != null || RecipientContains != null || SubjectContains != null || BodyContains != null || Importance != null) {\n            rule.Conditions = new GraphInboxRulePredicates {\n                SenderContains = SenderContains != null ? new List<string>(SenderContains) : null,\n                RecipientContains = RecipientContains != null ? new List<string>(RecipientContains) : null,\n                SubjectContains = SubjectContains != null ? new List<string>(SubjectContains) : null,\n                BodyContains = BodyContains != null ? new List<string>(BodyContains) : null,\n                Importance = Importance\n            };\n        }\n\n        if (!string.IsNullOrEmpty(MoveToFolder) || !string.IsNullOrEmpty(CopyToFolder) || Delete.IsPresent || ForwardTo != null || StopProcessing.IsPresent) {\n            rule.Actions = new GraphInboxRuleActions {\n                MoveToFolder = MoveToFolder,\n                CopyToFolder = CopyToFolder,\n                Delete = Delete.IsPresent ? true : null,\n                ForwardTo = ForwardTo != null ? new List<GraphEmailAddress>(ForwardTo.Select(a => new GraphEmailAddress { Email = new GraphEmail { Address = a } })) : null,\n                StopProcessingRules = StopProcessing.IsPresent ? true : null\n            };\n        }\n\n        return rule;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphInboxRuleBuilder.cs",
    "content": "using System.Linq;\nusing System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a <see cref=\"GraphInboxRuleBuilder\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphInboxRuleBuilder\")]\n[OutputType(typeof(GraphInboxRuleBuilder))]\npublic sealed class CmdletNewGraphInboxRuleBuilder : PSCmdlet\n{\n    /// <summary>\n    /// Display name for the inbox rule.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Rule processing order.\n    /// </summary>\n    [Parameter]\n    public int Sequence { get; set; }\n\n    /// <summary>\n    /// Enables the rule when set.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Enabled { get; set; }\n\n    /// <summary>\n    /// Destination folder to move messages to.\n    /// </summary>\n    [Parameter]\n    public string? MoveToFolder { get; set; }\n\n    /// <summary>\n    /// Destination folder to copy messages to.\n    /// </summary>\n    [Parameter]\n    public string? CopyToFolder { get; set; }\n\n    /// <summary>\n    /// Deletes messages that match the rule.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// Addresses to forward matching messages to.\n    /// </summary>\n    [Parameter]\n    public string[]? ForwardTo { get; set; }\n\n    /// <summary>\n    /// Stops processing further rules when this rule matches.\n    /// </summary>\n    [Parameter]\n    public new SwitchParameter StopProcessing { get; set; }\n\n    /// <summary>\n    /// Sender address patterns to match.\n    /// </summary>\n    [Parameter]\n    public string[]? SenderContains { get; set; }\n\n    /// <summary>\n    /// Recipient address patterns to match.\n    /// </summary>\n    [Parameter]\n    public string[]? RecipientContains { get; set; }\n\n    /// <summary>\n    /// Subject text patterns to match.\n    /// </summary>\n    [Parameter]\n    public string[]? SubjectContains { get; set; }\n\n    /// <summary>\n    /// Body text patterns to match.\n    /// </summary>\n    [Parameter]\n    public string[]? BodyContains { get; set; }\n\n    /// <summary>\n    /// Message importance to match.\n    /// </summary>\n    [Parameter]\n    public string? Importance { get; set; }\n\n    /// <summary>\n    /// Builds the <see cref=\"GraphInboxRule\"/> based on provided parameters.\n    /// </summary>\n    protected override void ProcessRecord()\n    {\n        var builder = new GraphInboxRuleBuilder()\n            .DisplayName(DisplayName)\n            .Sequence(Sequence)\n            .Enabled(Enabled.IsPresent);\n\n        if (SenderContains != null)\n            builder.SenderContains(SenderContains!);\n        if (RecipientContains != null)\n            builder.RecipientContains(RecipientContains!);\n        if (SubjectContains != null)\n            builder.SubjectContains(SubjectContains!);\n        if (BodyContains != null)\n            builder.BodyContains(BodyContains!);\n        if (!string.IsNullOrEmpty(Importance))\n            builder.Importance(Importance!);\n        if (!string.IsNullOrEmpty(MoveToFolder))\n            builder.MoveToFolder(MoveToFolder!);\n        if (!string.IsNullOrEmpty(CopyToFolder))\n            builder.CopyToFolder(CopyToFolder!);\n        if (Delete.IsPresent)\n            builder.Delete();\n        if (ForwardTo != null)\n            builder.ForwardTo(ForwardTo);\n        if (StopProcessing.IsPresent)\n            builder.StopProcessingRules();\n\n        WriteObject(builder);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphInboxRuleObject.cs",
    "content": "using System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a <see cref=\"GraphInboxRule\"/> object.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphInboxRuleObject\")]\n[OutputType(typeof(GraphInboxRule))]\npublic sealed class CmdletNewGraphInboxRuleObject : PSCmdlet\n{\n    /// <summary>\n    /// Display name for the inbox rule.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Params\")]\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Builder object used to create the rule.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = \"Builder\")]\n    public GraphInboxRuleBuilder? Builder { get; set; }\n\n    /// <summary>\n    /// Rule processing sequence number.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public int Sequence { get; set; }\n\n    /// <summary>\n    /// Enables the rule when set.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public SwitchParameter Enabled { get; set; }\n\n    /// <summary>\n    /// Folder to move matching messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? MoveToFolder { get; set; }\n\n    /// <summary>\n    /// Folder to copy matching messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? CopyToFolder { get; set; }\n\n    /// <summary>\n    /// Deletes matching messages.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public SwitchParameter Delete { get; set; }\n\n    /// <summary>\n    /// Addresses to forward matching messages to.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? ForwardTo { get; set; }\n\n    /// <summary>\n    /// Stops processing additional rules when this rule matches.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public new SwitchParameter StopProcessing { get; set; }\n\n    /// <summary>\n    /// Sender address patterns to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? SenderContains { get; set; }\n\n    /// <summary>\n    /// Recipient address patterns to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? RecipientContains { get; set; }\n\n    /// <summary>\n    /// Subject text patterns to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? SubjectContains { get; set; }\n\n    /// <summary>\n    /// Body text patterns to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string[]? BodyContains { get; set; }\n\n    /// <summary>\n    /// Importance level to match.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? Importance { get; set; }\n\n    /// <summary>\n    /// Creates a <see cref=\"GraphInboxRule\"/> instance from the supplied parameters.\n    /// </summary>\n    protected override void ProcessRecord()\n    {\n        if (ParameterSetName == \"Builder\")\n        {\n            WriteObject(Builder!.Build());\n            return;\n        }\n\n        var rule = new GraphInboxRule\n        {\n            DisplayName = DisplayName,\n            Sequence = Sequence,\n            IsEnabled = Enabled.IsPresent\n        };\n\n        if (SenderContains != null || RecipientContains != null || SubjectContains != null || BodyContains != null || Importance != null)\n        {\n            rule.Conditions = new GraphInboxRulePredicates\n            {\n                SenderContains = SenderContains != null ? new List<string>(SenderContains) : null,\n                RecipientContains = RecipientContains != null ? new List<string>(RecipientContains) : null,\n                SubjectContains = SubjectContains != null ? new List<string>(SubjectContains) : null,\n                BodyContains = BodyContains != null ? new List<string>(BodyContains) : null,\n                Importance = Importance\n            };\n        }\n\n        if (!string.IsNullOrEmpty(MoveToFolder) || !string.IsNullOrEmpty(CopyToFolder) || Delete.IsPresent || ForwardTo != null || StopProcessing.IsPresent)\n        {\n            rule.Actions = new GraphInboxRuleActions\n            {\n                MoveToFolder = MoveToFolder,\n                CopyToFolder = CopyToFolder,\n                Delete = Delete.IsPresent ? true : null,\n                ForwardTo = ForwardTo != null ? new List<GraphEmailAddress>(ForwardTo.Select(a => new GraphEmailAddress { Email = new GraphEmail { Address = a } })) : null,\n                StopProcessingRules = StopProcessing.IsPresent ? true : null\n            };\n        }\n\n        WriteObject(rule);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphMailboxPermissionBuilder.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a <see cref=\"GraphMailboxPermissionBuilder\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphMailboxPermissionBuilder\")]\n[OutputType(typeof(GraphMailboxPermissionBuilder))]\npublic sealed class CmdletNewGraphMailboxPermissionBuilder : PSCmdlet {\n    /// <summary>\n    /// Mailbox owner user principal name.\n    /// </summary>\n    [Parameter]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// User to grant permissions to.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string GrantedToUser { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Roles to assign on the mailbox.\n    /// </summary>\n    [Parameter]\n    public GraphMailboxRole[]? Roles { get; set; }\n\n    /// <summary>\n    /// Optional permission identifier.\n    /// </summary>\n    [Parameter]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Builds the <see cref=\"GraphMailboxPermission\"/> from parameters.\n    /// </summary>\n    protected override void ProcessRecord() {\n        var builder = new GraphMailboxPermissionBuilder();\n        if (UserPrincipalName != null)\n            builder.UserPrincipalName(UserPrincipalName);\n        builder.GrantedToUser(GrantedToUser);\n        if (Roles != null)\n            builder.Roles(Roles);\n        if (Id != null)\n            builder.Id(Id);\n        WriteObject(builder);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewGraphMailboxPermissionObject.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Creates a <see cref=\"GraphMailboxPermission\"/> object.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"GraphMailboxPermissionObject\")]\n[OutputType(typeof(GraphMailboxPermission))]\npublic sealed class CmdletNewGraphMailboxPermissionObject : PSCmdlet {\n    /// <summary>\n    /// User principal name of the grantee.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Params\")]\n    public string GrantedToUser { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Roles assigned to the grantee.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public GraphMailboxRole[]? Roles { get; set; }\n\n    /// <summary>\n    /// Mailbox owner user principal name.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Permission identifier.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Params\")]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Permission builder object used to create the permission.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = \"Builder\")]\n    public GraphMailboxPermissionBuilder? Builder { get; set; }\n\n    /// <summary>\n    /// Constructs the <see cref=\"GraphMailboxPermission\"/> from parameters or builder.\n    /// </summary>\n    protected override void ProcessRecord() {\n        GraphMailboxPermission permission;\n        if (ParameterSetName == \"Builder\") {\n            permission = Builder!.Build();\n        } else {\n            permission = new GraphMailboxPermission {\n                UserPrincipalName = UserPrincipalName,\n                Id = Id,\n                Roles = Roles,\n                GrantedTo = new GraphMailboxGrantee { User = GrantedToUser }\n            };\n        }\n        WriteObject(permission);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletNewTemporaryMailCrypto.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Management.Automation;\nusing System.Security.Cryptography.X509Certificates;\n\n/// <summary>\n/// Creates temporary cryptographic material for testing mail encryption.\n/// </summary>\n[Cmdlet(VerbsCommon.New, \"TemporaryMailCrypto\")]\n[OutputType(typeof(TemporaryPgpKeyPair))]\n[OutputType(typeof(X509Certificate2))]\npublic sealed class CmdletNewTemporaryMailCrypto : PSCmdlet\n{\n    /// <summary>Generate a PGP key pair.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Pgp\")]\n    public SwitchParameter Pgp { get; set; }\n\n    /// <summary>Identity for the PGP key.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    public string Identity { get; set; } = \"Mailozaurr Test\";\n\n    /// <summary>Passphrase for the private key.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    public string PassPhrase { get; set; } = string.Empty;\n\n    /// <summary>Size of the RSA key.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    public int KeySize { get; set; } = 2048;\n\n    /// <summary>Optional path where the generated data should be stored.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    [Parameter(ParameterSetName = \"Smime\")]\n    public string OutputPath { get; set; } = string.Empty;\n\n    /// <summary>Do not delete generated files when disposed.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    [Parameter(ParameterSetName = \"Smime\")]\n    public SwitchParameter NoDispose { get; set; }\n\n    /// <summary>Generate an S/MIME certificate.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Smime\")]\n    public SwitchParameter Smime { get; set; }\n\n    /// <summary>Subject for the certificate.</summary>\n    [Parameter(ParameterSetName = \"Smime\")]\n    public string SubjectName { get; set; } = \"CN=Mailozaurr Test\";\n\n    /// <summary>Number of days the certificate is valid.</summary>\n    [Parameter(ParameterSetName = \"Smime\")]\n    public int ValidDays { get; set; } = 1;\n\n    /// <inheritdoc />\n    protected override void ProcessRecord()\n    {\n        if (ParameterSetName == \"Pgp\")\n        {\n            WriteObject(TemporaryPgpKeyPair.Create(Identity, PassPhrase, KeySize, OutputPath == string.Empty ? null : OutputPath, !NoDispose.IsPresent));\n        }\n        else\n        {\n            WriteObject(TemporarySmimeCertificate.CreateSelfSigned(SubjectName, ValidDays, OutputPath == string.Empty ? null : OutputPath));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveEmailPendingMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes pending messages from a file based repository.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"EmailPendingMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletRemoveEmailPendingMessage : AsyncPSCmdlet {\n    /// <summary>Identifier of the message to remove.</summary>\n    [Parameter(Mandatory = true)]\n    public string? MessageId { get; set; }\n\n    /// <summary>Directory containing pending message log file.</summary>\n    [Parameter(Mandatory = true)]\n    [Alias(\"PendingPath\")]\n    public string? PendingMessagesPath { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        if (!ShouldProcess(MessageId ?? string.Empty, \"Removing pending message\")) {\n            return;\n        }\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = PendingMessagesPath! };\n        var repo = new FilePendingMessageRepository(options);\n        await repo.RemoveAsync(MessageId!, CancelToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGmailMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Deletes a Gmail message.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GmailMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemoveGmailMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// Gmail account containing the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// OAuth credential used to authenticate.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Identifier of the Gmail message to remove.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Deletes the specified Gmail message.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var dryRun = !ShouldProcess(Id!, \"Deleting Gmail message\");\n        var net = Credential!.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n        var client = new GmailApiClient(oauth) { DryRun = dryRun };\n        await client.DeleteAsync(GmailAccount!, Id!, CancelToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphEvent.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes a calendar event via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphEvent\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemoveGraphEvent : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name owning the event.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the event to remove.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? EventId { get; set; }\n\n    /// <summary>\n    /// Graph connection context.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Removes the specified event via Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Remove-GraphEvent - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(EventId!, \"Removing event\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        if (dryRun) {\n            await MicrosoftGraphUtils.RemoveEventAsync(cred, UserPrincipalName!, EventId!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? last = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.RemoveEventAsync(cred, UserPrincipalName!, EventId!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                last = ex;\n                WriteWarning($\"Remove-GraphEvent - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (last != null) WriteError(new ErrorRecord(last, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphFolder.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes a Microsoft Graph mail folder.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic class CmdletRemoveGraphFolder : AsyncPSCmdlet {\n    /// <summary>User principal name owning the folder.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Identifier of the folder to remove.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? FolderId { get; set; }\n\n    /// <summary>Remove subfolders as well. Microsoft Graph always deletes recursively.</summary>\n    [Parameter]\n    public SwitchParameter Recursive { get; set; }\n\n    /// <summary>Connection information for Microsoft Graph.</summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Use <c>Invoke-MgGraphRequest</c> instead of built-in logic.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>Request timeout in seconds.</summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Microsoft Graph requests allowed.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>Number of retries on transient errors.</summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>Delay between retries in milliseconds.</summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Removes the specified folder using Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Remove-GraphFolder - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(FolderId!, \"Removing Graph folder\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.RemoveFolderAsync(cred, UserPrincipalName!, FolderId!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.RemoveFolderAsync(cred, UserPrincipalName!, FolderId!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Remove-GraphFolder - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(FolderId!, \"Removing Graph folder\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/{FolderId}\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"DELETE\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphInboxRule.cs",
    "content": "using System.Collections.Generic;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes an inbox rule via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphInboxRule\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemoveGraphInboxRule : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name owning the inbox rule.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the rule to remove.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? RuleId { get; set; }\n\n    /// <summary>\n    /// Graph connection context.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Use <c>Invoke-MgGraphRequest</c> instead of built-in logic.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Remove-GraphInboxRule - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(RuleId!, \"Removing inbox rule\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        if (dryRun) {\n            await MicrosoftGraphUtils.RemoveRuleAsync(cred, UserPrincipalName!, RuleId!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.RemoveRuleAsync(cred, UserPrincipalName!, RuleId!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Remove-GraphInboxRule - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(RuleId!, \"Removing inbox rule\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/inbox/messageRules/{RuleId}\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"DELETE\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphMailboxPermission.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes mailbox permissions via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphMailboxPermission\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic class CmdletRemoveGraphMailboxPermission : AsyncPSCmdlet {\n    /// <summary>\n    /// Mailbox owner user principal name.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of permissions to remove.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNullOrEmpty]\n    public string[]? PermissionId { get; set; }\n\n    /// <summary>\n    /// Mailbox permission objects to remove.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Object\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphMailboxPermission[]? MailboxPermission { get; set; }\n\n    /// <summary>\n    /// Filters permissions by mailbox role.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Filter\")]\n    public GraphMailboxRole[]? Role { get; set; }\n\n    /// <summary>\n    /// Filters permissions by assigned users.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Filter\")]\n    public string[]? GrantedToUser { get; set; }\n\n    /// <summary>\n    /// Path to CSV file containing permission entries.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Csv\")]\n    [ValidateNotNullOrEmpty]\n    public string? CsvPath { get; set; }\n\n    /// <summary>\n    /// Graph connection to use for the request.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [Parameter(ParameterSetName = \"Object\", ValueFromPipeline = true)]\n    [Parameter(ParameterSetName = \"Csv\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Switch to use <c>Invoke-MgGraphRequest</c> instead of built-in logic.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Removes mailbox permissions based on the provided input.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        if (ParameterSetName == \"MgGraphRequest\") {\n            ProcessMgGraph();\n            return;\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Remove-GraphMailboxPermission - Connection not provided and no default session available.\");\n            return;\n        }\n\n        if (ParameterSetName == \"Csv\") {\n            await ProcessCsvAsync(conn.Credential);\n            return;\n        }\n\n        if (ParameterSetName == \"Object\") {\n            foreach (var perm in MailboxPermission!) {\n                if (perm is null) continue;\n                if (perm.UserPrincipalName == null)\n                    perm.UserPrincipalName = UserPrincipalName;\n                await ProcessGraphAsync(conn.Credential, perm);\n            }\n            return;\n        }\n\n        if (ParameterSetName == \"Filter\") {\n            await ProcessFilterAsync(conn.Credential);\n            return;\n        }\n\n        foreach (var id in PermissionId!) {\n            if (id is not null) {\n                await ProcessGraphAsync(conn.Credential, id);\n            }\n        }\n        return;\n    }\n\n    private async Task ProcessCsvAsync(GraphCredential cred) {\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Import-Csv\").AddParameter(\"Path\", CsvPath);\n        var rows = ps.Invoke();\n        foreach (var row in rows.OfType<PSObject>()) {\n            var id = row.Properties[\"PermissionId\"].Value?.ToString();\n            if (!string.IsNullOrWhiteSpace(id))\n                await ProcessGraphAsync(cred, id!);\n        }\n    }\n\n    private async Task ProcessFilterAsync(GraphCredential cred) {\n        var perms = await MicrosoftGraphUtils.GetMailboxPermissionsAsync(cred, UserPrincipalName!);\n        var filtered = perms.AsEnumerable();\n        if (Role != null && Role.Length > 0)\n            filtered = filtered.Where(p => p.Roles != null && p.Roles.Intersect(Role).Any());\n        if (GrantedToUser != null && GrantedToUser.Length > 0)\n            filtered = filtered.Where(p => p.GrantedTo?.User != null && GrantedToUser.Contains(p.GrantedTo.User, StringComparer.OrdinalIgnoreCase));\n        foreach (var p in filtered)\n            await ProcessGraphAsync(cred, p);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred, object permission) {\n        var id = permission switch {\n            GraphMailboxPermission p => p.Id,\n            string s => s,\n            _ => null\n        };\n        if (id is null) return;\n        var dryRun = !ShouldProcess(id, \"Removing mailbox permission\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        if (dryRun) {\n            await MicrosoftGraphUtils.RemoveMailboxPermissionAsync(cred, UserPrincipalName!, id, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.RemoveMailboxPermissionAsync(cred, UserPrincipalName!, id, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Remove-GraphMailboxPermission - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex)\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null)\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n\n    private void ProcessMgGraph() {\n        if (CsvPath != null) {\n            var psCsv = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n            psCsv.AddCommand(\"Import-Csv\").AddParameter(\"Path\", CsvPath);\n            var rows = psCsv.Invoke();\n            foreach (var row in rows.OfType<PSObject>()) {\n                var id = row.Properties[\"PermissionId\"].Value?.ToString();\n                if (!string.IsNullOrWhiteSpace(id))\n                    InvokeMgGraph(id!);\n            }\n        } else if (MailboxPermission != null) {\n            foreach (var perm in MailboxPermission) {\n                if (perm.Id != null) {\n                    if (!ShouldProcess(perm.Id, \"Removing mailbox permission\")) continue;\n                    InvokeMgGraph(perm.Id!);\n                }\n            }\n        } else if (PermissionId != null) {\n            foreach (var id in PermissionId) {\n                if (!ShouldProcess(id, \"Removing mailbox permission\")) continue;\n                InvokeMgGraph(id);\n            }\n        }\n    }\n\n    private void InvokeMgGraph(string id) {\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/permissions/{id}\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"DELETE\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Deletes a message from Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic class CmdletRemoveGraphMessage : AsyncPSCmdlet {\n    /// <summary>User principal name owning the message.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Identifier of the message to delete.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? MessageId { get; set; }\n\n    /// <summary>Connection used for Graph operations.</summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Indicates using Invoke-MgGraphRequest.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <inheritdoc />\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Remove-GraphMessage - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(MessageId!, \"Deleting message via Graph\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.DeleteMailMessageAsync(cred, UserPrincipalName!, MessageId!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.DeleteMailMessageAsync(cred, UserPrincipalName!, MessageId!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Remove-GraphMessage - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(MessageId!, \"Deleting message via Graph\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/messages/{MessageId}\");\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"DELETE\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveGraphMessageAttachment.cs",
    "content": "using System.Management.Automation;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes attachments from a <see cref=\"GraphMessage\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"GraphMessageAttachment\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]\n[OutputType(typeof(GraphMessage))]\npublic sealed class CmdletRemoveGraphMessageAttachment : PSCmdlet {\n    /// <summary>\n    /// Graph message to process.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphMessage? Message { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (Message == null) {\n            return;\n        }\n        if (!ShouldProcess(\"GraphMessage\", \"Removing attachments\")) {\n            WriteObject(Message);\n            return;\n        }\n        Message.Attachments = null;\n        WriteObject(Message);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveIMAPFolder.cs",
    "content": "using System.Management.Automation;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes an IMAP folder.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"IMAPFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemoveIMAPFolder : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>Name of the folder to remove.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    [ValidateNotNullOrEmpty]\n    public string? Folder { get; set; }\n\n    /// <summary>Remove subfolders as well.</summary>\n    [Parameter]\n    public SwitchParameter Recursive { get; set; }\n\n    /// <summary>\n    /// Removes the specified folder from the connected IMAP server.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var dryRun = !ShouldProcess(Folder!, \"Removing IMAP folder\");\n            await FolderOperations.RemoveFolderAsync(conn.Data, Folder!, Recursive.IsPresent, dryRun, CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Remove-IMAPFolder - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveIMAPMessage.cs",
    "content": "using System.Management.Automation;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\nusing System.Linq;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes messages from an IMAP folder by UID.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"IMAPMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemoveIMAPMessage : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>UIDs of messages to delete.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public uint[]? Uid { get; set; }\n\n    /// <summary>Optional folder containing the messages.</summary>\n    [Parameter(Position = 2)]\n    public string? Folder { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var folder = Folder ?? conn.Folder?.FullName;\n            var dryRun = !ShouldProcess(string.Join(\",\", Uid ?? System.Array.Empty<uint>()), \"Deleting IMAP message\");\n            var uniqueIds = (Uid ?? System.Array.Empty<uint>()).Select(u => new UniqueId(u));\n            await MessageRemover.DeleteAsync(conn.Data, uniqueIds, dryRun, folder, CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Remove-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemoveIMAPMessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes attachments from an IMAP <see cref=\"MimeMessage\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"IMAPMessageAttachment\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]\n[OutputType(typeof(MimeMessage))]\npublic sealed class CmdletRemoveIMAPMessageAttachment : PSCmdlet {\n    /// <summary>\n    /// MIME message to process.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public MimeMessage? Message { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (Message == null) {\n            return;\n        }\n        if (!ShouldProcess(\"MimeMessage\", \"Removing attachments\")) {\n            WriteObject(Message);\n            return;\n        }\n        RemoveMimeAttachments(Message.Body);\n        WriteObject(Message);\n    }\n\n    private static void RemoveMimeAttachments(MimeEntity? entity) {\n        if (entity is Multipart multipart) {\n            for (int i = multipart.Count - 1; i >= 0; i--) {\n                var part = multipart[i];\n                if (part is MimePart mp && mp.IsAttachment || part is MessagePart) {\n                    multipart.RemoveAt(i);\n                } else {\n                    RemoveMimeAttachments(part);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemovePOP3Message.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes messages from a POP3 mailbox by index.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"POP3Message\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]\npublic sealed class CmdletRemovePOP3Message : AsyncPSCmdlet {\n    /// <summary>Active POP3 connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>Indexes of messages to delete.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public int[]? Index { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            var dryRun = !ShouldProcess(string.Join(\",\", Index ?? System.Array.Empty<int>()), \"Deleting POP3 message\");\n            var valid = new List<int>();\n            foreach (var i in Index!) {\n                if (i < conn.Data.Count) {\n                    valid.Add(i);\n                } else {\n                    WriteWarning($\"Remove-POP3Message - Index is out of range. Use index less than {conn.Data.Count}.\");\n                }\n            }\n            await MessageRemover.DeleteAsync(conn.Data, valid, dryRun, CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Remove-POP3Message - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRemovePOP3MessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Removes attachments from a POP3 <see cref=\"MimeMessage\"/> instance.\n/// </summary>\n[Cmdlet(VerbsCommon.Remove, \"POP3MessageAttachment\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]\n[OutputType(typeof(MimeMessage))]\npublic sealed class CmdletRemovePOP3MessageAttachment : PSCmdlet {\n    /// <summary>\n    /// MIME message to process.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public MimeMessage? Message { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (Message == null) {\n            return;\n        }\n        if (!ShouldProcess(\"MimeMessage\", \"Removing attachments\")) {\n            WriteObject(Message);\n            return;\n        }\n        RemoveMimeAttachments(Message.Body);\n        WriteObject(Message);\n    }\n\n    private static void RemoveMimeAttachments(MimeEntity? entity) {\n        if (entity is Multipart multipart) {\n            for (int i = multipart.Count - 1; i >= 0; i--) {\n                var part = multipart[i];\n                if (part is MimePart mp && mp.IsAttachment || part is MessagePart) {\n                    multipart.RemoveAt(i);\n                } else {\n                    RemoveMimeAttachments(part);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRenameGraphFolder.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Renames a Microsoft Graph mail folder.\n/// </summary>\n[Cmdlet(\"Rename\", \"GraphFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic class CmdletRenameGraphFolder : AsyncPSCmdlet {\n    /// <summary>User principal name owning the folder.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>Identifier of the folder to rename.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? FolderId { get; set; }\n\n    /// <summary>The new folder name.</summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? NewName { get; set; }\n\n    /// <summary>Connection information for Microsoft Graph.</summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>Use <c>Invoke-MgGraphRequest</c> instead of built-in logic.</summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>Request timeout in seconds.</summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent requests during rename.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>Number of retries on transient errors.</summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>Delay between retries in milliseconds.</summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Renames a folder using Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Rename-GraphFolder - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(FolderId!, \"Renaming Graph folder\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.RenameFolderAsync(cred, UserPrincipalName!, FolderId!, NewName!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.RenameFolderAsync(cred, UserPrincipalName!, FolderId!, NewName!, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Rename-GraphFolder - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(FolderId!, \"Renaming Graph folder\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/{FolderId}\");\n        var body = JsonSerializer.Serialize(\n            new GraphFolderRenameRequest { DisplayName = NewName },\n            MailozaurrJsonContext.Default.GraphFolderRenameRequest);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"PATCH\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletRenameIMAPFolder.cs",
    "content": "using System.Management.Automation;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Renames an IMAP folder.\n/// </summary>\n[Cmdlet(\"Rename\", \"IMAPFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletRenameIMAPFolder : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection info.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>Folder name to rename.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    [ValidateNotNullOrEmpty]\n    public string? Folder { get; set; }\n\n    /// <summary>New name for the folder.</summary>\n    [Parameter(Mandatory = true, Position = 2)]\n    [ValidateNotNullOrEmpty]\n    public string? NewName { get; set; }\n\n    /// <summary>\n    /// Renames an IMAP folder on the connected server.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var dryRun = !ShouldProcess(Folder!, \"Renaming IMAP folder\");\n            await FolderOperations.RenameFolderAsync(conn.Data, Folder!, NewName!, dryRun, CancelToken);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Rename-IMAPFolder - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveGmailMessageAttachment.cs",
    "content": "using System.IO;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Saves attachments from a Gmail message to disk.\n/// </summary>\n[Cmdlet(VerbsData.Save, \"GmailMessageAttachment\")]\npublic sealed class CmdletSaveGmailMessageAttachment : AsyncPSCmdlet {\n    /// <summary>\n    /// Gmail account containing the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// OAuth credential used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Identifier of the Gmail message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Destination path for saving attachments.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Downloads attachments from the specified Gmail message.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var net = Credential!.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n        var client = new GmailApiClient(oauth);\n        var list = await client.ListAttachmentsAsync(GmailAccount!, Id!, CancelToken);\n        Directory.CreateDirectory(Path!);\n        foreach (var att in list) {\n            var bytes = await client.DownloadAttachmentAsync(GmailAccount!, Id!, att.Id!, CancelToken);\n            var filePath = System.IO.Path.Combine(Path!, att.FileName ?? att.Id!);\n            File.WriteAllBytes(filePath, bytes);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveGraphMessage.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Saves Microsoft Graph email messages to disk in a specified format.</para>\n/// <para type=\"description\">The <c>Save-GraphMessage</c> cmdlet saves one or more <see cref=\"EmailGraphMessage\"/> objects to disk at the specified path. Use this to archive, export, or process messages retrieved from Microsoft Graph.</para>\n/// <example>\n///   <summary>Save mail messages to a folder</summary>\n///   <code>Get-EmailGraphMessage ... | Save-GraphMessage -Path \"C:\\Archive\"</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to export or archive messages for backup, migration, or compliance scenarios.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.Save, \"GraphMessage\")]\npublic class CmdletSaveGraphMessage : PSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">Specifies the <see cref=\"EmailGraphMessage\"/> objects to save. Accepts pipeline input.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNullOrEmpty]\n    public PSObject[]? Message { get; set; }\n    /// <summary>\n    /// <para type=\"description\">Specifies the path where the messages will be saved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Saves the specified mail messages to disk at the given path.\n    /// </summary>\n    protected override void ProcessRecord() {\n        var targetPath = Path;\n        if (Message is null || string.IsNullOrEmpty(targetPath)) {\n            return;\n        }\n        foreach (var m in Message) {\n            if (m.BaseObject is EmailGraphMessage gm) {\n                MicrosoftGraphUtils.SaveMailMessages(new[] { gm }, targetPath!);\n            } else if (m.BaseObject is MimeKit.MimeMessage mm) {\n                var resolved = System.IO.Path.GetFullPath(targetPath!);\n                if (!System.IO.Directory.Exists(resolved)) System.IO.Directory.CreateDirectory(resolved);\n                var file = System.IO.Path.Combine(resolved, System.IO.Path.ChangeExtension(System.IO.Path.GetRandomFileName(), \"eml\"));\n                mm.WriteTo(file);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveGraphMessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Saves attachments from Microsoft Graph message objects.\n/// </summary>\n[Cmdlet(VerbsData.Save, \"GraphMessageAttachment\")]\npublic class CmdletSaveGraphMessageAttachment : PSCmdlet {\n    /// <summary>\n    /// Attachments to save from the message.\n    /// </summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNullOrEmpty]\n    public Attachment[]? Attachment { get; set; }\n\n    /// <summary>\n    /// Destination path for saved attachments.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Processes the cmdlet invocation.\n    /// </summary>\n    protected override void ProcessRecord() {\n        if (Attachment?.Length > 0 && Path is not null) {\n            MicrosoftGraphUtils.SaveAttachments(Attachment, Path);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveIMAPMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Saves an IMAP message to disk at the specified path.</para>\n/// <para type=\"description\">The <c>Save-IMAPMessage</c> cmdlet saves a message from an IMAP mailbox (using a <see cref=\"ImapConnectionInfo\"/> object from <c>Connect-IMAP</c>) to disk at the given path. Provide the unique identifier of the message to export or archive it.</para>\n/// <example>\n///   <summary>Save an IMAP message to a file</summary>\n///   <code>\n/// $client = Connect-IMAP ...; Save-IMAPMessage -Client $client -Uid 123 -Path \"C:\\Mail\\message.eml\"\n/// $client = Connect-IMAP ...; Save-IMAPMessage -Client $client -Uid 123 -Path \"C:\\Mail\\message.msg\"\n///   </code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to export or archive messages retrieved from an IMAP server.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectIMAP\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.Save, \"IMAPMessage\")]\npublic sealed class CmdletSaveIMAPMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object representing the active IMAP connection. This is the object returned by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the UID of the message to save.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public uint Uid { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional folder name from which to fetch the message. Defaults to Inbox.</para>\n    /// </summary>\n    [Parameter(Position = 2)]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the path where the message will be saved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 3)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n\n    /// <summary>\n    /// Saves the specified IMAP message to disk at the given path.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var uid = new UniqueId(Uid);\n            var mailFolder = conn.Data.GetCachedFolder(Folder ?? conn.Folder?.FullName, FolderAccess.ReadOnly);\n            conn.Folders[mailFolder.FullName] = (ImapFolder)mailFolder;\n            var message = mailFolder.GetMessage(uid);\n\n            if (!string.IsNullOrEmpty(Path)) {\n                var fullPath = System.IO.Path.GetFullPath(Path);\n                var directory = System.IO.Path.GetDirectoryName(fullPath);\n                if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory)) {\n                    System.IO.Directory.CreateDirectory(directory);\n                }\n\n                if (fullPath.EndsWith(\".msg\", System.StringComparison.OrdinalIgnoreCase)) {\n                    var tempEml = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $\"{System.Guid.NewGuid()}.eml\");\n                    try {\n                        message.WriteTo(tempEml);\n                        EmailMessage.ConvertEmlToMsg(new System.IO.FileInfo(tempEml), new System.IO.FileInfo(fullPath), true);\n                    } finally {\n                        if (System.IO.File.Exists(tempEml)) {\n                            System.IO.File.Delete(tempEml);\n                        }\n                    }\n                } else {\n                    message.WriteTo(fullPath);\n                }\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Save-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveIMAPMessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Saves attachments from an IMAP message to disk.</para>\n/// <para type=\"description\">The <c>Save-IMAPMessageAttachment</c> cmdlet saves all attachments from an IMAP message identified by its UID to the specified directory.</para>\n/// </summary>\n[Cmdlet(VerbsData.Save, \"IMAPMessageAttachment\")]\npublic sealed class CmdletSaveIMAPMessageAttachment : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> object representing the active IMAP connection.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the UID of the message to process.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public uint Uid { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional folder from which to retrieve the message. Defaults to Inbox.</para>\n    /// </summary>\n    [Parameter(Position = 2)]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the directory path where attachments will be saved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 3)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Saves attachments from the specified IMAP message to disk.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var uid = new UniqueId(Uid);\n            var mailFolder = conn.Data.GetCachedFolder(Folder ?? conn.Folder?.FullName, FolderAccess.ReadOnly);\n            conn.Folders[mailFolder.FullName] = (ImapFolder)mailFolder;\n            var message = mailFolder.GetMessage(uid);\n            if (Path is not null) {\n                MimeKitUtils.SaveAttachments(message.Attachments, Path);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Save-IMAPMessageAttachment - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSaveMimeMessage.cs",
    "content": "using System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Saves a MIME message or wrapper object to disk.\n/// </summary>\n[Cmdlet(VerbsData.Save, \"MimeMessage\")]\npublic sealed class CmdletSaveMimeMessage : PSCmdlet {\n    /// <summary>Message to save.</summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [Alias(\"Message\")]\n    public object? InputObject { get; set; }\n\n    /// <summary>Destination file path.</summary>\n    [Parameter(Mandatory = true)]\n    public string? Path { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (InputObject == null || string.IsNullOrEmpty(Path)) return;\n        MimeMessage? message = InputObject switch {\n            MimeMessage m => m,\n            ImapMessageInfo info => info.Raw.Message,\n            ImapEmailMessage imap => imap.Message,\n            Pop3MessageInfo pinfo => pinfo.Raw.Message,\n            Pop3EmailMessage pop => pop.Message,\n            GraphEmailMessage g => g.Message,\n            _ => null\n        };\n        if (message == null) return;\n\n        var resolved = System.IO.Path.GetFullPath(Path);\n        var directory = System.IO.Path.GetDirectoryName(resolved);\n        if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory))\n            System.IO.Directory.CreateDirectory(directory);\n\n        if (resolved.EndsWith(\".msg\", System.StringComparison.OrdinalIgnoreCase)) {\n            var temp = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.Guid.NewGuid() + \".eml\");\n            try {\n                message.WriteTo(temp);\n                EmailMessage.ConvertEmlToMsg(new System.IO.FileInfo(temp), new System.IO.FileInfo(resolved), true);\n            } finally {\n                if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);\n            }\n        } else {\n            message.WriteTo(resolved);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSavePOP3Message.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Saves a POP3 message to disk in either EML or MSG format.</para>\n/// <para type=\"description\">The <c>Save-POP3Message</c> cmdlet saves a message from a POP3 mailbox (using a <see cref=\"PopConnectionInfo\"/> object from <c>Connect-POP3</c>) to disk at the specified path. Use this to archive, export, or process messages retrieved from a POP3 server. The message can be saved as an EML file or converted to MSG format.</para>\n/// <example>\n///   <summary>Save a POP3 message to a file</summary>\n///   <code>\n/// $client = Connect-POP3 ...; Save-POP3Message -Client $client -Index 0 -Path \"C:\\Mail\\message.eml\"\n/// $client = Connect-POP3 ...; Save-POP3Message -Client $client -Index 0 -Path \"C:\\Mail\\message.msg\"\n///   </code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to export or archive messages for backup, migration, or compliance scenarios.\n/// </remarks>\n/// <seealso cref=\"CmdletConnectPOP3\"/>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsData.Save, \"POP3Message\")]\npublic sealed class CmdletSavePOP3Message : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"PopConnectionInfo\"/> object representing the active POP3 connection. This is the object returned by <c>Connect-POP3</c>.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the index of the message to save.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public int Index { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the path where the message will be saved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 2)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n\n    /// <summary>\n    /// Saves the specified POP3 message to disk at the given path.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            if (Index < conn.Data.Count) {\n                var message = conn.Data.GetMessage(Index);\n                if (Path != null) {\n                    var resolved = System.IO.Path.GetFullPath(Path);\n                    var directory = System.IO.Path.GetDirectoryName(resolved);\n                    if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory)) {\n                        System.IO.Directory.CreateDirectory(directory);\n                    }\n                    if (resolved.EndsWith(\".msg\", System.StringComparison.OrdinalIgnoreCase)) {\n                        var tempEml = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $\"{System.Guid.NewGuid()}.eml\");\n                        try {\n                            message.WriteTo(tempEml);\n                            EmailMessage.ConvertEmlToMsg(new System.IO.FileInfo(tempEml), new System.IO.FileInfo(resolved), true);\n                        } finally {\n                            if (System.IO.File.Exists(tempEml)) {\n                                System.IO.File.Delete(tempEml);\n                            }\n                        }\n                    } else {\n                        message.WriteTo(resolved);\n                    }\n                }\n            } else {\n                WriteWarning($\"Save-POP3Message - Index is out of range. Use index less than {conn.Data.Count}.\");\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Save-POP3Message - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSavePOP3MessageAttachment.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Saves attachments from a POP3 message to disk.</para>\n/// <para type=\"description\">The <c>Save-POP3MessageAttachment</c> cmdlet saves all attachments from a POP3 message identified by its index to the specified directory.</para>\n/// </summary>\n[Cmdlet(VerbsData.Save, \"POP3MessageAttachment\")]\npublic sealed class CmdletSavePOP3MessageAttachment : AsyncPSCmdlet {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"PopConnectionInfo\"/> object representing the active POP3 connection.</para>\n    /// </summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the index of the message to process.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public int Index { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the directory path where attachments will be saved.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 2)]\n    [ValidateNotNullOrEmpty]\n    public string? Path { get; set; }\n\n    /// <summary>\n    /// Saves attachments from the specified POP3 message to disk.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            if (Index < conn.Data.Count) {\n                var message = conn.Data.GetMessage(Index);\n                if (Path is not null) {\n                    MimeKitUtils.SaveAttachments(message.Attachments, Path);\n                }\n            } else {\n                WriteWarning($\"Save-POP3MessageAttachment - Index is out of range. Use index less than {conn.Data.Count}.\");\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Save-POP3MessageAttachment - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSearchGraphMailbox.cs",
    "content": "using System.Management.Automation;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Searches one or more mailboxes using Microsoft Graph.</para>\n/// <para type=\"description\">The <c>Search-GraphMailbox</c> cmdlet queries Microsoft Graph using application permissions. Provide multiple user principal names to search across several mailboxes. Results are returned as <see cref=\"GraphMessageInfo\"/> objects.</para>\n/// </summary>\n[Cmdlet(VerbsCommon.Search, \"GraphMailbox\")]\n[OutputType(typeof(GraphMessageInfo))]\npublic class CmdletSearchGraphMailbox : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal names to search across.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string[]? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Query string used to filter messages.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? Query { get; set; }\n\n    /// <summary>\n    /// Graph connection information.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Message index to start from.\n    /// </summary>\n    [Parameter]\n    public int From { get; set; }\n\n    /// <summary>\n    /// Number of messages to retrieve.\n    /// </summary>\n    [Parameter]\n    [Alias(\"Count\")]\n    [ValidateRange(1, int.MaxValue)]\n    public int Size { get; set; } = 25;\n\n    /// <summary>\n    /// Maximum number of parallel Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Search-GraphMailbox - Connection not provided and no default session available.\");\n            return;\n        }\n\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        try {\n            var results = await MicrosoftGraphUtils.SearchMailboxesAsync(\n                conn.Credential,\n                UserPrincipalName!,\n                Query!,\n                From,\n                Size);\n\n            foreach (var info in results) {\n                WriteObject(info);\n            }\n        } catch (GraphApiException ex) {\n            WriteError(new ErrorRecord(ex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSearchIMAPMailbox.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing MailKit.Search;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Searches an IMAP mailbox and returns matching messages.\n/// </summary>\n[Cmdlet(VerbsCommon.Search, \"IMAPMailbox\")]\n[OutputType(typeof(ImapEmailMessage))]\npublic sealed class CmdletSearchIMAPMailbox : AsyncPSCmdlet {\n    /// <summary>\n    /// Active IMAP connection.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// Folder to search. Defaults to inbox.\n    /// </summary>\n    [Parameter]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// Additional MailKit search queries.\n    /// </summary>\n    [Parameter]\n    public SearchQuery[]? SearchQuery { get; set; }\n\n    /// <summary>\n    /// Query language string to filter messages.\n    /// </summary>\n    [Parameter]\n    public string? Query { get; set; }\n\n    /// <summary>\n    /// Filters messages by subject.\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Filters messages by sender content.\n    /// </summary>\n    [Parameter]\n    public string? FromContains { get; set; }\n\n    /// <summary>\n    /// Filters messages by recipient content.\n    /// </summary>\n    [Parameter]\n    public string? ToContains { get; set; }\n\n    /// <summary>\n    /// Filters messages where the body contains this text.\n    /// </summary>\n    [Parameter]\n    public string? BodyContains { get; set; }\n\n    /// <summary>\n    /// Filters messages by priority.\n    /// </summary>\n    [Parameter]\n    public MessagePriority? Priority { get; set; }\n\n    /// <summary>\n    /// Only messages received since this date are returned.\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// Only messages received before this date are returned.\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// Filters messages that contain attachments.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter HasAttachment { get; set; }\n\n    /// <summary>\n    /// Maximum number of messages to return.\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int Count { get; set; }\n\n    /// <summary>\n    /// Searches the IMAP mailbox using the specified criteria.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var messages = await MailboxSearcher.SearchImapAsync(\n                conn.Data,\n                Folder,\n                Subject,\n                FromContains,\n                ToContains,\n                BodyContains,\n                Priority,\n                Since,\n                Before,\n                HasAttachment.IsPresent,\n                SearchQuery,\n                Count,\n                CancelToken,\n                Query);\n            foreach (var msg in messages) {\n                WriteObject(msg);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Search-IMAPMailbox - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSearchPOP3Mailbox.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Searches a POP3 mailbox and returns matching messages.\n/// </summary>\n[Cmdlet(VerbsCommon.Search, \"POP3Mailbox\")]\n[OutputType(typeof(Pop3EmailMessage))]\npublic sealed class CmdletSearchPOP3Mailbox : AsyncPSCmdlet {\n    /// <summary>\n    /// Active POP3 connection.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// Filters messages by subject.\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// Filters messages where the sender contains this string.\n    /// </summary>\n    [Parameter]\n    public string? FromContains { get; set; }\n\n    /// <summary>\n    /// Filters messages where the recipient contains this string.\n    /// </summary>\n    [Parameter]\n    public string? ToContains { get; set; }\n\n    /// <summary>\n    /// Filters messages where the body contains this string.\n    /// </summary>\n    [Parameter]\n    public string? BodyContains { get; set; }\n\n    /// <summary>\n    /// Filters messages by priority.\n    /// </summary>\n    [Parameter]\n    public MessagePriority? Priority { get; set; }\n\n    /// <summary>\n    /// Only return messages sent since this date.\n    /// </summary>\n    [Parameter]\n    public DateTime? Since { get; set; }\n\n    /// <summary>\n    /// Only return messages sent before this date.\n    /// </summary>\n    [Parameter]\n    public DateTime? Before { get; set; }\n\n    /// <summary>\n    /// Filters messages that have attachments.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter HasAttachment { get; set; }\n\n    /// <summary>\n    /// Query language string to filter messages.\n    /// </summary>\n    [Parameter]\n    public string? Query { get; set; }\n\n    /// <summary>\n    /// Maximum number of messages to return.\n    /// </summary>\n    [Parameter]\n    [ValidateRange(1, int.MaxValue)]\n    public int Count { get; set; }\n\n    /// <summary>\n    /// Searches the POP3 mailbox using the specified criteria.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            if (!conn.IsConnected) {\n                WriteWarning(\"Search-POP3Mailbox - Client is not connected.\");\n                return;\n            }\n            var messages = await MailboxSearcher.SearchPop3Async(\n                conn.Data,\n                Subject,\n                FromContains,\n                ToContains,\n                BodyContains,\n                Priority,\n                Since,\n                Before,\n                HasAttachment.IsPresent,\n                Count,\n                CancelToken,\n                Query);\n            foreach (var msg in messages) {\n                WriteObject(msg);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Search-POP3Mailbox - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSendEmailMessage.Parameters.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.IO;\nusing System.Security.Cryptography.X509Certificates;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\npublic sealed partial class CmdletSendEmailMessage : PSCmdlet\n{\n    /// <summary>\n    /// <para>Specifies the SMTP server to use for sending the email message. Required for SMTP scenarios.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"DefaultCredentials\")]\n    [Alias(\"SmtpServer\")]\n    [ValidateNotNullOrEmpty]\n    public string? Server { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the port to use on the SMTP server. The default is 587.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    public int Port { get; set; } = 587;\n\n    /// <summary>\n    /// <para>Specifies the sender's email address. Can be a string or a hashtable with Name and Email keys.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"EmailProviders\")]\n    [ValidateNotNullOrEmpty]\n    public object? From { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the reply-to address for the email. If not set, defaults to the From address.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public string? ReplyTo { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the email addresses to which a carbon copy (CC) of the email message is sent.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public object[]? Cc { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the email addresses that receive a blind carbon copy (BCC) of the email message.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public object[]? Bcc { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the recipient email addresses. Accepts a single address or an array of addresses.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public object[]? To { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the subject of the email message.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the priority of the email message. Acceptable values are Normal, High, and Low.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    [Alias(\"Importance\")]\n    public MessagePriority Priority { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the encoding for the email message. Recommended to leave as default.</para>\n    /// <para>Acceptable values: ASCII, BigEndianUnicode, Default, Unicode, UTF32, UTF7, UTF8.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [ValidateSet(\"ASCII\", \"BigEndianUnicode\", \"Default\", \"Unicode\", \"UTF32\", \"UTF7\", \"UTF8\")]\n    public string? Encoding { get; set; } = \"Default\";\n\n    /// <summary>\n    /// <para>Specifies the delivery notification options for the email message. Multiple options can be chosen.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public Mailozaurr.DeliveryNotification[]? DeliveryNotificationOption { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the delivery status notification type. Options are Full, HeadersOnly, Unspecified.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public MailKit.Net.Smtp.DeliveryStatusNotificationType? DeliveryStatusNotificationType { get; set; }\n\n    /// <summary>\n    /// <para>Specifies a user account or API key/token for authentication. Used for SMTP, SendGrid, and Graph API.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"EmailProviders\")]\n    [Parameter(Mandatory = true, ParameterSetName = \"oAuth\")]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the username for SMTP authentication. Used with Password.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    public string? Username { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the password for SMTP authentication. Used with Username. Can be clear text or secure string.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    public string? Password { get; set; }\n    /// <summary>\n    /// <para>Specifies the SASL mechanism for authentication. Defaults to Plain.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    public AuthenticationMechanism AuthenticationMechanism { get; set; } = AuthenticationMechanism.Plain;\n    /// <summary>\n    /// <para>Specifies the secure socket options for SMTP connection. Options: None, Auto, StartTls, StartTlsWhenAvailable, SslOnConnect. Default is Auto.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public MailKit.Security.SecureSocketOptions SecureSocketOptions { get; set; } = MailKit.Security.SecureSocketOptions.Auto;\n\n    /// <summary>\n    /// <para>Enables the use of SSL/TLS for the SMTP connection. If\n    /// <see cref=\"SecureSocketOptions\"/> remains <c>Auto</c>, this switch causes\n    /// <c>StartTls</c> to be used automatically.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public SwitchParameter UseSsl { get; set; }\n\n    /// <summary>\n    /// <para>Skips certificate revocation check during SMTP connection.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public SwitchParameter SkipCertificateRevocation { get; set; }\n\n    /// <summary>\n    /// <para>Skips certificate validation. Useful for self-signed certificates or IP-based SMTP servers.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Alias(\"SkipCertificateValidatation\")]\n    public SwitchParameter SkipCertificateValidation { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the HTML body of the email message. Use for rich content emails. Alias: Body, HtmlBody.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    [Alias(\"Body\", \"HtmlBody\")]\n    public string[]? HTML { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the plain text body of the email message. Alias: TextBody.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    [Alias(\"TextBody\")]\n    public string[]? Text { get; set; }\n\n    /// <summary>\n    /// <para>Specifies file paths to attach to the email message. Alias: Attachments.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    [Alias(\"Attachments\")]\n    public object[]? Attachment { get; set; }\n\n    /// <summary>\n    /// <para>Specifies inline attachments for the email message.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Alias(\"InlineAttachments\")]\n    public object[]? InlineAttachment { get; set; }\n\n    /// <summary>\n    /// Custom message headers to include with the email.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public Hashtable? Headers { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the maximum time (in milliseconds) to wait for the SMTP operation to complete. Default is 12000.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public int Timeout { get; set; } = 12000;\n\n    /// <summary>\n    /// <para>Specifies how many times the cmdlet should retry sending the message when an error occurs. Default is 0 (no retries).</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// <para>Delay in milliseconds between retry attempts.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para>Multiplicative backoff applied to the retry delay. Value of 1 disables backoff.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public double RetryDelayBackoff { get; set; } = 1.0;\n\n    /// <summary>\n    /// <para>Maximum delay in milliseconds between retries. 0 disables capping. Applies to all providers.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public int MaxDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para>Jitter window in milliseconds added to each retry delay. 0 disables jitter. Applies to all providers.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public int JitterMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// <para>When specified, retries are attempted regardless of the error\n    /// type. Without this switch, only transient errors are retried.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter RetryAlways { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the AWS region when using the SES provider.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public string? Region { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the Gmail account when using the Gmail provider.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"EmailProviders\")]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// <para>Enables reuse of SMTP connections via a connection pool.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter UseConnectionPool { get; set; }\n\n    /// <summary>\n    /// <para>Maximum number of connections to keep in the pool.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    [ValidateRange(1, int.MaxValue)]\n    public int ConnectionPoolSize { get; set; } = 2;\n\n    /// <summary>\n    /// <para>Overrides Graph concurrency for this invocation. When set, caps parallel Graph HTTP requests.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public int GraphMaxConcurrency { get; set; } = 0;\n\n    /// <summary>\n    /// <para>Enables SMTP fallback when Graph ultimately fails. Requires a configured factory via [Mailozaurr.MailozaurrOptions]::SmtpFallbackFactory.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    public SwitchParameter EnableSmtpFallback { get; set; }\n\n\n    /// <summary>\n    /// <para>Specifies chunk size in bytes used for Graph attachment uploads. Default is 4MB.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [ValidateRange(1, Mailozaurr.Graph.MaxChunkSize)]\n    public int ChunkSize { get; set; } = Mailozaurr.Graph.MaxChunkSize;\n\n    /// <summary>\n    /// <para>Enables sending email via OAuth2 authentication for SMTP.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Alias(\"oAuth\")]\n    public SwitchParameter OAuth2 { get; set; }\n\n    /// <summary>\n    /// <para>Requests a read receipt for the email message (Graph API only).</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter RequestReadReceipt { get; set; }\n\n    /// <summary>\n    /// <para>Requests a delivery receipt for the email message (Graph API only).</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter RequestDeliveryReceipt { get; set; }\n\n    /// <summary>\n    /// <para>Enables sending email via Microsoft Graph API.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter Graph { get; set; }\n\n    /// <summary>\n    /// <para>Enables sending email via Microsoft Graph API using Invoke-MgGraphRequest (requires Connect-MgGraph authentication).</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// <para>Indicates that the provided password is a SecureString.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    public SwitchParameter AsSecureString { get; set; }\n\n    /// <summary>\n    /// <para>Enables sending email via SendGrid API.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    public SwitchParameter SendGrid { get; set; }\n\n    /// <summary>\n    /// <para>Sends each recipient in the To field as a separate email (SendGrid only). BCC/CC are ignored in this mode.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    public SwitchParameter SeparateTo { get; set; }\n\n    /// <summary>\n    /// <para>Prevents saving the email to Sent Items (Graph API only).</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter DoNotSaveToSentItems { get; set; }\n\n    /// <summary>\n    /// <para>Suppresses output of the summary object.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Graph\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"MgGraphRequest\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SendGrid\")]\n    public SwitchParameter Suppress { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the path to save the communication log with the server.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? LogPath { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the path used to persist sent message metadata (opt-in).</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? SentLogPath { get; set; }\n\n    /// <summary>\n    /// <para>Enables logging of communication with the server to the console.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter LogConsole { get; set; }\n\n    /// <summary>\n    /// <para>Enables logging of communication with the server to an object as a message property.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter LogObject { get; set; }\n\n    /// <summary>\n    /// <para>Enables timestamps in the log output.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter LogTimestamps { get; set; }\n\n    /// <summary>\n    /// <para>Includes secrets in the log output.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter LogSecrets { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the format for timestamps in the log file.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? LogTimeStampsFormat { get; set; }\n\n    /// <summary>\n    /// <para>Sets the log prefix for the server.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? LogServerPrefix { get; set; }\n\n    /// <summary>\n    /// <para>Sets the log prefix for the client.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public string? LogClientPrefix { get; set; }\n\n    /// <summary>\n    /// <para>Overwrites the existing log file when using <c>-LogPath</c>.</para>\n    /// </summary>\n    [Parameter(Mandatory = false)]\n    public SwitchParameter LogOverwrite { get; set; }\n\n    /// <summary>\n    /// <para>Saves the email message to a file for troubleshooting purposes.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? MimeMessagePath { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the local domain name for the SMTP client.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? LocalDomain { get; set; }\n\n    /// <summary>\n    /// <para>Enables the use of default credentials for SMTP authentication.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    public bool UseDefaultCredentials { get; set; }\n\n    /// <summary>\n    /// <para>Specifies whether to sign or encrypt the email message. Requires certificate parameters.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public EmailActionEncryption SignOrEncrypt { get; set; } = EmailActionEncryption.None;\n\n    /// <summary>\n    /// <para>Specifies the path to the certificate used for signing or encrypting the email.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? CertificatePath { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the password for the certificate used in signing or encrypting the email.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? CertificatePassword { get; set; }\n\n    /// <summary>\n    /// <para>Indicates that the certificate password is provided as a SecureString.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public bool CertificatePasswordAsSecureString { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the thumbprint of the certificate used for signing or encrypting the email.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? CertificateThumbprint { get; set; }\n\n    /// <summary>\n    /// <para>Provides a certificate object used for S/MIME operations.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public X509Certificate2? Certificate { get; set; }\n\n    /// <summary>\n    /// Path to the recipient's public key used for PGP operations.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? PublicKeyPath { get; set; }\n\n    /// <summary>\n    /// Path to the sender's private key used for PGP signing.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? PrivateKeyPath { get; set; }\n\n    /// <summary>\n    /// Password for the private key when required.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public string? PrivateKeyPassword { get; set; }\n\n    /// <summary>\n    /// Indicates that <see cref=\"PrivateKeyPassword\"/> is provided as a secure string.\n    /// </summary>\n    [Parameter(Mandatory = false, ParameterSetName = \"DefaultCredentials\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"SecureString\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"oAuth\")]\n    [Parameter(Mandatory = false, ParameterSetName = \"Compatibility\")]\n    public bool PrivateKeyPasswordAsSecureString { get; set; }\n\n    /// <summary>\n    /// <para>Specifies the email provider to use (e.g., SendGrid, Mailgun, etc.).</para>\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"EmailProviders\")]\n    public EmailProvider EmailProvider { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSendEmailMessage.Process.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.IO;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Threading.Tasks;\nusing System.Threading;\nusing Mailozaurr;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr.PowerShell;\n\npublic sealed partial class CmdletSendEmailMessage : PSCmdlet\n{\n    private ActionPreference errorAction;\n    private InternalLogger? _logger;\n    private InternalLogger? _previousLogger;\n    private LogCollector? _logCollector;\n    private EventHandler<LogEventArgs>? _onVerbose;\n    private EventHandler<LogEventArgs>? _onWarning;\n    private EventHandler<LogEventArgs>? _onError;\n    private EventHandler<LogEventArgs>? _onInformation;\n\n    /// <summary>\n    /// Begin block\n    /// </summary>\n    protected override void BeginProcessing() {\n        // Initialize the logger to be able to see verbose, warning, debug, error, progress, and information messages.\n        _logger = new InternalLogger();\n        _logCollector = new LogCollector();\n\n        _onVerbose = (_, e) => _logCollector!.LogVerbose(e.FullMessage);\n        _onWarning = (_, e) => _logCollector!.LogWarning(e.FullMessage);\n        _onError =  (_, e) => _logCollector!.LogError(e.FullMessage);\n        _onInformation = (_, e) => _logCollector!.LogInformation(e.FullMessage);\n\n        _logger.OnVerboseMessage += _onVerbose;\n        _logger.OnWarningMessage += _onWarning;\n        _logger.OnErrorMessage += _onError;\n        _logger.OnInformationMessage += _onInformation;\n\n        _previousLogger = LoggingMessages.Logger;\n        LoggingMessages.Logger = _logger;\n\n        // Get the error action preference as user requested\n        // It first sets the error action to the default error action preference\n        // If the user has specified the error action, it will set the error action to the user specified error action\n        errorAction = (ActionPreference)this.SessionState.PSVariable.GetValue(\"ErrorActionPreference\");\n        if (this.MyInvocation.BoundParameters.ContainsKey(\"ErrorAction\")) {\n            string? errorActionString = this.MyInvocation.BoundParameters[\"ErrorAction\"]?.ToString();\n            if (errorActionString != null && Enum.TryParse(errorActionString, true, out ActionPreference actionPreference)) {\n                errorAction = actionPreference;\n            }\n        }\n\n        if (UseConnectionPool.IsPresent) {\n            SmtpConnectionPool.SetMaxPoolSize(ConnectionPoolSize);\n        }\n    }\n    /// <summary>\n    /// Process the record.\n    /// </summary>\n    protected override void ProcessRecord() {\n        Attachment = FilterExistingPaths(Attachment, nameof(Attachment));\n        InlineAttachment = FilterExistingPaths(InlineAttachment, nameof(InlineAttachment));\n        var (fromEmailRaw, fromNameRaw) = Helpers.GetEmailAndName(From);\n        string fromEmail = fromEmailRaw ?? string.Empty;\n        string fromName = fromNameRaw ?? string.Empty;\n\n        try {\n            if (SendGrid || EmailProvider == EmailProvider.SendGrid) {\n                ProcessSendGrid(fromEmail, fromName);\n            } else if (EmailProvider == EmailProvider.Mailgun) {\n                ProcessMailgun(fromEmail, fromName);\n            } else if (EmailProvider == EmailProvider.SES) {\n                ProcessSes(fromEmail, fromName);\n            } else if (EmailProvider == EmailProvider.Gmail) {\n                ProcessGmail(fromEmail, fromName);\n            } else if (Graph) {\n                ProcessGraph(fromEmail, fromName);\n            } else if (MgGraphRequest) {\n                ProcessMgGraphRequest(fromEmail, fromName).GetAwaiter().GetResult();\n            } else {\n                ProcessSmtp(fromEmail, fromName);\n            }\n        } finally {\n            if (_logCollector != null) {\n                LogEmitter.EmitLogs(_logCollector, this);\n            }\n        }\n    }\n\n    private void ProcessSendGrid(string fromEmail, string fromName) {\n        if (Credential == null) {\n            throw new InvalidOperationException(\"Credential is required for SendGrid processing.\");\n        }\n        var logCollector = new LogCollector();\n        SendGridClient sendGrid = new SendGridClient();\n        sendGrid.LogCollector = logCollector;\n        sendGrid.From = Helpers.GetFromObject(fromEmail, fromName);\n        if (Bcc != null) sendGrid.Bcc = Bcc.ToList();\n        if (Cc != null) sendGrid.Cc = Cc.ToList();\n        if (To != null) sendGrid.To = To.ToList();\n        sendGrid.ReplyTo = ReplyTo;\n        sendGrid.Subject = Subject ?? string.Empty;\n        if (Text != null) sendGrid.Text = string.Join(\"\", Text);\n        if (HTML != null) sendGrid.Html = string.Join(\"\", HTML);\n        sendGrid.Priority = Priority;\n        var sendGridAttachments = ConvertToAttachmentDescriptors(Attachment);\n        if (sendGridAttachments != null) {\n            sendGrid.Attachments = sendGridAttachments;\n        }\n        if (Headers != null) sendGrid.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        sendGrid.SeparateTo = SeparateTo;\n        sendGrid.ErrorAction = errorAction;\n        sendGrid.RetryCount = RetryCount;\n        sendGrid.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        sendGrid.RetryDelayBackoff = RetryDelayBackoff;\n        sendGrid.RetryAlways = RetryAlways.IsPresent;\n        sendGrid.MaxDelayMilliseconds = MaxDelayMilliseconds;\n        sendGrid.JitterMilliseconds = JitterMilliseconds;\n        NetworkCredential networkCredential = new NetworkCredential(Credential.UserName, Credential.Password);\n        sendGrid.Credentials = networkCredential;\n        sendGrid.CreateMessage();\n        sendGrid.DryRun = !ShouldProcess(sendGrid.SentTo, \"Sending email message via SendGrid\");\n        var result = sendGrid.SendEmailAsync().GetAwaiter().GetResult();\n        LogEmitter.EmitLogs(logCollector, this);\n        if (!Suppress) {\n            WriteObject(result);\n        }\n    }\n\n    private void ProcessMailgun(string fromEmail, string fromName) {\n        if (Credential == null) {\n            throw new InvalidOperationException(\"Credential is required for Mailgun processing.\");\n        }\n        var logCollector = new LogCollector();\n        using MailgunClient mailgun = new MailgunClient();\n        mailgun.LogCollector = logCollector;\n        mailgun.From = Helpers.GetFromObject(fromEmail, fromName);\n        if (Bcc != null) mailgun.Bcc = Bcc.ToList();\n        if (Cc != null) mailgun.Cc = Cc.ToList();\n        if (To != null) mailgun.To = To.ToList();\n        mailgun.ReplyTo = ReplyTo;\n        mailgun.Subject = Subject ?? string.Empty;\n        if (Text != null) mailgun.Text = string.Join(\"\", Text);\n        if (HTML != null) mailgun.Html = string.Join(\"\", HTML);\n        if (Attachment != null) {\n            mailgun.Attachment = Attachment.Select(a => a?.ToString() ?? string.Empty).ToArray();\n        }\n        if (InlineAttachment != null) {\n            mailgun.InlineAttachment = InlineAttachment.Select(a => a?.ToString() ?? string.Empty).ToArray();\n        }\n        if (Headers != null) mailgun.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        mailgun.ErrorAction = errorAction;\n        mailgun.RetryCount = RetryCount;\n        mailgun.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        mailgun.RetryDelayBackoff = RetryDelayBackoff;\n        mailgun.RetryAlways = RetryAlways.IsPresent;\n        mailgun.MaxDelayMilliseconds = MaxDelayMilliseconds;\n        mailgun.JitterMilliseconds = JitterMilliseconds;\n        NetworkCredential networkCredential = new NetworkCredential(Credential.UserName, Credential.Password);\n        mailgun.Credentials = networkCredential;\n        mailgun.DryRun = !ShouldProcess(mailgun.SentTo, \"Sending email message via Mailgun\");\n        var result = mailgun.SendEmailAsync().GetAwaiter().GetResult();\n        LogEmitter.EmitLogs(logCollector, this);\n        if (!Suppress) {\n            WriteObject(result);\n        }\n    }\n\n    private void ProcessSes(string fromEmail, string fromName) {\n        if (Credential == null) {\n            throw new InvalidOperationException(\"Credential is required for SES processing.\");\n        }\n        var logCollector = new LogCollector();\n        using SesClient ses = new SesClient();\n        ses.LogCollector = logCollector;\n        ses.From = Helpers.GetFromObject(fromEmail, fromName);\n        if (Bcc != null) ses.Bcc = Bcc.ToList();\n        if (Cc != null) ses.Cc = Cc.ToList();\n        if (To != null) ses.To = To.ToList();\n        ses.ReplyTo = ReplyTo;\n        ses.Subject = Subject ?? string.Empty;\n        if (Text != null) ses.Text = string.Join(\"\", Text);\n        if (HTML != null) ses.Html = string.Join(\"\", HTML);\n        if (Attachment != null) {\n            ses.Attachment = Attachment.Select(a => a?.ToString() ?? string.Empty).ToArray();\n        }\n        if (InlineAttachment != null) {\n            ses.InlineAttachment = InlineAttachment.Select(a => a?.ToString() ?? string.Empty).ToArray();\n        }\n        if (Headers != null) ses.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        ses.ErrorAction = errorAction;\n        ses.RetryCount = RetryCount;\n        ses.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        ses.RetryDelayBackoff = RetryDelayBackoff;\n        ses.RetryAlways = RetryAlways.IsPresent;\n        ses.MaxDelayMilliseconds = MaxDelayMilliseconds;\n        ses.JitterMilliseconds = JitterMilliseconds;\n        var region = Region;\n        if (region != null && region.Length > 0) {\n            ses.Region = region;\n        }\n        NetworkCredential networkCredential = new NetworkCredential(Credential.UserName, Credential.Password);\n        ses.Credentials = networkCredential;\n        ses.DryRun = !ShouldProcess(ses.SentTo, \"Sending email message via SES\");\n        var result = ses.SendEmailAsync().GetAwaiter().GetResult();\n        LogEmitter.EmitLogs(logCollector, this);\n        if (!Suppress) {\n            WriteObject(result);\n        }\n    }\n\n    private void ProcessGmail(string fromEmail, string fromName) {\n        var smtp = new Smtp();\n        smtp.From = Helpers.GetFromObject(fromEmail, fromName);\n        if (Bcc != null) smtp.Bcc = Bcc.ToList();\n        if (Cc != null) smtp.Cc = Cc.ToList();\n        if (To != null) smtp.To = To.ToList();\n        smtp.ReplyTo = ReplyTo;\n        smtp.Subject = Subject ?? string.Empty;\n        if (Text != null) smtp.TextBody = string.Join(\"\", Text);\n        if (HTML != null) smtp.HtmlBody = string.Join(\"\", HTML);\n        smtp.Attachments = ConvertToAttachmentDescriptors(Attachment);\n        smtp.InlineAttachments = ConvertToAttachmentDescriptors(InlineAttachment);\n        smtp.Priority = Priority;\n        if (Headers != null) smtp.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        smtp.CreateMessage(CancellationToken.None);\n\n        if (Credential == null) {\n            throw new InvalidOperationException(\"Credential is required for Gmail processing.\");\n        }\n\n        var net = Credential.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n\n        var client = new GmailApiClient(oauth);\n        try {\n            if (ShouldProcess(smtp.SentTo, \"Sending email message via Gmail API\")) {\n                var account = GmailAccount;\n                if (account == null || account.Length == 0) {\n                    throw new ArgumentException(\"GmailAccount is required when using Gmail API.\", nameof(GmailAccount));\n                }\n                var msg = client.SendAsync(account, smtp.Message).GetAwaiter().GetResult();\n                if (!Suppress) {\n                    WriteObject(new SmtpResult(true, EmailAction.Send, smtp.SentTo, smtp.SentFrom, \"GmailApi\", 0, smtp.Stopwatch.Elapsed, msg.Id));\n                }\n            } else if (!Suppress) {\n                WriteObject(new SmtpResult(false, EmailAction.Send, smtp.SentTo, smtp.SentFrom, \"GmailApi\", 0, smtp.Stopwatch.Elapsed, \"\", \"Email not sent (WhatIf)\"));\n            }\n        } finally {\n            smtp.Dispose();\n        }\n    }\n\n    private void ProcessGraph(string fromEmail, string fromName) {\n        if (Credential == null) {\n            throw new InvalidOperationException(\"Credential is required for Graph processing.\");\n        }\n        using Graph graph = new Graph();\n        graph.ChunkSize = ChunkSize;\n        graph.From = Helpers.GetFromObject(fromEmail, fromName);\n        graph.To = To;\n        graph.Cc = Cc;\n        graph.Bcc = Bcc;\n        graph.ReplyTo = ReplyTo;\n        graph.Subject = Subject ?? string.Empty;\n        graph.Priority = Priority;\n        graph.DoNotSaveToSentItems = DoNotSaveToSentItems;\n        graph.ErrorAction = errorAction;\n        graph.RetryCount = RetryCount;\n        graph.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        graph.RetryDelayBackoff = RetryDelayBackoff;\n        graph.RetryAlways = RetryAlways.IsPresent;\n        graph.RequestReadReceipt = RequestReadReceipt;\n        graph.RequestDeliveryReceipt = RequestDeliveryReceipt;\n        graph.HTML = string.Join(\"\", HTML ?? Array.Empty<string>());\n        graph.ContentType = \"HTML\";\n        graph.Attachments = Attachment;\n        if (Headers != null) graph.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        // Apply optional Graph policy without introducing new cmdlets\n        var applyPolicy = MailozaurrOptions.DefaultGraphPolicy != null\n            || GraphMaxConcurrency > 0\n            || MaxDelayMilliseconds > 0\n            || JitterMilliseconds > 0\n            || EnableSmtpFallback.IsPresent\n            || (RetryCount > 0 && this.MyInvocation.BoundParameters.ContainsKey(nameof(RetryCount)))\n            || (RetryDelayMilliseconds > 0 && this.MyInvocation.BoundParameters.ContainsKey(nameof(RetryDelayMilliseconds)));\n\n        if (applyPolicy) {\n            var p = MailozaurrOptions.DefaultGraphPolicy != null\n                ? new GraphSendPolicy {\n                    MaxConcurrency = MailozaurrOptions.DefaultGraphPolicy.MaxConcurrency,\n                    MaxRetries = MailozaurrOptions.DefaultGraphPolicy.MaxRetries,\n                    BaseDelayMs = MailozaurrOptions.DefaultGraphPolicy.BaseDelayMs,\n                    MaxDelayMs = MailozaurrOptions.DefaultGraphPolicy.MaxDelayMs,\n                    JitterMs = MailozaurrOptions.DefaultGraphPolicy.JitterMs,\n                    RetryOnTransient = MailozaurrOptions.DefaultGraphPolicy.RetryOnTransient,\n                    EnableSmtpFallback = MailozaurrOptions.DefaultGraphPolicy.EnableSmtpFallback\n                }\n                : new GraphSendPolicy();\n            if (RetryCount > 0 && this.MyInvocation.BoundParameters.ContainsKey(nameof(RetryCount))) p.MaxRetries = RetryCount;\n            if (RetryDelayMilliseconds > 0 && this.MyInvocation.BoundParameters.ContainsKey(nameof(RetryDelayMilliseconds))) p.BaseDelayMs = RetryDelayMilliseconds;\n            if (MaxDelayMilliseconds > 0) p.MaxDelayMs = MaxDelayMilliseconds;\n            if (JitterMilliseconds > 0) p.JitterMs = JitterMilliseconds;\n            if (GraphMaxConcurrency > 0) p.MaxConcurrency = GraphMaxConcurrency;\n            if (EnableSmtpFallback.IsPresent) p.EnableSmtpFallback = true;\n\n            graph.WithSendPolicy(p);\n        }\n        graph.CreateAttachments();\n        long graphSize = graph.TotalAttachmentSizeBytes;\n        if (graphSize > GraphAttachmentLimitBytes) {\n            WriteError(new ErrorRecord(\n                new ArgumentException(\"Attachments exceed Graph limit of 150MB.\"),\n                \"GraphAttachmentLimitExceeded\",\n                ErrorCategory.InvalidData,\n                null));\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n            return;\n        }\n\n        graph.DryRun = !ShouldProcess(graph.SentTo, \"Sending email message via Graph\");\n        if (graph.DryRun) {\n            LoggingMessages.Logger.WriteVerbose(\"Send-EmailMessage - Skipping authentication\");\n            var status = graph.IsLargerAttachment\n                ? graph.SendMessageDraftAsync().GetAwaiter().GetResult()\n                : graph.SendMessageAsync().GetAwaiter().GetResult();\n            if (!Suppress) {\n                WriteObject(status);\n            }\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n            return;\n        }\n\n        NetworkCredential networkCredential = new NetworkCredential(Credential.UserName, Credential.Password);\n        graph.Authenticate(networkCredential);\n        try {\n            var status = graph.ConnectO365GraphAsync().GetAwaiter().GetResult();\n            if (!status.Status) {\n                if (!Suppress) {\n                    WriteObject(status);\n                }\n                LogEmitter.EmitLogs(graph.LogCollector, this);\n                return;\n            }\n            status = graph.IsLargerAttachment\n                ? graph.SendMessageDraftAsync().GetAwaiter().GetResult()\n                : graph.SendMessageAsync().GetAwaiter().GetResult();\n            if (!Suppress) {\n                WriteObject(status);\n            }\n        } catch (GraphApiException ex) {\n            WriteError(new ErrorRecord(ex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n        }\n\n        LogEmitter.EmitLogs(graph.LogCollector, this);\n    }\n\n    private async Task ProcessMgGraphRequest(string fromEmail, string fromName) {\n        if (GraphMaxConcurrency > 0) {\n            MicrosoftGraphUtils.MaxConcurrentRequests = GraphMaxConcurrency;\n        }\n        using Graph graph = new Graph();\n        graph.ChunkSize = ChunkSize;\n        graph.From = Helpers.GetFromObject(fromEmail, fromName);\n        graph.To = To;\n        graph.Cc = Cc;\n        graph.Bcc = Bcc;\n        graph.ReplyTo = ReplyTo;\n        graph.Subject = Subject ?? string.Empty;\n        graph.Priority = Priority;\n        graph.DoNotSaveToSentItems = DoNotSaveToSentItems;\n        graph.ErrorAction = errorAction;\n        graph.RetryCount = RetryCount;\n        graph.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        graph.RetryDelayBackoff = RetryDelayBackoff;\n        graph.RequestReadReceipt = RequestReadReceipt;\n        graph.RequestDeliveryReceipt = RequestDeliveryReceipt;\n        graph.HTML = string.Join(\"\", HTML ?? Array.Empty<string>());\n        graph.ContentType = \"HTML\";\n        graph.Attachments = Attachment;\n        if (Headers != null) graph.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        graph.CreateAttachments();\n        long size = graph.TotalAttachmentSizeBytes;\n        if (size > GraphAttachmentLimitBytes) {\n            WriteError(new ErrorRecord(\n                new ArgumentException(\"Attachments exceed Graph limit of 150MB.\"),\n                \"GraphAttachmentLimitExceeded\",\n                ErrorCategory.InvalidData,\n                null));\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n            return;\n        }\n        if (!ShouldProcess(graph.SentTo, \"Sending email message via Graph (MgGraphRequest)\")) {\n            LoggingMessages.Logger.WriteVerbose(\"Send-EmailMessage - Skipping authentication\");\n            if (!Suppress) {\n                WriteObject(new SmtpResult(false, EmailAction.Send, graph.SentTo, graph.SentFrom, \"GraphAPI\", 0, graph.Stopwatch.Elapsed, \"\", \"Email not sent (WhatIf)\"));\n            }\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n            return;\n        }\n        if (graph.IsLargerAttachment) {\n            var json = graph.CreateDraftForMg();\n            var draftMessageId = InvokeMgGraphRequestPOST1($\"v1.0/users/{graph.From}/mailfolders/drafts/messages\", EmailAction.SendDraftMessage, json, graph.SentFrom, graph.SentTo, graph.Stopwatch.Elapsed);\n            await graph.PrepareAttachments();\n            foreach (var attachment in graph.AttachmentsPlaceHolders) {\n                var uploadUrl = InvokeMgGraphRequestPOST(attachment.Json, EmailAction.Send, attachment.Json, graph.SentFrom, graph.SentTo, graph.Stopwatch.Elapsed);\n                if (uploadUrl != string.Empty) {\n                    await InvokeMgGraphRequestPUT(uploadUrl, EmailAction.SendAttachment, attachment, graph.SentFrom, graph.SentTo, graph.Stopwatch.Elapsed);\n                } else {\n                    graph.LogCollector.LogVerbose(\"PlaceHolders not working?\");\n                }\n            }\n            var sendUri = MicrosoftGraphUtils.BuildGraphUri(\n                GraphEndpoint.V1,\n                $\"/users('{graph.SentFrom}')/messages/{draftMessageId}/send\");\n            InvokeMgGraphRequest(sendUri, EmailAction.Send, graph.MessageJson, graph.SentFrom, graph.SentTo, graph.Stopwatch.Elapsed);\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n        } else {\n            graph.CreateMessage();\n            InvokeMgGraphRequest($\"v1.0/users/{fromEmail}/sendMail\", EmailAction.Send, graph.MessageJson, graph.SentFrom, graph.SentTo, graph.Stopwatch.Elapsed);\n            LogEmitter.EmitLogs(graph.LogCollector, this);\n        }\n    }\n\n    private void ProcessSmtp(string fromEmail, string fromName) {\n        // Create a LogCollector to capture logs from async operations\n        var logCollector = new LogCollector();\n        \n        Smtp smtpClient = new Smtp(LogPath ?? string.Empty, LogConsole, LogObject, LogTimestamps, LogSecrets, LogTimeStampsFormat, LogServerPrefix, LogClientPrefix, LogOverwrite);\n        \n        // Attach the LogCollector to the SMTP client's logger\n        smtpClient.LogCollector = logCollector;\n        \n        if (!string.IsNullOrWhiteSpace(SentLogPath)) {\n            smtpClient.SentMessageRepository = new FileSentMessageRepository(SentLogPath!);\n        }\n        smtpClient.From = Helpers.GetFromObject(fromEmail, fromName);\n        smtpClient.ReplyTo = ReplyTo;\n        smtpClient.Cc = Cc;\n        smtpClient.Bcc = Bcc;\n        smtpClient.To = To;\n        smtpClient.Subject = Subject ?? string.Empty;\n        smtpClient.Priority = Priority;\n\n        smtpClient.DeliveryNotificationOption = DeliveryNotificationOption;\n        smtpClient.DeliveryStatusNotificationType = DeliveryStatusNotificationType;\n\n        smtpClient.CheckCertificateRevocation = !SkipCertificateRevocation;\n        smtpClient.SkipCertificateValidation = SkipCertificateValidation;\n        if (HTML != null) smtpClient.HtmlBody = string.Join(\"\", HTML);\n        if (Text != null) smtpClient.TextBody = string.Join(\"\", Text);\n\n        smtpClient.Attachments = ConvertToAttachmentDescriptors(Attachment);\n        smtpClient.InlineAttachments = ConvertToAttachmentDescriptors(InlineAttachment);\n        if (Headers != null) smtpClient.Headers = Headers.Cast<DictionaryEntry>().ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        smtpClient.Timeout = Timeout;\n\n        smtpClient.ErrorAction = errorAction;\n        smtpClient.RetryCount = RetryCount;\n        smtpClient.RetryDelayMilliseconds = RetryDelayMilliseconds;\n        smtpClient.RetryDelayBackoff = RetryDelayBackoff;\n        smtpClient.RetryAlways = RetryAlways.IsPresent;\n        smtpClient.MaxDelayMilliseconds = MaxDelayMilliseconds;\n        smtpClient.JitterMilliseconds = JitterMilliseconds;\n        smtpClient.UseConnectionPool = UseConnectionPool.IsPresent;\n        if (UseDefaultCredentials) {\n            smtpClient.ConnectionPoolIdentity = \"default\";\n        } else if (Credential != null) {\n            smtpClient.ConnectionPoolIdentity = Credential.UserName;\n        } else if (!string.IsNullOrWhiteSpace(Username)) {\n            smtpClient.ConnectionPoolIdentity = Username;\n        }\n        smtpClient.DryRun = !ShouldProcess(smtpClient.SentTo, \"Sending email message\");\n        if (smtpClient.DryRun) {\n            logCollector.LogVerbose(\"Send-EmailMessage - Skipping authentication\");\n            var useSslFlagDryRun = UseSsl.IsPresent && !this.MyInvocation.BoundParameters.ContainsKey(nameof(SecureSocketOptions));\n            smtpClient.Connect(Server ?? string.Empty, Port, SecureSocketOptions, useSslFlagDryRun);\n            smtpClient.CreateMessage(CancellationToken.None);\n            var dryRunStatus = smtpClient.Send();\n            LogEmitter.EmitLogs(logCollector, this);\n            if (!Suppress) {\n                WriteObject(dryRunStatus);\n            }\n            smtpClient.Dispose();\n            return;\n        }\n\n        var useSslFlag = UseSsl.IsPresent && !this.MyInvocation.BoundParameters.ContainsKey(nameof(SecureSocketOptions));\n        var status = smtpClient.Connect(Server ?? string.Empty, Port, SecureSocketOptions, useSslFlag);\n        \n        // Emit logs after Connect operation\n        LogEmitter.EmitLogs(logCollector, this);\n        \n        if (!status.Status) {\n            if (!Suppress) {\n                WriteObject(status);\n            }\n\n            smtpClient.Dispose();\n            return;\n        }\n\n        smtpClient.CreateMessage(CancellationToken.None);\n\n        if (SignOrEncrypt != EmailActionEncryption.None) {\n            if (SignOrEncrypt == EmailActionEncryption.PGPEncrypt && PublicKeyPath != null) {\n                status = smtpClient.PgpEncrypt(PublicKeyPath);\n            } else if (SignOrEncrypt == EmailActionEncryption.PGPSign && PublicKeyPath != null && PrivateKeyPath != null) {\n                status = smtpClient.PgpSign(PublicKeyPath, PrivateKeyPath, PrivateKeyPassword ?? string.Empty, PrivateKeyPasswordAsSecureString);\n            } else if (SignOrEncrypt == EmailActionEncryption.PGPSignAndEncrypt && PublicKeyPath != null && PrivateKeyPath != null) {\n                status = smtpClient.PgpSignAndEncrypt(PublicKeyPath, PrivateKeyPath, PrivateKeyPassword ?? string.Empty, PrivateKeyPasswordAsSecureString);\n            } else if (Certificate != null) {\n                status = smtpClient.Encrypt(SignOrEncrypt, Certificate);\n            } else if (CertificateThumbprint != null) {\n                status = smtpClient.Encrypt(SignOrEncrypt, CertificateThumbprint);\n            } else if (CertificatePath != null && CertificatePassword != null) {\n                status = smtpClient.Encrypt(SignOrEncrypt, CertificatePath, CertificatePassword,\n                    CertificatePasswordAsSecureString);\n            }\n\n            if (!status.Status) {\n                if (!Suppress) {\n                    WriteObject(status);\n                }\n\n                smtpClient.Dispose();\n                return;\n            }\n        }\n\n        if (UseDefaultCredentials) {\n            status = smtpClient.AuthenticateDefaultCredentials();\n        } else if (Credential != null) {\n            NetworkCredential networkCredential = new NetworkCredential(Credential.UserName, Credential.Password);\n            status = smtpClient.Authenticate(networkCredential, OAuth2);\n        } else if (!string.IsNullOrWhiteSpace(Username) || !string.IsNullOrWhiteSpace(Password)) {\n            status = smtpClient.Authenticate(Username ?? string.Empty, Password ?? string.Empty, AsSecureString, AuthenticationMechanism);\n        } else {\n            logCollector.LogVerbose(\"Send-EmailMessage - Skipping authentication\");\n            status = new SmtpResult(true, EmailAction.Authenticate, smtpClient.SentTo, smtpClient.SentFrom, smtpClient.Server, smtpClient.Port, smtpClient.Stopwatch.Elapsed, \"Authentication skipped\");\n        }\n\n        // Emit logs after Authentication operation\n        LogEmitter.EmitLogs(logCollector, this);\n\n        if (!status.Status) {\n            if (!Suppress) {\n                WriteObject(status);\n            }\n\n            smtpClient.Dispose();\n            return;\n        }\n\n        status = smtpClient.Send();\n        \n        // Emit logs after Send operation\n        LogEmitter.EmitLogs(logCollector, this);\n        \n        if (!Suppress) {\n            WriteObject(status);\n        }\n\n        var mimePath = MimeMessagePath;\n        if (mimePath != null && mimePath.Length > 0) {\n            smtpClient.SaveMessage(mimePath);\n        }\n\n        smtpClient.Dispose();\n    }\n\n\n    /// <summary>\n    /// Method to invoke the MgGraphRequest cmdlet\n    /// </summary>\n    /// <param name=\"uri\"></param>\n    /// <param name=\"action\"></param>\n    /// <param name=\"jsonBody\"></param>\n    /// <param name=\"sentFrom\"></param>\n    /// <param name=\"sentTo\"></param>\n    /// <param name=\"elapsed\"></param>\n    private void InvokeMgGraphRequest(string uri, EmailAction action, string jsonBody, string sentFrom, string sentTo, TimeSpan elapsed) {\n        var parameters = new Hashtable {\n            { \"Method\", \"POST\" },\n            { \"Uri\", uri },\n            { \"ContentType\", \"application/json; charset=UTF-8\"},\n            { \"Body\", jsonBody }\n        };\n\n        using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) {\n            powerShell.AddCommand(\"Invoke-MgGraphRequest\");\n            powerShell.AddParameters(parameters);\n            try {\n                powerShell.Invoke();\n                if (!Suppress) {\n                    WriteObject(new SmtpResult(true, action, sentTo, sentFrom, \"GraphAPI\", 0, elapsed, \"\", \"\"));\n                }\n            } catch (RuntimeException ex) {\n                LoggingMessages.Logger.WriteWarning($\"Send-EmailMessage - Error during sending using Graph Api (MgGraphRequest): {ex.Message}\");\n                if (errorAction == ActionPreference.Stop) {\n                    throw;\n                }\n                if (!Suppress) {\n                    var result = new SmtpResult(false, action, sentTo, sentFrom, \"GraphAPI\", 0, elapsed, \"\", ex.Message) {\n                        GraphError = GraphApiErrorParser.Parse(ex.Message)\n                    };\n                    WriteObject(result);\n                }\n            }\n        }\n    }\n\n    private async Task InvokeMgGraphRequestPUT(string uri, EmailAction action, GraphAttachmentPlaceHolder attachment, string sentFrom, string sentTo, TimeSpan elapsed) {\n        foreach (var body in attachment.Content) {\n            var parameters = new Hashtable {\n                { \"Method\", \"PUT\" },\n                { \"Uri\", uri },\n                { \"ContentType\", \"application/json; charset=UTF-8\" },\n                { \"Body\",  await body.ReadAsByteArrayAsync() },\n                { \"Headers\", new Hashtable {\n                         { \"Content-Range\", body.Headers.ContentRange },\n                        // { \"AnchorMailbox\", sentFrom }\n                    }\n                }\n            };\n\n            using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) {\n                powerShell.AddCommand(\"Invoke-MgGraphRequest\");\n                powerShell.AddParameters(parameters);\n                try {\n                    powerShell.Invoke();\n                } catch (RuntimeException ex) {\n                    LoggingMessages.Logger.WriteWarning($\"Send-EmailMessage - Error during sending using Graph Api (MgGraphRequest): {ex.Message}\");\n                    if (errorAction == ActionPreference.Stop) {\n                        throw;\n                    }\n\n                    if (!Suppress) {\n                        var result = new SmtpResult(false, action, sentTo, sentFrom, \"GraphAPI\", 0, elapsed, \"\", ex.Message) {\n                            GraphError = GraphApiErrorParser.Parse(ex.Message)\n                        };\n                        WriteObject(result);\n                    }\n                }\n            }\n        }\n    }\n\n\n    private string InvokeMgGraphRequestPOST(string uri, EmailAction action, string jsonBody, string sentFrom, string sentTo, TimeSpan elapsed) {\n        var parameters = new Hashtable {\n            { \"Method\", \"POST\" },\n            { \"Uri\", uri },\n            { \"ContentType\", \"application/json; charset=UTF-8\"},\n            { \"Body\", jsonBody }\n        };\n\n        using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) {\n            powerShell.AddCommand(\"Invoke-MgGraphRequest\");\n            powerShell.AddParameters(parameters);\n            try {\n                var results = powerShell.Invoke();\n                if (results.Count > 0) {\n                    // Assuming the first result contains the property you're interested in\n                    var result = results[0];\n                    if (result.BaseObject is IDictionary dictionary) {\n                        var uploadUrl = dictionary[\"uploadUrl\"]?.ToString();\n                        if (!string.IsNullOrEmpty(uploadUrl)) {\n                            return uploadUrl!;\n                        }\n                        // Handle the case where the property is not present\n                        throw new InvalidOperationException(\"The result does not contain an 'uploadUrl' property.\");\n                    }\n                    throw new InvalidOperationException(\"The result does not contain an 'uploadUrl' property.\");\n                } else {\n                    // Handle the case where no results were returned\n                    throw new InvalidOperationException(\"No results were returned from the Invoke-MgGraphRequest command.\");\n                }\n            } catch (RuntimeException ex) {\n                LoggingMessages.Logger.WriteWarning($\"Send-EmailMessage - Error during sending using Graph Api (MgGraphRequest): {ex.Message}\");\n                if (errorAction == ActionPreference.Stop) {\n                    throw;\n                }\n                if (!Suppress) {\n                    var result = new SmtpResult(false, action, sentTo, sentFrom, \"GraphAPI\", 0, elapsed, \"\", ex.Message) {\n                        GraphError = GraphApiErrorParser.Parse(ex.Message)\n                    };\n                    WriteObject(result);\n                }\n            }\n        }\n\n        return \"\";\n    }\n\n    private string InvokeMgGraphRequestPOST1(string uri, EmailAction action, string jsonBody, string sentFrom, string sentTo, TimeSpan elapsed) {\n        var parameters = new Hashtable {\n            { \"Method\", \"POST\" },\n            { \"Uri\", uri },\n            { \"ContentType\", \"application/json; charset=UTF-8\"},\n            { \"Body\", jsonBody }\n        };\n        using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) {\n            powerShell.AddCommand(\"Invoke-MgGraphRequest\");\n            powerShell.AddParameters(parameters);\n            try {\n                var results = powerShell.Invoke();\n                if (results.Count > 0) {\n                    // Assuming the first result contains the property you're interested in\n                    var result = results[0];\n                    if (result.BaseObject is IDictionary dictionary) {\n                        var id = dictionary[\"id\"]?.ToString();\n                        if (!string.IsNullOrEmpty(id)) {\n                            return id!;\n                        }\n                        // Handle the case where the property is not present\n                        throw new InvalidOperationException(\"The result does not contain an 'id' property.\");\n                    }\n                    throw new InvalidOperationException(\"The result does not contain an 'id' property.\");\n                } else {\n                    // Handle the case where no results were returned\n                    throw new InvalidOperationException(\"No results were returned from the Invoke-MgGraphRequest command.\");\n                }\n            } catch (RuntimeException ex) {\n                LoggingMessages.Logger.WriteWarning($\"Send-EmailMessage - Error during sending using Graph Api (MgGraphRequest): {ex.Message}\");\n                if (errorAction == ActionPreference.Stop) {\n                    throw;\n                }\n                if (!Suppress) {\n                    var result = new SmtpResult(false, action, sentTo, sentFrom, \"GraphAPI\", 0, elapsed, \"\", ex.Message) {\n                        GraphError = GraphApiErrorParser.Parse(ex.Message)\n                    };\n                    WriteObject(result);\n                }\n            }\n        }\n\n        return \"\";\n    }\n\n    private static List<AttachmentDescriptor>? ConvertToAttachmentDescriptors(object[]? attachments) {\n        if (attachments == null) {\n            return null;\n        }\n\n        return attachments\n            .Where(entry => entry != null)\n            .Select(entry => entry!)\n            .Select(entry => entry switch {\n                AttachmentDescriptor descriptor => descriptor,\n                string path => new FileAttachmentDescriptor(path),\n                FileInfo fileInfo => new FileAttachmentDescriptor(fileInfo.FullName),\n                _ => throw new ArgumentException($\"Unsupported attachment type: {entry.GetType().Name}\")\n            })\n            .ToList();\n    }\n\n    private object[]? FilterExistingPaths(object[]? paths, string parameterName) {\n        if (paths == null) {\n            return null;\n        }\n\n        List<object> valid = new();\n\n        foreach (var item in paths) {\n            string? path = item switch {\n                string s => s,\n                FileInfo fi => fi.FullName,\n                _ => null\n            };\n\n            if (path != null) {\n                if (path.IndexOfAny(new[] { '*', '?' }) >= 0) {\n                    string directory = Path.GetDirectoryName(path) ?? Directory.GetCurrentDirectory();\n                    string pattern = Path.GetFileName(path);\n                    int startCount = valid.Count;\n                    foreach (var file in Directory.GetFiles(directory, pattern)) {\n                        if (File.Exists(file)) {\n                            valid.Add(new FileInfo(file));\n                        }\n                    }\n                    if (valid.Count == startCount) {\n                        WriteWarning($\"Send-EmailMessage - No files found for wildcard pattern: {path}. Removing from '{parameterName}'.\");\n                    }\n                    continue;\n                }\n\n                if (!File.Exists(path)) {\n                    WriteWarning($\"Send-EmailMessage - File not found: {path}. Removing from '{parameterName}'.\");\n                    continue;\n                }\n            }\n\n            if (item != null) {\n                valid.Add(item);\n            }\n        }\n\n        return valid.Count > 0 ? valid.ToArray() : null;\n    }\n\n    /// <summary>\n    /// Cleans up logging resources after the cmdlet finishes executing.\n    /// </summary>\n    protected override void EndProcessing()\n    {\n        if (_logger != null) {\n            if (_onVerbose != null) _logger.OnVerboseMessage -= _onVerbose;\n            if (_onWarning != null) _logger.OnWarningMessage -= _onWarning;\n            if (_onError != null) _logger.OnErrorMessage -= _onError;\n            if (_onInformation != null) _logger.OnInformationMessage -= _onInformation;\n        }\n        _onVerbose = null;\n        _onWarning = null;\n        _onError = null;\n        _onInformation = null;\n        _logCollector = null;\n        if (_previousLogger != null) {\n            LoggingMessages.Logger = _previousLogger;\n        }\n        _previousLogger = null;\n        _logger = null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSendEmailMessage.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.IO;\nusing System.Security.Cryptography.X509Certificates;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Sends an email message using SMTP, SendGrid, or Microsoft Graph from within PowerShell. Replaces the deprecated Send-MailMessage.</para>\n/// <para type=\"description\">The Send-EmailMessage cmdlet sends an email message using a variety of providers and authentication methods. It supports SMTP (with or without SSL/TLS), SendGrid API, and Microsoft Graph API. The cmdlet allows for rich email composition, including HTML and text bodies, attachments, delivery notifications, and advanced logging. It is designed as a modern, secure, and flexible replacement for Send-MailMessage.</para>\n/// <para type=\"description\">Authentication can be provided via credentials, OAuth2, or provider-specific tokens. The cmdlet supports multiple parameter sets for compatibility with different authentication and provider scenarios.</para>\n/// </summary>\n/// <example>\n///   <summary>Send a basic email via SMTP with credentials</summary>\n///   <prefix>PS&gt; </prefix>\n///   <code>Send-EmailMessage -From @{ Name = 'John Doe'; Email = 'john.doe@example.com' } -To 'recipient@example.com' -Server 'smtp.office365.com' -Credential (Get-Credential) -HTML '<b>Hello</b>' -Subject 'Test Email'</code>\n/// </example>\n/// <example>\n///   <summary>Send an email with an attachment and high priority</summary>\n///   <prefix>PS&gt; </prefix>\n///   <code>Send-EmailMessage -From 'john.doe@example.com' -To 'recipient@example.com' -Subject 'Report' -Body 'See attached.' -Attachment 'C:\\Reports\\report.pdf' -Priority High -Server 'smtp.office365.com' -Credential (Get-Credential)</code>\n/// </example>\n/// <example>\n///   <summary>Send an email using SendGrid API</summary>\n///   <prefix>PS&gt; </prefix>\n///   <code>$cred = ConvertTo-SendGridCredential -SecretName 'sendgrid-api-key' -VaultName 'MailSecrets'\n/// Send-EmailMessage -From 'john.doe@example.com' -To 'recipient@example.com' -Subject 'SendGrid Test' -Body 'Hello from SendGrid' -SendGrid -Credential $cred</code>\n/// </example>\n/// <example>\n///   <summary>Send an email using Microsoft Graph API</summary>\n///   <prefix>PS&gt; </prefix>\n///   <code>$cred = ConvertTo-GraphCredential -ClientID 'CLIENT_ID' -SecretName 'graph-client-secret' -VaultName 'MailSecrets' -DirectoryID 'TENANT_ID'\n/// Send-EmailMessage -From @{ Name = 'John Doe'; Email = 'john.doe@example.com' } -To 'recipient@example.com' -Credential $cred -HTML '<b>Hello</b>' -Subject 'Graph API Email' -Graph</code>\n/// </example>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n[Cmdlet(VerbsCommunications.Send, \"EmailMessage\", DefaultParameterSetName = \"Compatibility\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\n[CmdletBinding()]\npublic sealed partial class CmdletSendEmailMessage : PSCmdlet {\n    private const long GraphAttachmentLimitBytes = 150_000_000;\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSendEmailPendingMessage.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Sends pending messages stored in a file-based repository.\n/// </summary>\n[Cmdlet(VerbsCommunications.Send, \"EmailPendingMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletSendEmailPendingMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// Allows tests to override the sender factory used for processing.\n    /// </summary>\n    public static Func<PendingMessageSenderFactory>? SenderFactoryProvider { get; set; }\n\n    /// <summary>Directory containing pending message log file.</summary>\n    [Parameter(Mandatory = true)]\n    [Alias(\"PendingPath\")]\n    public string? PendingMessagesPath { get; set; }\n\n    /// <summary>\n    /// Filters queued messages to the specified provider when supplied.\n    /// </summary>\n    [Parameter]\n    public EmailProvider Provider { get; set; }\n\n    /// <summary>\n    /// Identifiers of specific messages that should be retried immediately.\n    /// </summary>\n    [Parameter]\n    public string[]? MessageId { get; set; }\n\n    /// <summary>\n    /// Processes all messages regardless of their scheduled retry time.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter ProcessAll { get; set; }\n\n    /// <inheritdoc />\n    protected override async Task ProcessRecordAsync() {\n        if (!ShouldProcess(PendingMessagesPath!, \"Sending pending email messages\")) {\n            return;\n        }\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = PendingMessagesPath! };\n        var repository = new FilePendingMessageRepository(options);\n        var logger = new InternalLogger();\n        using var bridge = new InternalLoggerPowerShell(\n            logger,\n            writeVerboseAction: WriteVerbose,\n            writeWarningAction: WriteWarning,\n            writeDebugAction: WriteDebug,\n            writeErrorAction: WriteError,\n            writeProgressAction: WriteProgress,\n            writeInformationAction: WriteInformation);\n\n        var filteredIds = NormalizeMessageIds(MessageId);\n        if (filteredIds != null) {\n            foreach (var id in filteredIds) {\n                var record = await repository.GetByMessageIdAsync(id, CancelToken).ConfigureAwait(false);\n                if (record == null) {\n                    WriteWarning($\"Message with id '{id}' was not found in the pending repository.\");\n                }\n            }\n        }\n\n        var providerFilter = MyInvocation.BoundParameters.ContainsKey(nameof(Provider));\n        var forceProcessing = ProcessAll.IsPresent || filteredIds != null;\n\n        var repositoryView = new FilteredPendingMessageRepository(\n            repository,\n            filteredIds,\n            providerFilter ? Provider : (EmailProvider?)null,\n            forceProcessing,\n            () => DateTimeOffset.UtcNow);\n\n        var observer = new CmdletPendingMessageObserver(this);\n        var senderFactory = SenderFactoryProvider?.Invoke() ?? new PendingMessageSenderFactory();\n        var processor = new PendingMessageProcessor(repositoryView, senderFactory, logger: logger, observer: observer);\n        await processor.ProcessAsync(CancelToken).ConfigureAwait(false);\n    }\n\n    private static IReadOnlyCollection<string>? NormalizeMessageIds(string[]? ids) {\n        if (ids == null || ids.Length == 0) {\n            return null;\n        }\n\n        var normalized = ids\n            .Where(id => !string.IsNullOrWhiteSpace(id))\n            .Select(id => id.Trim())\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n\n        return normalized.Length == 0 ? null : normalized;\n    }\n\n    private sealed class CmdletPendingMessageObserver : IPendingMessageProcessorObserver {\n        private readonly CmdletSendEmailPendingMessage _cmdlet;\n\n        internal CmdletPendingMessageObserver(CmdletSendEmailPendingMessage cmdlet) {\n            _cmdlet = cmdlet;\n        }\n\n        public void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason) {\n            var message = reason switch {\n                PendingMessageSkipReason.LeaseNotAcquired => $\"Skipping message '{record.MessageId}' because another processor already claimed it.\",\n                _ => $\"Skipping message '{record.MessageId}' ({reason}).\"\n            };\n            _cmdlet.WriteVerbose(message);\n        }\n\n        public void MessageAttemptStarted(PendingMessageRecord record, int attempt) {\n            _cmdlet.WriteVerbose($\"Sending pending message '{record.MessageId}' (attempt {attempt}).\");\n        }\n\n        public void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration) {\n            _cmdlet.WriteVerbose($\"Message '{record.MessageId}' delivered in {duration.TotalMilliseconds:N0} ms.\");\n        }\n\n        public void MessageFailed(\n            PendingMessageRecord record,\n            int attempt,\n            Exception exception,\n            TimeSpan duration,\n            bool willRetry,\n            TimeSpan? retryDelay) {\n            var retryText = willRetry\n                ? retryDelay.HasValue\n                    ? $\"Retry scheduled in {retryDelay.Value.TotalSeconds:N0} seconds.\"\n                    : \"Retry scheduled.\"\n                : \"No further retries will be attempted.\";\n            _cmdlet.WriteWarning($\"Failed to send message '{record.MessageId}' on attempt {attempt}: {exception.Message} {retryText}\");\n        }\n\n        public void MessageDropped(\n            PendingMessageRecord record,\n            int attempt,\n            PendingMessageDropReason reason,\n            Exception? exception) {\n            var message = $\"Message '{record.MessageId}' removed from the queue ({reason}).\";\n            if (exception != null) {\n                message += $\" Error: {exception.Message}\";\n            }\n            _cmdlet.WriteWarning(message);\n        }\n    }\n\n    private sealed class FilteredPendingMessageRepository : IPendingMessageRepository {\n        private readonly IPendingMessageRepository _inner;\n        private readonly HashSet<string>? _messageIds;\n        private readonly EmailProvider? _provider;\n        private readonly bool _forceProcessing;\n        private readonly Func<DateTimeOffset> _clock;\n\n        internal FilteredPendingMessageRepository(\n            IPendingMessageRepository inner,\n            IReadOnlyCollection<string>? messageIds,\n            EmailProvider? provider,\n            bool forceProcessing,\n            Func<DateTimeOffset> clock) {\n            _inner = inner ?? throw new ArgumentNullException(nameof(inner));\n            _messageIds = messageIds != null\n                ? new HashSet<string>(messageIds, StringComparer.OrdinalIgnoreCase)\n                : null;\n            _provider = provider;\n            _forceProcessing = forceProcessing;\n            _clock = clock ?? throw new ArgumentNullException(nameof(clock));\n        }\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) =>\n            _inner.SaveAsync(record, cancellationToken);\n\n        public async Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var record = await _inner.GetByMessageIdAsync(messageId, cancellationToken).ConfigureAwait(false);\n            if (record == null) {\n                return null;\n            }\n\n            if (_messageIds != null && !ContainsMessageId(record.MessageId)) {\n                return null;\n            }\n\n            if (_provider.HasValue && record.Provider != _provider.Value) {\n                return null;\n            }\n\n            var effectiveDueTime = _forceProcessing ? DateTimeOffset.MaxValue : dueBeforeOrAt;\n            return await _inner.TryAcquireLeaseAsync(messageId, effectiveDueTime, leaseUntil, cancellationToken).ConfigureAwait(false);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            _inner.GetByMessageIdAsync(messageId, cancellationToken);\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync(\n            [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            var now = _clock();\n            await foreach (var record in _inner.GetAllAsync(cancellationToken).ConfigureAwait(false)) {\n                cancellationToken.ThrowIfCancellationRequested();\n                if (record == null) {\n                    continue;\n                }\n\n                if (_messageIds != null && !ContainsMessageId(record.MessageId)) {\n                    continue;\n                }\n\n                if (_provider.HasValue && record.Provider != _provider.Value) {\n                    continue;\n                }\n\n                if (_forceProcessing && record.NextAttemptAt > now) {\n                    var forcedRecord = record.Clone();\n                    forcedRecord.NextAttemptAt = now;\n                    yield return forcedRecord;\n                    continue;\n                }\n\n                yield return record;\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) =>\n            _inner.RemoveAsync(messageId, cancellationToken);\n\n        private bool ContainsMessageId(string? id) {\n            if (string.IsNullOrEmpty(id) || _messageIds == null) {\n                return false;\n            }\n            return _messageIds.Contains(id!);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSendGmailMessage.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing Mailozaurr;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Sends an email using the Gmail API.\n/// </summary>\n[Cmdlet(VerbsCommunications.Send, \"GmailMessage\", SupportsShouldProcess = true)]\n[OutputType(typeof(GmailMessage))]\npublic sealed class CmdletSendGmailMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// Gmail account used to send the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? GmailAccount { get; set; }\n\n    /// <summary>\n    /// OAuth credential used for authentication.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public PSCredential? Credential { get; set; }\n\n    /// <summary>\n    /// Address used in the From header.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public object? From { get; set; }\n\n    /// <summary>\n    /// Recipients of the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public object[]? To { get; set; }\n\n    /// <summary>\n    /// Subject line for the email message.\n    /// </summary>\n    [Parameter]\n    public string? Subject { get; set; }\n\n    /// <summary>\n    /// HTML body content of the message.\n    /// </summary>\n    [Parameter]\n    public string[]? HtmlBody { get; set; }\n\n    /// <summary>\n    /// Plain text body content of the message.\n    /// </summary>\n    [Parameter]\n    public string[]? TextBody { get; set; }\n\n    /// <summary>\n    /// Attachments to include with the message.\n    /// </summary>\n    [Parameter]\n    public object[]? Attachment { get; set; }\n\n    /// <summary>\n    /// Custom headers to include with the message.\n    /// </summary>\n    [Parameter]\n    public Hashtable? Headers { get; set; }\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var net = Credential!.GetNetworkCredential();\n        var oauth = new OAuthCredential {\n            UserName = net.UserName,\n            AccessToken = net.Password,\n            ExpiresOn = System.DateTimeOffset.MaxValue\n        };\n\n        var smtp = new Smtp {\n            From = From!,\n            To = To,\n            Subject = Subject ?? string.Empty,\n            HtmlBody = HtmlBody is null ? string.Empty : string.Join(System.Environment.NewLine, HtmlBody),\n            TextBody = TextBody is null ? string.Empty : string.Join(System.Environment.NewLine, TextBody),\n            Attachments = ConvertAttachments(Attachment)\n        };\n        if (Headers != null) {\n            smtp.Headers = Headers.Cast<DictionaryEntry>()\n                .ToDictionary(d => d.Key?.ToString() ?? string.Empty, d => d.Value?.ToString() ?? string.Empty);\n        }\n        try {\n            smtp.CreateMessage();\n            var client = new GmailApiClient(oauth);\n            if (ShouldProcess(GmailAccount!, \"Sending email message via Gmail API\")) {\n                var result = await client.SendAsync(GmailAccount!, smtp.Message, CancelToken);\n                WriteObject(result);\n            } else {\n                WriteObject(new SmtpResult(false, EmailAction.Send, smtp.SentTo, smtp.SentFrom, \"GmailApi\", 0, smtp.Stopwatch.Elapsed, string.Empty, \"Email not sent (WhatIf)\"));\n            }\n        } finally {\n            smtp.Dispose();\n        }\n    }\n    private static List<AttachmentDescriptor>? ConvertAttachments(object[]? attachments)\n    {\n        if (attachments == null)\n        {\n            return null;\n        }\n\n        var result = new List<AttachmentDescriptor>();\n        foreach (var entry in attachments)\n        {\n            if (entry == null)\n            {\n                continue;\n            }\n\n            switch (entry)\n            {\n                case AttachmentDescriptor descriptor:\n                    result.Add(descriptor);\n                    break;\n                case string path:\n                    result.Add(new FileAttachmentDescriptor(path));\n                    break;\n                case System.IO.FileInfo fileInfo:\n                    result.Add(new FileAttachmentDescriptor(fileInfo.FullName));\n                    break;\n                default:\n                    throw new ArgumentException($\"Unsupported attachment type: {entry.GetType().Name}\");\n            }\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetGraphEvent.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Updates an existing calendar event via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"GraphEvent\", SupportsShouldProcess = true)]\n[OutputType(typeof(GraphEvent))]\npublic sealed class CmdletSetGraphEvent : AsyncPSCmdlet {\n    /// <summary>\n    /// User principal name owning the event.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the event to update.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? EventId { get; set; }\n\n    /// <summary>\n    /// Updated event object.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNull]\n    public GraphEvent? Event { get; set; }\n\n    /// <summary>\n    /// Graph connection context.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Updates the specified event via Microsoft Graph.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null) {\n            WriteWarning(\"Set-GraphEvent - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(EventId!, \"Updating event\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        if (dryRun) {\n            await MicrosoftGraphUtils.UpdateEventAsync(cred, UserPrincipalName!, EventId!, Event!, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? last = null;\n        do {\n            try {\n                var result = await MicrosoftGraphUtils.UpdateEventAsync(cred, UserPrincipalName!, EventId!, Event!, dryRun: false);\n                WriteObject(result);\n                return;\n            } catch (Exception ex) {\n                last = ex;\n                WriteWarning($\"Set-GraphEvent - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    else WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (last != null) WriteError(new ErrorRecord(last, \"GraphError\", ErrorCategory.InvalidOperation, null));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetGraphInboxRule.cs",
    "content": "using System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Mailozaurr;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Updates an existing inbox rule via Microsoft Graph.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"GraphInboxRule\", SupportsShouldProcess = true)]\n[OutputType(typeof(GraphInboxRule))]\npublic sealed class CmdletSetGraphInboxRule : AsyncPSCmdlet\n{\n    /// <summary>\n    /// User principal name owning the rule.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the rule to update.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? RuleId { get; set; }\n\n    /// <summary>\n    /// Hashtable representing the rule properties.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public Hashtable? Rule { get; set; }\n\n    /// <summary>\n    /// Existing rule object used for update.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphInboxRule? RuleObject { get; set; }\n\n    /// <summary>\n    /// Graph connection context used for the update.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\", ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// When set, uses <c>Invoke-MgGraphRequest</c> for the update instead of the SDK.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Timeout in seconds for Graph operations.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Number of retry attempts when a request fails.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retry attempts in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override Task ProcessRecordAsync()\n    {\n        if (ParameterSetName == \"MgGraphRequest\")\n        {\n            ProcessMgGraph();\n            return Task.CompletedTask;\n        }\n\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null)\n        {\n            WriteWarning(\"Set-GraphInboxRule - Connection not provided and no default session available.\");\n            return Task.CompletedTask;\n        }\n\n        return ProcessGraphAsync(conn.Credential);\n    }\n\n    private async Task ProcessGraphAsync(GraphCredential cred)\n    {\n        var dryRun = !ShouldProcess(RuleId!, \"Updating inbox rule\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        int attempts = 0;\n        Exception? lastException = null;\n        var rulePayload = Rule ?? throw new PSArgumentNullException(nameof(Rule), \"Rule has to be provided or built.\");\n        var obj = RuleObject ?? JsonSerializer.Deserialize(JsonSerializer.Serialize(rulePayload, MailozaurrJsonContext.Default.Object), MailozaurrJsonContext.Default.GraphInboxRule);\n        if (obj is null) {\n            throw new PSArgumentException(\"Graph inbox rule definition cannot be null.\");\n        }\n        if (dryRun) {\n            await MicrosoftGraphUtils.UpdateRuleAsync(cred, UserPrincipalName!, RuleId!, obj, dryRun: true);\n            return;\n        }\n        do\n        {\n            try\n            {\n                var res = await MicrosoftGraphUtils.UpdateRuleAsync(cred, UserPrincipalName!, RuleId!, obj, dryRun: false);\n                WriteObject(res);\n                return;\n            }\n            catch (Exception ex)\n            {\n                lastException = ex;\n                WriteWarning($\"Set-GraphInboxRule - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount)\n                {\n                    if (ex is GraphApiException gex)\n                    {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    else\n                    {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) await Task.Delay(RetryDelayMilliseconds);\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null)\n        {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    private void ProcessMgGraph()\n    {\n        if (!ShouldProcess(RuleId!, \"Updating inbox rule\"))\n        {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/mailFolders/inbox/messageRules/{RuleId}\");\n        var rulePayload = Rule ?? throw new PSArgumentNullException(nameof(Rule), \"Rule has to be provided or built.\");\n        var bodyObj = RuleObject ?? JsonSerializer.Deserialize(JsonSerializer.Serialize(rulePayload, MailozaurrJsonContext.Default.Object), MailozaurrJsonContext.Default.GraphInboxRule);\n        if (bodyObj is null) {\n            throw new PSArgumentException(\"Graph inbox rule definition cannot be null.\");\n        }\n        if (bodyObj is null) {\n            throw new PSArgumentException(\"Graph inbox rule definition cannot be null.\");\n        }\n        var body = JsonSerializer.Serialize(bodyObj, MailozaurrJsonContext.Default.GraphInboxRule);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"PATCH\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        var results = ps.Invoke();\n        foreach (var res in results) WriteObject(res);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetGraphMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Management.Automation.Runspaces;\nusing Mailozaurr;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Updates properties of an existing Microsoft Graph message.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"GraphMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic class CmdletSetGraphMessage : AsyncPSCmdlet {\n    /// <summary>\n    /// UPN of the mailbox owner containing the message.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Identifier of the message to modify.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? MessageId { get; set; }\n\n    /// <summary>\n    /// Marks the message as read when set.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter Read { get; set; }\n\n    /// <summary>\n    /// Optional Graph connection used when updating the message.\n    /// </summary>\n    [Parameter(ParameterSetName = \"Graph\")]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n\n    /// <summary>\n    /// Indicates that the message should be updated using Invoke-MgGraphRequest.\n    /// </summary>\n    [Parameter(Mandatory = true, ParameterSetName = \"MgGraphRequest\")]\n    public SwitchParameter MgGraphRequest { get; set; }\n\n    /// <summary>\n    /// Request timeout in seconds.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; } = 100;\n\n    /// <summary>\n    /// Maximum number of concurrent Graph requests.\n    /// </summary>\n    [Parameter]\n    public int MaxConcurrentRequests { get; set; } = 5;\n\n    /// <summary>\n    /// Number of retry attempts on failure.\n    /// </summary>\n    [Parameter]\n    public int RetryCount { get; set; } = 0;\n\n    /// <summary>\n    /// Delay between retries in milliseconds.\n    /// </summary>\n    [Parameter]\n    public int RetryDelayMilliseconds { get; set; } = 0;\n\n    /// <summary>\n    /// Processes the cmdlet invocation.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        switch (ParameterSetName) {\n            case \"Graph\":\n                var conn = Connection ?? DefaultSessions.GraphSession;\n                if (conn == null) {\n                    WriteWarning(\"Set-GraphMessage - Connection not provided and no default session available.\");\n                    return Task.CompletedTask;\n                }\n                return ProcessGraphAsync(conn.Credential);\n            case \"MgGraphRequest\":\n                ProcessMgGraph();\n                return Task.CompletedTask;\n            default:\n                return Task.CompletedTask;\n        }\n    }\n\n    /// <summary>\n    /// Executes the update operation using the Graph SDK.\n    /// </summary>\n    /// <param name=\"cred\">Credential used to access Graph.</param>\n    private async Task ProcessGraphAsync(GraphCredential cred) {\n        var dryRun = !ShouldProcess(MessageId!, \"Updating message via Graph\");\n        MicrosoftGraphUtils.TimeoutSeconds = TimeoutSeconds;\n        MicrosoftGraphUtils.MaxConcurrentRequests = MaxConcurrentRequests;\n        if (dryRun) {\n            await MicrosoftGraphUtils.SetMailMessageAsync(cred, UserPrincipalName!, MessageId!, Read.IsPresent, dryRun: true);\n            return;\n        }\n        int attempts = 0;\n        Exception? lastException = null;\n        do {\n            try {\n                await MicrosoftGraphUtils.SetMailMessageAsync(cred, UserPrincipalName!, MessageId!, Read.IsPresent, dryRun: false);\n                return;\n            } catch (Exception ex) {\n                lastException = ex;\n                WriteWarning($\"Set-GraphMessage - {ex.Message}\");\n                if (!Helpers.IsTransient(ex) || attempts >= RetryCount) {\n                    if (ex is GraphApiException gex) {\n                        WriteError(new ErrorRecord(gex, \"GraphApiError\", ErrorCategory.InvalidOperation, null));\n                    } else {\n                        WriteError(new ErrorRecord(ex, \"GraphError\", ErrorCategory.InvalidOperation, null));\n                    }\n                    return;\n                }\n                if (RetryDelayMilliseconds > 0) {\n                    await Task.Delay(RetryDelayMilliseconds);\n                }\n            }\n            attempts++;\n        } while (attempts <= RetryCount);\n        if (lastException is not null) {\n            WriteError(new ErrorRecord(lastException, \"GraphError\", ErrorCategory.InvalidOperation, null));\n        }\n    }\n\n    /// <summary>\n    /// Executes the update operation using the <c>Invoke-MgGraphRequest</c> cmdlet.\n    /// </summary>\n    private void ProcessMgGraph() {\n        if (!ShouldProcess(MessageId!, \"Updating message via Graph\")) {\n            return;\n        }\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\n            GraphEndpoint.V1,\n            $\"/users/{UserPrincipalName}/messages/{MessageId}\");\n        var body = JsonSerializer.Serialize(new GraphMarkReadRequest { IsRead = Read.IsPresent }, MailozaurrJsonContext.Default.GraphMarkReadRequest);\n        var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        ps.AddCommand(\"Invoke-MgGraphRequest\")\n            .AddParameter(\"Method\", \"PATCH\")\n            .AddParameter(\"Uri\", uri)\n            .AddParameter(\"Body\", body)\n            .AddParameter(\"ContentType\", \"application/json\");\n        ps.Invoke();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetIMAPFolder.cs",
    "content": "using System.Management.Automation;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Sets the working IMAP folder for subsequent operations.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"IMAPFolder\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]\npublic sealed class CmdletSetIMAPFolder : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>Folder path to open.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public string? Path { get; set; }\n\n    /// <summary>Folder access mode.</summary>\n    [Parameter]\n    public FolderAccess FolderAccess { get; set; } = FolderAccess.ReadOnly;\n\n    /// <summary>\n    /// Executes the cmdlet logic asynchronously.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            if (!ShouldProcess(Path!, \"Setting IMAP folder\")) {\n                return Task.CompletedTask;\n            }\n            var folder = (ImapFolder)conn.Data.GetCachedFolder(Path!, FolderAccess);\n            conn.Messages = folder;\n            conn.Count = folder.Count;\n            conn.Recent = folder.Recent;\n            conn.Folder = folder;\n            conn.Folders[folder.FullName] = folder;\n            DefaultSessions.ImapSession = conn;\n            WriteObject(conn);\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Set-IMAPFolder - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetIMAPMessage.cs",
    "content": "using System.Management.Automation;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Updates flags on an IMAP message.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"IMAPMessage\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletSetIMAPMessage : AsyncPSCmdlet {\n    /// <summary>Active IMAP connection.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>UID of the message.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public uint Uid { get; set; }\n\n    /// <summary>Optional folder containing the message.</summary>\n    [Parameter(Position = 2)]\n    public string? Folder { get; set; }\n\n    /// <summary>Marks the message as read.</summary>\n    [Parameter]\n    public SwitchParameter Read { get; set; }\n\n    /// <summary>Marks the message as unread.</summary>\n    [Parameter]\n    public SwitchParameter Unread { get; set; }\n\n    /// <summary>\n    /// Processes the cmdlet, updating message flags as requested.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        if (Read.IsPresent && Unread.IsPresent) {\n            ThrowTerminatingError(new ErrorRecord(new PSArgumentException(\"Specify only -Read or -Unread.\"), \"InvalidFlags\", ErrorCategory.InvalidArgument, null));\n            return;\n        }\n        if (!(Read.IsPresent || Unread.IsPresent)) {\n            return;\n        }\n        var actionText = Read.IsPresent ? \"Marking IMAP message as read\" : \"Marking IMAP message as unread\";\n        var dryRun = !ShouldProcess(Uid.ToString(), actionText);\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn != null && conn.Data != null) {\n            var uid = new UniqueId(Uid);\n            if (Read) {\n                await MessageFlagSetter.SetFlagsAsync(conn.Data, uid, MessageFlags.Seen, true, dryRun, Folder ?? conn.Folder?.FullName, CancelToken);\n            } else if (Unread) {\n                await MessageFlagSetter.SetFlagsAsync(conn.Data, uid, MessageFlags.Seen, false, dryRun, Folder ?? conn.Folder?.FullName, CancelToken);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Set-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletSetPOP3Message.cs",
    "content": "using System.Management.Automation;\nusing MailKit.Net.Pop3;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Updates local flags on a POP3 message.\n/// </summary>\n[Cmdlet(VerbsCommon.Set, \"POP3Message\", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]\npublic sealed class CmdletSetPOP3Message : AsyncPSCmdlet {\n    /// <summary>Active POP3 connection.</summary>\n    [Parameter(Position = 0, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>Message index.</summary>\n    [Parameter(Mandatory = true, Position = 1)]\n    public int Index { get; set; }\n\n    /// <summary>Marks the message as read.</summary>\n    [Parameter]\n    public SwitchParameter Read { get; set; }\n\n    /// <summary>Marks the message as unread.</summary>\n    [Parameter]\n    public SwitchParameter Unread { get; set; }\n\n    /// <summary>\n    /// Processes the cmdlet, updating POP3 message flags.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (Read.IsPresent && Unread.IsPresent) {\n            ThrowTerminatingError(new ErrorRecord(new PSArgumentException(\"Specify only -Read or -Unread.\"), \"InvalidFlags\", ErrorCategory.InvalidArgument, null));\n            return Task.CompletedTask;\n        }\n        if (!(Read.IsPresent || Unread.IsPresent)) {\n            return Task.CompletedTask;\n        }\n        var actionText = Read.IsPresent ? \"Marking POP3 message as read\" : \"Marking POP3 message as unread\";\n        var dryRun = !ShouldProcess(Index.ToString(), actionText);\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn != null && conn.Data != null) {\n            if (Read) {\n                return MessageFlagSetter.SetReadAsync(conn.Data, Index, true, dryRun, CancelToken);\n            }\n            if (Unread) {\n                return MessageFlagSetter.SetReadAsync(conn.Data, Index, false, dryRun, CancelToken);\n            }\n        } else {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Set-POP3Message - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n        }\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletTestEmailAddress.cs",
    "content": "﻿namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Validates one or more email addresses for format and standards compliance.</para>\n/// <para type=\"description\">The <c>Test-EmailAddress</c> cmdlet checks if one or more email addresses are valid according to standard email address rules. Supports validation for international addresses and top-level domains. Returns validation results for each address.</para>\n/// <example>\n///   <summary>Check if an email address is valid</summary>\n///   <code>Test-EmailAddress -EmailAddress \"test@example.com\"</code>\n/// </example>\n/// <example>\n///   <summary>Check if an email address is valid using pipeline input</summary>\n///   <code>\"test@example.com\" | Test-EmailAddress</code>\n/// </example>\n/// <example>\n///   <summary>Check if an email address is a valid international email address</summary>\n///   <code>Test-EmailAddress -EmailAddress \"test@exámple.com\" -AllowInternational</code>\n/// </example>\n/// <example>\n///   <summary>Check if an email address is valid with a top level domain</summary>\n///   <code>Test-EmailAddress -EmailAddress \"test@email\" -AllowTopLevelDomains</code>\n/// </example>\n/// <remarks>\n/// Use this cmdlet to validate email addresses before sending, importing, or processing them in automation scenarios.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsDiagnostic.Test, \"EmailAddress\")]\npublic sealed class CmdletTestEmailAddress : AsyncPSCmdlet {\n\n    /// <summary>\n    /// <para type=\"description\">Specifies the email addresses to check. Accepts an array of strings. This parameter is mandatory.</para>\n    /// </summary>\n    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]\n    [ValidateNotNullOrEmpty]\n    public string[]? EmailAddress { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, the cmdlet will use the newer international email standards to validate the email addresses.</para>\n    /// </summary>\n    [Parameter(Mandatory = false, Position = 1)]\n    public SwitchParameter AllowInternational { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">If set, the cmdlet will allow top level domains in the email addresses (such as test@email).</para>\n    /// </summary>\n    [Parameter(Mandatory = false, Position = 2)]\n    public SwitchParameter AllowTopLevelDomains { get; set; }\n\n    private InternalLogger _logger = null!;\n    private InternalLoggerPowerShell? _listener;\n\n    /// <summary>\n    /// Initializes the logger for verbose, warning, debug, error, progress, and information messages.\n    /// </summary>\n    protected override Task BeginProcessingAsync() {\n        // Initialize the logger to be able to see verbose, warning, debug, error, progress, and information messages.\n        _logger = new InternalLogger(false);\n        _listener = new InternalLoggerPowerShell(_logger, this.WriteVerbose, this.WriteWarning, this.WriteDebug, this.WriteError, this.WriteProgress, this.WriteInformation);\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Processes each email address and checks if it is valid. Writes results to the output pipeline.\n    /// </summary>\n    protected override Task ProcessRecordAsync() {\n        if (EmailAddress is null) {\n            return Task.CompletedTask;\n        }\n        foreach (var email in EmailAddress) {\n            if (string.IsNullOrWhiteSpace(email)) {\n                continue;\n            }\n            _logger.WriteVerbose(\"Processing email: {0}\", email);\n            WriteObject(Validator.ValidateEmail(email, AllowInternational, AllowTopLevelDomains));\n        }\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Cleans up resources used by the cmdlet.\n    /// </summary>\n    protected override Task EndProcessingAsync() {\n        _listener?.Dispose();\n        _listener = null;\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletTestMimeMessageSignature.cs",
    "content": "using System.Management.Automation;\nusing System.Security.Cryptography.X509Certificates;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Verifies PGP or S/MIME signatures on a <see cref=\"MimeMessage\"/>.\n/// </summary>\n[Cmdlet(VerbsDiagnostic.Test, \"MimeMessageSignature\", DefaultParameterSetName = \"Auto\")]\n[OutputType(typeof(bool))]\npublic sealed class CmdletTestMimeMessageSignature : PSCmdlet {\n    /// <summary>Message to verify.</summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public MimeMessage? Message { get; set; }\n\n    /// <summary>Public key for PGP signature verification.</summary>\n    [Parameter(ParameterSetName = \"Pgp\")]\n    public string? PublicKeyPath { get; set; }\n\n    /// <summary>Certificates for S/MIME signature verification.</summary>\n    [Parameter(ParameterSetName = \"Smime\")]\n    public X509Certificate2[]? Certificate { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        if (Message == null) { WriteObject(false); return; }\n\n        var enc = MimeKitUtils.GetEncryption(Message);\n        bool result = enc switch {\n            EmailEncryption.PgpSigned => MimeKitUtils.VerifyPgpSignature(Message, PublicKeyPath!),\n            EmailEncryption.SmimeSigned => MimeKitUtils.VerifySmimeSignature(Message, Certificate!),\n            _ => false\n        };\n        WriteObject(result);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletTestSmtpConnection.cs",
    "content": "using System.Management.Automation;\nusing System.Threading.Tasks;\nusing MailKit.Security;\nusing MailKit.Net.Smtp;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Tests SMTP connectivity and reports server capabilities.</para>\n/// <para type=\"description\">The <c>Test-SmtpConnection</c> cmdlet connects to an\n/// SMTP server and returns information about supported features. It also checks\n/// if the connection remains open after a NOOP command which indicates support\n/// for persistent connections.</para>\n/// <example>\n///   <summary>Check capabilities of an SMTP server</summary>\n///   <code>Test-SmtpConnection -Server \"smtp.example.com\" -Port 25</code>\n/// </example>\n/// </summary>\n[Cmdlet(VerbsDiagnostic.Test, \"SmtpConnection\")]\npublic sealed class CmdletTestSmtpConnection : AsyncPSCmdlet {\n    /// <summary>\n    /// SMTP server hostname to test.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    public string? Server { get; set; }\n\n    /// <summary>\n    /// TCP port used for the SMTP connection.\n    /// </summary>\n    [Parameter]\n    public int Port { get; set; } = 587;\n\n    /// <summary>\n    /// Indicates whether to test SSL connectivity.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter UseSsl { get; set; }\n\n    /// <summary>\n    /// Tests the SMTP server connection and outputs capability information.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected override Task ProcessRecordAsync() {\n        var info = Smtp.TestConnection(Server!, Port, SecureSocketOptions.Auto, UseSsl.IsPresent);\n        WriteObject(info);\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletUnprotectMimeMessage.cs",
    "content": "using System.Management.Automation;\nusing System.Security.Cryptography.X509Certificates;\nusing MimeKit;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Decrypts an encrypted <see cref=\"MimeMessage\"/> using PGP or S/MIME.\n/// </summary>\n[Cmdlet(VerbsSecurity.Unprotect, \"MimeMessage\")]\n[OutputType(typeof(MimeMessage))]\npublic sealed class CmdletUnprotectMimeMessage : PSCmdlet {\n    /// <summary>Object containing the MIME message.</summary>\n    [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]\n    [Alias(\"Message\")]\n    public object? InputObject { get; set; }\n\n    /// <summary>Path to PGP private key.</summary>\n    [Parameter]\n    public string? PrivateKeyPath { get; set; }\n\n    /// <summary>Password for PGP private key.</summary>\n    [Parameter]\n    public string? PrivateKeyPassword { get; set; }\n\n    /// <summary>Certificate for S/MIME decryption.</summary>\n    [Parameter]\n    public X509Certificate2? Certificate { get; set; }\n\n    /// <inheritdoc />\n    protected override void ProcessRecord() {\n        var input = InputObject is PSObject psObject ? psObject.BaseObject : InputObject;\n\n        MimeMessage? message = input switch {\n            MimeMessage m => m,\n            ImapMessageInfo info => info.Raw.Message,\n            ImapEmailMessage imap => imap.Message,\n            Pop3MessageInfo pinfo => pinfo.Raw.Message,\n            Pop3EmailMessage pop => pop.Message,\n            GraphEmailMessage g => g.Message,\n            _ => null\n        };\n\n        if (message == null) {\n            WriteObject(InputObject);\n            return;\n        }\n\n        var enc = MimeKitUtils.GetEncryption(message);\n        switch (enc) {\n            case EmailEncryption.PgpEncrypted:\n                if (string.IsNullOrEmpty(PrivateKeyPath)) {\n                    ThrowTerminatingError(new ErrorRecord(new PSArgumentException(\"PrivateKeyPath is required\"), \"MissingPrivateKeyPath\", ErrorCategory.InvalidArgument, InputObject));\n                }\n                var decryptedPgp = MimeKitUtils.DecryptPgp(message, PrivateKeyPath!, PrivateKeyPassword ?? string.Empty);\n                WriteObject(decryptedPgp);\n                break;\n            case EmailEncryption.SmimeEncrypted:\n                if (Certificate == null) {\n                    ThrowTerminatingError(new ErrorRecord(new PSArgumentException(\"Certificate is required\"), \"MissingCertificate\", ErrorCategory.InvalidArgument, InputObject));\n                }\n                var decryptedSmime = MimeKitUtils.DecryptSmime(message, Certificate!);\n                WriteObject(decryptedSmime);\n                break;\n            default:\n                WriteObject(message);\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletWaitGraphMessage.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Waits for new Graph messages by polling Microsoft Graph.</para>\n/// <para type=\"description\">The <c>Wait-GraphMessage</c> cmdlet listens for new messages for the specified user principal name. Messages are written to the pipeline as they arrive.</para>\n/// <example>\n///   <summary>Listen for new messages</summary>\n///   <code>\n/// $listener = Wait-GraphMessage -Connection $graph -UserPrincipalName 'user@example.com'\n///   </code>\n/// </example>\n/// </summary>\n[Cmdlet(VerbsLifecycle.Wait, \"GraphMessage\")]\n[OutputType(typeof(object))]\npublic sealed class CmdletWaitGraphMessage : AsyncPSCmdlet, IDisposable {\n    /// <summary>\n    /// Graph connection information used when polling for messages.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public GraphConnectionInfo? Connection { get; set; }\n\n    /// <summary>\n    /// User principal name whose mailbox should be monitored.\n    /// </summary>\n    [Parameter(Mandatory = true)]\n    [ValidateNotNullOrEmpty]\n    public string? UserPrincipalName { get; set; }\n\n    /// <summary>\n    /// Optional action executed for each received message.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Action { get; set; }\n\n    /// <summary>\n    /// Script block that determines when to stop waiting for messages.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Until { get; set; }\n\n    /// <summary>\n    /// Stops waiting when a message matches the <c>Until</c> condition.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter StopOnMatch { get; set; }\n\n    /// <summary>\n    /// Optional timeout after which listening will stop automatically.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; }\n\n    private GraphMessageListener? _listener;\n    private CancellationTokenSource? _timeoutSource;\n    private CancellationTokenSource? _linkedSource;\n\n    /// <inheritdoc />\n    /// <summary>\n    /// Starts listening for new Graph messages until stopped or timed out.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Connection ?? DefaultSessions.GraphSession;\n        if (conn == null || conn.Credential == null) {\n            WriteWarning(\"Wait-GraphMessage - Connection not provided and no default session available.\");\n            return;\n        }\n\n        _listener = new GraphMessageListener(conn.Credential, UserPrincipalName!);\n        _listener.MessageArrived += OnMessageArrived;\n        await _listener.StartAsync(CancelToken);\n\n        CancellationToken token = CancelToken;\n        if (TimeoutSeconds > 0) {\n            _timeoutSource = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds));\n            _linkedSource = CancellationTokenSource.CreateLinkedTokenSource(CancelToken, _timeoutSource.Token);\n            token = _linkedSource.Token;\n        }\n\n        try {\n            await Task.Delay(-1, token);\n        } catch (TaskCanceledException) { }\n    }\n\n    private void OnMessageArrived(object? sender, System.Collections.Generic.Dictionary<string, object> message) {\n        var match = Until == null || LanguagePrimitives.IsTrue(Until.InvokeReturnAsIs(message));\n        if (match) {\n            WriteObject(message);\n            if (Action != null) {\n                try {\n                    Action.Invoke(message);\n                } catch (RuntimeException ex) {\n                    WriteError(ex.ErrorRecord);\n                }\n            }\n            if (StopOnMatch) {\n                StopProcessing();\n            }\n        }\n    }\n\n    /// <inheritdoc />\n    protected override Task EndProcessingAsync() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n        return Task.CompletedTask;\n    }\n\n    /// <inheritdoc />\n    public new void Dispose() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n    }\n\n    /// <inheritdoc />\n    protected override void StopProcessing() {\n        _listener?.Stop();\n        _timeoutSource?.Cancel();\n        _linkedSource?.Cancel();\n        base.StopProcessing();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletWaitIMAPMessage.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Search;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Waits for new IMAP messages using the IMAP IDLE command.</para>\n/// <para type=\"description\">The <c>Wait-IMAPMessage</c> cmdlet listens for new messages arriving in the specified folder and writes them to the pipeline as they are received.</para>\n/// <example>\n///   <summary>Listen for new messages in the inbox</summary>\n///   <code>\n/// $listener = Wait-IMAPMessage -Client $client\n///   </code>\n/// </example>\n/// <remarks>\n/// Use Ctrl+C to stop waiting for messages. The cmdlet relies on a connected <see cref=\"ImapConnectionInfo\"/> object from <c>Connect-IMAP</c>.\n/// </remarks>\n/// <seealso href=\"https://github.com/EvotecIT/Mailozaurr\">Mailozaurr Documentation</seealso>\n/// </summary>\n[Cmdlet(VerbsLifecycle.Wait, \"IMAPMessage\")]\n[OutputType(typeof(ImapEmailMessage))]\npublic sealed class CmdletWaitIMAPMessage : AsyncPSCmdlet, System.IDisposable {\n    /// <summary>\n    /// <para type=\"description\">The <see cref=\"ImapConnectionInfo\"/> representing the active IMAP session. Defaults to the last session created by <c>Connect-IMAP</c>.</para>\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public ImapConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// <para type=\"description\">Optional folder name to monitor. Defaults to Inbox.</para>\n    /// </summary>\n    [Parameter]\n    public string? Folder { get; set; }\n\n    /// <summary>\n    /// Additional MailKit search queries applied when listening for messages.\n    /// </summary>\n    [Parameter]\n    public SearchQuery[]? SearchQuery { get; set; }\n\n    /// <summary>\n    /// Optional action executed for each received message.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Action { get; set; }\n\n    /// <summary>\n    /// Script block that determines when to stop waiting for messages.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Until { get; set; }\n\n    /// <summary>\n    /// Stops listening when the <c>Until</c> condition is satisfied.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter StopOnMatch { get; set; }\n\n    /// <summary>\n    /// Optional timeout after which the cmdlet stops waiting.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; }\n\n    private ImapIdleListener? _listener;\n    private CancellationTokenSource? _timeoutSource;\n    private CancellationTokenSource? _linkedSource;\n\n    /// <inheritdoc />\n    /// <summary>\n    /// Begins listening for new IMAP messages using the IDLE command.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.ImapSession;\n        if (conn == null || conn.Data == null) {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Wait-IMAPMessage - IMAP client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n            return;\n        }\n\n        SearchQuery? query = null;\n        if (SearchQuery != null && SearchQuery.Length > 0) {\n            query = SearchQuery[0];\n            for (int i = 1; i < SearchQuery.Length; i++) {\n                query = query.And(SearchQuery[i]);\n            }\n        }\n\n        _listener = new ImapIdleListener(conn.Data, Folder, query);\n        _listener.MessageArrived += OnMessageArrived;\n        await _listener.StartAsync(CancelToken);\n\n        CancellationToken token = CancelToken;\n        if (TimeoutSeconds > 0) {\n            _timeoutSource = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds));\n            _linkedSource = CancellationTokenSource.CreateLinkedTokenSource(CancelToken, _timeoutSource.Token);\n            token = _linkedSource.Token;\n        }\n\n        try {\n            await Task.Delay(-1, token);\n        } catch (TaskCanceledException) { }\n    }\n\n    private void OnMessageArrived(object? sender, ImapEmailMessage message) {\n        var match = Until == null || LanguagePrimitives.IsTrue(Until.InvokeReturnAsIs(message));\n        if (match) {\n            WriteObject(message);\n            if (Action != null) {\n                try {\n                    Action.Invoke(message);\n                } catch (RuntimeException ex) {\n                    WriteError(ex.ErrorRecord);\n                }\n            }\n            if (StopOnMatch) {\n                StopProcessing();\n            }\n        }\n    }\n\n    /// <inheritdoc />\n    protected override Task EndProcessingAsync() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n        return Task.CompletedTask;\n    }\n\n    /// <inheritdoc />\n    public new void Dispose() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n    }\n\n    /// <inheritdoc />\n    protected override void StopProcessing() {\n        _listener?.Stop();\n        _timeoutSource?.Cancel();\n        _linkedSource?.Cancel();\n        base.StopProcessing();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletWaitPOP3Message.cs",
    "content": "using System;\nusing System.Management.Automation;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Waits for new POP3 messages by polling the server.</para>\n/// <para type=\"description\">The <c>Wait-POP3Message</c> cmdlet listens for new messages arriving in the connected POP3 mailbox. Messages are written to the pipeline as they are received.</para>\n/// </summary>\n[Cmdlet(VerbsLifecycle.Wait, \"POP3Message\")]\n[OutputType(typeof(Pop3EmailMessage))]\npublic sealed class CmdletWaitPOP3Message : AsyncPSCmdlet, IDisposable {\n    /// <summary>\n    /// Connection information for the POP3 session used for polling.\n    /// </summary>\n    [Parameter(ValueFromPipeline = true)]\n    [ValidateNotNull]\n    public PopConnectionInfo? Client { get; set; }\n\n    /// <summary>\n    /// Optional action invoked for each message that arrives.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Action { get; set; }\n\n    /// <summary>\n    /// Script block determining when to stop waiting for messages.\n    /// </summary>\n    [Parameter]\n    public ScriptBlock? Until { get; set; }\n\n    /// <summary>\n    /// Stops polling when the <c>Until</c> condition is satisfied.\n    /// </summary>\n    [Parameter]\n    public SwitchParameter StopOnMatch { get; set; }\n\n    /// <summary>\n    /// Optional timeout after which the cmdlet stops polling.\n    /// </summary>\n    [Parameter]\n    public int TimeoutSeconds { get; set; }\n\n    private Pop3PollListener? _listener;\n    private CancellationTokenSource? _timeoutSource;\n    private CancellationTokenSource? _linkedSource;\n\n    /// <inheritdoc />\n    /// <summary>\n    /// Begins polling the POP3 mailbox for new messages until stopped.\n    /// </summary>\n    protected override async Task ProcessRecordAsync() {\n        var conn = Client ?? DefaultSessions.Pop3Session;\n        if (conn == null || conn.Data == null) {\n            ThrowTerminatingError(new ErrorRecord(\n                new InvalidOperationException(\"Wait-POP3Message - POP3 client not provided or not connected.\"),\n                \"ClientNotConnected\",\n                ErrorCategory.InvalidOperation,\n                null));\n            return;\n        }\n\n        _listener = new Pop3PollListener(conn.Data);\n        _listener.MessageArrived += OnMessageArrived;\n        await _listener.StartAsync(CancelToken);\n\n        CancellationToken token = CancelToken;\n        if (TimeoutSeconds > 0) {\n            _timeoutSource = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds));\n            _linkedSource = CancellationTokenSource.CreateLinkedTokenSource(CancelToken, _timeoutSource.Token);\n            token = _linkedSource.Token;\n        }\n\n        try {\n            await Task.Delay(-1, token);\n        } catch (TaskCanceledException) { }\n    }\n\n    private void OnMessageArrived(object? sender, Pop3EmailMessage message) {\n        var match = Until == null || LanguagePrimitives.IsTrue(Until.InvokeReturnAsIs(message));\n        if (match) {\n            WriteObject(message);\n            if (Action != null) {\n                try {\n                    Action.Invoke(message);\n                } catch (RuntimeException ex) {\n                    WriteError(ex.ErrorRecord);\n                }\n            }\n            if (StopOnMatch) {\n                StopProcessing();\n            }\n        }\n    }\n\n    /// <inheritdoc />\n    protected override Task EndProcessingAsync() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n        return Task.CompletedTask;\n    }\n\n    /// <inheritdoc />\n    public new void Dispose() {\n        if (_listener != null) {\n            _listener.MessageArrived -= OnMessageArrived;\n            _listener.Dispose();\n        }\n        _timeoutSource?.Dispose();\n        _linkedSource?.Dispose();\n    }\n\n    /// <inheritdoc />\n    protected override void StopProcessing() {\n        _listener?.Stop();\n        _timeoutSource?.Cancel();\n        _linkedSource?.Cancel();\n        base.StopProcessing();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CmdletWatchSmtpConnectionPool.cs",
    "content": "using System;\nusing System.Management.Automation;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"synopsis\">Subscribes to SMTP connection pool updates.</para>\n/// <para type=\"description\">The <c>Watch-SmtpConnectionPool</c> cmdlet registers a handler\n/// that executes a provided script block whenever the SMTP connection pool\n/// changes. The handler is returned so it can be removed when no longer\n/// needed.</para>\n/// <example>\n///   <summary>Watch pool changes</summary>\n///   <code>Watch-SmtpConnectionPool -Action { param($s) $s.CurrentPoolSize }</code>\n/// </example>\n/// </summary>\n[Cmdlet(\"Watch\", \"SmtpConnectionPool\")]\n[OutputType(typeof(Action<int>))]\npublic sealed class CmdletWatchSmtpConnectionPool : PSCmdlet\n{\n    /// <summary>Script block executed for each snapshot.</summary>\n    [Parameter(Mandatory = true)]\n    public ScriptBlock Action { get; set; } = null!;\n\n    /// <summary>Registers the handler and writes it to the pipeline.</summary>\n    protected override void ProcessRecord()\n    {\n        Action<int> handler = _ => Action.Invoke(SmtpConnectionPool.GetSnapshot());\n        SmtpConnectionPool.PoolSizeChanged += handler;\n        Action.Invoke(SmtpConnectionPool.GetSnapshot());\n        WriteObject(handler);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Communication/AsyncPSCmdlet.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Management.Automation;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// An abstract base class for asynchronous PowerShell cmdlets.\n/// </summary>\npublic abstract class AsyncPSCmdlet : PSCmdlet, IDisposable {\n    /// <summary>\n    /// Defines the types of pipelines used in the cmdlet.\n    /// </summary>\n    private enum PipelineType {\n        Output,\n        OutputEnumerate,\n        Error,\n        Warning,\n        Verbose,\n        Debug,\n        Information,\n        Progress,\n        ShouldProcess,\n    }\n\n    /// <summary>\n    /// Cancels the processing of the cmdlet.\n    /// </summary>\n    private CancellationTokenSource _cancelSource = new();\n\n    private BlockingCollection<(object?, PipelineType)>? _currentOutPipe;\n    private BlockingCollection<object?>? _currentReplyPipe;\n\n    /// <summary>\n    /// Gets the cancellation token that is triggered when the cmdlet is stopped.\n    /// </summary>\n    protected internal CancellationToken CancelToken { get => _cancelSource.Token; }\n\n    /// <summary>\n    /// Begins processing the cmdlet asynchronously.\n    /// </summary>\n    protected override void BeginProcessing()\n        => RunBlockInAsync(BeginProcessingAsync);\n\n    /// <summary>\n    /// Override this method to implement asynchronous begin processing logic.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected virtual Task BeginProcessingAsync()\n        => Task.CompletedTask;\n\n    /// <summary>\n    /// Processes a record asynchronously.\n    /// </summary>\n    protected override void ProcessRecord()\n        => RunBlockInAsync(ProcessRecordAsync);\n\n    /// <summary>\n    /// Override this method to implement asynchronous record processing logic.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected virtual Task ProcessRecordAsync()\n        => Task.CompletedTask;\n\n    /// <summary>\n    /// Ends processing the cmdlet asynchronously.\n    /// </summary>\n    protected override void EndProcessing()\n        => RunBlockInAsync(EndProcessingAsync);\n\n    /// <summary>\n    /// Override this method to implement asynchronous end processing logic.\n    /// </summary>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    protected virtual Task EndProcessingAsync()\n        => Task.CompletedTask;\n\n    /// <summary>\n    /// Stops the processing of the cmdlet.\n    /// </summary>\n    protected override void StopProcessing()\n        => _cancelSource?.Cancel();\n\n    /// <summary>\n    /// Runs the specified task asynchronously and handles the output and reply pipelines.\n    /// </summary>\n    /// <param name=\"task\">The task to run asynchronously.</param>\n    private void RunBlockInAsync(Func<Task> task) {\n        using BlockingCollection<(object?, PipelineType)> outPipe = new();\n        using BlockingCollection<object?> replyPipe = new();\n        Task blockTask = Task.Run(async () => {\n            try {\n                _currentOutPipe = outPipe;\n                _currentReplyPipe = replyPipe;\n                await task();\n            } finally {\n                _currentOutPipe = null;\n                _currentReplyPipe = null;\n                outPipe.CompleteAdding();\n                replyPipe.CompleteAdding();\n            }\n        });\n\n        foreach ((object? data, PipelineType pipelineType) in outPipe.GetConsumingEnumerable()) {\n            switch (pipelineType) {\n                case PipelineType.Output:\n                    base.WriteObject(data);\n                    break;\n\n                case PipelineType.OutputEnumerate:\n                    base.WriteObject(data, true);\n                    break;\n\n                case PipelineType.Error:\n                    base.WriteError((ErrorRecord)data!);\n                    break;\n\n                case PipelineType.Warning:\n                    base.WriteWarning((string)data!);\n                    break;\n\n                case PipelineType.Verbose:\n                    base.WriteVerbose((string)data!);\n                    break;\n\n                case PipelineType.Debug:\n                    base.WriteDebug((string)data!);\n                    break;\n\n                case PipelineType.Information:\n                    base.WriteInformation((InformationRecord)data!);\n                    break;\n\n                case PipelineType.Progress:\n                    base.WriteProgress((ProgressRecord)data!);\n                    break;\n\n                case PipelineType.ShouldProcess:\n                    (string target, string action) = (ValueTuple<string, string>)data!;\n                    bool res = base.ShouldProcess(target, action);\n                    replyPipe.Add(res);\n                    break;\n            }\n        }\n\n        blockTask.GetAwaiter().GetResult();\n    }\n\n    /// <summary>\n    /// Determines whether the cmdlet should continue processing.\n    /// </summary>\n    /// <param name=\"target\">The target of the operation.</param>\n    /// <param name=\"action\">The action to be performed.</param>\n    /// <returns>True if the cmdlet should continue processing; otherwise, false.</returns>\n    public new bool ShouldProcess(string target, string action) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add(((target, action), PipelineType.ShouldProcess));\n        return (bool)_currentReplyPipe?.Take(CancelToken)!;\n    }\n\n    /// <summary>\n    /// Writes an object to the output pipeline.\n    /// </summary>\n    /// <param name=\"sendToPipeline\">The object to send to the pipeline.</param>\n    public new void WriteObject(object? sendToPipeline) => WriteObject(sendToPipeline, false);\n\n    /// <summary>\n    /// Writes an object to the output pipeline, optionally enumerating collections.\n    /// </summary>\n    /// <param name=\"sendToPipeline\">The object to send to the pipeline.</param>\n    /// <param name=\"enumerateCollection\">If true, enumerates the collection.</param>\n    public new void WriteObject(object? sendToPipeline, bool enumerateCollection) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add(\n            (sendToPipeline, enumerateCollection ? PipelineType.OutputEnumerate : PipelineType.Output));\n    }\n\n    /// <summary>\n    /// Writes an error record to the error pipeline.\n    /// </summary>\n    /// <param name=\"errorRecord\">The error record to write.</param>\n    public new void WriteError(ErrorRecord errorRecord) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((errorRecord, PipelineType.Error));\n    }\n\n    /// <summary>\n    /// Writes a warning message to the warning pipeline.\n    /// </summary>\n    /// <param name=\"message\">The warning message to write.</param>\n    public new void WriteWarning(string message) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((message, PipelineType.Warning));\n    }\n\n    /// <summary>\n    /// Writes a verbose message to the verbose pipeline.\n    /// </summary>\n    /// <param name=\"message\">The verbose message to write.</param>\n    public new void WriteVerbose(string message) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((message, PipelineType.Verbose));\n    }\n\n    /// <summary>\n    /// Writes a debug message to the debug pipeline.\n    /// </summary>\n    /// <param name=\"message\">The debug message to write.</param>\n    public new void WriteDebug(string message) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((message, PipelineType.Debug));\n    }\n\n    /// <summary>\n    /// Writes an information record to the information pipeline.\n    /// </summary>\n    /// <param name=\"informationRecord\">The information record to write.</param>\n    public new void WriteInformation(InformationRecord informationRecord) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((informationRecord, PipelineType.Information));\n    }\n\n    /// <summary>\n    /// Writes a progress record to the progress pipeline.\n    /// </summary>\n    /// <param name=\"progressRecord\">The progress record to write.</param>\n    public new void WriteProgress(ProgressRecord progressRecord) {\n        ThrowIfStopped();\n        _currentOutPipe?.Add((progressRecord, PipelineType.Progress));\n    }\n\n    /// <summary>\n    /// Throws a <see cref=\"PipelineStoppedException\"/> if the cmdlet has been stopped.\n    /// </summary>\n    internal void ThrowIfStopped() {\n        if (_cancelSource.IsCancellationRequested) {\n            throw new PipelineStoppedException();\n        }\n    }\n\n    /// <summary>\n    /// Disposes the resources used by the cmdlet.\n    /// </summary>\n    public void Dispose() {\n        _cancelSource?.Dispose();\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Communication/InternalLoggerPowerShell.cs",
    "content": "using System;\nusing System.Management.Automation;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Enables routing of <see cref=\"InternalLogger\"/> events to PowerShell streams.\n/// </summary>\npublic class InternalLoggerPowerShell : IDisposable {\n    private readonly InternalLogger _logger;\n    private readonly Action<string>? _writeVerboseAction;\n    private readonly Action<string>? _writeDebugAction;\n    private readonly Action<InformationRecord>? _writeInformationAction;\n    private readonly Action<string>? _writeWarningAction;\n    private readonly Action<ErrorRecord>? _writeErrorAction;\n    private readonly Action<ProgressRecord>? _writeProgressAction;\n\n    /// <summary>\n    /// Creates a new instance of the <see cref=\"InternalLoggerPowerShell\"/> class.\n    /// </summary>\n    /// <param name=\"logger\">Source logger that exposes events.</param>\n    /// <param name=\"writeVerboseAction\">Delegate used to write verbose messages.</param>\n    /// <param name=\"writeWarningAction\">Delegate used to write warning messages.</param>\n    /// <param name=\"writeDebugAction\">Delegate used to write debug messages.</param>\n    /// <param name=\"writeErrorAction\">Delegate used to write error records.</param>\n    /// <param name=\"writeProgressAction\">Delegate used to write progress records.</param>\n    /// <param name=\"writeInformationAction\">Delegate used to write information records.</param>\n    public InternalLoggerPowerShell(InternalLogger logger, Action<string>? writeVerboseAction = null, Action<string>? writeWarningAction = null, Action<string>? writeDebugAction = null, Action<ErrorRecord>? writeErrorAction = null, Action<ProgressRecord>? writeProgressAction = null, Action<InformationRecord>? writeInformationAction = null) {\n        _logger = logger;\n\n        if (writeVerboseAction != null) {\n            _writeVerboseAction = writeVerboseAction;\n            _logger.OnVerboseMessage += Logger_OnVerboseMessage;\n        }\n\n        if (writeWarningAction != null) {\n            _writeWarningAction = writeWarningAction;\n            _logger.OnWarningMessage += Logger_OnWarningMessage;\n        }\n\n        if (writeDebugAction != null) {\n            _writeDebugAction = writeDebugAction;\n            _logger.OnDebugMessage += Logger_OnDebugMessage;\n        }\n\n        if (writeErrorAction != null) {\n            _writeErrorAction = writeErrorAction;\n            _logger.OnErrorMessage += Logger_OnErrorMessage;\n        }\n\n        if (writeProgressAction != null) {\n            _writeProgressAction = writeProgressAction;\n            _logger.OnProgressMessage += Logger_OnProgressMessage;\n        }\n\n        if (writeInformationAction != null) {\n            _writeInformationAction = writeInformationAction;\n            _logger.OnInformationMessage += Logger_OnInformationMessage;\n        }\n    }\n\n    /// <summary>\n    /// Handles verbose messages from the logger.\n    /// </summary>\n    /// <param name=\"sender\">Event source.</param>\n    /// <param name=\"e\">Event data.</param>\n    private void Logger_OnVerboseMessage(object? sender, LogEventArgs e) {\n        if (e.Args != null && e.Args.Length > 0) {\n            WriteVerbose(e.Message, e.Args);\n        } else {\n            WriteVerbose(e.Message);\n        }\n    }\n\n    private void Logger_OnDebugMessage(object? sender, LogEventArgs e) {\n        WriteDebug(e.Message);\n    }\n\n    private void Logger_OnWarningMessage(object? sender, LogEventArgs e) {\n        WriteWarning(e.Message);\n    }\n\n    private void Logger_OnErrorMessage(object? sender, LogEventArgs e) {\n        ErrorRecord errorRecord = new ErrorRecord(new Exception(e.Message), \"1\", ErrorCategory.NotSpecified, null);\n        WriteError(errorRecord);\n    }\n\n    private int _activityIdCounter = 0;\n    private int _currentActivityId = 1;\n    private bool _isCurrentActivityCompleted = true;\n\n    private int GetNextActivityId() {\n        return ++_activityIdCounter;\n    }\n\n    private void Logger_OnProgressMessage(object? sender, LogEventArgs e) {\n        if (_isCurrentActivityCompleted) {\n            _currentActivityId = GetNextActivityId();\n            _isCurrentActivityCompleted = false;\n        }\n        var progressMessage = e.ProgressCurrentOperation ?? \"Processing...: \";\n        var progressRecord = new ProgressRecord(_currentActivityId, e.ProgressActivity, progressMessage);\n        if (e.ProgressPercentage.HasValue) {\n            if (e.ProgressPercentage.Value >= 0 && e.ProgressPercentage.Value <= 100) {\n                progressRecord.PercentComplete = e.ProgressPercentage.Value;\n            } else {\n                progressRecord.PercentComplete = 100;\n            }\n        } else {\n            progressRecord.PercentComplete = 50;\n        }\n        if (progressRecord.PercentComplete == 100) {\n            progressRecord.RecordType = ProgressRecordType.Completed;\n            _isCurrentActivityCompleted = true;\n        }\n        WriteProgress(progressRecord);\n    }\n\n    private void Logger_OnInformationMessage(object? sender, LogEventArgs e) {\n        WriteInformation(e.Message);\n    }\n\n    private void WriteVerbose(string message) {\n        _writeVerboseAction?.Invoke(message);\n    }\n\n    /// <summary>\n    /// Writes a formatted verbose message.\n    /// </summary>\n    /// <param name=\"message\">Message template.</param>\n    /// <param name=\"eArgs\">Arguments for the template.</param>\n    private void WriteVerbose(string message, object[] eArgs) {\n        var fullMessage = string.Format(message, eArgs);\n        _writeVerboseAction?.Invoke(fullMessage);\n    }\n\n    private void WriteDebug(string message) {\n        _writeDebugAction?.Invoke(message);\n    }\n\n    private void WriteInformation(string message) {\n        InformationRecord informationRecord = new InformationRecord(message, \"Mailozaurr\");\n        _writeInformationAction?.Invoke(informationRecord);\n    }\n\n    private void WriteWarning(string message) {\n        _writeWarningAction?.Invoke(message);\n    }\n\n    private void WriteError(ErrorRecord errorRecord) {\n        _writeErrorAction?.Invoke(errorRecord);\n    }\n\n    private void WriteProgress(ProgressRecord progressRecord) {\n        _writeProgressAction?.Invoke(progressRecord);\n    }\n\n    /// <inheritdoc />\n    public void Dispose() {\n        if (_writeVerboseAction != null) {\n            _logger.OnVerboseMessage -= Logger_OnVerboseMessage;\n        }\n        if (_writeWarningAction != null) {\n            _logger.OnWarningMessage -= Logger_OnWarningMessage;\n        }\n        if (_writeDebugAction != null) {\n            _logger.OnDebugMessage -= Logger_OnDebugMessage;\n        }\n        if (_writeErrorAction != null) {\n            _logger.OnErrorMessage -= Logger_OnErrorMessage;\n        }\n        if (_writeProgressAction != null) {\n            _logger.OnProgressMessage -= Logger_OnProgressMessage;\n        }\n        if (_writeInformationAction != null) {\n            _logger.OnInformationMessage -= Logger_OnInformationMessage;\n        }\n        GC.SuppressFinalize(this);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Communication/LogEmitter.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// LogEmitter is a static class that emits logs to the PowerShell cmdlet.\n/// It's used as a way to decouple the logging from the cmdlet.\n/// - `LoggingMessages.Logger.WriteXXX` static method should be used for AsyncPSCmdlet.\n/// - `LogCollector.LogWarning($\"Send-EmailMessage - Error during sending using SendGrid: {ex.Message}\")` should be used for native PSCmdlet.\n/// </summary>\npublic static class LogEmitter {\n    /// <summary>\n    /// Emits logs to the PowerShell cmdlet.\n    /// </summary>\n    /// <param name=\"collector\">The log collector.</param>\n    /// <param name=\"cmdlet\">The PowerShell cmdlet.</param>\n    public static void EmitLogs(LogCollector collector, PSCmdlet cmdlet) {\n        while (collector.Logs.TryDequeue(out var log)) {\n            switch (log.Type) {\n                case LogType.Warning:\n                    cmdlet.WriteWarning(log.Message);\n                    break;\n                case LogType.Error:\n                    cmdlet.WriteError(new ErrorRecord(new Exception(log.Message), \"SendGridError\", ErrorCategory.NotSpecified, null));\n                    break;\n                case LogType.Verbose:\n                    cmdlet.WriteVerbose(log.Message);\n                    break;\n                case LogType.Information:\n                    cmdlet.WriteInformation(log.Message, new string[0]);\n                    break;\n            }\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/CredentialHelpers.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Security;\nusing System.Net;\nusing System.Management.Automation;\nusing System.Collections.ObjectModel;\n\n/// <summary>\n/// Helper class for converting strings to SecureString.\n/// </summary>\npublic static class CredentialHelpers {\n    /// <summary>\n    /// Converts a string to a SecureString.\n    /// </summary>\n    /// <param name=\"s\"></param>\n    /// <returns></returns>\n    public static SecureString ToSecureString(string? s) {\n        if (string.IsNullOrWhiteSpace(s))\n            return new SecureString();\n        // Try to convert from encrypted string, fallback to plain text\n        try {\n            return new NetworkCredential(\"\", s).SecurePassword;\n        } catch (ArgumentException ex) {\n            LoggingMessages.Logger?.WriteWarning($\"Failed to convert to SecureString: {ex.Message}\");\n            var ss = new SecureString();\n            foreach (char c in s!) ss.AppendChar(c);\n            ss.MakeReadOnly();\n            return ss;\n        }\n    }\n\n    /// <summary>\n    /// Converts a SecureString to plain text.\n    /// </summary>\n    /// <param name=\"secureString\"></param>\n    /// <returns></returns>\n    public static string ToPlainText(SecureString? secureString) {\n        if (secureString == null || secureString.Length == 0) {\n            return string.Empty;\n        }\n\n        return new NetworkCredential(string.Empty, secureString).Password;\n    }\n\n    /// <summary>\n    /// Resolves a secret from the current PowerShell runspace using Get-Secret.\n    /// </summary>\n    /// <param name=\"secretName\"></param>\n    /// <param name=\"vaultName\"></param>\n    /// <returns></returns>\n    public static SecureString ResolveSecretFromVault(string secretName, string? vaultName = null) {\n        if (string.IsNullOrWhiteSpace(secretName)) {\n            throw new PSArgumentException(\"Secret name is required.\", nameof(secretName));\n        }\n\n        using var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);\n        powerShell.AddCommand(\"Get-Secret\").AddParameter(\"Name\", secretName);\n        if (!string.IsNullOrWhiteSpace(vaultName)) {\n            powerShell.AddParameter(\"Vault\", vaultName);\n        }\n\n        Collection<PSObject> results;\n        try {\n            results = powerShell.Invoke();\n        } catch (CommandNotFoundException ex) {\n            throw new PSInvalidOperationException(\n                \"Get-Secret is not available in the current PowerShell session. Install/import Microsoft.PowerShell.SecretManagement or provide the secret directly.\", ex);\n        }\n\n        if (powerShell.HadErrors) {\n            var firstError = powerShell.Streams.Error.FirstOrDefault();\n            throw new PSInvalidOperationException(\n                firstError?.Exception?.Message ?? $\"Secret '{secretName}' could not be resolved from the current PowerShell session.\",\n                firstError?.Exception);\n        }\n\n        var value = results.FirstOrDefault()?.BaseObject;\n        if (value == null) {\n            throw new PSInvalidOperationException($\"Secret '{secretName}' was not found.\");\n        }\n\n        return value switch {\n            SecureString secureString => secureString,\n            string text => ToSecureString(text),\n            PSCredential credential => credential.Password,\n            byte[] bytes => ToSecureString(System.Text.Encoding.UTF8.GetString(bytes)),\n            _ => ToSecureString(value.ToString())\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/ConnectionInfoBase.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Base class for connection information.\n/// </summary>\npublic class ConnectionInfoBase {\n    /// <summary>Indicates whether authentication succeeded.</summary>\n    public bool IsConnected { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/DefaultSessions.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\nusing System.Threading;\n\n/// <summary>\n/// Provides access to the last successful connection objects used by the module.\n/// These are automatically updated by Connect-EmailGraph, Connect-IMAP and\n/// Connect-POP3 and consumed by other cmdlets when a connection is not\n/// explicitly provided.\n/// </summary>\npublic static class DefaultSessions {\n    private static readonly AsyncLocal<GraphConnectionInfo?> _graphSession = new();\n    private static readonly AsyncLocal<ImapConnectionInfo?> _imapSession = new();\n    private static readonly AsyncLocal<PopConnectionInfo?> _pop3Session = new();\n\n    /// <summary>Last Microsoft Graph connection.</summary>\n    public static GraphConnectionInfo? GraphSession {\n        get => _graphSession.Value;\n        set => _graphSession.Value = value;\n    }\n\n    /// <summary>Last IMAP connection.</summary>\n    public static ImapConnectionInfo? ImapSession {\n        get => _imapSession.Value;\n        set => _imapSession.Value = value;\n    }\n\n    /// <summary>Last POP3 connection.</summary>\n    public static PopConnectionInfo? Pop3Session {\n        get => _pop3Session.Value;\n        set => _pop3Session.Value = value;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/EmailProtocol.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// <para type=\"description\">Specifies the protocol used for mailbox operations.</para>\n/// </summary>\npublic enum EmailProtocol\n{\n    /// <summary>\n    /// <para type=\"description\">Use the IMAP protocol.</para>\n    /// </summary>\n    Imap,\n\n    /// <summary>\n    /// <para type=\"description\">Use the POP3 protocol.</para>\n    /// </summary>\n    Pop3,\n\n    /// <summary>\n    /// <para type=\"description\">Use Microsoft Graph.</para>\n    /// </summary>\n    Graph,\n\n    /// <summary>\n    /// <para type=\"description\">Use the Gmail REST API.</para>\n    /// </summary>\n    GmailApi\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/EmlConversionResult.cs",
    "content": "﻿namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Eml conversion result\n/// </summary>\npublic class EmlConversionResult {\n    /// <summary>\n    /// Eml file path to file that was converted\n    /// </summary>\n    public string EmlFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Msg file path to file that was created\n    /// </summary>\n    public string MsgFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Status of the conversion\n    /// </summary>\n    public bool Status { get; set; }\n    /// <summary>\n    /// Error message if conversion failed\n    /// </summary>\n    public string? Error { get; set; }\n}"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/GraphConnectionInfo.cs",
    "content": "using Mailozaurr;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Represents an authenticated Microsoft Graph connection.\n/// </summary>\npublic class GraphConnectionInfo : ConnectionInfoBase {\n    /// <summary>Graph credentials.</summary>\n    public GraphCredential Credential { get; set; } = null!;\n\n    /// <summary>\n    /// OAuth credential obtained using device code or on-behalf-of flow.\n    /// </summary>\n    public OAuthCredential? OAuthCredential { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/ImapConnectionInfo.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing MailKit;\nusing MailKit.Net.Imap;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Represents the result of a successful IMAP connection, including the client and connection details.\n/// </summary>\npublic class ImapConnectionInfo : ConnectionInfoBase {\n    /// <summary>IMAP server URI.</summary>\n    public string Uri { get; set; } = string.Empty;\n    /// <summary>Authentication mechanisms supported by the server.</summary>\n    public ISet<string>? AuthenticationMechanisms { get; set; }\n    /// <summary>Server capabilities.</summary>\n    public ImapCapabilities Capabilities { get; set; }\n    /// <summary>The underlying stream.</summary>\n    public Stream? Stream { get; set; }\n    /// <summary>Current state of the connection.</summary>\n    public object? State { get; set; }\n    /// <summary>APOP token, if any.</summary>\n    public string? ApopToken { get; set; }\n    /// <summary>Expire policy.</summary>\n    public TimeSpan? ExpirePolicy { get; set; }\n    /// <summary>Server implementation info.</summary>\n    public ImapImplementation? Implementation { get; set; }\n    /// <summary>Login delay, if any.</summary>\n    public TimeSpan? LoginDelay { get; set; }\n    /// <summary>Whether the client is authenticated.</summary>\n    public bool IsAuthenticated { get; set; }\n    /// <summary>Whether the connection is secure.</summary>\n    public bool IsSecure { get; set; }\n    /// <summary>The actual MailKit ImapClient instance.</summary>\n    public ImapClient? Data { get; set; }\n    /// <summary>Message count.</summary>\n    public int Count { get; set; }\n    /// <summary>Messages collection (if available).</summary>\n    public ImapFolder? Messages { get; set; }\n    /// <summary>Recent message count.</summary>\n    public int Recent { get; set; }\n    /// <summary>\n    /// IMAP folder.\n    /// </summary>\n    public ImapFolder? Folder { get; set; }\n    /// <summary>\n    /// Cached folders for this connection.\n    /// </summary>\n    public Dictionary<string, ImapFolder> Folders { get; } = new();\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/MsgConversionResult.cs",
    "content": "namespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Msg conversion result\n/// </summary>\npublic class MsgConversionResult {\n    /// <summary>\n    /// Msg file path to file that was converted\n    /// </summary>\n    public string MsgFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Eml file path to file that was created\n    /// </summary>\n    public string EmlFile { get; set; } = string.Empty;\n    /// <summary>\n    /// Status of the conversion\n    /// </summary>\n    public bool Status { get; set; }\n    /// <summary>\n    /// Error message if conversion failed\n    /// </summary>\n    public string? Error { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Definitions/PopConnectionInfo.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing MailKit;\nusing MailKit.Net.Pop3;\n\nnamespace Mailozaurr.PowerShell;\n\n/// <summary>\n/// Represents the result of a successful POP3 connection, including the client and connection details.\n/// </summary>\npublic class PopConnectionInfo : ConnectionInfoBase {\n    /// <summary>POP3 server URI.</summary>\n    public string Uri { get; set; } = string.Empty;\n    /// <summary>Authentication mechanisms supported by the server.</summary>\n    public ISet<string>? AuthenticationMechanisms { get; set; }\n    /// <summary>Server capabilities.</summary>\n    public Pop3Capabilities Capabilities { get; set; }\n    /// <summary>The underlying stream.</summary>\n    public Stream? Stream { get; set; }\n    /// <summary>Current state of the connection.</summary>\n    public object? State { get; set; }\n    /// <summary>APOP token, if any.</summary>\n    public string? ApopToken { get; set; }\n    /// <summary>Expire policy.</summary>\n    public TimeSpan? ExpirePolicy { get; set; }\n    /// <summary>Server implementation info.</summary>\n    public object? Implementation { get; set; }\n    /// <summary>Login delay, if any.</summary>\n    public TimeSpan? LoginDelay { get; set; }\n    /// <summary>Whether the client is authenticated.</summary>\n    public bool IsAuthenticated { get; set; }\n    /// <summary>Whether the connection is secure.</summary>\n    public bool IsSecure { get; set; }\n    /// <summary>The actual MailKit Pop3Client instance.</summary>\n    public Pop3Client? Data { get; set; }\n    /// <summary>Message count.</summary>\n    public int Count { get; set; }\n    /// <summary>Messages collection (if available).</summary>\n    public object? Messages { get; set; }\n    /// <summary>Recent message count.</summary>\n    public int Recent { get; set; }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Mailozaurr.PowerShell.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <Company>Evotec</Company>\n        <Authors>Przemyslaw Klys</Authors>\n        <VersionPrefix>2.0.4</VersionPrefix>\n        <TargetFrameworks>net472;netstandard2.0;net8.0</TargetFrameworks>\n        <AssemblyName>Mailozaurr.PowerShell</AssemblyName>\n        <Nullable>enable</Nullable>\n        <Copyright>(c) 2011 - 2024 Przemyslaw Klys @ Evotec. All rights reserved.</Copyright>\n        <LangVersion>latest</LangVersion>\n        <PackageIcon>Mailozaurr.png</PackageIcon>\n        <PackageReadmeFile>README.MD</PackageReadmeFile>\n        <RepositoryUrl>https://github.com/EvotecIT/Mailozaurr</RepositoryUrl>\n        <PackageTags>\n            Windows;MacOS;Linux;Mail;Email;MX;SPF;DMARC;DKIM;GraphApi;SendGrid;Graph;IMAP;POP3</PackageTags>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Mailozaurr\\Mailozaurr.csproj\" />\n        <ProjectReference Include=\"..\\Mailozaurr.Msg\\Mailozaurr.Msg.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"PowerShellStandard.Library\" Version=\"5.1.1\" PrivateAssets=\"all\" />\n    </ItemGroup>\n\n    <!-- Make sure the output DLL's from library are included in the output -->\n    <PropertyGroup>\n        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    </PropertyGroup>\n\n    <!-- We need to remove PowerShell conflicting libraries as it will break output -->\n    <Target Name=\"RemoveFilesAfterBuild\" AfterTargets=\"Build\">\n        <Delete Files=\"$(OutDir)System.Management.Automation.dll\" />\n        <Delete Files=\"$(OutDir)System.Management.dll\" />\n    </Target>\n\n    <!-- Do not ship framework assemblies for PowerShell 7+ (net8.0) to avoid version conflicts with\n    the host -->\n    <Target Name=\"RemoveFilesAfterPublish\" AfterTargets=\"Publish\"\n        Condition=\" '$(TargetFramework)' == 'net8.0' \">\n        <ItemGroup>\n            <FilesToDelete Include=\"$(PublishDir)System.Formats.Asn1.dll\" />\n            <FilesToDelete Include=\"$(PublishDir)System.Security.Cryptography.Pkcs.dll\" />\n            <FilesToDelete Include=\"$(PublishDir)System.Security.Cryptography.ProtectedData.dll\" />\n            <FilesToDelete Include=\"$(PublishDir)System.Management.Automation.dll\" />\n            <FilesToDelete Include=\"$(PublishDir)System.Management.dll\" />\n        </ItemGroup>\n        <Delete Files=\"@(FilesToDelete)\" TreatErrorsAsWarnings=\"true\" />\n    </Target>\n\n    <ItemGroup>\n        <Using Include=\"Mailozaurr\" />\n        <Using Include=\"System.Collections\" />\n        <Using Include=\"System.Management.Automation\" />\n        <Using Include=\"System.Threading.Tasks\" />\n        <Using Include=\"System.Collections.Concurrent\" />\n        <Using Include=\"System.Threading\" />\n        <Using Include=\"System\" />\n        <Using Include=\"System.Collections.Generic\" />\n        <Using Include=\"System.Linq\" />\n        <Using Include=\"System.Text\" />\n        <Using Include=\"System.IO\" />\n        <Using Include=\"System.Net\" />\n        <Using Include=\"System.Diagnostics\" />\n    </ItemGroup>\n\n    <PropertyGroup>\n        <!-- This is needed for XmlDoc2CmdletDoc to generate a PowerShell documentation file. -->\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <None Include=\"..\\..\\Mailozaurr.png\" Pack=\"true\" PackagePath=\"\\\" />\n        <None Include=\"..\\..\\README.MD\" Pack=\"true\" PackagePath=\"\\\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <!-- This is needed for XmlDoc2CmdletDoc to generate a PowerShell documentation file. -->\n        <PackageReference Include=\"MatejKafka.XmlDoc2CmdletDoc\" Version=\"0.7.0\" GeneratePathProperty=\"true\">\n            <PrivateAssets>all</PrivateAssets>\n            <ExcludeAssets>build;buildTransitive</ExcludeAssets>\n        </PackageReference>\n    </ItemGroup>\n\n    <!-- XmlDoc2CmdletDoc help is only needed for the hostable PowerShell builds.\n    Skipping netstandard2.0 avoids duplicate standalone-build help generation and file locking. -->\n    <Target Name=\"XmlDoc2CmdletDoc\"\n        BeforeTargets=\"PostBuildEvent\"\n        Inputs=\"$(TargetPath)\"\n        Outputs=\"$(TargetPath)-Help.xml\"\n        Condition=\"'$(IsCrossTargetingBuild)' != 'true' and '$(TargetFramework)' != 'netstandard2.0'\">\n        <Exec Command='dotnet --roll-forward Major \"$(PkgMatejKafka_XmlDoc2CmdletDoc)\\tools\\MatejKafka.XmlDoc2CmdletDoc.dll\" $(XmlDoc2CmdletDocArguments) \"$(TargetPath)\"'\n            IgnoreExitCode=\"true\" />\n    </Target>\n\n    <!-- Copy help documentation to publish output after publish -->\n    <Target Name=\"CopyHelpDocumentationToPublishOutput\" AfterTargets=\"Publish\">\n        <Copy SourceFiles=\"$(OutputPath)$(AssemblyName).dll-Help.xml\"\n            DestinationFiles=\"$(PublishDir)$(AssemblyName).dll-Help.xml\"\n            Condition=\"Exists('$(OutputPath)$(AssemblyName).dll-Help.xml')\" />\n    </Target>\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/Mailozaurr.PowerShell.csproj.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=communication/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=definitions/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "Sources/Mailozaurr.PowerShell/OnImportAndRemove.cs",
    "content": "using System;\nusing System.IO;\nusing System.Reflection;\n#if NET5_0_OR_GREATER\nusing System.Runtime.Loader;\n#endif\n\n/// <summary>\n/// OnModuleImportAndRemove is a class that implements the IModuleAssemblyInitializer and IModuleAssemblyCleanup interfaces.\n/// This class is used to handle the assembly resolve event when the module is imported and removed.\n/// </summary>\npublic class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup {\n    /// <summary>\n    /// OnImport is called when the module is imported.\n    /// </summary>\n    public void OnImport() {\n        if (IsNetFramework()) {\n            AppDomain.CurrentDomain.AssemblyResolve += MyResolveEventHandler;\n        }\n#if NET5_0_OR_GREATER\n        else {\n            AssemblyLoadContext.Default.Resolving += ResolveAlc;\n        }\n#endif\n    }\n\n    /// <summary>\n    /// OnRemove is called when the module is removed.\n    /// </summary>\n    /// <param name=\"module\"></param>\n    public void OnRemove(PSModuleInfo module) {\n        if (IsNetFramework()) {\n            AppDomain.CurrentDomain.AssemblyResolve -= MyResolveEventHandler;\n        }\n#if NET5_0_OR_GREATER\n        else {\n            AssemblyLoadContext.Default.Resolving -= ResolveAlc;\n        }\n#endif\n    }\n\n    /// <summary>\n    /// MyResolveEventHandler is a method that handles the AssemblyResolve event.\n    /// </summary>\n    /// <param name=\"sender\"></param>\n    /// <param name=\"args\"></param>\n    /// <returns></returns>\n    private static Assembly? MyResolveEventHandler(object? sender, ResolveEventArgs args) {\n        var libDirectory = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location);\n        var directoriesToSearch = new List<string>();\n\n        if (libDirectory is not null) {\n            directoriesToSearch.Add(libDirectory);\n\n            if (Directory.Exists(libDirectory)) {\n                directoriesToSearch.AddRange(Directory.GetDirectories(libDirectory, \"*\", SearchOption.AllDirectories));\n            }\n        }\n\n        var requestedAssemblyName = new AssemblyName(args.Name).Name + \".dll\";\n\n        foreach (var directory in directoriesToSearch) {\n            var assemblyPath = Path.Combine(directory, requestedAssemblyName);\n\n            if (File.Exists(assemblyPath)) {\n                try {\n                    return Assembly.LoadFrom(assemblyPath);\n                } catch (BadImageFormatException ex) {\n                    Console.WriteLine($\"Failed to load assembly from {assemblyPath}: {ex.Message}\");\n                }\n            }\n        }\n\n        return null;\n    }\n\n#if NET5_0_OR_GREATER\n    private sealed class LoadContext : AssemblyLoadContext {\n        private readonly string _assemblyDir;\n\n        public LoadContext(string assemblyDir)\n            : base(name: \"Mailozaurr\", isCollectible: false) {\n            _assemblyDir = assemblyDir;\n        }\n\n        protected override Assembly? Load(AssemblyName assemblyName) {\n            string asmPath = Path.Combine(_assemblyDir, $\"{assemblyName.Name}.dll\");\n            return File.Exists(asmPath) ? LoadFromAssemblyPath(asmPath) : null;\n        }\n    }\n\n    private static readonly string _assemblyDir =\n        Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location)!;\n\n    private static readonly LoadContext _alc = new LoadContext(_assemblyDir);\n\n    private static Assembly? ResolveAlc(AssemblyLoadContext defaultAlc, AssemblyName assemblyToResolve) {\n        string asmPath = Path.Combine(_assemblyDir, $\"{assemblyToResolve.Name}.dll\");\n        if (IsSatisfyingAssembly(assemblyToResolve, asmPath)) {\n            return _alc.LoadFromAssemblyName(assemblyToResolve);\n        }\n\n        return null;\n    }\n\n    private static bool IsSatisfyingAssembly(AssemblyName requiredAssemblyName, string assemblyPath) {\n        if (requiredAssemblyName.Name == \"Mailozaurr.PowerShell\" || !File.Exists(assemblyPath)) {\n            return false;\n        }\n\n        AssemblyName asmToLoadName = AssemblyName.GetAssemblyName(assemblyPath);\n\n        return string.Equals(asmToLoadName.Name, requiredAssemblyName.Name, StringComparison.OrdinalIgnoreCase)\n            && asmToLoadName.Version >= requiredAssemblyName.Version;\n    }\n#endif\n\n    /// <summary>\n    /// Determine if the current runtime is .NET Framework\n    /// </summary>\n    /// <returns></returns>\n    private bool IsNetFramework() {\n        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET Framework\", StringComparison.OrdinalIgnoreCase);\n    }\n\n    // Determine if the current runtime is .NET Core\n    private bool IsNetCore() {\n        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET Core\", StringComparison.OrdinalIgnoreCase);\n    }\n\n    /// <summary>\n    /// Determine if the current runtime is .NET 5 or higher\n    /// </summary>\n    /// <returns></returns>\n    private bool IsNet5OrHigher() {\n        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET 5\", StringComparison.OrdinalIgnoreCase) ||\n               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET 6\", StringComparison.OrdinalIgnoreCase) ||\n               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET 7\", StringComparison.OrdinalIgnoreCase) ||\n               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET 8\", StringComparison.OrdinalIgnoreCase) ||\n               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(\".NET 9\", StringComparison.OrdinalIgnoreCase);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/AdditionalCoverageTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class AdditionalCoverageTests\n{\n    private class FakeFolder : MessageFlagSetter.IImapFolder\n    {\n        public bool AddCalled;\n        public bool RemoveCalled;\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, System.Threading.CancellationToken cancellationToken = default)\n        {\n            AddCalled = true;\n            return Task.CompletedTask;\n        }\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, System.Threading.CancellationToken cancellationToken = default)\n        {\n            RemoveCalled = true;\n            return Task.CompletedTask;\n        }\n    }\n\n    [Fact]\n    public async Task MessageFlagSetter_AddOrRemoveFlags()\n    {\n        var folder = new FakeFolder();\n        var uid = new UniqueId(1);\n        await MessageFlagSetter.SetFlagsAsync(folder, uid, MessageFlags.Seen, true);\n        Assert.True(folder.AddCalled);\n        await MessageFlagSetter.SetFlagsAsync(folder, uid, MessageFlags.Seen, false);\n        Assert.True(folder.RemoveCalled);\n    }\n\n    [Fact]\n    public async Task MessageFlagSetter_Pop3ReadState()\n    {\n        var client = new MailKit.Net.Pop3.Pop3Client();\n        await MessageFlagSetter.SetReadAsync(client, 5, true);\n        Assert.True(MessageFlagSetter.TryGetPop3Read(client, 5, out var read) && read);\n    }\n\n    [Fact]\n    public void MimeKitUtils_SavesAttachments()\n    {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var part = new MimePart(\"text/plain\")\n        {\n            Content = new MimeContent(new MemoryStream(new byte[] {1,2,3})),\n            FileName = \"a.txt\"\n        };\n        var msg = new MimeMessage();\n        var msgPart = new MessagePart { Message = msg };\n        MimeKitUtils.SaveAttachments(new MimeEntity[] { part, msgPart }, dir);\n        var files = Directory.GetFiles(dir);\n        Assert.Equal(2, files.Length);\n        Directory.Delete(dir, true);\n    }\n\n    [Fact]\n    public void EmailMessage_ConvertMissingFiles()\n    {\n        var tmpDir = Path.GetTempPath();\n        var eml = new FileInfo(Path.Combine(tmpDir, \"missing.eml\"));\n        var msg = new FileInfo(Path.Combine(tmpDir, \"out.msg\"));\n        var res1 = EmailMessage.ConvertEmlToMsg(eml, msg, false);\n        Assert.False(res1.Status);\n        Assert.Contains(\"does not exist\", res1.Error, StringComparison.OrdinalIgnoreCase);\n\n        var res2 = EmailMessage.ConvertMsgToEml(msg, eml, false);\n        Assert.False(res2.Status);\n        Assert.Contains(\"does not exist\", res2.Error, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void LoggingMessages_PropertyDelegates()\n    {\n        LoggingMessages.Verbose = true;\n        Assert.True(LoggingMessages.Logger.IsVerbose);\n        LoggingMessages.Verbose = false;\n        Assert.False(LoggingMessages.Logger.IsVerbose);\n    }\n\n    [Fact]\n    public void InternalLogger_WarningEventRaised()\n    {\n        var logger = new InternalLogger { IsWarning = true };\n        string? msg = null;\n        logger.OnWarningMessage += (_, e) => msg = e.Message;\n        logger.WriteWarning(\"warn\");\n        Assert.Equal(\"warn\", msg);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationBuilderTests.cs",
    "content": "using MailKit.Net.Imap;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationBuilderTests {\n    [Fact]\n    public void BuildCreatesDefaultApplicationWithBuiltInReadAndSendHandlers() {\n        var profileDirectory = CreateTemporaryDirectory();\n        var secretDirectory = CreateTemporaryDirectory();\n        var builder = new MailApplicationBuilder(new MailApplicationOptions {\n            ProfileStore = new MailProfileStoreOptions { DirectoryPath = profileDirectory },\n            SecretStore = new MailSecretStoreOptions { DirectoryPath = secretDirectory }\n        });\n\n        var app = builder.Build();\n\n        Assert.NotNull(app.ProfileStore);\n        Assert.NotNull(app.SecretStore);\n        Assert.NotNull(app.DraftStore);\n        Assert.NotNull(app.Profiles);\n        Assert.NotNull(app.ProfileOverview);\n        Assert.NotNull(app.ProfileConnections);\n        Assert.NotNull(app.Drafts);\n        Assert.NotNull(app.DraftExchange);\n        Assert.NotNull(app.ProfileBootstrap);\n        Assert.NotNull(app.ProfileAuth);\n        Assert.NotNull(app.FolderAliases);\n        Assert.NotNull(app.Read);\n        Assert.NotNull(app.MessageActionPreview);\n        Assert.NotNull(app.MessageActionPlans);\n        Assert.NotNull(app.MessageActionPlanExchange);\n        Assert.NotNull(app.MessageActionPlanRegistry);\n        Assert.NotNull(app.MessageActionBatch);\n        Assert.NotNull(app.MessageActions);\n        Assert.NotNull(app.Send);\n        Assert.NotNull(app.Queue);\n        Assert.Contains(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Imap);\n        Assert.Contains(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.Contains(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Gmail);\n        Assert.Contains(app.MessageActionHandlers, handler => handler.Kind == MailProfileKind.Imap);\n        Assert.Contains(app.MessageActionHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.Contains(app.MessageActionHandlers, handler => handler.Kind == MailProfileKind.Gmail);\n        Assert.Contains(app.SendHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.Contains(app.SendHandlers, handler => handler.Kind == MailProfileKind.Gmail);\n        Assert.Contains(app.SendHandlers, handler => handler.Kind == MailProfileKind.Smtp);\n    }\n\n    [Fact]\n    public void BuildHonorsDisabledImapHandlerOption() {\n        var builder = new MailApplicationBuilder(new MailApplicationOptions {\n            EnableImapReadHandler = false,\n            EnableGraphReadHandler = false,\n            EnableGraphSendHandler = false,\n            EnableGmailReadHandler = false,\n            EnableGmailSendHandler = false,\n            EnableSmtpSendHandler = false,\n            ProfileStore = new MailProfileStoreOptions { DirectoryPath = CreateTemporaryDirectory() },\n            SecretStore = new MailSecretStoreOptions { DirectoryPath = CreateTemporaryDirectory() }\n        });\n\n        var app = builder.Build();\n\n        Assert.DoesNotContain(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Imap);\n        Assert.DoesNotContain(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.DoesNotContain(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Gmail);\n        Assert.DoesNotContain(app.SendHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.DoesNotContain(app.SendHandlers, handler => handler.Kind == MailProfileKind.Gmail);\n        Assert.DoesNotContain(app.SendHandlers, handler => handler.Kind == MailProfileKind.Smtp);\n    }\n\n    [Fact]\n    public void BuildUsesInjectedHandlersAndFactories() {\n        var builder = new MailApplicationBuilder(new MailApplicationOptions {\n            EnableImapReadHandler = false,\n            EnableGraphReadHandler = false,\n            EnableGraphSendHandler = false,\n            EnableGmailReadHandler = false,\n            EnableGmailSendHandler = false,\n            EnableSmtpSendHandler = false,\n            ProfileStore = new MailProfileStoreOptions { DirectoryPath = CreateTemporaryDirectory() },\n            SecretStore = new MailSecretStoreOptions { DirectoryPath = CreateTemporaryDirectory() }\n        });\n        builder.UseImapSessionFactory(new FakeImapSessionFactory());\n        builder.UseGraphSessionFactory(new FakeGraphSessionFactory());\n        builder.UseGmailSessionFactory(new FakeGmailSessionFactory());\n        builder.UseSmtpSessionFactory(new FakeSmtpSessionFactory());\n        builder.AddReadHandler(new FakeReadHandler(MailProfileKind.Graph));\n        builder.AddMessageActionHandler(new FakeMessageActionHandler(MailProfileKind.Graph));\n        builder.AddSendHandler(new FakeSendHandler(MailProfileKind.Graph));\n\n        var app = builder.Build();\n\n        Assert.Contains(app.ReadHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.Contains(app.MessageActionHandlers, handler => handler.Kind == MailProfileKind.Graph);\n        Assert.Contains(app.SendHandlers, handler => handler.Kind == MailProfileKind.Graph);\n    }\n\n    private static string CreateTemporaryDirectory() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return directory;\n    }\n\n    private sealed class FakeImapSessionFactory : IImapSessionFactory {\n        public Task<ImapClient> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new ImapClient());\n    }\n\n    private sealed class FakeGraphSessionFactory : IGraphSessionFactory {\n        public Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GraphSession(new GraphApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n\n    private sealed class FakeGmailSessionFactory : IGmailSessionFactory {\n        public Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GmailSession(new GmailApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n\n    private sealed class FakeSmtpSessionFactory : ISmtpSessionFactory {\n        public Task<Smtp> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new Smtp());\n    }\n\n    private sealed class FakeReadHandler : IMailReadHandler {\n        public FakeReadHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<FolderRef>>(Array.Empty<FolderRef>());\n\n        public Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(null);\n\n        public Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success());\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummary>>(Array.Empty<MessageSummary>());\n    }\n\n    private sealed class FakeSendHandler : IMailSendHandler {\n        public FakeSendHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new SendResult { Succeeded = true, ProfileKind = Kind });\n    }\n\n    private sealed class FakeMessageActionHandler : IMailMessageActionHandler {\n        public FakeMessageActionHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult { Succeeded = true, ProfileId = profile.Id });\n\n        public Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult { Succeeded = true, ProfileId = profile.Id });\n\n        public Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult { Succeeded = true, ProfileId = profile.Id });\n\n        public Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult { Succeeded = true, ProfileId = profile.Id });\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationCapabilitiesTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationCapabilitiesTests {\n    [Theory]\n    [InlineData(MailProfileKind.Imap, MailCapability.ListFolders | MailCapability.SearchMessages | MailCapability.ReadMessages | MailCapability.MoveMessages | MailCapability.WaitForMessages)]\n    [InlineData(MailProfileKind.Pop3, MailCapability.SearchMessages | MailCapability.ReadMessages | MailCapability.DeleteMessages)]\n    [InlineData(MailProfileKind.Graph, MailCapability.ListFolders | MailCapability.SendMessages | MailCapability.ManageRules | MailCapability.ManageEvents | MailCapability.ManagePermissions)]\n    [InlineData(MailProfileKind.Gmail, MailCapability.SearchMessages | MailCapability.MarkMessages | MailCapability.MoveMessages | MailCapability.SendMessages | MailCapability.UseThreads | MailCapability.UseLabels)]\n    [InlineData(MailProfileKind.Smtp, MailCapability.SendMessages)]\n    [InlineData(MailProfileKind.SendGrid, MailCapability.SendMessages)]\n    public void CatalogExposesExpectedCapabilities(MailProfileKind kind, MailCapability required) {\n        var capabilities = MailCapabilityCatalog.For(kind);\n\n        Assert.Equal(kind, capabilities.Kind);\n        Assert.True(capabilities.Supports(required));\n    }\n\n    [Fact]\n    public void ProfileFallsBackToDefaultCapabilitiesWhenOverrideIsMissing() {\n        var profile = new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap\n        };\n\n        var capabilities = profile.GetCapabilities();\n\n        Assert.True(capabilities.Supports(MailCapability.SearchMessages));\n        Assert.True(capabilities.Supports(MailCapability.WaitForMessages));\n        Assert.False(capabilities.Supports(MailCapability.SendMessages));\n    }\n\n    [Fact]\n    public void OperationResultHelpersProduceExpectedStatus() {\n        var success = OperationResult.Success(\"Queued\");\n        var failure = OperationResult.Failure(\"auth_failed\", \"Authentication failed.\");\n\n        Assert.True(success.Succeeded);\n        Assert.Equal(\"Queued\", success.Message);\n        Assert.False(failure.Succeeded);\n        Assert.Equal(\"auth_failed\", failure.Code);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationDraftServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationDraftServiceTests {\n    [Fact]\n    public async Task SaveAsyncRejectsDraftWhenProfileIsMissing() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var draftStore = new FileMailDraftStore(CreateTemporaryFilePath(\"drafts.json\"));\n        var service = new MailDraftService(draftStore, profileStore);\n\n        var result = await service.SaveAsync(new MailDraft {\n            Id = \"draft-1\",\n            Name = \"Draft 1\",\n            Message = new DraftMessage {\n                ProfileId = \"missing-profile\"\n            }\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"draft_profile_not_found\", result.Code);\n    }\n\n    [Fact]\n    public async Task SaveAsyncStoresDraftWhenProfileExists() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var draftStore = new FileMailDraftStore(CreateTemporaryFilePath(\"drafts.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me\"\n        });\n        var service = new MailDraftService(draftStore, profileStore);\n\n        var result = await service.SaveAsync(new MailDraft {\n            Id = \"draft-1\",\n            Name = \"Draft 1\",\n            Message = new DraftMessage {\n                ProfileId = \"work-gmail\",\n                Subject = \"Hello\"\n            }\n        });\n        var saved = await service.GetDraftAsync(\"draft-1\");\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(saved);\n        Assert.Equal(\"Hello\", saved!.Message.Subject);\n    }\n\n    [Fact]\n    public async Task GetDraftsCompactReturnsLightweightProjection() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var draftStore = new FileMailDraftStore(CreateTemporaryFilePath(\"drafts.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me\"\n        });\n        var service = new MailDraftService(draftStore, profileStore);\n        await service.SaveAsync(new MailDraft {\n            Id = \"draft-1\",\n            Name = \"Draft 1\",\n            Message = new DraftMessage {\n                ProfileId = \"work-gmail\",\n                Subject = \"Hello\"\n            }\n        });\n\n        var drafts = await service.GetDraftsCompactAsync();\n\n        var draft = Assert.Single(drafts);\n        Assert.Equal(\"draft-1\", draft.Id);\n        Assert.Equal(\"work-gmail\", draft.ProfileId);\n        Assert.Equal(\"Hello\", draft.Subject);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationDraftStoreTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationDraftStoreTests {\n    [Fact]\n    public async Task FileDraftStoreRoundTripsDrafts() {\n        var store = new FileMailDraftStore(CreateTemporaryFilePath(\"drafts.json\"));\n        var draft = new MailDraft {\n            Id = \"report-draft\",\n            Name = \"Quarterly report\",\n            Message = new DraftMessage {\n                ProfileId = \"work-gmail\",\n                Subject = \"Report\",\n                TextBody = \"Hello\",\n                To = {\n                    new MessageRecipient { Address = \"alice@example.com\" }\n                }\n            }\n        };\n\n        await store.SaveAsync(draft);\n        var saved = await store.GetByIdAsync(\"report-draft\");\n\n        Assert.NotNull(saved);\n        Assert.Equal(\"Quarterly report\", saved!.Name);\n        Assert.Equal(\"work-gmail\", saved.Message.ProfileId);\n        Assert.Equal(\"alice@example.com\", saved.Message.To[0].Address);\n        Assert.NotEqual(default, saved.CreatedAt);\n        Assert.NotEqual(default, saved.UpdatedAt);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationFolderAliasServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationFolderAliasServiceTests {\n    [Fact]\n    public async Task FolderAliasServiceResolvesKnownAliasesFromFolderMetadata() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"graph-work\",\n            DisplayName = \"Graph Work\",\n            Kind = MailProfileKind.Graph,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\",\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\"\n            }\n        });\n\n        var aliases = await new MailFolderAliasService(store, new FakeReadService()).GetAliasesAsync(\"graph-work\", \"shared@example.com\");\n\n        var archive = Assert.Single(aliases, alias => alias.Alias == MailFolderAliases.Archive);\n        Assert.True(archive.IsSupported);\n        Assert.True(archive.IsResolved);\n        Assert.Equal(\"archive\", archive.FolderId);\n        Assert.Equal(\"Archive\", archive.FolderDisplayName);\n        Assert.Equal(\"Archive -> Archive\", archive.Summary);\n    }\n\n    [Fact]\n    public async Task FolderAliasServiceFallsBackToAliasOnlyWhenFolderLookupFails() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"imap-work\",\n            DisplayName = \"IMAP Work\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var aliases = await new MailFolderAliasService(store, new ThrowingReadService()).GetAliasesAsync(\"imap-work\");\n\n        var archive = Assert.Single(aliases, alias => alias.Alias == MailFolderAliases.Archive);\n        Assert.True(archive.IsSupported);\n        Assert.False(archive.IsResolved);\n        Assert.Null(archive.FolderId);\n        Assert.Equal(\"Archive [alias-only]\", archive.Summary);\n    }\n\n    [Fact]\n    public async Task FolderAliasServiceResolvesExplicitFolderTargetsWithoutAliasLookup() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"imap-work\",\n            DisplayName = \"IMAP Work\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var resolution = await new MailFolderAliasService(store, new FakeReadService()).ResolveAsync(\"imap-work\", \"Projects/2026\");\n\n        Assert.False(resolution.IsAlias);\n        Assert.True(resolution.IsSupported);\n        Assert.True(resolution.IsResolved);\n        Assert.Equal(\"Projects/2026\", resolution.EffectiveFolderId);\n        Assert.Equal(\"Projects/2026 [explicit]\", resolution.Summary);\n    }\n\n    [Fact]\n    public async Task FolderAliasServiceResolvesKnownAliasesToEffectiveFolderTargets() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"graph-work\",\n            DisplayName = \"Graph Work\",\n            Kind = MailProfileKind.Graph,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\",\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\"\n            }\n        });\n\n        var resolution = await new MailFolderAliasService(store, new FakeReadService()).ResolveAsync(\"graph-work\", \"archive\", \"shared@example.com\");\n\n        Assert.True(resolution.IsAlias);\n        Assert.True(resolution.IsSupported);\n        Assert.True(resolution.IsResolved);\n        Assert.Equal(MailFolderAliases.Archive, resolution.Alias);\n        Assert.Equal(\"archive\", resolution.EffectiveFolderId);\n        Assert.Equal(\"Archive -> Archive\", resolution.Summary);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class FakeReadService : IMailReadService {\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\",\n                    SpecialUse = \"inbox\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"archive\",\n                    DisplayName = \"Archive\",\n                    Path = \"Archive\",\n                    SpecialUse = \"archive\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"trash\",\n                    DisplayName = \"Trash\",\n                    Path = \"Trash\",\n                    SpecialUse = \"trash\"\n                }\n            });\n\n        public Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<FolderRefCompact>>(Array.Empty<FolderRefCompact>());\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummary>>(Array.Empty<MessageSummary>());\n\n        public Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummaryCompact>>(Array.Empty<MessageSummaryCompact>());\n\n        public Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<AttachmentSummary>>(Array.Empty<AttachmentSummary>());\n\n        public Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(null);\n\n        public Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageDetail>>(Array.Empty<MessageDetail>());\n\n        public Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetailCompact?>(null);\n\n        public Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageDetailCompact>>(Array.Empty<MessageDetailCompact>());\n\n        public Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new SaveAttachmentsResult());\n\n        public Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new SaveAttachmentsManyResult());\n\n        public Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success());\n    }\n\n    private sealed class ThrowingReadService : IMailReadService {\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            throw new InvalidOperationException(\"Lookup failed.\");\n\n        public Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<FolderRefCompact>>(Array.Empty<FolderRefCompact>());\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummary>>(Array.Empty<MessageSummary>());\n\n        public Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummaryCompact>>(Array.Empty<MessageSummaryCompact>());\n\n        public Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<AttachmentSummary>>(Array.Empty<AttachmentSummary>());\n\n        public Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(null);\n\n        public Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageDetail>>(Array.Empty<MessageDetail>());\n\n        public Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetailCompact?>(null);\n\n        public Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageDetailCompact>>(Array.Empty<MessageDetailCompact>());\n\n        public Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new SaveAttachmentsResult());\n\n        public Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new SaveAttachmentsManyResult());\n\n        public Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGmailMailReadHandlerTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGmailMailReadHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedFolderDelegate() {\n        var handler = new GmailMailReadHandler(\n            new FakeGmailSessionFactory(),\n            getFoldersAsync: (session, profile, query, cancellationToken) => Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = profile.Id,\n                    MailboxId = session.UserId,\n                    Id = \"INBOX\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\"\n                }\n            }));\n\n        var results = await handler.GetFoldersAsync(\n            new MailProfile {\n                Id = \"personal-gmail\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultMailbox = \"me\"\n            },\n            new MailFolderQuery {\n                ProfileId = \"personal-gmail\"\n            });\n\n        Assert.Single(results);\n        Assert.Equal(\"INBOX\", results[0].Id);\n    }\n\n    [Fact]\n    public async Task HandlerUsesInjectedSearchDelegate() {\n        var handler = new GmailMailReadHandler(\n            new FakeGmailSessionFactory(),\n            searchAsync: (session, profile, request, cancellationToken) => Task.FromResult<IReadOnlyList<MessageSummary>>(new[] {\n                new MessageSummary {\n                    ProfileId = profile.Id,\n                    Id = \"gmail-42\",\n                    Subject = request.QueryText\n                }\n            }));\n\n        var results = await handler.SearchAsync(\n            new MailProfile {\n                Id = \"personal-gmail\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultMailbox = \"me\"\n            },\n            new MailSearchRequest {\n                ProfileId = \"personal-gmail\",\n                QueryText = \"reports\"\n            });\n\n        Assert.Single(results);\n        Assert.Equal(\"gmail-42\", results[0].Id);\n        Assert.Equal(\"reports\", results[0].Subject);\n    }\n\n    private sealed class FakeGmailSessionFactory : IGmailSessionFactory {\n        public Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GmailSession(new GmailApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGmailMailSendHandlerTests.cs",
    "content": "using System.Net.Http;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGmailMailSendHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedSendDelegate() {\n        var handler = new GmailMailSendHandler(\n            new FakeGmailSessionFactory(),\n            sendAsync: (session, profile, request, message, cancellationToken) => {\n                Assert.Equal(\"sender@example.com\", message.From.Mailboxes.First().Address);\n                Assert.Equal(\"alice@example.com\", message.To.Mailboxes.First().Address);\n                Assert.Equal(\"Hello Gmail\", message.Subject);\n                return Task.FromResult(new GmailMessage {\n                    Id = \"gmail-123\",\n                    ThreadId = \"thread-123\"\n                });\n            });\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"personal-gmail\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultSender = \"sender@example.com\",\n                DefaultMailbox = \"me\"\n            },\n            new SendMessageRequest {\n                ProfileId = \"personal-gmail\",\n                Message = new DraftMessage {\n                    Subject = \"Hello Gmail\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.False(result.Queued);\n        Assert.Equal(\"personal-gmail\", result.ProfileId);\n        Assert.Equal(MailProfileKind.Gmail, result.ProfileKind);\n        Assert.Equal(\"gmail-123\", result.ProviderMessageId);\n    }\n\n    [Fact]\n    public async Task HandlerReturnsQueuedResultWhenRepositoryCapturesFailedSend() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n\n        var handler = new GmailMailSendHandler(\n            new FakeGmailSessionFactory(),\n            pendingMessageRepository: repository,\n            sendAsync: async (session, profile, request, message, cancellationToken) => {\n                await repository.SaveAsync(new PendingMessageRecord {\n                    MessageId = message.MessageId ?? \"queued-1\",\n                    Timestamp = DateTimeOffset.UtcNow,\n                    NextAttemptAt = DateTimeOffset.UtcNow,\n                    Provider = EmailProvider.Gmail,\n                    MimeMessage = Convert.ToBase64String(Array.Empty<byte>())\n                }, cancellationToken).ConfigureAwait(false);\n\n                throw new HttpRequestException(\"temporary gmail failure\");\n            });\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"personal-gmail\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultSender = \"sender@example.com\",\n                DefaultMailbox = \"me\"\n            },\n            new SendMessageRequest {\n                ProfileId = \"personal-gmail\",\n                PreferQueue = true,\n                Message = new DraftMessage {\n                    Subject = \"Hello Gmail\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.True(result.Queued);\n        Assert.NotNull(result.QueueMessageId);\n        Assert.Contains(\"queued\", result.Message ?? string.Empty, StringComparison.OrdinalIgnoreCase);\n    }\n\n    private static string CreateTemporaryDirectory() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return directory;\n    }\n\n    private sealed class FakeGmailSessionFactory : IGmailSessionFactory {\n        public Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GmailSession(new GmailApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGmailSessionFactoryTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGmailSessionFactoryTests {\n    [Fact]\n    public async Task FactoryUsesAccessTokenSecretWhenAvailable() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.AccessToken, \"gmail-token\");\n\n        GmailSessionRequest? captured = null;\n        var factory = new GmailSessionFactory(\n            secretStore,\n            connectAsync: (request, cancellationToken) => {\n                captured = request;\n                return Task.FromResult(new GmailSession(new GmailApiClient(request.Credential, request.RefreshAccessTokenAsync), request.UserId));\n            });\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"personal-gmail\",\n            DisplayName = \"Personal Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me\"\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"me\", captured!.UserId);\n        Assert.Equal(\"gmail-token\", captured.Credential.AccessToken);\n        Assert.Equal(\"me\", captured.Credential.UserName);\n    }\n\n    [Fact]\n    public async Task FactoryRefreshesAccessTokenWhenOnlyRefreshTokenIsAvailable() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.RefreshToken, \"refresh-token\");\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        GmailSessionFactory.GmailRefreshRequest? captured = null;\n        GmailSessionRequest? connected = null;\n        var factory = new GmailSessionFactory(\n            secretStore,\n            refreshCredentialAsync: (request, cancellationToken) => {\n                captured = request;\n                return Task.FromResult(new OAuthCredential {\n                    UserName = request.UserId,\n                    AccessToken = \"issued-token\",\n                    RefreshToken = request.RefreshToken,\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                });\n            },\n            connectAsync: (request, cancellationToken) => {\n                connected = request;\n                return Task.FromResult(new GmailSession(new GmailApiClient(request.Credential, request.RefreshAccessTokenAsync), request.UserId));\n            });\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"personal-gmail\",\n            DisplayName = \"Personal Gmail\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.Mailbox] = \"me\"\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"client-id\", captured!.ClientId);\n        Assert.Equal(\"client-secret\", captured.ClientSecret);\n        Assert.Equal(\"refresh-token\", captured.RefreshToken);\n        Assert.NotNull(connected);\n        Assert.Equal(\"issued-token\", connected!.Credential.AccessToken);\n    }\n\n    [Fact]\n    public async Task FactoryRefreshesExpiredAccessTokenWhenRefreshTokenIsAvailable() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.AccessToken, \"expired-token\");\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.RefreshToken, \"refresh-token\");\n        await secretStore.SetSecretAsync(\"personal-gmail\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        GmailSessionFactory.GmailRefreshRequest? captured = null;\n        GmailSessionRequest? connected = null;\n        var factory = new GmailSessionFactory(\n            secretStore,\n            refreshCredentialAsync: (request, cancellationToken) => {\n                captured = request;\n                return Task.FromResult(new OAuthCredential {\n                    UserName = request.UserId,\n                    AccessToken = \"renewed-token\",\n                    RefreshToken = request.RefreshToken,\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                });\n            },\n            connectAsync: (request, cancellationToken) => {\n                connected = request;\n                return Task.FromResult(new GmailSession(new GmailApiClient(request.Credential, request.RefreshAccessTokenAsync), request.UserId));\n            });\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"personal-gmail\",\n            DisplayName = \"Personal Gmail\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.Mailbox] = \"me\",\n                [MailProfileSettingsKeys.TokenExpiresOn] = DateTimeOffset.UtcNow.AddMinutes(-10).ToString(\"o\", System.Globalization.CultureInfo.InvariantCulture)\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"refresh-token\", captured!.RefreshToken);\n        Assert.NotNull(connected);\n        Assert.Equal(\"renewed-token\", connected!.Credential.AccessToken);\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGraphMailReadHandlerTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGraphMailReadHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedFolderDelegate() {\n        var handler = new GraphMailReadHandler(\n            new FakeGraphSessionFactory(),\n            getFoldersAsync: (session, profile, query, cancellationToken) => Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = profile.Id,\n                    MailboxId = query.MailboxId ?? session.UserId,\n                    Id = \"inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\"\n                }\n            }));\n\n        var results = await handler.GetFoldersAsync(\n            new MailProfile {\n                Id = \"work-graph\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultMailbox = \"user@example.com\"\n            },\n            new MailFolderQuery {\n                ProfileId = \"work-graph\"\n            });\n\n        Assert.Single(results);\n        Assert.Equal(\"inbox\", results[0].Id);\n        Assert.Equal(\"Inbox\", results[0].DisplayName);\n    }\n\n    [Fact]\n    public async Task HandlerUsesInjectedSearchDelegate() {\n        var handler = new GraphMailReadHandler(\n            new FakeGraphSessionFactory(),\n            searchAsync: (session, profile, request, cancellationToken) => Task.FromResult<IReadOnlyList<MessageSummary>>(new[] {\n                new MessageSummary {\n                    ProfileId = profile.Id,\n                    Id = \"graph-42\",\n                    Subject = request.QueryText\n                }\n            }));\n\n        var results = await handler.SearchAsync(\n            new MailProfile {\n                Id = \"work-graph\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultMailbox = \"user@example.com\"\n            },\n            new MailSearchRequest {\n                ProfileId = \"work-graph\",\n                QueryText = \"reports\"\n            });\n\n        Assert.Single(results);\n        Assert.Equal(\"graph-42\", results[0].Id);\n        Assert.Equal(\"reports\", results[0].Subject);\n    }\n\n    private sealed class FakeGraphSessionFactory : IGraphSessionFactory {\n        public Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GraphSession(new GraphApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGraphMailSendHandlerTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGraphMailSendHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedSendDelegate() {\n        var handler = new GraphMailSendHandler(\n            new FakeGraphSessionFactory(),\n            sendAsync: (session, profile, request, message, cancellationToken) => {\n                Assert.Equal(\"sender@example.com\", message.From.Mailboxes.First().Address);\n                Assert.Equal(\"alice@example.com\", message.To.Mailboxes.First().Address);\n                Assert.Equal(\"Hello Graph\", message.Subject);\n                return Task.FromResult(new GraphMessage {\n                    Id = \"graph-123\"\n                });\n            });\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"work-graph\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultSender = \"sender@example.com\",\n                DefaultMailbox = \"user@example.com\"\n            },\n            new SendMessageRequest {\n                ProfileId = \"work-graph\",\n                RequireImmediateSend = true,\n                Message = new DraftMessage {\n                    Subject = \"Hello Graph\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.False(result.Queued);\n        Assert.Equal(\"work-graph\", result.ProfileId);\n        Assert.Equal(MailProfileKind.Graph, result.ProfileKind);\n        Assert.Equal(\"graph-123\", result.ProviderMessageId);\n        Assert.NotNull(result.QueueMessageId);\n    }\n\n    [Fact]\n    public async Task HandlerQueuesMessageWhenQueueIsPreferred() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n        var handler = new GraphMailSendHandler(\n            new FakeGraphSessionFactory(),\n            pendingMessageRepository: repository,\n            sendAsync: (session, profile, request, message, cancellationToken) => Task.FromResult(new GraphMessage {\n                Id = \"graph-123\"\n            }));\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"work-graph\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultSender = \"sender@example.com\",\n                DefaultMailbox = \"user@example.com\"\n            },\n            new SendMessageRequest {\n                ProfileId = \"work-graph\",\n                Message = new DraftMessage {\n                    Subject = \"Hello Graph\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.True(result.Queued);\n        Assert.NotNull(result.QueueMessageId);\n        var queued = await repository.GetByMessageIdAsync(result.QueueMessageId!, CancellationToken.None);\n        Assert.NotNull(queued);\n        Assert.Equal(EmailProvider.Graph, queued!.Provider);\n        Assert.Equal(\"user@example.com\", queued.ProviderData[GraphPendingMessageSender.UserIdKey]);\n    }\n\n    [Fact]\n    public async Task HandlerQueuesScheduledSendRequests() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n        var handler = new GraphMailSendHandler(\n            new FakeGraphSessionFactory(),\n            pendingMessageRepository: repository,\n            sendAsync: (session, profile, request, message, cancellationToken) => Task.FromResult(new GraphMessage {\n                Id = \"graph-123\"\n            }));\n\n        var scheduledFor = DateTimeOffset.UtcNow.AddMinutes(15);\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"work-graph\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultSender = \"sender@example.com\",\n                DefaultMailbox = \"user@example.com\",\n                Settings = new Dictionary<string, string> {\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                    [MailProfileSettingsKeys.TenantId] = \"tenant-id\"\n                }\n            },\n            new SendMessageRequest {\n                ProfileId = \"work-graph\",\n                NotBefore = scheduledFor,\n                Message = new DraftMessage {\n                    Subject = \"Hello Graph\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.True(result.Queued);\n        var queued = await repository.GetByMessageIdAsync(result.QueueMessageId!, CancellationToken.None);\n        Assert.NotNull(queued);\n        Assert.Equal(EmailProvider.Graph, queued!.Provider);\n        Assert.Equal(\"tenant-id\", queued.ProviderData[GraphPendingMessageSender.TenantIdKey]);\n        Assert.True(queued.NextAttemptAt >= scheduledFor.ToUniversalTime().AddSeconds(-1));\n    }\n\n    private sealed class FakeGraphSessionFactory : IGraphSessionFactory {\n        public Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GraphSession(\n                new GraphApiClient(new OAuthCredential {\n                    UserName = profile.DefaultMailbox ?? \"me\",\n                    AccessToken = \"token\",\n                    ExpiresOn = DateTimeOffset.MaxValue\n                }),\n                profile.DefaultMailbox ?? \"me\",\n                new OAuthCredential {\n                    UserName = profile.DefaultMailbox ?? \"me\",\n                    AccessToken = \"token\",\n                    ExpiresOn = DateTimeOffset.MaxValue\n                },\n                new GraphCredential {\n                    ClientId = \"client-id\",\n                    DirectoryId = \"tenant-id\",\n                    ClientSecret = \"secret\"\n                }));\n    }\n\n    private static string CreateTemporaryDirectory() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return directory;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationGraphSessionFactoryTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationGraphSessionFactoryTests {\n    [Fact]\n    public async Task FactoryUsesAccessTokenSecretWhenAvailable() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"work-graph\", MailSecretNames.AccessToken, \"graph-token\");\n\n        GraphSessionRequest? captured = null;\n        var factory = new GraphSessionFactory(\n            secretStore,\n            connectAsync: (request, cancellationToken) => {\n                captured = request;\n                return Task.FromResult(new GraphSession(new GraphApiClient(request.Credential), request.UserId));\n            });\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"work-graph\",\n            DisplayName = \"Work Graph\",\n            Kind = MailProfileKind.Graph,\n            DefaultMailbox = \"user@example.com\"\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"user@example.com\", captured!.UserId);\n        Assert.Equal(\"graph-token\", captured.Credential.AccessToken);\n        Assert.Equal(\"user@example.com\", captured.Credential.UserName);\n    }\n\n    [Fact]\n    public async Task FactoryBuildsClientCredentialRequestWhenAccessTokenIsMissing() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"tenant-graph\", MailSecretNames.ClientSecret, \"top-secret\");\n\n        GraphCredential? captured = null;\n        var factory = new GraphSessionFactory(\n            secretStore,\n            acquireCredentialAsync: (profile, credential, cancellationToken) => {\n                captured = credential;\n                return Task.FromResult(new OAuthCredential {\n                    AccessToken = \"issued-token\",\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                });\n            },\n            connectAsync: (request, cancellationToken) =>\n                Task.FromResult(new GraphSession(new GraphApiClient(request.Credential), request.UserId)));\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"tenant-graph\",\n            DisplayName = \"Tenant Graph\",\n            Kind = MailProfileKind.Graph,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\",\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\"\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"client-id\", captured!.ClientId);\n        Assert.Equal(\"tenant-id\", captured.DirectoryId);\n        Assert.Equal(\"top-secret\", captured.ClientSecret);\n        Assert.Equal(\"shared@example.com\", session.UserId);\n    }\n\n    [Fact]\n    public async Task FactoryUsesSilentInteractiveRefreshWhenStoredTokenIsExpired() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"work-graph\", MailSecretNames.AccessToken, \"expired-token\");\n        GraphSessionRequest? captured = null;\n\n        var factory = new GraphSessionFactory(\n            secretStore,\n            acquireSilentCredentialAsync: (profile, cancellationToken) =>\n                Task.FromResult<OAuthCredential?>(new OAuthCredential {\n                    UserName = \"user@example.com\",\n                    AccessToken = \"silent-token\",\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                }),\n            connectAsync: (request, cancellationToken) => {\n                captured = request;\n                return Task.FromResult(new GraphSession(new GraphApiClient(request.Credential), request.UserId, request.Credential, request.GraphCredential));\n            });\n\n        using var session = await factory.ConnectAsync(new MailProfile {\n            Id = \"work-graph\",\n            DisplayName = \"Work Graph\",\n            Kind = MailProfileKind.Graph,\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.AuthFlow] = MailProfileAuthFlowNames.Interactive,\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\",\n                [MailProfileSettingsKeys.RedirectUri] = MailProfileAuthDefaults.GraphRedirectUri,\n                [MailProfileSettingsKeys.TokenExpiresOn] = DateTimeOffset.UtcNow.AddMinutes(-10).ToString(\"o\", System.Globalization.CultureInfo.InvariantCulture)\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"silent-token\", captured!.Credential.AccessToken);\n        Assert.Equal(\"user@example.com\", session.UserId);\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationImapMailReadHandlerTests.cs",
    "content": "using MailKit.Net.Imap;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationImapMailReadHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedSearchDelegate() {\n        var handler = new ImapMailReadHandler(\n            new FakeImapSessionFactory(),\n            searchAsync: (client, profile, request, cancellationToken) => Task.FromResult<IReadOnlyList<MessageSummary>>(new[] {\n                new MessageSummary {\n                    ProfileId = profile.Id,\n                    Id = \"42\",\n                    Subject = request.QueryText\n                }\n            }));\n\n        var results = await handler.SearchAsync(\n            new MailProfile {\n                Id = \"work-imap\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                Settings = new Dictionary<string, string> {\n                    [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                }\n            },\n            new MailSearchRequest {\n                ProfileId = \"work-imap\",\n                QueryText = \"reports\"\n            });\n\n        Assert.Single(results);\n        Assert.Equal(\"42\", results[0].Id);\n        Assert.Equal(\"reports\", results[0].Subject);\n    }\n\n    [Fact]\n    public async Task HandlerUsesInjectedAttachmentDelegate() {\n        var handler = new ImapMailReadHandler(\n            new FakeImapSessionFactory(),\n            saveAttachmentAsync: (client, profile, request, cancellationToken) =>\n                Task.FromResult(OperationResult.Success($\"Saved {request.AttachmentId}\")));\n\n        var result = await handler.SaveAttachmentAsync(\n            new MailProfile {\n                Id = \"work-imap\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                Settings = new Dictionary<string, string> {\n                    [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                }\n            },\n            new SaveAttachmentRequest {\n                ProfileId = \"work-imap\",\n                MessageId = \"10\",\n                AttachmentId = \"0\",\n                DestinationPath = Path.Combine(Path.GetTempPath(), \"attachment.bin\")\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.Contains(\"Saved 0\", result.Message, StringComparison.Ordinal);\n    }\n\n    private sealed class FakeImapSessionFactory : IImapSessionFactory {\n        public Task<ImapClient> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new ImapClient());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationImapSessionFactoryTests.cs",
    "content": "using MailKit.Net.Imap;\nusing MailKit.Security;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationImapSessionFactoryTests {\n    [Fact]\n    public async Task FactoryBuildsBasicAuthSessionRequestFromProfileAndSecrets() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"work-imap\", MailSecretNames.Password, \"super-secret\");\n\n        ImapSessionRequest? captured = null;\n        var factory = new ImapSessionFactory(secretStore, (request, _) => {\n            captured = request;\n            return Task.FromResult(new ImapClient());\n        });\n\n        await factory.ConnectAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\",\n                [MailProfileSettingsKeys.Port] = \"1993\",\n                [MailProfileSettingsKeys.SecureSocketOptions] = SecureSocketOptions.SslOnConnect.ToString()\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"imap.example.com\", captured!.Connection.Server);\n        Assert.Equal(1993, captured.Connection.Port);\n        Assert.Equal(SecureSocketOptions.SslOnConnect, captured.Connection.Options);\n        Assert.Equal(\"user@example.com\", captured.UserName);\n        Assert.Equal(\"super-secret\", captured.Secret);\n        Assert.Equal(ProtocolAuthMode.Basic, captured.AuthMode);\n    }\n\n    [Fact]\n    public async Task FactoryUsesOAuthAccessTokenWhenConfigured() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-imap\", MailSecretNames.AccessToken, \"oauth-token\");\n\n        ImapSessionRequest? captured = null;\n        var factory = new ImapSessionFactory(secretStore, (request, _) => {\n            captured = request;\n            return Task.FromResult(new ImapClient());\n        });\n\n        await factory.ConnectAsync(new MailProfile {\n            Id = \"gmail-imap\",\n            DisplayName = \"Gmail IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"imap.gmail.com\",\n                [MailProfileSettingsKeys.UserName] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.AuthMode] = \"oauth2\"\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(ProtocolAuthMode.OAuth2, captured!.AuthMode);\n        Assert.Equal(\"oauth-token\", captured.Secret);\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionBatchServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationMessageActionBatchServiceTests {\n    [Fact]\n    public async Task ExecuteAggregatesSuccessfulPlans() {\n        var planService = new FakePlanService(plan => Task.FromResult(new MessageActionResult {\n            Succeeded = true,\n            ProfileId = plan.ProfileId,\n            RequestedCount = plan.UniqueMessageCount,\n            SucceededCount = plan.UniqueMessageCount,\n            FailedCount = 0,\n            Message = $\"Executed '{plan.Action}'.\"\n        }));\n        var batchService = new MailMessageActionBatchService(planService);\n\n        var result = await batchService.ExecuteAsync(new[] {\n            CreatePlan(\"mark-read\", \"SetReadState\", \"work-imap\", \"msg-1\"),\n            CreatePlan(\"delete\", \"Delete\", \"work-imap\", \"msg-2\")\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, result.RequestedPlanCount);\n        Assert.Equal(2, result.AttemptedPlanCount);\n        Assert.Equal(2, result.SucceededPlanCount);\n        Assert.Equal(0, result.FailedPlanCount);\n        Assert.Equal(0, result.SkippedPlanCount);\n        Assert.Equal(2, planService.ExecutedPlans.Count);\n    }\n\n    [Fact]\n    public async Task ExecuteStopsAfterFailureWhenContinueOnErrorIsFalse() {\n        var planService = new FakePlanService(plan => Task.FromResult(plan.Action == \"delete\"\n            ? new MessageActionResult {\n                Succeeded = false,\n                Code = \"delete_failed\",\n                Message = \"Delete failed.\",\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.UniqueMessageCount,\n                FailedCount = plan.UniqueMessageCount\n            }\n            : new MessageActionResult {\n                Succeeded = true,\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.UniqueMessageCount,\n                SucceededCount = plan.UniqueMessageCount,\n                Message = $\"Executed '{plan.Action}'.\"\n            }));\n        var batchService = new MailMessageActionBatchService(planService);\n\n        var result = await batchService.ExecuteAsync(new[] {\n            CreatePlan(\"mark-read\", \"SetReadState\", \"work-imap\", \"msg-1\"),\n            CreatePlan(\"delete\", \"Delete\", \"work-imap\", \"msg-2\"),\n            CreatePlan(\"move\", \"Move\", \"work-imap\", \"msg-3\")\n        }, continueOnError: false);\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(3, result.RequestedPlanCount);\n        Assert.Equal(2, result.AttemptedPlanCount);\n        Assert.Equal(1, result.SucceededPlanCount);\n        Assert.Equal(1, result.FailedPlanCount);\n        Assert.Equal(1, result.SkippedPlanCount);\n        Assert.Equal(2, planService.ExecutedPlans.Count);\n        Assert.Contains(result.Results, item => item.Code == \"skipped_after_failure\" && item.Index == 2);\n    }\n\n    private static MessageActionExecutionPlan CreatePlan(string action, string executionKind, string profileId, string messageId) =>\n        new() {\n            Succeeded = true,\n            Action = action,\n            ExecutionKind = executionKind,\n            ProfileId = profileId,\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            MessageIds = { messageId }\n        };\n\n    private sealed class FakePlanService : IMailMessageActionPlanService {\n        private readonly Func<MessageActionExecutionPlan, Task<MessageActionResult>> _executor;\n\n        public FakePlanService(Func<MessageActionExecutionPlan, Task<MessageActionResult>> executor) {\n            _executor = executor;\n        }\n\n        public List<MessageActionExecutionPlan> ExecutedPlans { get; } = new();\n\n        public Task<MessageActionExecutionPlan> CreatePlanAsync(MessageActionExecutionPlanRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public async Task<MessageActionResult> ExecuteAsync(MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            ExecutedPlans.Add(plan);\n            return await _executor(plan).ConfigureAwait(false);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionConfirmationTokensTests.cs",
    "content": "using Mailozaurr.Application;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ApplicationMessageActionConfirmationTokensTests {\n    [Fact]\n    public void CreateDeleteTokenPreservesMessageIdCasing() {\n        var lowerToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            \"work-imap\",\n            \"shared@example.com\",\n            \"Inbox\",\n            new[] { \"abc123\" });\n\n        var upperToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            \"work-imap\",\n            \"shared@example.com\",\n            \"Inbox\",\n            new[] { \"ABC123\" });\n\n        Assert.NotEqual(lowerToken, upperToken);\n    }\n\n    [Fact]\n    public void CreateDeleteTokenIgnoresMessageIdOrdering() {\n        var firstToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            \"work-imap\",\n            \"shared@example.com\",\n            \"Inbox\",\n            new[] { \"msg-b\", \"msg-a\" });\n\n        var secondToken = MessageActionConfirmationTokens.CreateDeleteToken(\n            \"work-imap\",\n            \"shared@example.com\",\n            \"Inbox\",\n            new[] { \"msg-a\", \"msg-b\" });\n\n        Assert.Equal(firstToken, secondToken);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionPlanBatchStoreTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationMessageActionPlanBatchStoreTests {\n    [Fact]\n    public async Task FileActionPlanBatchStoreRoundTripsBatches() {\n        var store = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var batch = new MailMessageActionPlanBatch {\n            Id = \"quarterly-cleanup\",\n            Name = \"Quarterly cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive newsletter\",\n                    Summary = \"Archive newsletter (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        };\n\n        await store.SaveAsync(batch);\n        var saved = await store.GetByIdAsync(\"quarterly-cleanup\");\n\n        Assert.NotNull(saved);\n        Assert.Equal(\"Quarterly cleanup\", saved!.Name);\n        Assert.Single(saved.Plans);\n        Assert.Equal(\"Archive newsletter\", saved.Plans[0].Name);\n        Assert.Equal(\"Archive newsletter (1 message)\", saved.Plans[0].Summary);\n        Assert.Equal(\"move\", saved.Plans[0].Action);\n        Assert.NotEqual(default, saved.CreatedAt);\n        Assert.NotEqual(default, saved.UpdatedAt);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionPlanRegistryServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationMessageActionPlanRegistryServiceTests {\n    [Fact]\n    public async Task SaveAsyncRejectsBatchWhenPlanProfileIsMissing() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var planService = new PassThroughPlanService();\n        var previewService = new FakePreviewService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            previewService,\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        var result = await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"missing-profile-batch\",\n            Name = \"Missing profile batch\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"missing-profile\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"action_plan_profile_not_found\", result.Code);\n    }\n\n    [Fact]\n    public async Task ImportAndExecuteUsesSharedRegistryPipeline() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var importPath = CreateTemporaryFilePath(\"plans.json\");\n        await exchange.SaveBatchAsync(importPath, new[] {\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"mark-read\",\n                ExecutionKind = \"SetReadState\",\n                ProfileId = \"work-gmail\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                DesiredState = true,\n                MessageIds = { \"msg-1\" }\n            }\n        });\n\n        var planService = new PassThroughPlanService();\n        var batchService = new MailMessageActionBatchService(planService);\n        var service = new MailMessageActionPlanRegistryService(batchStore, exchange, new FakePreviewService(), planService, batchService, profileStore);\n\n        var importResult = await service.ImportAsync(\"cleanup\", \"Cleanup\", importPath);\n        var compact = await service.GetBatchCompactAsync(\"cleanup\");\n        var execution = await service.ExecuteAsync(\"cleanup\");\n\n        Assert.True(importResult.Succeeded);\n        Assert.NotNull(compact);\n        Assert.Equal(1, compact!.PlanCount);\n        Assert.Equal(new[] { \"mark-read (1 message)\" }, compact.PlanNames);\n        Assert.True(execution.Succeeded);\n        Assert.Equal(1, execution.SucceededPlanCount);\n    }\n\n    [Fact]\n    public async Task GetBatchesCompactCanFilterByPlanNames() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            new FakePreviewService(),\n            new PassThroughPlanService(),\n            new MailMessageActionBatchService(new PassThroughPlanService()),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete spam\",\n                    Summary = \"Delete spam (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"review\",\n            Name = \"Review\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive newsletter\",\n                    Summary = \"Archive newsletter (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-2\" }\n                }\n            }\n        });\n\n        var compact = await service.GetBatchesCompactAsync(new MailMessageActionPlanBatchQuery {\n            PlanNames = { \"Delete spam\" }\n        });\n\n        var batch = Assert.Single(compact);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.Equal(new[] { \"Delete spam\" }, batch.PlanNames);\n    }\n\n    [Fact]\n    public async Task GetBatchesCompactCanFilterByProfileIds() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"ops-imap\",\n            DisplayName = \"Ops IMAP\",\n            Kind = MailProfileKind.Imap\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            new FakePreviewService(),\n            new PassThroughPlanService(),\n            new MailMessageActionBatchService(new PassThroughPlanService()),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete spam\",\n                    Summary = \"Delete spam (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"ops-review\",\n            Name = \"Ops review\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive alerts\",\n                    Summary = \"Archive alerts (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"ops-imap\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-2\" }\n                }\n            }\n        });\n\n        var compact = await service.GetBatchesCompactAsync(new MailMessageActionPlanBatchQuery {\n            ProfileIds = { \"ops-imap\" }\n        });\n\n        var batch = Assert.Single(compact);\n        Assert.Equal(\"ops-review\", batch.Id);\n        Assert.Equal(new[] { \"Archive alerts\" }, batch.PlanNames);\n    }\n\n    [Fact]\n    public async Task GetBatchesCompactCanFilterByActions() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            new FakePreviewService(),\n            new PassThroughPlanService(),\n            new MailMessageActionBatchService(new PassThroughPlanService()),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete spam\",\n                    Summary = \"Delete spam (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"review\",\n            Name = \"Review\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive newsletter\",\n                    Summary = \"Archive newsletter (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-2\" }\n                }\n            }\n        });\n\n        var compact = await service.GetBatchesCompactAsync(new MailMessageActionPlanBatchQuery {\n            Actions = { \"delete\" }\n        });\n\n        var batch = Assert.Single(compact);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.Equal(new[] { \"Delete spam\" }, batch.PlanNames);\n    }\n\n    [Fact]\n    public async Task GetBatchSummaryProvidesProfileIdsAndActionCounts() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"ops-imap\",\n            DisplayName = \"Ops IMAP\",\n            Kind = MailProfileKind.Imap\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            new FakePreviewService(),\n            new PassThroughPlanService(),\n            new MailMessageActionBatchService(new PassThroughPlanService()),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete spam\",\n                    Summary = \"Delete spam (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive alerts\",\n                    Summary = \"Archive alerts (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"ops-imap\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-2\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete junk\",\n                    Summary = \"Delete junk (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-3\" }\n                }\n            }\n        });\n\n        var summary = await service.GetBatchSummaryAsync(\"cleanup\");\n\n        Assert.NotNull(summary);\n        Assert.Equal(new[] { \"ops-imap\", \"work-gmail\" }, summary!.ProfileIds);\n        Assert.Equal(2, summary.ActionCounts[\"delete\"]);\n        Assert.Equal(1, summary.ActionCounts[\"move\"]);\n        Assert.Equal(3, summary.PlanCount);\n    }\n\n    [Fact]\n    public async Task GetBatchesSummaryCanSortByPlanCountDescending() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            new FakePreviewService(),\n            new PassThroughPlanService(),\n            new MailMessageActionBatchService(new PassThroughPlanService()),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"small\",\n            Name = \"Small\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"large\",\n            Name = \"Large\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-2\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    RequestedDestinationFolderId = \"Archive\",\n                    MessageIds = { \"msg-3\" }\n                }\n            }\n        });\n\n        var summaries = await service.GetBatchesSummaryAsync(new MailMessageActionPlanBatchQuery {\n            SortBy = MailMessageActionPlanBatchSortBy.PlanCount,\n            Descending = true\n        });\n\n        Assert.Equal(new[] { \"large\", \"small\" }, summaries.Select(summary => summary.Id));\n    }\n\n    [Fact]\n    public async Task AppendAndRemovePlanMutateStoredBatch() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var planService = new PassThroughPlanService();\n        var previewService = new FakePreviewService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            new JsonMailMessageActionPlanExchangeService(),\n            previewService,\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"mark-read\",\n                    ExecutionKind = \"SetReadState\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    DesiredState = true,\n                    MessageIds = { \"msg-1\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-2\" }\n                }\n            }\n        });\n\n        var appendResult = await service.AppendPlanAsync(\"cleanup\", new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"move\",\n            ExecutionKind = \"Move\",\n            ProfileId = \"work-gmail\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            RequestedDestinationFolderId = \"Archive\",\n            MessageIds = { \"msg-3\" }\n        });\n        var afterAppend = await service.GetBatchAsync(\"cleanup\");\n        var removeResult = await service.RemovePlanAtAsync(\"cleanup\", 1);\n        var afterRemove = await service.GetBatchAsync(\"cleanup\");\n\n        Assert.True(appendResult.Succeeded);\n        Assert.NotNull(afterAppend);\n        Assert.Equal(3, afterAppend!.Plans.Count);\n        Assert.True(removeResult.Succeeded);\n        Assert.NotNull(afterRemove);\n        Assert.Equal(2, afterRemove!.Plans.Count);\n        Assert.DoesNotContain(afterRemove.Plans, plan => plan.Action == \"delete\");\n    }\n\n    [Fact]\n    public async Task CloneAndReplacePlanMutateStoredBatch() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new PassThroughPlanService();\n        var previewService = new FakePreviewService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            previewService,\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"work-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n\n        var cloneResult = await service.CloneAsync(\"cleanup\", \"cleanup-copy\", \"Cleanup copy\");\n        var cloned = await service.GetBatchAsync(\"cleanup-copy\");\n        var replaceResult = await service.ReplacePlanAtAsync(\"cleanup-copy\", 0, new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"move\",\n            ExecutionKind = \"Move\",\n            ProfileId = \"work-gmail\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            RequestedDestinationFolderId = \"Archive\",\n            MessageIds = { \"msg-2\" }\n        });\n        var replaced = await service.GetBatchAsync(\"cleanup-copy\");\n\n        Assert.True(cloneResult.Succeeded);\n        Assert.NotNull(cloned);\n        Assert.Equal(\"Cleanup copy\", cloned!.Name);\n        Assert.True(replaceResult.Succeeded);\n        Assert.NotNull(replaced);\n        Assert.Single(replaced!.Plans);\n        Assert.Equal(\"move\", replaced.Plans[0].Action);\n    }\n\n    [Fact]\n    public async Task CreateCommonBatchBuildsAndStoresSupportedPlans() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var previewService = new FakePreviewService {\n            NextCommonPreview = new CommonMessageActionsPreview {\n                Succeeded = true,\n                ProfileId = \"work-gmail\",\n                MailboxId = \"primary\",\n                FolderId = \"Inbox\",\n                RequestedDestinationFolderId = \"Archive\",\n                RequestedCount = 2,\n                UniqueMessageCount = 1,\n                DuplicateOrEmptyCount = 1,\n                MessageIds = { \"msg-1\" },\n                IncludedActionCount = 3,\n                SucceededActionCount = 2,\n                FailedActionCount = 1,\n                Actions = {\n                    new MessageActionPreviewItem {\n                        Action = \"mark-read\",\n                        DisplayName = \"Mark as read\",\n                        Succeeded = true,\n                        ConfirmationToken = \"token-read\"\n                    },\n                    new MessageActionPreviewItem {\n                        Action = \"move\",\n                        DisplayName = \"Move to Archive\",\n                        Succeeded = true,\n                        RequestedDestinationFolderId = \"Archive\",\n                        Destination = new MailFolderTargetResolution {\n                            ProfileId = \"work-gmail\",\n                            MailboxId = \"primary\",\n                            RequestedValue = \"Archive\",\n                            EffectiveFolderId = \"Archive\",\n                            IsSupported = true,\n                            IsResolved = true,\n                            Summary = \"Archive\"\n                        },\n                        ConfirmationToken = \"token-move\"\n                    },\n                    new MessageActionPreviewItem {\n                        Action = \"unsupported\",\n                        DisplayName = \"Unsupported\",\n                        Succeeded = false,\n                        Code = \"action_not_supported\"\n                    }\n                }\n            }\n        };\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            previewService,\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        var result = await service.CreateCommonBatchAsync(\n            \"cleanup\",\n            \"Cleanup\",\n            new CommonMessageActionsPreviewRequest {\n                ProfileId = \"work-gmail\",\n                MailboxId = \"primary\",\n                FolderId = \"Inbox\",\n                MessageIds = { \"msg-1\", \"MSG-1\" },\n                DestinationFolderId = \"Archive\"\n            },\n            new[] { \"mark-read\", \"move\", \"unsupported\" },\n            \"Created from common actions\");\n        var stored = await service.GetBatchAsync(\"cleanup\");\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(previewService.LastCommonRequest);\n        Assert.Equal(\"work-gmail\", previewService.LastCommonRequest!.ProfileId);\n        Assert.Equal(\"primary\", previewService.LastCommonRequest.MailboxId);\n        Assert.Equal(\"Inbox\", previewService.LastCommonRequest.FolderId);\n        Assert.Equal(\"Archive\", previewService.LastCommonRequest.DestinationFolderId);\n        Assert.Equal(new[] { \"msg-1\", \"MSG-1\" }, previewService.LastCommonRequest.MessageIds);\n        Assert.Equal(2, planService.Requests.Count);\n        Assert.NotNull(stored);\n        Assert.Equal(\"Created from common actions\", stored!.Description);\n        Assert.Equal(new[] { \"mark-read\", \"move\" }, stored.Plans.Select(plan => plan.Action));\n        Assert.Equal(\"Archive\", stored.Plans.Single(plan => plan.Action == \"move\").RequestedDestinationFolderId);\n        Assert.Equal(new[] { \"msg-1\" }, stored.Plans[0].MessageIds);\n        Assert.Equal(\"token-read\", planService.Requests[0].ConfirmationToken);\n        Assert.Equal(\"token-move\", planService.Requests[1].ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task CreateCommonBatchFromPreviewUsesPreviewedActionsWithoutRestatingSelection() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-gmail\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        var result = await service.CreateCommonBatchFromPreviewAsync(\n            \"cleanup-previewed\",\n            \"Cleanup Previewed\",\n            new CommonMessageActionsPreview {\n                Succeeded = true,\n                ProfileId = \"work-gmail\",\n                MailboxId = \"primary\",\n                FolderId = \"Inbox\",\n                RequestedDestinationFolderId = \"Projects/2026\",\n                RequestedCount = 3,\n                UniqueMessageCount = 2,\n                DuplicateOrEmptyCount = 1,\n                MessageIds = { \"msg-1\", \"msg-2\" },\n                Actions = {\n                    new MessageActionPreviewItem {\n                        Action = \"flag\",\n                        DisplayName = \"Flag\",\n                        Succeeded = true,\n                        DesiredState = true,\n                        ConfirmationToken = \"token-flag\"\n                    },\n                    new MessageActionPreviewItem {\n                        Action = \"move\",\n                        DisplayName = \"Move to Projects\",\n                        Succeeded = true,\n                        RequestedDestinationFolderId = \"Projects/2026\",\n                        Destination = new MailFolderTargetResolution {\n                            ProfileId = \"work-gmail\",\n                            MailboxId = \"primary\",\n                            RequestedValue = \"Projects/2026\",\n                            EffectiveFolderId = \"Projects/2026\",\n                            IsSupported = true,\n                            IsResolved = true,\n                            Summary = \"Projects/2026\"\n                        },\n                        ConfirmationToken = \"token-move\"\n                    },\n                    new MessageActionPreviewItem {\n                        Action = \"delete\",\n                        DisplayName = \"Delete\",\n                        Succeeded = false,\n                        Code = \"delete_not_supported\"\n                    }\n                }\n            },\n            description: \"Built from preview\");\n        var stored = await service.GetBatchAsync(\"cleanup-previewed\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, planService.Requests.Count);\n        Assert.Equal(new[] { \"flag\", \"move\" }, planService.Requests.Select(request => request.Action));\n        Assert.All(planService.Requests, request => Assert.Equal(new[] { \"msg-1\", \"msg-2\" }, request.MessageIds));\n        Assert.Equal(\"token-flag\", planService.Requests[0].ConfirmationToken);\n        Assert.Equal(\"token-move\", planService.Requests[1].ConfirmationToken);\n        Assert.Equal(\"Projects/2026\", planService.Requests[1].DestinationFolderId);\n        Assert.NotNull(stored);\n        Assert.Equal(\"Built from preview\", stored!.Description);\n        Assert.Equal(2, stored.Plans.Count);\n    }\n\n    [Fact]\n    public async Task TransformCloneRewritesTargetsAndRegeneratesConfirmationTokens() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"source-gmail\",\n            DisplayName = \"Source Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"target-gmail\",\n            DisplayName = \"Target Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"mark-read\",\n                    ExecutionKind = \"SetReadState\",\n                    ProfileId = \"source-gmail\",\n                    MailboxId = \"source@example.com\",\n                    FolderId = \"Inbox\",\n                    RequestedCount = 2,\n                    UniqueMessageCount = 2,\n                    DesiredState = true,\n                    MessageIds = { \"msg-1\", \"msg-2\" },\n                    ConfirmationToken = MessageActionConfirmationTokens.CreateReadStateToken(\"source-gmail\", \"source@example.com\", \"Inbox\", new[] { \"msg-1\", \"msg-2\" }, true),\n                    ConfirmationProvided = true,\n                    ConfirmationValidated = true\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive newsletter\",\n                    Summary = \"Archive newsletter (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"source-gmail\",\n                    MailboxId = \"source@example.com\",\n                    FolderId = \"Inbox\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-3\" },\n                    RequestedDestinationFolderId = \"Archive\",\n                    Destination = new MailFolderTargetResolution {\n                        ProfileId = \"source-gmail\",\n                        MailboxId = \"source@example.com\",\n                        RequestedValue = \"Archive\",\n                        EffectiveFolderId = \"Archive\",\n                        IsSupported = true,\n                        IsResolved = true,\n                        Summary = \"Archive\"\n                    },\n                    ConfirmationToken = MessageActionConfirmationTokens.CreateMoveToken(\"source-gmail\", \"source@example.com\", \"Inbox\", new[] { \"msg-3\" }, \"Archive\"),\n                    ConfirmationProvided = true,\n                    ConfirmationValidated = true\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"source-gmail\",\n                    MailboxId = \"source@example.com\",\n                    FolderId = \"Inbox\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-4\" },\n                    ConfirmationToken = MessageActionConfirmationTokens.CreateDeleteToken(\"source-gmail\", \"source@example.com\", \"Inbox\", new[] { \"msg-4\" }),\n                    ConfirmationProvided = true,\n                    ConfirmationValidated = true\n                }\n            }\n        });\n\n        var result = await service.TransformCloneAsync(\n            \"cleanup\",\n            \"cleanup-target\",\n            \"Cleanup Target\",\n            new MessageActionPlanBatchTransformRequest {\n                ProfileId = \"target-gmail\",\n                MailboxId = \"target@example.com\",\n                FolderId = \"Projects\",\n                DestinationFolderId = \"Projects/Archive\"\n            },\n            \"Remapped batch\");\n        var transformed = await service.GetBatchAsync(\"cleanup-target\");\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(transformed);\n        Assert.Equal(\"Remapped batch\", transformed!.Description);\n        Assert.All(transformed.Plans, plan => {\n            Assert.Equal(\"target-gmail\", plan.ProfileId);\n            Assert.Equal(\"target@example.com\", plan.MailboxId);\n            Assert.Equal(\"Projects\", plan.FolderId);\n            Assert.False(plan.ConfirmationProvided);\n            Assert.True(plan.ConfirmationValidated);\n            Assert.False(string.IsNullOrWhiteSpace(plan.ConfirmationToken));\n        });\n\n        var transformedMove = transformed.Plans.Single(plan => plan.Action == \"move\");\n        Assert.Equal(\"Projects/Archive\", transformedMove.RequestedDestinationFolderId);\n        Assert.Null(transformedMove.Destination);\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateMoveToken(\"target-gmail\", \"target@example.com\", \"Projects\", new[] { \"msg-3\" }, \"Projects/Archive\"),\n            transformedMove.ConfirmationToken);\n\n        var transformedRead = transformed.Plans.Single(plan => plan.Action == \"mark-read\");\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateReadStateToken(\"target-gmail\", \"target@example.com\", \"Projects\", new[] { \"msg-1\", \"msg-2\" }, true),\n            transformedRead.ConfirmationToken);\n\n        var transformedDelete = transformed.Plans.Single(plan => plan.Action == \"delete\");\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateDeleteToken(\"target-gmail\", \"target@example.com\", \"Projects\", new[] { \"msg-4\" }),\n            transformedDelete.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task PreviewTransformCloneReportsChangesAndValidationState() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"source-gmail\",\n            DisplayName = \"Source Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"target-gmail\",\n            DisplayName = \"Target Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"mark-read\",\n                    ExecutionKind = \"SetReadState\",\n                    ProfileId = \"source-gmail\",\n                    MailboxId = \"source@example.com\",\n                    FolderId = \"Inbox\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    DesiredState = true,\n                    MessageIds = { \"msg-1\" },\n                    ConfirmationToken = MessageActionConfirmationTokens.CreateReadStateToken(\"source-gmail\", \"source@example.com\", \"Inbox\", new[] { \"msg-1\" }, true),\n                    ConfirmationProvided = true,\n                    ConfirmationValidated = true\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"source-gmail\",\n                    MailboxId = \"source@example.com\",\n                    FolderId = \"Inbox\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-2\" },\n                    RequestedDestinationFolderId = \"Archive\",\n                    Destination = new MailFolderTargetResolution {\n                        ProfileId = \"source-gmail\",\n                        MailboxId = \"source@example.com\",\n                        RequestedValue = \"Archive\",\n                        EffectiveFolderId = \"Archive\",\n                        IsSupported = true,\n                        IsResolved = true,\n                        Summary = \"Archive\"\n                    },\n                    ConfirmationToken = MessageActionConfirmationTokens.CreateMoveToken(\"source-gmail\", \"source@example.com\", \"Inbox\", new[] { \"msg-2\" }, \"Archive\"),\n                    ConfirmationProvided = true,\n                    ConfirmationValidated = true\n                }\n            }\n        });\n\n        var preview = await service.PreviewTransformCloneAsync(\n            \"cleanup\",\n            new MessageActionPlanBatchTransformRequest {\n                ProfileId = \"target-gmail\",\n                MailboxId = \"target@example.com\",\n                FolderId = \"Projects\",\n                DestinationFolderId = \"Projects/Archive\"\n            });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(\"cleanup\", preview.SourceBatchId);\n        Assert.Equal(\"Cleanup\", preview.SourceBatchName);\n        Assert.Equal(2, preview.PlanCount);\n        Assert.Equal(2, preview.ChangedPlanCount);\n        Assert.Equal(2, preview.ConfirmationTokenChangedCount);\n        Assert.True(preview.TargetProfileExists);\n        Assert.Equal(MailProfileKind.Gmail, preview.TargetProfileKind);\n        Assert.Empty(preview.Errors);\n        Assert.Equal(2, preview.Plans.Count);\n        Assert.All(preview.Plans, plan => Assert.True(plan.WillChange));\n        Assert.Contains(preview.Plans, plan => plan.Action == \"move\" && plan.TargetDestinationFolderId == \"Projects/Archive\" && plan.ConfirmationTokenWillChange);\n    }\n\n    [Fact]\n    public async Task PreviewTransformCloneFailsWhenTargetProfileIsMissing() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"source-gmail\",\n            DisplayName = \"Source Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-1\" }\n                }\n            }\n        });\n\n        var preview = await service.PreviewTransformCloneAsync(\n            \"cleanup\",\n            new MessageActionPlanBatchTransformRequest {\n                ProfileId = \"missing-profile\"\n            });\n\n        Assert.False(preview.Succeeded);\n        Assert.Equal(\"action_plan_profile_not_found\", preview.Code);\n        Assert.False(preview.TargetProfileExists);\n        Assert.Contains(preview.Errors, error => error.IndexOf(\"missing-profile\", StringComparison.Ordinal) >= 0);\n        Assert.Single(preview.Plans);\n    }\n\n    [Fact]\n    public async Task TransformCloneCanLimitSelectionToSpecificPlanIndexes() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"source-gmail\",\n            DisplayName = \"Source Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"target-gmail\",\n            DisplayName = \"Target Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"mark-read\",\n                    ExecutionKind = \"SetReadState\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    DesiredState = true,\n                    MessageIds = { \"msg-1\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-2\" },\n                    RequestedDestinationFolderId = \"Archive\"\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-3\" }\n                }\n            }\n        });\n\n        var preview = await service.PreviewTransformCloneAsync(\n            \"cleanup\",\n            new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = { 1, 2 },\n                ProfileId = \"target-gmail\",\n                DestinationFolderId = \"Projects/Archive\"\n            });\n        var result = await service.TransformCloneAsync(\n            \"cleanup\",\n            \"cleanup-subset\",\n            \"Cleanup Subset\",\n            new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = { 1, 2 },\n                ProfileId = \"target-gmail\",\n                DestinationFolderId = \"Projects/Archive\"\n            });\n        var transformed = await service.GetBatchAsync(\"cleanup-subset\");\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(2, preview.PlanCount);\n        Assert.Equal(new[] { 1, 2 }, preview.Plans.Select(plan => plan.Index));\n        Assert.True(result.Succeeded);\n        Assert.NotNull(transformed);\n        Assert.Equal(2, transformed!.Plans.Count);\n        Assert.Equal(new[] { \"move\", \"delete\" }, transformed.Plans.Select(plan => plan.Action));\n    }\n\n    [Fact]\n    public async Task TransformCloneCanLimitSelectionToSpecificPlanNames() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"source-gmail\",\n            DisplayName = \"Source Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"target-gmail\",\n            DisplayName = \"Target Gmail\",\n            Kind = MailProfileKind.Gmail\n        });\n        var batchStore = new FileMailMessageActionPlanBatchStore(CreateTemporaryFilePath(\"action-plan-batches.json\"));\n        var exchange = new JsonMailMessageActionPlanExchangeService();\n        var planService = new RecordingPlanService();\n        var service = new MailMessageActionPlanRegistryService(\n            batchStore,\n            exchange,\n            new FakePreviewService(),\n            planService,\n            new MailMessageActionBatchService(planService),\n            profileStore);\n\n        await service.SaveAsync(new MailMessageActionPlanBatch {\n            Id = \"cleanup\",\n            Name = \"Cleanup\",\n            Plans = {\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Read invoices\",\n                    Summary = \"Read invoices (1 message)\",\n                    Action = \"mark-read\",\n                    ExecutionKind = \"SetReadState\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    DesiredState = true,\n                    MessageIds = { \"msg-1\" }\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Archive newsletter\",\n                    Summary = \"Archive newsletter (1 message)\",\n                    Action = \"move\",\n                    ExecutionKind = \"Move\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-2\" },\n                    RequestedDestinationFolderId = \"Archive\"\n                },\n                new MessageActionExecutionPlan {\n                    Succeeded = true,\n                    Name = \"Delete spam\",\n                    Summary = \"Delete spam (1 message)\",\n                    Action = \"delete\",\n                    ExecutionKind = \"Delete\",\n                    ProfileId = \"source-gmail\",\n                    RequestedCount = 1,\n                    UniqueMessageCount = 1,\n                    MessageIds = { \"msg-3\" }\n                }\n            }\n        });\n\n        var preview = await service.PreviewTransformCloneAsync(\n            \"cleanup\",\n            new MessageActionPlanBatchTransformRequest {\n                PlanNames = { \"Archive newsletter\", \"Delete spam\" },\n                ProfileId = \"target-gmail\"\n            });\n        var result = await service.TransformCloneAsync(\n            \"cleanup\",\n            \"cleanup-named\",\n            \"Cleanup Named\",\n            new MessageActionPlanBatchTransformRequest {\n                PlanNames = { \"Archive newsletter\", \"Delete spam\" },\n                ProfileId = \"target-gmail\"\n            });\n        var transformed = await service.GetBatchAsync(\"cleanup-named\");\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(2, preview.PlanCount);\n        Assert.Equal(new[] { \"Archive newsletter\", \"Delete spam\" }, preview.Plans.Select(plan => plan.Action == \"move\" ? \"Archive newsletter\" : \"Delete spam\"));\n        Assert.True(result.Succeeded);\n        Assert.NotNull(transformed);\n        Assert.Equal(2, transformed!.Plans.Count);\n        Assert.Equal(new[] { \"Archive newsletter\", \"Delete spam\" }, transformed.Plans.Select(plan => plan.Name));\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class PassThroughPlanService : IMailMessageActionPlanService {\n        public Task<MessageActionExecutionPlan> CreatePlanAsync(MessageActionExecutionPlanRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<MessageActionResult> ExecuteAsync(MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult {\n                Succeeded = plan.Succeeded,\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.UniqueMessageCount,\n                SucceededCount = plan.Succeeded ? plan.UniqueMessageCount : 0,\n                FailedCount = plan.Succeeded ? 0 : plan.UniqueMessageCount,\n                Message = $\"Executed '{plan.Action}'.\"\n            });\n    }\n\n    private sealed class RecordingPlanService : IMailMessageActionPlanService {\n        public List<MessageActionExecutionPlanRequest> Requests { get; } = new();\n\n        public Task<MessageActionExecutionPlan> CreatePlanAsync(MessageActionExecutionPlanRequest request, CancellationToken cancellationToken = default) {\n            Requests.Add(new MessageActionExecutionPlanRequest {\n                Action = request.Action,\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                MessageIds = request.MessageIds.ToList(),\n                DestinationFolderId = request.DestinationFolderId,\n                ConfirmationToken = request.ConfirmationToken\n            });\n\n            if (string.Equals(request.Action, \"unsupported\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new MessageActionExecutionPlan {\n                    Succeeded = false,\n                    Code = \"action_not_supported\",\n                    Action = request.Action,\n                    ProfileId = request.ProfileId,\n                    MailboxId = request.MailboxId,\n                    FolderId = request.FolderId,\n                    RequestedCount = request.MessageIds.Count,\n                    UniqueMessageCount = request.MessageIds.Distinct(StringComparer.Ordinal).Count(),\n                    MessageIds = request.MessageIds.ToList(),\n                    RequestedDestinationFolderId = request.DestinationFolderId\n                });\n            }\n\n            return Task.FromResult(new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = request.Action,\n                ExecutionKind = request.Action switch {\n                    \"mark-read\" or \"mark-unread\" => \"SetReadState\",\n                    \"flag\" or \"unflag\" => \"SetFlaggedState\",\n                    \"move\" or \"archive\" or \"trash\" => \"Move\",\n                    \"delete\" => \"Delete\",\n                    _ => \"Custom\"\n                },\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                RequestedCount = request.MessageIds.Count,\n                UniqueMessageCount = request.MessageIds.Distinct(StringComparer.Ordinal).Count(),\n                MessageIds = request.MessageIds.ToList(),\n                RequestedDestinationFolderId = request.DestinationFolderId,\n                DesiredState = request.Action switch {\n                    \"mark-read\" => true,\n                    \"mark-unread\" => false,\n                    \"flag\" => true,\n                    \"unflag\" => false,\n                    _ => null\n                }\n            });\n        }\n\n        public Task<MessageActionResult> ExecuteAsync(MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult {\n                Succeeded = plan.Succeeded,\n                ProfileId = plan.ProfileId,\n                RequestedCount = plan.UniqueMessageCount,\n                SucceededCount = plan.Succeeded ? plan.UniqueMessageCount : 0,\n                FailedCount = plan.Succeeded ? 0 : plan.UniqueMessageCount,\n                Message = $\"Executed '{plan.Action}'.\"\n            });\n    }\n\n    private sealed class FakePreviewService : IMailMessageActionPreviewService {\n        public CommonMessageActionsPreviewRequest? LastCommonRequest { get; private set; }\n\n        public CommonMessageActionsPreview? NextCommonPreview { get; set; }\n\n        public Task<CommonMessageActionsPreview> PreviewCommonActionsAsync(CommonMessageActionsPreviewRequest request, CancellationToken cancellationToken = default) {\n            LastCommonRequest = new CommonMessageActionsPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                DestinationFolderId = request.DestinationFolderId,\n                MessageIds = request.MessageIds.ToList()\n            };\n            return Task.FromResult(NextCommonPreview ?? new CommonMessageActionsPreview {\n                Succeeded = false,\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                RequestedDestinationFolderId = request.DestinationFolderId,\n                RequestedCount = request.MessageIds.Count,\n                UniqueMessageCount = request.MessageIds.Count,\n                MessageIds = request.MessageIds.ToList(),\n                Code = \"no_supported_actions\",\n                Message = \"No supported actions.\"\n            });\n        }\n\n        public Task<MessageStateChangePreview> PreviewReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<MessageStateChangePreview> PreviewFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<MoveMessagesPreview> PreviewMoveAsync(MoveMessagesPreviewRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<DeleteMessagesPreview> PreviewDeleteAsync(DeleteMessagesPreviewRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<StandardMessageActionsPreview> PreviewStandardActionsAsync(StandardMessageActionsPreviewRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionPlanServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationMessageActionPlanServiceTests {\n    [Fact]\n    public async Task CreatePlanResolvesMoveDestinationAndValidatesToken() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var previewService = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var actionService = new CapturingMessageActionService();\n        var planningService = new MailMessageActionPlanService(previewService, actionService);\n        var preview = await previewService.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\" },\n            DestinationFolderId = MailFolderAliases.Archive\n        });\n\n        var plan = await planningService.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n            Action = \"archive\",\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\" },\n            ConfirmationToken = preview.ConfirmationToken\n        });\n\n        Assert.True(plan.Succeeded);\n        Assert.Equal(\"Move\", plan.ExecutionKind);\n        Assert.Equal(2, plan.UniqueMessageCount);\n        Assert.True(plan.ConfirmationProvided);\n        Assert.True(plan.ConfirmationValidated);\n        Assert.Equal(\"archive-folder\", plan.Destination!.EffectiveFolderId);\n    }\n\n    [Fact]\n    public async Task CreatePlanRejectsMismatchedToken() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var planningService = new MailMessageActionPlanService(\n            new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService()),\n            new CapturingMessageActionService());\n\n        var plan = await planningService.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n            Action = \"delete\",\n            ProfileId = \"work-imap\",\n            MessageIds = { \"msg-1\" },\n            ConfirmationToken = \"mact_v1_invalid\"\n        });\n\n        Assert.False(plan.Succeeded);\n        Assert.Equal(\"confirmation_token_mismatch\", plan.Code);\n        Assert.True(plan.ConfirmationProvided);\n        Assert.False(plan.ConfirmationValidated);\n    }\n\n    [Fact]\n    public async Task ExecutePlanDispatchesNormalizedReadStateRequest() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var previewService = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var actionService = new CapturingMessageActionService();\n        var planningService = new MailMessageActionPlanService(previewService, actionService);\n        var plan = await planningService.CreatePlanAsync(new MessageActionExecutionPlanRequest {\n            Action = \"mark-unread\",\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\" }\n        });\n\n        var result = await planningService.ExecuteAsync(plan);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(actionService.LastReadStateRequest);\n        Assert.Equal(\"work-imap\", actionService.LastReadStateRequest!.ProfileId);\n        Assert.Equal(\"shared@example.com\", actionService.LastReadStateRequest.MailboxId);\n        Assert.Equal(\"Inbox\", actionService.LastReadStateRequest.FolderId);\n        Assert.False(actionService.LastReadStateRequest.IsRead);\n        Assert.Equal(new[] { \"msg-1\", \"MSG-1\" }, actionService.LastReadStateRequest.MessageIds);\n        Assert.Equal(plan.ConfirmationToken, actionService.LastReadStateRequest.ConfirmationToken);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class CapturingMessageActionService : IMailMessageActionService {\n        public SetReadStateRequest? LastReadStateRequest { get; private set; }\n\n        public Task<MessageActionResult> SetReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) {\n            LastReadStateRequest = request;\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n        }\n\n        public Task<MessageActionResult> SetFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n\n        public Task<MessageActionResult> MoveAsync(MoveMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n\n        public Task<MessageActionResult> DeleteAsync(DeleteMessagesRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n    }\n\n    private sealed class FakeFolderAliasService : IMailFolderAliasService {\n        public Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(string profileId, string? mailboxId = null, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailFolderAliasSummary>>(new[] {\n                new MailFolderAliasSummary {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    Alias = MailFolderAliases.Archive,\n                    DisplayName = \"Archive\",\n                    IsSupported = true,\n                    IsResolved = true,\n                    FolderId = \"archive-folder\",\n                    FolderDisplayName = \"Archive\",\n                    FolderPath = \"Archive\",\n                    Summary = \"Archive -> Archive\"\n                }\n            });\n\n        public Task<MailFolderTargetResolution> ResolveAsync(string profileId, string targetFolderId, string? mailboxId = null, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = targetFolderId,\n                IsAlias = true,\n                Alias = MailFolderAliases.Archive,\n                IsSupported = true,\n                IsResolved = true,\n                EffectiveFolderId = \"archive-folder\",\n                FolderDisplayName = \"Archive\",\n                FolderPath = \"Archive\",\n                Summary = \"Archive -> Archive\"\n            });\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationMessageActionPreviewServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationMessageActionPreviewServiceTests {\n    [Fact]\n    public async Task PreviewReadStateNormalizesMessageIdsAndProducesToken() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewReadStateAsync(new SetReadStateRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"\", \"msg-2\" },\n            IsRead = false\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(\"read-state\", preview.Action);\n        Assert.False(preview.DesiredState);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateReadStateToken(\"work-imap\", \"shared@example.com\", \"Inbox\", new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }, isRead: false),\n            preview.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task PreviewFlaggedStateNormalizesMessageIdsAndProducesToken() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewFlaggedStateAsync(new SetFlaggedStateRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"\", \"msg-2\" },\n            IsFlagged = false\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(\"flagged-state\", preview.Action);\n        Assert.False(preview.DesiredState);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateFlaggedStateToken(\"work-imap\", \"shared@example.com\", \"Inbox\", new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }, isFlagged: false),\n            preview.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task PreviewMoveNormalizesMessageIdsAndResolvesDestination() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"\", \"msg-2\" },\n            DestinationFolderId = \"archive\"\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(4, preview.RequestedCount);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Equal(1, preview.DuplicateOrEmptyCount);\n        Assert.Equal(new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }, preview.MessageIds);\n        Assert.NotNull(preview.Destination);\n        Assert.Equal(\"archive-folder\", preview.Destination!.EffectiveFolderId);\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateMoveToken(\"work-imap\", \"shared@example.com\", null, new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }, \"archive-folder\"),\n            preview.ConfirmationToken);\n        Assert.Contains(preview.Warnings, warning => warning.IndexOf(\"Ignored 1 duplicate or empty\", StringComparison.Ordinal) >= 0);\n    }\n\n    [Fact]\n    public async Task PreviewMoveFailsWhenDestinationIsUnsupported() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new UnsupportedFolderAliasService());\n        var preview = await service.PreviewMoveAsync(new MoveMessagesPreviewRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"msg-1\" },\n            DestinationFolderId = MailFolderAliases.Archive\n        });\n\n        Assert.False(preview.Succeeded);\n        Assert.Equal(\"destination_not_supported\", preview.Code);\n    }\n\n    [Fact]\n    public async Task PreviewDeleteNormalizesMessageIds() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"\", \"msg-2\" }\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(4, preview.RequestedCount);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Equal(1, preview.DuplicateOrEmptyCount);\n        Assert.Equal(new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }, preview.MessageIds);\n        Assert.Equal(\n            MessageActionConfirmationTokens.CreateDeleteToken(\"work-imap\", null, null, new[] { \"msg-1\", \"MSG-1\", \"msg-2\" }),\n            preview.ConfirmationToken);\n        Assert.Contains(preview.Warnings, warning => warning.IndexOf(\"Ignored 1 duplicate or empty\", StringComparison.Ordinal) >= 0);\n    }\n\n    [Fact]\n    public async Task PreviewDeleteFailsWhenCapabilityIsUnsupported() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-smtp\",\n            DisplayName = \"Work SMTP\",\n            Kind = MailProfileKind.Smtp,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n            ProfileId = \"work-smtp\",\n            MessageIds = { \"msg-1\" }\n        });\n\n        Assert.False(preview.Succeeded);\n        Assert.Equal(\"delete_not_supported\", preview.Code);\n    }\n\n    [Fact]\n    public async Task PreviewStandardActionsCombinesArchiveTrashMoveAndDelete() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewStandardActionsAsync(new StandardMessageActionsPreviewRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"\", \"msg-2\" },\n            DestinationFolderId = \"Projects/2026\"\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(4, preview.RequestedCount);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Equal(1, preview.DuplicateOrEmptyCount);\n        Assert.Equal(4, preview.IncludedActionCount);\n        Assert.Equal(4, preview.SucceededActionCount);\n        Assert.Equal(0, preview.FailedActionCount);\n        Assert.Contains(preview.Warnings, warning => warning.IndexOf(\"Ignored 1 duplicate or empty\", StringComparison.Ordinal) >= 0);\n\n        var archive = Assert.Single(preview.Actions, action => action.Action == \"archive\");\n        Assert.Equal(\"archive-folder\", archive.Destination!.EffectiveFolderId);\n        Assert.NotNull(archive.ConfirmationToken);\n\n        var trash = Assert.Single(preview.Actions, action => action.Action == \"trash\");\n        Assert.Equal(\"trash-folder\", trash.Destination!.EffectiveFolderId);\n        Assert.NotNull(trash.ConfirmationToken);\n\n        var move = Assert.Single(preview.Actions, action => action.Action == \"move\");\n        Assert.Equal(\"Projects/2026\", move.Destination!.EffectiveFolderId);\n        Assert.NotNull(move.ConfirmationToken);\n\n        var delete = Assert.Single(preview.Actions, action => action.Action == \"delete\");\n        Assert.True(delete.Succeeded);\n        Assert.NotNull(delete.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task PreviewStandardActionsReportsUnsupportedActions() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-smtp\",\n            DisplayName = \"Work SMTP\",\n            Kind = MailProfileKind.Smtp,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewStandardActionsAsync(new StandardMessageActionsPreviewRequest {\n            ProfileId = \"work-smtp\",\n            MessageIds = { \"msg-1\" },\n            DestinationFolderId = \"Archive\"\n        });\n\n        Assert.False(preview.Succeeded);\n        Assert.Equal(\"no_supported_actions\", preview.Code);\n        Assert.Equal(4, preview.IncludedActionCount);\n        Assert.Equal(0, preview.SucceededActionCount);\n        Assert.Equal(4, preview.FailedActionCount);\n        Assert.All(preview.Actions, action => Assert.False(action.Succeeded));\n        Assert.Contains(preview.Actions, action => action.Action == \"move\" && action.Code == \"move_not_supported\");\n        Assert.Contains(preview.Actions, action => action.Action == \"delete\" && action.Code == \"delete_not_supported\");\n    }\n\n    [Fact]\n    public async Task PreviewCommonActionsIncludesStateAndMailboxActions() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var service = new MailMessageActionPreviewService(profileStore, new FakeFolderAliasService());\n        var preview = await service.PreviewCommonActionsAsync(new CommonMessageActionsPreviewRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"msg-2\" },\n            DestinationFolderId = \"Projects/2026\"\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.Equal(8, preview.IncludedActionCount);\n        Assert.Equal(8, preview.SucceededActionCount);\n        Assert.Equal(0, preview.FailedActionCount);\n        Assert.Equal(3, preview.UniqueMessageCount);\n        Assert.Contains(preview.Actions, action => action.Action == \"mark-read\" && action.DesiredState == true);\n        Assert.Contains(preview.Actions, action => action.Action == \"mark-unread\" && action.DesiredState == false);\n        Assert.Contains(preview.Actions, action => action.Action == \"flag\" && action.DesiredState == true);\n        Assert.Contains(preview.Actions, action => action.Action == \"unflag\" && action.DesiredState == false);\n        Assert.Contains(preview.Actions, action => action.Action == \"archive\" && action.Destination!.EffectiveFolderId == \"archive-folder\");\n        Assert.Contains(preview.Actions, action => action.Action == \"move\" && action.Destination!.EffectiveFolderId == \"Projects/2026\");\n        Assert.All(preview.Actions, action => Assert.NotNull(action.ConfirmationToken));\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class FakeFolderAliasService : IMailFolderAliasService {\n        public Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(string profileId, string? mailboxId = null, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailFolderAliasSummary>>(new[] {\n                new MailFolderAliasSummary {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    Alias = MailFolderAliases.Archive,\n                    DisplayName = \"Archive\",\n                    IsSupported = true,\n                    IsResolved = true,\n                    FolderId = \"archive-folder\",\n                    FolderDisplayName = \"Archive\",\n                    FolderPath = \"Archive\",\n                    Summary = \"Archive -> Archive\"\n                },\n                new MailFolderAliasSummary {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    Alias = MailFolderAliases.Trash,\n                    DisplayName = \"Trash\",\n                    IsSupported = true,\n                    IsResolved = true,\n                    FolderId = \"trash-folder\",\n                    FolderDisplayName = \"Trash\",\n                    FolderPath = \"Trash\",\n                    Summary = \"Trash -> Trash\"\n                }\n            });\n\n        public Task<MailFolderTargetResolution> ResolveAsync(string profileId, string targetFolderId, string? mailboxId = null, CancellationToken cancellationToken = default) {\n            if (string.Equals(targetFolderId, MailFolderAliases.Trash, StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new MailFolderTargetResolution {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    RequestedValue = targetFolderId,\n                    IsAlias = true,\n                    Alias = MailFolderAliases.Trash,\n                    IsSupported = true,\n                    IsResolved = true,\n                    EffectiveFolderId = \"trash-folder\",\n                    FolderDisplayName = \"Trash\",\n                    FolderPath = \"Trash\",\n                    Summary = \"Trash -> Trash\"\n                });\n            }\n\n            if (string.Equals(targetFolderId, \"Projects/2026\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new MailFolderTargetResolution {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    RequestedValue = targetFolderId,\n                    IsAlias = false,\n                    IsSupported = true,\n                    IsResolved = true,\n                    EffectiveFolderId = \"Projects/2026\",\n                    FolderDisplayName = \"Projects/2026\",\n                    FolderPath = \"Projects/2026\",\n                    Summary = \"Projects/2026\"\n                });\n            }\n\n            return Task.FromResult(new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = targetFolderId,\n                IsAlias = true,\n                Alias = MailFolderAliases.Archive,\n                IsSupported = true,\n                IsResolved = true,\n                EffectiveFolderId = \"archive-folder\",\n                FolderDisplayName = \"Archive\",\n                FolderPath = \"Archive\",\n                Summary = \"Archive -> Archive\"\n            });\n        }\n    }\n\n    private sealed class UnsupportedFolderAliasService : IMailFolderAliasService {\n        public Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(string profileId, string? mailboxId = null, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailFolderAliasSummary>>(Array.Empty<MailFolderAliasSummary>());\n\n        public Task<MailFolderTargetResolution> ResolveAsync(string profileId, string targetFolderId, string? mailboxId = null, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = targetFolderId,\n                IsAlias = true,\n                Alias = MailFolderAliases.Archive,\n                IsSupported = false,\n                IsResolved = false,\n                EffectiveFolderId = MailFolderAliases.Archive,\n                Summary = \"Archive [unsupported]\"\n            });\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileAuthServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileAuthServiceTests {\n    [Fact]\n    public async Task GetStatusAsyncReportsInteractiveGmailProfileState() {\n        var expiresOn = DateTimeOffset.UtcNow.AddHours(2);\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultMailbox = \"user@gmail.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                    [MailProfileSettingsKeys.AuthFlow] = MailProfileAuthFlowNames.Interactive,\n                    [MailProfileSettingsKeys.LoginHint] = \"user@gmail.com\",\n                    [MailProfileSettingsKeys.TokenExpiresOn] = expiresOn.ToString(\"o\", System.Globalization.CultureInfo.InvariantCulture)\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"access-token\");\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken, \"refresh-token\");\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret, \"client-secret\");\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var status = await service.GetStatusAsync(\"gmail-work\");\n\n        Assert.NotNull(status);\n        Assert.Equal(\"interactive\", status!.Mode);\n        Assert.Equal(MailProfileKind.Gmail, status.ProfileKind);\n        Assert.Equal(\"user@gmail.com\", status.Mailbox);\n        Assert.True(status.HasAccessToken);\n        Assert.True(status.HasRefreshToken);\n        Assert.True(status.HasClientSecret);\n        Assert.True(status.CanRefresh);\n        Assert.True(status.CanLoginInteractively);\n        Assert.False(status.IsTokenExpired);\n        Assert.Equal(expiresOn, status.TokenExpiresOn);\n    }\n\n    [Fact]\n    public async Task GetStatusAsyncReportsGraphAppOnlyState() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"graph-app\",\n                DisplayName = \"Graph App\",\n                Kind = MailProfileKind.Graph,\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                    [MailProfileSettingsKeys.TenantId] = \"tenant-id\",\n                    [MailProfileSettingsKeys.CertificatePath] = \"C:\\\\certs\\\\graph.pfx\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"graph-app\", MailSecretNames.ClientSecret, \"client-secret\");\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var status = await service.GetStatusAsync(\"graph-app\");\n\n        Assert.NotNull(status);\n        Assert.Equal(\"appOnly\", status!.Mode);\n        Assert.True(status.HasClientId);\n        Assert.True(status.HasTenantId);\n        Assert.True(status.HasClientSecret);\n        Assert.True(status.HasCertificatePath);\n        Assert.False(status.CanRefresh);\n        Assert.True(status.CanLoginInteractively);\n    }\n\n    [Fact]\n    public async Task LoginGmailAsyncPersistsTokensAndProfileSettings() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret, \"client-secret\");\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore,\n            (request, _) => Task.FromResult(new OAuthCredential {\n                UserName = request.GmailAccount!,\n                AccessToken = \"gmail-access-token\",\n                RefreshToken = \"gmail-refresh-token\",\n                ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n            }));\n\n        var result = await service.LoginGmailAsync(new GmailProfileLoginRequest {\n            ProfileId = \"gmail-work\"\n        });\n\n        var profile = await profileStore.GetByIdAsync(\"gmail-work\");\n        var accessToken = await secretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken);\n        var refreshToken = await secretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(profile);\n        Assert.Equal(\"user@gmail.com\", profile!.DefaultMailbox);\n        Assert.Equal(\"user@gmail.com\", profile.DefaultSender);\n        Assert.Equal(MailProfileAuthFlowNames.Interactive, profile.Settings[MailProfileSettingsKeys.AuthFlow]);\n        Assert.Equal(\"user@gmail.com\", profile.Settings[MailProfileSettingsKeys.LoginHint]);\n        Assert.True(profile.Settings.ContainsKey(MailProfileSettingsKeys.TokenExpiresOn));\n        Assert.Equal(\"gmail-access-token\", accessToken);\n        Assert.Equal(\"gmail-refresh-token\", refreshToken);\n    }\n\n    [Fact]\n    public async Task LoginGmailAsyncSupportsClientSecretReferences() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.ClientSecret, \"client-secret-from-reference\");\n        GmailProfileLoginRequest? capturedRequest = null;\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore,\n            (request, _) => {\n                capturedRequest = request;\n                return Task.FromResult(new OAuthCredential {\n                    UserName = request.GmailAccount!,\n                    AccessToken = \"gmail-access-token\",\n                    RefreshToken = \"gmail-refresh-token\",\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                });\n            });\n\n        var result = await service.LoginGmailAsync(new GmailProfileLoginRequest {\n            ProfileId = \"gmail-work\",\n            ClientSecretReference = $\"shared-secrets:{MailSecretNames.ClientSecret}\"\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(capturedRequest);\n        Assert.Equal(\"client-secret-from-reference\", capturedRequest!.ClientSecret);\n        Assert.Null(capturedRequest.ClientSecretReference);\n    }\n\n    [Fact]\n    public async Task LoginGraphAsyncPersistsAccessTokenAndProfileSettings() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"graph-work\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                    [MailProfileSettingsKeys.TenantId] = \"tenant-id\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore,\n            loginGraphAsync: (request, _) => Task.FromResult(new OAuthCredential {\n                UserName = request.Login ?? \"user@example.com\",\n                AccessToken = \"graph-access-token\",\n                ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n            }));\n\n        var result = await service.LoginGraphAsync(new GraphProfileLoginRequest {\n            ProfileId = \"graph-work\",\n            Login = \"user@example.com\"\n        });\n\n        var profile = await profileStore.GetByIdAsync(\"graph-work\");\n        var accessToken = await secretStore.GetSecretAsync(\"graph-work\", MailSecretNames.AccessToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(profile);\n        Assert.Equal(\"user@example.com\", profile!.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(MailProfileAuthFlowNames.Interactive, profile.Settings[MailProfileSettingsKeys.AuthFlow]);\n        Assert.Equal(\"user@example.com\", profile.Settings[MailProfileSettingsKeys.LoginHint]);\n        Assert.True(profile.Settings.ContainsKey(MailProfileSettingsKeys.TokenExpiresOn));\n        Assert.Equal(\"https://login.microsoftonline.com/common/oauth2/nativeclient\", profile.Settings[MailProfileSettingsKeys.RedirectUri]);\n        Assert.Equal(\"graph-access-token\", accessToken);\n    }\n\n    [Fact]\n    public async Task RefreshAsyncRoutesToSavedGmailProfile() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret, \"client-secret\");\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore,\n            (request, _) => Task.FromResult(new OAuthCredential {\n                UserName = request.GmailAccount!,\n                AccessToken = \"gmail-access-token\",\n                RefreshToken = \"gmail-refresh-token\",\n                ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n            }));\n\n        var result = await service.RefreshAsync(\"gmail-work\");\n\n        var accessToken = await secretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken);\n        Assert.True(result.Succeeded);\n        Assert.Equal(MailProfileKind.Gmail, result.ProfileKind);\n        Assert.Equal(\"gmail-access-token\", accessToken);\n    }\n\n    [Fact]\n    public async Task RefreshAsyncReturnsUnsupportedForNonOauthProfiles() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"smtp-work\",\n                DisplayName = \"Work SMTP\",\n                Kind = MailProfileKind.Smtp\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileAuthService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.RefreshAsync(\"smtp-work\");\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"refresh_not_supported\", result.Code);\n    }\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles;\n\n        public InMemoryProfileStore(IEnumerable<MailProfile> profiles) {\n            _profiles = profiles.ToDictionary(profile => profile.Id, CloneProfile, StringComparer.OrdinalIgnoreCase);\n        }\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.Select(CloneProfile).ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult(profile == null ? null : CloneProfile(profile));\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = CloneProfile(profile);\n            return Task.CompletedTask;\n        }\n\n        private static MailProfile CloneProfile(MailProfile profile) => new() {\n            Id = profile.Id,\n            DisplayName = profile.DisplayName,\n            Description = profile.Description,\n            Kind = profile.Kind,\n            DefaultSender = profile.DefaultSender,\n            DefaultMailbox = profile.DefaultMailbox,\n            IsDefault = profile.IsDefault,\n            Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n            Capabilities = profile.Capabilities == null\n                ? null\n                : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n        };\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileBootstrapServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileBootstrapServiceTests {\n    [Fact]\n    public async Task SaveGraphProfileAsyncPersistsProfileSettingsAndSecrets() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileBootstrapService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.SaveGraphProfileAsync(new GraphProfileBootstrapRequest {\n            ProfileId = \"graph-work\",\n            DisplayName = \"Work Graph\",\n            Mailbox = \"shared@example.com\",\n            ClientId = \"client-id\",\n            TenantId = \"tenant-id\",\n            ClientSecret = \"client-secret\",\n            IsDefault = true\n        });\n\n        var profile = await profileStore.GetByIdAsync(\"graph-work\");\n        var clientSecret = await secretStore.GetSecretAsync(\"graph-work\", MailSecretNames.ClientSecret);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(profile);\n        Assert.Equal(MailProfileKind.Graph, profile!.Kind);\n        Assert.Equal(\"shared@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"shared@example.com\", profile.DefaultSender);\n        Assert.True(profile.IsDefault);\n        Assert.Equal(\"shared@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"tenant-id\", profile.Settings[MailProfileSettingsKeys.TenantId]);\n        Assert.Equal(\"client-secret\", clientSecret);\n    }\n\n    [Fact]\n    public async Task SaveGraphProfileAsyncSupportsSecretReferences() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.ClientSecret, \"shared-client-secret\");\n        var service = new MailProfileBootstrapService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.SaveGraphProfileAsync(new GraphProfileBootstrapRequest {\n            ProfileId = \"graph-work\",\n            DisplayName = \"Work Graph\",\n            Mailbox = \"shared@example.com\",\n            ClientId = \"client-id\",\n            TenantId = \"tenant-id\",\n            ClientSecretReference = $\"shared-secrets:{MailSecretNames.ClientSecret}\"\n        });\n\n        var clientSecret = await secretStore.GetSecretAsync(\"graph-work\", MailSecretNames.ClientSecret);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"shared-client-secret\", clientSecret);\n    }\n\n    [Fact]\n    public async Task SaveGraphProfileAsyncRequiresAuthenticationMaterial() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileBootstrapService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.SaveGraphProfileAsync(new GraphProfileBootstrapRequest {\n            ProfileId = \"graph-work\",\n            DisplayName = \"Work Graph\",\n            Mailbox = \"shared@example.com\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"graph_auth_required\", result.Code);\n    }\n\n    [Fact]\n    public async Task SaveGmailProfileAsyncPersistsProfileSettingsAndSecrets() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileBootstrapService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.SaveGmailProfileAsync(new GmailProfileBootstrapRequest {\n            ProfileId = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Mailbox = \"me@example.com\",\n            ClientId = \"client-id\",\n            ClientSecret = \"client-secret\",\n            RefreshToken = \"refresh-token\",\n            IsDefault = true\n        });\n\n        var profile = await profileStore.GetByIdAsync(\"gmail-work\");\n        var clientSecret = await secretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret);\n        var refreshToken = await secretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(profile);\n        Assert.Equal(MailProfileKind.Gmail, profile!.Kind);\n        Assert.Equal(\"me@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"me@example.com\", profile.DefaultSender);\n        Assert.True(profile.IsDefault);\n        Assert.Equal(\"me@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"client-secret\", clientSecret);\n        Assert.Equal(\"refresh-token\", refreshToken);\n    }\n\n    [Fact]\n    public async Task SaveGmailProfileAsyncRequiresAuthenticationMaterial() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        var service = new MailProfileBootstrapService(\n            new MailProfileService(profileStore, secretStore),\n            new MailProfileSecretService(profileStore, secretStore),\n            secretStore);\n\n        var result = await service.SaveGmailProfileAsync(new GmailProfileBootstrapRequest {\n            ProfileId = \"gmail-work\",\n            DisplayName = \"Work Gmail\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"gmail_auth_required\", result.Code);\n    }\n\n    [Fact]\n    public async Task DiagnoseAsyncReportsMissingGmailSecrets() {\n        var profileStore = new InMemoryProfileStore();\n        var secretStore = new InMemorySecretStore();\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"me@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        var service = new MailProfileService(profileStore, secretStore);\n\n        var result = await service.DiagnoseAsync(\"gmail-work\");\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"profile_not_ready\", result.Code);\n        Assert.Contains(result.Errors, error => error.IndexOf(\"Gmail profiles need an access token\", StringComparison.Ordinal) >= 0);\n    }\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult<MailProfile?>(profile);\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = profile;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileConnectionServiceTests.cs",
    "content": "using MailKit.Net.Imap;\nusing Mailozaurr;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileConnectionServiceTests {\n    [Fact]\n    public async Task TestAsyncUsesMailboxScopeByDefaultForGmail() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultMailbox = \"user@gmail.com\"\n            }\n        });\n        var authProbeCalls = 0;\n        var mailboxProbeCalls = 0;\n        var service = new MailProfileConnectionService(\n            profileStore,\n            gmailSessionFactory: new FakeGmailSessionFactory(),\n            probeGmailAsync: (_, _) => {\n                authProbeCalls++;\n                return Task.CompletedTask;\n            },\n            probeGmailMailboxAsync: (session, _) => {\n                mailboxProbeCalls++;\n                Assert.Equal(\"user@gmail.com\", session.UserId);\n                return Task.CompletedTask;\n            });\n\n        var result = await service.TestAsync(\"gmail-work\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(MailProfileConnectionTestScope.Auto, result.RequestedScope);\n        Assert.Equal(MailProfileConnectionTestScope.Mailbox, result.ExecutedScope);\n        Assert.Equal(\"listFolders\", result.Probe);\n        Assert.Equal(0, authProbeCalls);\n        Assert.Equal(1, mailboxProbeCalls);\n    }\n\n    [Fact]\n    public async Task TestAsyncUsesAuthScopeWhenRequestedForGraph() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"graph-work\",\n                DisplayName = \"Work Graph\",\n                Kind = MailProfileKind.Graph,\n                DefaultMailbox = \"user@example.com\"\n            }\n        });\n        var authProbeCalls = 0;\n        var mailboxProbeCalls = 0;\n        var service = new MailProfileConnectionService(\n            profileStore,\n            graphSessionFactory: new FakeGraphSessionFactory(),\n            probeGraphAsync: (_, _) => {\n                authProbeCalls++;\n                return Task.CompletedTask;\n            },\n            probeGraphMailboxAsync: (_, _) => {\n                mailboxProbeCalls++;\n                return Task.CompletedTask;\n            });\n\n        var result = await service.TestAsync(\"graph-work\", MailProfileConnectionTestScope.Auth);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(MailProfileConnectionTestScope.Auth, result.RequestedScope);\n        Assert.Equal(MailProfileConnectionTestScope.Auth, result.ExecutedScope);\n        Assert.Equal(\"connect\", result.Probe);\n        Assert.Equal(1, authProbeCalls);\n        Assert.Equal(0, mailboxProbeCalls);\n    }\n\n    [Fact]\n    public async Task TestAsyncFallsBackFromSendToAuthForImap() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"imap-work\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                DefaultMailbox = \"user@example.com\"\n            }\n        });\n        var service = new MailProfileConnectionService(\n            profileStore,\n            imapSessionFactory: new FakeImapSessionFactory(),\n            probeImapAsync: (_, _) => Task.CompletedTask,\n            probeImapMailboxAsync: (_, _) => Task.CompletedTask);\n\n        var result = await service.TestAsync(\"imap-work\", MailProfileConnectionTestScope.Send);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(MailProfileConnectionTestScope.Send, result.RequestedScope);\n        Assert.Equal(MailProfileConnectionTestScope.Auth, result.ExecutedScope);\n        Assert.Equal(\"connect\", result.Probe);\n    }\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles;\n\n        public InMemoryProfileStore(IEnumerable<MailProfile> profiles) {\n            _profiles = profiles.ToDictionary(profile => profile.Id, CloneProfile, StringComparer.OrdinalIgnoreCase);\n        }\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.Select(CloneProfile).ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult(profile == null ? null : CloneProfile(profile));\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = CloneProfile(profile);\n            return Task.CompletedTask;\n        }\n\n        private static MailProfile CloneProfile(MailProfile profile) => new() {\n            Id = profile.Id,\n            DisplayName = profile.DisplayName,\n            Description = profile.Description,\n            Kind = profile.Kind,\n            DefaultSender = profile.DefaultSender,\n            DefaultMailbox = profile.DefaultMailbox,\n            IsDefault = profile.IsDefault,\n            Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n            Capabilities = profile.Capabilities == null\n                ? null\n                : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n        };\n    }\n\n    private sealed class FakeImapSessionFactory : IImapSessionFactory {\n        public Task<ImapClient> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new ImapClient());\n    }\n\n    private sealed class FakeGraphSessionFactory : IGraphSessionFactory {\n        public Task<GraphSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GraphSession(new GraphApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n\n    private sealed class FakeGmailSessionFactory : IGmailSessionFactory {\n        public Task<GmailSession> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new GmailSession(new GmailApiClient(new OAuthCredential {\n                UserName = profile.DefaultMailbox ?? \"me\",\n                AccessToken = \"token\",\n                ExpiresOn = DateTimeOffset.MaxValue\n            }), profile.DefaultMailbox ?? \"me\"));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileOverviewServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileOverviewServiceTests {\n    [Fact]\n    public async Task GetOverviewAggregatesCapabilitiesAuthAndReadiness() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-work\",\n                DisplayName = \"Work Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultSender = \"user@example.com\",\n                DefaultMailbox = \"user@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"token\");\n        var profiles = new MailProfileService(profileStore, secretStore);\n        var auth = new FakeProfileAuthService();\n        var service = new MailProfileOverviewService(profiles, auth);\n\n        var overview = await service.GetOverviewAsync(\"gmail-work\");\n\n        Assert.NotNull(overview);\n        Assert.Equal(\"gmail-work\", overview!.Profile.Id);\n        Assert.True(overview.SupportsRead);\n        Assert.True(overview.SupportsSend);\n        Assert.True(overview.IsReady);\n        Assert.Equal(0, overview.ErrorCount);\n        Assert.Equal(0, overview.WarningCount);\n        Assert.NotNull(overview.Capabilities);\n        Assert.NotNull(overview.AuthStatus);\n        Assert.Contains(\"read=yes\", overview.Summary, StringComparison.Ordinal);\n        Assert.Contains(\"send=yes\", overview.Summary, StringComparison.Ordinal);\n        Assert.Contains(\"auth=interactive\", overview.Summary, StringComparison.Ordinal);\n        Assert.Contains(\"readiness=ready\", overview.Summary, StringComparison.Ordinal);\n        Assert.Equal(\"gmail-work\", auth.LastProfileId);\n    }\n\n    [Fact]\n    public async Task GetOverviewReturnsNullWhenProfileDoesNotExist() {\n        var profiles = new MailProfileService(new InMemoryProfileStore(Array.Empty<MailProfile>()), new InMemorySecretStore());\n        var service = new MailProfileOverviewService(profiles, new FakeProfileAuthService());\n\n        var overview = await service.GetOverviewAsync(\"missing\");\n\n        Assert.Null(overview);\n    }\n\n    [Fact]\n    public async Task GetOverviewsReturnsAggregatedSummariesForAllProfiles() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"imap-work\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                IsDefault = true,\n                DefaultMailbox = \"work@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                }\n            },\n            new MailProfile {\n                Id = \"smtp-alerts\",\n                DisplayName = \"Alerts SMTP\",\n                Kind = MailProfileKind.Smtp,\n                DefaultSender = \"alerts@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n                }\n            }\n        });\n        var profiles = new MailProfileService(profileStore, new InMemorySecretStore());\n        var auth = new FakeProfileAuthService();\n        var service = new MailProfileOverviewService(profiles, auth);\n\n        var overviews = await service.GetOverviewsAsync();\n\n        Assert.Equal(2, overviews.Count);\n        Assert.Contains(overviews, overview => overview.Profile.Id == \"imap-work\" && overview.SupportsRead);\n        Assert.Contains(overviews, overview => overview.Profile.Id == \"smtp-alerts\" && overview.SupportsSend);\n    }\n\n    [Fact]\n    public async Task GetOverviewsAppliesSharedFilters() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"imap-work\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                IsDefault = true,\n                DefaultMailbox = \"work@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                }\n            },\n            new MailProfile {\n                Id = \"smtp-alerts\",\n                DisplayName = \"Alerts SMTP\",\n                Kind = MailProfileKind.Smtp,\n                DefaultSender = \"alerts@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n                }\n            }\n        });\n        var profiles = new MailProfileService(profileStore, new InMemorySecretStore());\n        var service = new MailProfileOverviewService(profiles, new FakeProfileAuthService());\n\n        var overviews = await service.GetOverviewsAsync(new MailProfileOverviewQuery {\n            CanSendOnly = true,\n            DefaultOnly = false,\n            Kind = MailProfileKind.Smtp\n        });\n\n        var overview = Assert.Single(overviews);\n        Assert.Equal(\"smtp-alerts\", overview.Profile.Id);\n        Assert.True(overview.SupportsSend);\n    }\n\n    [Fact]\n    public async Task GetOverviewsSupportsReadinessSorting() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"gmail-broken\",\n                DisplayName = \"Broken Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultMailbox = \"broken@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Mailbox] = \"broken@example.com\",\n                    [MailProfileSettingsKeys.ClientId] = \"client-id\"\n                }\n            },\n            new MailProfile {\n                Id = \"smtp-ready\",\n                DisplayName = \"Ready SMTP\",\n                Kind = MailProfileKind.Smtp,\n                DefaultSender = \"alerts@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n                }\n            }\n        });\n        var secretStore = new InMemorySecretStore();\n        var profiles = new MailProfileService(profileStore, secretStore);\n        var service = new MailProfileOverviewService(profiles, new FakeProfileAuthService());\n\n        var overviews = await service.GetOverviewsAsync(new MailProfileOverviewQuery {\n            SortBy = MailProfileOverviewSortBy.Readiness\n        });\n\n        Assert.Equal(2, overviews.Count);\n        Assert.Equal(\"gmail-broken\", overviews[0].Profile.Id);\n        Assert.False(overviews[0].IsReady);\n        Assert.Equal(\"smtp-ready\", overviews[1].Profile.Id);\n        Assert.True(overviews[1].IsReady);\n    }\n\n    [Fact]\n    public async Task GetCompactOverviewsReturnsLightweightProjection() {\n        var profileStore = new InMemoryProfileStore(new[] {\n            new MailProfile {\n                Id = \"imap-work\",\n                DisplayName = \"Work IMAP\",\n                Kind = MailProfileKind.Imap,\n                DefaultMailbox = \"work@example.com\",\n                Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                    [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                }\n            }\n        });\n        var profiles = new MailProfileService(profileStore, new InMemorySecretStore());\n        var service = new MailProfileOverviewService(profiles, new FakeProfileAuthService());\n\n        var overviews = await service.GetCompactOverviewsAsync();\n\n        var overview = Assert.Single(overviews);\n        Assert.Equal(\"imap-work\", overview.Id);\n        Assert.Equal(\"Work IMAP\", overview.DisplayName);\n        Assert.Equal(MailProfileKind.Imap, overview.Kind);\n        Assert.True(overview.SupportsRead);\n        Assert.False(overview.SupportsSend);\n    }\n\n    private sealed class FakeProfileAuthService : IMailProfileAuthService {\n        public string? LastProfileId { get; private set; }\n\n        public Task<MailProfileAuthStatus?> GetStatusAsync(string profileId, CancellationToken cancellationToken = default) {\n            LastProfileId = profileId;\n            return Task.FromResult<MailProfileAuthStatus?>(new MailProfileAuthStatus {\n                ProfileId = profileId,\n                ProfileKind = MailProfileKind.Gmail,\n                Mode = \"interactive\",\n                Mailbox = \"user@example.com\",\n                HasAccessToken = true,\n                CanRefresh = true,\n                CanLoginInteractively = true,\n                Summary = $\"{profileId} auth status available.\"\n            });\n        }\n\n        public Task<MailProfileAuthenticationResult> LoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<MailProfileAuthenticationResult> LoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n\n        public Task<MailProfileAuthenticationResult> RefreshAsync(string profileId, CancellationToken cancellationToken = default) =>\n            throw new NotSupportedException();\n    }\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles;\n\n        public InMemoryProfileStore(IEnumerable<MailProfile> profiles) {\n            _profiles = profiles.ToDictionary(profile => profile.Id, CloneProfile, StringComparer.OrdinalIgnoreCase);\n        }\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.Select(CloneProfile).ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult(profile == null ? null : CloneProfile(profile));\n        }\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = CloneProfile(profile);\n            return Task.CompletedTask;\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        private static MailProfile CloneProfile(MailProfile profile) => new() {\n            Id = profile.Id,\n            DisplayName = profile.DisplayName,\n            Description = profile.Description,\n            Kind = profile.Kind,\n            DefaultSender = profile.DefaultSender,\n            DefaultMailbox = profile.DefaultMailbox,\n            IsDefault = profile.IsDefault,\n            Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n            Capabilities = profile.Capabilities == null\n                ? null\n                : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n        };\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _secrets = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _secrets.TryGetValue(CreateKey(profileId, secretName), out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _secrets[CreateKey(profileId, secretName)] = secretValue;\n            return Task.CompletedTask;\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_secrets.Remove(CreateKey(profileId, secretName)));\n\n        private static string CreateKey(string profileId, string secretName) => $\"{profileId}::{secretName}\";\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileSecretServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileSecretServiceTests {\n    [Fact]\n    public async Task SetSecretAsyncRequiresExistingProfile() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var secretStore = new FileMailSecretStore(CreateTemporaryFilePath(\"secrets.json\"), new TestCredentialProtector());\n        var service = new MailProfileSecretService(profileStore, secretStore);\n\n        var result = await service.SetSecretAsync(\"missing\", MailSecretNames.Password, \"secret\");\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"profile_not_found\", result.Code);\n    }\n\n    [Fact]\n    public async Task SetSecretAsyncPersistsSecretForExistingProfile() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var secretStore = new FileMailSecretStore(CreateTemporaryFilePath(\"secrets.json\"), new TestCredentialProtector());\n        var service = new MailProfileSecretService(profileStore, secretStore);\n\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n\n        var result = await service.SetSecretAsync(\"work-imap\", MailSecretNames.Password, \"secret\");\n        var stored = await secretStore.GetSecretAsync(\"work-imap\", MailSecretNames.Password);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"secret\", stored);\n    }\n\n    [Fact]\n    public async Task SetSecretAsyncSupportsReferenceCopyForExistingProfile() {\n        var profileStore = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var secretStore = new FileMailSecretStore(CreateTemporaryFilePath(\"secrets.json\"), new TestCredentialProtector());\n        var service = new MailProfileSecretService(profileStore, secretStore);\n\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n            }\n        });\n        await profileStore.SaveAsync(new MailProfile {\n            Id = \"shared-secrets\",\n            DisplayName = \"Shared Secrets\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await secretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.Password, \"copied-secret\");\n\n        var result = await service.SetSecretAsync(\"work-imap\", MailSecretNames.Password, null, $\"shared-secrets:{MailSecretNames.Password}\");\n        var stored = await secretStore.GetSecretAsync(\"work-imap\", MailSecretNames.Password);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"copied-secret\", stored);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class TestCredentialProtector : ICredentialProtector {\n        public string Protect(string plainText) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($\"protected::{plainText}\"));\n\n        public string Unprotect(string protectedData) {\n            var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(protectedData));\n            return decoded.StartsWith(\"protected::\", StringComparison.Ordinal)\n                ? decoded.Substring(\"protected::\".Length)\n                : decoded;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileServiceTests {\n    [Fact]\n    public async Task SaveAsyncReturnsValidationErrorForInvalidProfile() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var service = new MailProfileService(store);\n\n        var result = await service.SaveAsync(new MailProfile());\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"profile_invalid\", result.Code);\n    }\n\n    [Fact]\n    public async Task SetDefaultAsyncMarksRequestedProfileAsDefault() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        var service = new MailProfileService(store);\n\n        await service.SaveAsync(new MailProfile {\n            Id = \"imap\",\n            DisplayName = \"IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n        await service.SaveAsync(new MailProfile {\n            Id = \"graph\",\n            DisplayName = \"Graph\",\n            Kind = MailProfileKind.Graph,\n            DefaultMailbox = \"user@example.com\"\n        });\n\n        var result = await service.SetDefaultAsync(\"graph\");\n        var profiles = await service.GetProfilesAsync();\n\n        Assert.True(result.Succeeded);\n        Assert.False(profiles.Single(p => p.Id == \"imap\").IsDefault);\n        Assert.True(profiles.Single(p => p.Id == \"graph\").IsDefault);\n    }\n\n    [Fact]\n    public async Task DeleteAsyncRemovesKnownSecretsWhenSecretStoreIsProvided() {\n        var profilePath = CreateTemporaryFilePath(\"profiles.json\");\n        var secretPath = CreateTemporaryFilePath(\"secrets.json\");\n        var store = new FileMailProfileStore(profilePath);\n        var secretStore = new FileMailSecretStore(secretPath, new TestCredentialProtector());\n        var service = new MailProfileService(store, secretStore);\n\n        await service.SaveAsync(new MailProfile {\n            Id = \"smtp\",\n            DisplayName = \"SMTP\",\n            Kind = MailProfileKind.Smtp,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"smtp.example.com\" }\n        });\n        await secretStore.SetSecretAsync(\"smtp\", MailSecretNames.Password, \"secret\");\n\n        var result = await service.DeleteAsync(\"smtp\");\n        var secret = await secretStore.GetSecretAsync(\"smtp\", MailSecretNames.Password);\n\n        Assert.True(result.Succeeded);\n        Assert.Null(secret);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class TestCredentialProtector : ICredentialProtector {\n        public string Protect(string plainText) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($\"protected::{plainText}\"));\n\n        public string Unprotect(string protectedData) {\n            var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(protectedData));\n            return decoded.StartsWith(\"protected::\", StringComparison.Ordinal)\n                ? decoded.Substring(\"protected::\".Length)\n                : decoded;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationProfileStoreTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationProfileStoreTests {\n    [Fact]\n    public async Task FileProfileStoreRoundTripsProfiles() {\n        var filePath = CreateTemporaryFilePath();\n        var store = new FileMailProfileStore(filePath);\n\n        var profile = new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [\"server\"] = \"imap.example.com\"\n            }\n        };\n\n        await store.SaveAsync(profile);\n        var loaded = await store.GetByIdAsync(profile.Id);\n        var all = await store.GetAllAsync();\n\n        Assert.NotNull(loaded);\n        Assert.Equal(\"Work IMAP\", loaded!.DisplayName);\n        Assert.Equal(\"imap.example.com\", loaded.Settings[\"server\"]);\n        Assert.Single(all);\n    }\n\n    [Fact]\n    public async Task SavingDefaultProfileClearsPreviousDefault() {\n        var filePath = CreateTemporaryFilePath();\n        var store = new FileMailProfileStore(filePath);\n\n        await store.SaveAsync(new MailProfile {\n            Id = \"one\",\n            DisplayName = \"One\",\n            Kind = MailProfileKind.Imap,\n            IsDefault = true\n        });\n        await store.SaveAsync(new MailProfile {\n            Id = \"two\",\n            DisplayName = \"Two\",\n            Kind = MailProfileKind.Graph,\n            IsDefault = true\n        });\n\n        var all = await store.GetAllAsync();\n\n        Assert.Equal(2, all.Count);\n        Assert.False(all.Single(p => p.Id == \"one\").IsDefault);\n        Assert.True(all.Single(p => p.Id == \"two\").IsDefault);\n    }\n\n    [Fact]\n    public async Task RemoveReturnsFalseWhenProfileDoesNotExist() {\n        var filePath = CreateTemporaryFilePath();\n        var store = new FileMailProfileStore(filePath);\n\n        var removed = await store.RemoveAsync(\"missing\");\n\n        Assert.False(removed);\n    }\n\n    private static string CreateTemporaryFilePath() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, \"profiles.json\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationQueueServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationQueueServiceTests {\n    [Fact]\n    public async Task ListAsyncReturnsQueuedMessagesInScheduleOrder() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n        await repository.SaveAsync(new PendingMessageRecord {\n            MessageId = \"b\",\n            Provider = EmailProvider.Gmail,\n            Timestamp = DateTimeOffset.UtcNow.AddMinutes(-10),\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(10),\n            AttemptCount = 1,\n            MimeMessage = Convert.ToBase64String(Array.Empty<byte>())\n        });\n        await repository.SaveAsync(new PendingMessageRecord {\n            MessageId = \"a\",\n            Provider = EmailProvider.None,\n            Timestamp = DateTimeOffset.UtcNow.AddMinutes(-20),\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1),\n            AttemptCount = 0,\n            MimeMessage = Convert.ToBase64String(Array.Empty<byte>())\n        });\n\n        var service = new PendingMailQueueService(repository);\n        var queued = await service.ListAsync();\n\n        Assert.Equal(2, queued.Count);\n        Assert.Equal(\"a\", queued[0].MessageId);\n        Assert.Equal(MailProfileKind.Smtp, queued[0].ProfileKind);\n        Assert.Equal(MailProfileKind.Gmail, queued[1].ProfileKind);\n    }\n\n    [Fact]\n    public async Task RemoveAsyncReturnsFailureWhenMessageDoesNotExist() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n\n        var service = new PendingMailQueueService(repository);\n        var result = await service.RemoveAsync(\"missing\");\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"queue_message_not_found\", result.Code);\n    }\n\n    [Fact]\n    public async Task ProcessAsyncReturnsObserverCounts() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n        var record = new PendingMessageRecord {\n            MessageId = \"queued-1\",\n            Provider = EmailProvider.Gmail,\n            Timestamp = DateTimeOffset.UtcNow.AddMinutes(-30),\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(-1),\n            AttemptCount = 0,\n            MimeMessage = Convert.ToBase64String(Array.Empty<byte>())\n        };\n        await repository.SaveAsync(record);\n\n        var service = new PendingMailQueueService(\n            repository,\n            (pendingRepository, observer, cancellationToken) => {\n                observer.MessageAttemptStarted(record, 1);\n                observer.MessageSent(record, 1, TimeSpan.FromSeconds(1));\n                return Task.CompletedTask;\n            });\n\n        var result = await service.ProcessAsync();\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, result.AttemptedCount);\n        Assert.Equal(1, result.SentCount);\n        Assert.Equal(0, result.FailedCount);\n    }\n\n    private static string CreateTemporaryDirectory() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return directory;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationRoutingServicesTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationRoutingServicesTests {\n    [Fact]\n    public async Task RoutedReadServiceDispatchesToMatchingHandler() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var results = await service.SearchAsync(new MailSearchRequest {\n            ProfileId = \"work-imap\",\n            QueryText = \"reports\"\n        });\n\n        Assert.Single(results);\n        Assert.Equal(\"msg-1\", results[0].Id);\n        Assert.Equal(1, handler.SearchCalls);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceBuildsCompactFolderProjection() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var results = await service.GetFoldersCompactAsync(new MailFolderQuery {\n            ProfileId = \"work-imap\"\n        });\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"inbox\", result.Id);\n        Assert.Equal(\"Inbox\", result.DisplayName);\n        Assert.Equal(\"inbox Inbox\", result.Summary);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceBuildsCompactSearchProjection() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var results = await service.SearchCompactAsync(new MailSearchRequest {\n            ProfileId = \"work-imap\",\n            QueryText = \"reports\"\n        });\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"msg-1\", result.Id);\n        Assert.Equal(\"reports\", result.Subject);\n        Assert.Equal(\"sender@example.com\", result.From);\n        Assert.Equal(\"msg-1 reports\", result.Summary);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceListsAttachmentsThroughSharedMessageProjection() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var results = await service.GetAttachmentsAsync(new ListAttachmentsRequest {\n            ProfileId = \"work-imap\",\n            MessageId = \"msg-1\"\n        });\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"att-1\", result.Id);\n        Assert.Equal(\"report.pdf\", result.FileName);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceBuildsCompactMessageProjection() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var result = await service.GetMessageCompactAsync(new GetMessageRequest {\n            ProfileId = \"work-imap\",\n            MessageId = \"msg-1\",\n            IncludeRawContent = true\n        });\n\n        Assert.NotNull(result);\n        Assert.Equal(\"msg-1\", result!.Id);\n        Assert.Equal(\"msg-1 Subject\", result.SummaryText);\n        Assert.True(result.HasRawContent);\n        Assert.Single(result.Attachments);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceBuildsBatchCompactMessageProjection() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var results = await service.GetMessagesCompactAsync(new GetMessagesRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"msg-1\", \"msg-2\" }\n        });\n\n        Assert.Equal(2, results.Count);\n        Assert.Equal(\"msg-1\", results[0].Id);\n        Assert.Equal(\"msg-2\", results[1].Id);\n        Assert.Equal(\"msg-1 Subject\", results[0].SummaryText);\n        Assert.Equal(\"msg-2 Subject\", results[1].SummaryText);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceSavesFilteredAttachmentsThroughSharedBatchWorkflow() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var result = await service.SaveAttachmentsAsync(new SaveAttachmentsRequest {\n            ProfileId = \"work-imap\",\n            MessageId = \"msg-1\",\n            DestinationPath = @\"C:\\Temp\",\n            FileNameContains = \"report\"\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, result.MatchedCount);\n        Assert.Equal(1, result.SavedCount);\n        Assert.Equal(0, result.FailedCount);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceSavesFilteredAttachmentsAcrossMultipleMessages() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Imap);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var result = await service.SaveAttachmentsManyAsync(new SaveAttachmentsManyRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"msg-1\", \"msg-2\" },\n            DestinationPath = @\"C:\\Temp\",\n            FileNameContains = \"report\"\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, result.RequestedMessageCount);\n        Assert.Equal(2, result.AttemptedMessageCount);\n        Assert.Equal(2, result.SucceededMessageCount);\n        Assert.Equal(2, result.MatchedCount);\n        Assert.Equal(2, result.SavedCount);\n        Assert.Equal(0, result.FailedCount);\n        Assert.Equal(2, result.MessageResults.Count);\n    }\n\n    [Fact]\n    public async Task RoutedSendServiceDispatchesToMatchingHandler() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-smtp\",\n            DisplayName = \"Work SMTP\",\n            Kind = MailProfileKind.Smtp,\n            DefaultSender = \"sender@example.com\",\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var handler = new FakeSendHandler(MailProfileKind.Smtp);\n        var service = new RoutedMailSendService(store, new[] { handler });\n\n        var result = await service.SendAsync(new SendMessageRequest {\n            ProfileId = \"work-smtp\",\n            Message = new DraftMessage {\n                ProfileId = \"work-smtp\",\n                Subject = \"Hello\"\n            }\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, handler.SendCalls);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceDispatchesToMatchingHandler() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.SetReadStateAsync(new SetReadStateRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"1\", \"2\" },\n            IsRead = true\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, handler.SetReadStateCalls);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceRejectsMismatchedReadStateConfirmationToken() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.SetReadStateAsync(new SetReadStateRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"1\", \"2\" },\n            IsRead = true,\n            ConfirmationToken = \"mact_v1_invalid\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"confirmation_token_mismatch\", result.Code);\n        Assert.Equal(0, handler.SetReadStateCalls);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceDispatchesFlaggedStateToMatchingHandler() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.SetFlaggedStateAsync(new SetFlaggedStateRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"1\", \"2\" },\n            IsFlagged = true\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, handler.SetFlaggedStateCalls);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceRejectsMismatchedFlaggedStateConfirmationToken() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.SetFlaggedStateAsync(new SetFlaggedStateRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"1\", \"2\" },\n            IsFlagged = true,\n            ConfirmationToken = \"mact_v1_invalid\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"confirmation_token_mismatch\", result.Code);\n        Assert.Equal(0, handler.SetFlaggedStateCalls);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceCanonicalizesKnownFolderAlias() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.MoveAsync(new MoveMessagesRequest {\n            ProfileId = \"work-imap\",\n            MessageIds = { \"1\", \"2\" },\n            DestinationFolderId = \"archive\"\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(handler.LastMoveRequest);\n        Assert.Equal(MailFolderAliases.Archive, handler.LastMoveRequest!.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceResolvesKnownFolderAliasThroughSharedAliasService() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var aliasService = new FakeFolderAliasService();\n        var service = new RoutedMailMessageActionService(store, new[] { handler }, aliasService);\n\n        var result = await service.MoveAsync(new MoveMessagesRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            MessageIds = { \"1\", \"2\" },\n            DestinationFolderId = MailFolderAliases.Archive\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"work-imap\", aliasService.LastProfileId);\n        Assert.Equal(\"shared@example.com\", aliasService.LastMailboxId);\n        Assert.NotNull(handler.LastMoveRequest);\n        Assert.Equal(\"archive-folder\", handler.LastMoveRequest!.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceRejectsMismatchedMoveConfirmationToken() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var aliasService = new FakeFolderAliasService();\n        var service = new RoutedMailMessageActionService(store, new[] { handler }, aliasService);\n\n        var result = await service.MoveAsync(new MoveMessagesRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"1\", \"2\" },\n            DestinationFolderId = MailFolderAliases.Archive,\n            ConfirmationToken = \"mact_v1_invalid\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"confirmation_token_mismatch\", result.Code);\n        Assert.Null(handler.LastMoveRequest);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceAcceptsMatchingMoveConfirmationToken() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var aliasService = new FakeFolderAliasService();\n        var service = new RoutedMailMessageActionService(store, new[] { handler }, aliasService);\n        var confirmationToken = MessageActionConfirmationTokens.CreateMoveToken(\n            \"work-imap\",\n            \"shared@example.com\",\n            \"Inbox\",\n            new[] { \"1\", \"2\" },\n            \"archive-folder\");\n\n        var result = await service.MoveAsync(new MoveMessagesRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"1\", \"2\" },\n            DestinationFolderId = MailFolderAliases.Archive,\n            ConfirmationToken = confirmationToken\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(handler.LastMoveRequest);\n        Assert.Equal(confirmationToken, handler.LastMoveRequest!.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceAcceptsPreviewGeneratedDeleteConfirmationTokenWithCaseDistinctIds() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var previewService = new MailMessageActionPreviewService(store, new FakeFolderAliasService());\n        var preview = await previewService.PreviewDeleteAsync(new DeleteMessagesPreviewRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"msg-2\" }\n        });\n\n        Assert.True(preview.Succeeded);\n        Assert.NotNull(preview.ConfirmationToken);\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler }, new FakeFolderAliasService());\n        var result = await service.DeleteAsync(new DeleteMessagesRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"msg-1\", \"MSG-1\", \"msg-2\" },\n            ConfirmationToken = preview.ConfirmationToken\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(handler.LastDeleteRequest);\n        Assert.Equal(preview.ConfirmationToken, handler.LastDeleteRequest!.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task RoutedMessageActionServiceRejectsMismatchedDeleteConfirmationToken() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-imap\",\n            DisplayName = \"Work IMAP\",\n            Kind = MailProfileKind.Imap,\n            Settings = new Dictionary<string, string> { [MailProfileSettingsKeys.Server] = \"imap.example.com\" }\n        });\n\n        var handler = new FakeMessageActionHandler(MailProfileKind.Imap);\n        var service = new RoutedMailMessageActionService(store, new[] { handler });\n\n        var result = await service.DeleteAsync(new DeleteMessagesRequest {\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            MessageIds = { \"1\", \"2\" },\n            ConfirmationToken = \"mact_v1_invalid\"\n        });\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"confirmation_token_mismatch\", result.Code);\n    }\n\n    [Fact]\n    public async Task RoutedReadServiceRejectsUnsupportedCapabilities() {\n        var store = new FileMailProfileStore(CreateTemporaryFilePath(\"profiles.json\"));\n        await store.SaveAsync(new MailProfile {\n            Id = \"work-smtp\",\n            DisplayName = \"Work SMTP\",\n            Kind = MailProfileKind.Smtp,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var handler = new FakeReadHandler(MailProfileKind.Smtp);\n        var service = new RoutedMailReadService(store, new[] { handler });\n\n        var exception = await Assert.ThrowsAsync<NotSupportedException>(() => service.SearchAsync(new MailSearchRequest {\n            ProfileId = \"work-smtp\"\n        }));\n\n        Assert.Contains(\"SearchMessages\", exception.Message, StringComparison.Ordinal);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n\n    private sealed class FakeReadHandler : IMailReadHandler {\n        public FakeReadHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public int SearchCalls { get; private set; }\n\n        public int SaveAttachmentCalls { get; private set; }\n\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailProfile profile, MailFolderQuery query, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = profile.Id,\n                    MailboxId = query.MailboxId,\n                    Id = \"inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\"\n                }\n            });\n\n        public Task<MessageDetail?> GetMessageAsync(MailProfile profile, GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(new MessageDetail {\n                ProfileId = profile.Id,\n                Id = request.MessageId,\n                Summary = new MessageSummary {\n                    ProfileId = profile.Id,\n                    Id = request.MessageId,\n                    Subject = \"Subject\"\n                },\n                TextBody = \"Body\",\n                Attachments = {\n                    new AttachmentSummary {\n                        MessageId = request.MessageId,\n                        Id = \"att-1\",\n                        FileName = \"report.pdf\",\n                        SizeInBytes = 1024\n                    }\n                },\n                RawContent = request.IncludeRawContent ? \"raw\" : null\n            });\n\n        public Task<OperationResult> SaveAttachmentAsync(MailProfile profile, SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n            SaveAttachmentCalls++;\n            return Task.FromResult(OperationResult.Success());\n        }\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailProfile profile, MailSearchRequest request, CancellationToken cancellationToken = default) {\n            SearchCalls++;\n            IReadOnlyList<MessageSummary> results = new[] {\n                new MessageSummary {\n                    ProfileId = profile.Id,\n                    Id = \"msg-1\",\n                    Subject = request.QueryText,\n                    From = {\n                        new MessageRecipient {\n                            Address = \"sender@example.com\"\n                        }\n                    }\n                }\n            };\n            return Task.FromResult(results);\n        }\n    }\n\n    private sealed class FakeSendHandler : IMailSendHandler {\n        public FakeSendHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public int SendCalls { get; private set; }\n\n        public Task<SendResult> SendAsync(MailProfile profile, SendMessageRequest request, CancellationToken cancellationToken = default) {\n            SendCalls++;\n            return Task.FromResult(new SendResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind\n            });\n        }\n    }\n\n    private sealed class FakeMessageActionHandler : IMailMessageActionHandler {\n        public FakeMessageActionHandler(MailProfileKind kind) {\n            Kind = kind;\n        }\n\n        public MailProfileKind Kind { get; }\n\n        public int SetReadStateCalls { get; private set; }\n\n        public int SetFlaggedStateCalls { get; private set; }\n\n        public MoveMessagesRequest? LastMoveRequest { get; private set; }\n\n        public DeleteMessagesRequest? LastDeleteRequest { get; private set; }\n\n        public Task<MessageActionResult> SetReadStateAsync(MailProfile profile, SetReadStateRequest request, CancellationToken cancellationToken = default) {\n            SetReadStateCalls++;\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n        }\n\n        public Task<MessageActionResult> SetFlaggedStateAsync(MailProfile profile, SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n            SetFlaggedStateCalls++;\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n        }\n\n        public Task<MessageActionResult> MoveAsync(MailProfile profile, MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastMoveRequest = request;\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n        }\n\n        public Task<MessageActionResult> DeleteAsync(MailProfile profile, DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastDeleteRequest = request;\n            return Task.FromResult(new MessageActionResult {\n                Succeeded = true,\n                ProfileId = profile.Id,\n                RequestedCount = request.MessageIds.Count,\n                SucceededCount = request.MessageIds.Count\n            });\n        }\n    }\n\n    private sealed class FakeFolderAliasService : IMailFolderAliasService {\n        public string? LastProfileId { get; private set; }\n\n        public string? LastMailboxId { get; private set; }\n\n        public Task<IReadOnlyList<MailFolderAliasSummary>> GetAliasesAsync(string profileId, string? mailboxId = null, CancellationToken cancellationToken = default) {\n            LastProfileId = profileId;\n            LastMailboxId = mailboxId;\n            return Task.FromResult<IReadOnlyList<MailFolderAliasSummary>>(new[] {\n                new MailFolderAliasSummary {\n                    ProfileId = profileId,\n                    MailboxId = mailboxId,\n                    Alias = MailFolderAliases.Archive,\n                    DisplayName = \"Archive\",\n                    IsSupported = true,\n                    IsResolved = true,\n                    FolderId = \"archive-folder\",\n                    FolderDisplayName = \"Archive\",\n                    FolderPath = \"Archive\",\n                    Summary = \"Archive -> Archive\"\n                }\n            });\n        }\n\n        public Task<MailFolderTargetResolution> ResolveAsync(string profileId, string targetFolderId, string? mailboxId = null, CancellationToken cancellationToken = default) {\n            LastProfileId = profileId;\n            LastMailboxId = mailboxId;\n            return Task.FromResult(new MailFolderTargetResolution {\n                ProfileId = profileId,\n                MailboxId = mailboxId,\n                RequestedValue = targetFolderId,\n                IsAlias = true,\n                Alias = MailFolderAliases.Archive,\n                IsSupported = true,\n                IsResolved = true,\n                EffectiveFolderId = \"archive-folder\",\n                FolderDisplayName = \"Archive\",\n                FolderPath = \"Archive\",\n                Summary = \"Archive -> Archive\"\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationSecretStoreTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationSecretStoreTests {\n    [Fact]\n    public async Task SecretStoreProtectsAndReturnsValues() {\n        var filePath = CreateTemporaryFilePath();\n        var protector = new TestCredentialProtector();\n        var store = new FileMailSecretStore(filePath, protector);\n\n        await store.SetSecretAsync(\"work-imap\", \"password\", \"super-secret\");\n\n        var loaded = await store.GetSecretAsync(\"work-imap\", \"password\");\n        var fileContent = File.ReadAllText(filePath);\n\n        Assert.Equal(\"super-secret\", loaded);\n        Assert.DoesNotContain(\"super-secret\", fileContent, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task RemoveSecretReturnsFalseWhenEntryDoesNotExist() {\n        var filePath = CreateTemporaryFilePath();\n        var protector = new TestCredentialProtector();\n        var store = new FileMailSecretStore(filePath, protector);\n\n        var removed = await store.RemoveSecretAsync(\"work-imap\", \"password\");\n\n        Assert.False(removed);\n    }\n\n    [Fact]\n    public async Task RemoveSecretDeletesStoredValue() {\n        var filePath = CreateTemporaryFilePath();\n        var protector = new TestCredentialProtector();\n        var store = new FileMailSecretStore(filePath, protector);\n\n        await store.SetSecretAsync(\"work-imap\", \"password\", \"super-secret\");\n        var removed = await store.RemoveSecretAsync(\"work-imap\", \"password\");\n        var loaded = await store.GetSecretAsync(\"work-imap\", \"password\");\n\n        Assert.True(removed);\n        Assert.Null(loaded);\n    }\n\n    [Fact]\n    public async Task UpdatingSecretReplacesFileWithoutLeavingTemporaryArtifacts() {\n        var filePath = CreateTemporaryFilePath();\n        var protector = new TestCredentialProtector();\n        var store = new FileMailSecretStore(filePath, protector);\n\n        await store.SetSecretAsync(\"work-imap\", \"password\", \"initial\");\n        await store.SetSecretAsync(\"work-imap\", \"password\", \"updated\");\n\n        var loaded = await store.GetSecretAsync(\"work-imap\", \"password\");\n        var files = Directory.GetFiles(Path.GetDirectoryName(filePath)!);\n\n        Assert.Equal(\"updated\", loaded);\n        Assert.Single(files);\n        Assert.Equal(filePath, files[0], StringComparer.OrdinalIgnoreCase);\n    }\n\n    private static string CreateTemporaryFilePath() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, \"secrets.json\");\n    }\n\n    private sealed class TestCredentialProtector : ICredentialProtector {\n        public string Protect(string plainText) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($\"protected::{plainText}\"));\n\n        public string Unprotect(string protectedData) {\n            var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(protectedData));\n            return decoded.StartsWith(\"protected::\", StringComparison.Ordinal)\n                ? decoded.Substring(\"protected::\".Length)\n                : decoded;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationSmtpMailSendHandlerTests.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationSmtpMailSendHandlerTests {\n    [Fact]\n    public async Task HandlerUsesInjectedSendDelegate() {\n        var handler = new SmtpMailSendHandler(\n            new FakeSmtpSessionFactory(),\n            sendAsync: (session, profile, request, message, cancellationToken) => {\n                Assert.Equal(\"sender@example.com\", message.From.Mailboxes.First().Address);\n                Assert.Equal(\"alice@example.com\", message.To.Mailboxes.First().Address);\n                Assert.Equal(\"Hello SMTP\", message.Subject);\n                return Task.FromResult(new SmtpResult(true, EmailAction.Send, \"alice@example.com\", \"sender@example.com\", \"smtp.example.com\", 587, TimeSpan.Zero) {\n                    MessageId = \"smtp-msg-123\"\n                });\n            });\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"work-smtp\",\n                DisplayName = \"Work SMTP\",\n                Kind = MailProfileKind.Smtp,\n                DefaultSender = \"sender@example.com\",\n                Settings = new Dictionary<string, string> {\n                    [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n                }\n            },\n            new SendMessageRequest {\n                ProfileId = \"work-smtp\",\n                Message = new DraftMessage {\n                    Subject = \"Hello SMTP\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.False(result.Queued);\n        Assert.Equal(\"work-smtp\", result.ProfileId);\n        Assert.Equal(MailProfileKind.Smtp, result.ProfileKind);\n        Assert.Equal(\"smtp-msg-123\", result.ProviderMessageId);\n    }\n\n    [Fact]\n    public async Task HandlerReturnsQueuedResultWhenRepositoryCapturesFailedSend() {\n        var repository = new FilePendingMessageRepository(new PendingMessageRepositoryOptions {\n            DirectoryPath = CreateTemporaryDirectory()\n        });\n\n        var handler = new SmtpMailSendHandler(\n            new FakeSmtpSessionFactory(),\n            pendingMessageRepository: repository,\n            sendAsync: async (session, profile, request, message, cancellationToken) => {\n                await repository.SaveAsync(new PendingMessageRecord {\n                    MessageId = message.MessageId ?? \"queued-1\",\n                    Timestamp = DateTimeOffset.UtcNow,\n                    NextAttemptAt = DateTimeOffset.UtcNow,\n                    Provider = EmailProvider.None,\n                    MimeMessage = Convert.ToBase64String(Array.Empty<byte>())\n                }, cancellationToken).ConfigureAwait(false);\n\n                return new SmtpResult(false, EmailAction.Send, \"alice@example.com\", \"sender@example.com\", \"smtp.example.com\", 587, TimeSpan.Zero, error: \"temporary smtp failure\") {\n                    MessageId = message.MessageId ?? \"queued-1\"\n                };\n            });\n\n        var result = await handler.SendAsync(\n            new MailProfile {\n                Id = \"work-smtp\",\n                DisplayName = \"Work SMTP\",\n                Kind = MailProfileKind.Smtp,\n                DefaultSender = \"sender@example.com\",\n                Settings = new Dictionary<string, string> {\n                    [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n                }\n            },\n            new SendMessageRequest {\n                ProfileId = \"work-smtp\",\n                PreferQueue = true,\n                Message = new DraftMessage {\n                    Subject = \"Hello SMTP\",\n                    TextBody = \"hello\",\n                    To = {\n                        new MessageRecipient { Address = \"alice@example.com\" }\n                    }\n                }\n            });\n\n        Assert.True(result.Succeeded);\n        Assert.True(result.Queued);\n        Assert.NotNull(result.QueueMessageId);\n        Assert.Contains(\"queued\", result.Message ?? string.Empty, StringComparison.OrdinalIgnoreCase);\n    }\n\n    private static string CreateTemporaryDirectory() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return directory;\n    }\n\n    private sealed class FakeSmtpSessionFactory : ISmtpSessionFactory {\n        public Task<Smtp> ConnectAsync(MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new Smtp());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ApplicationSmtpSessionFactoryTests.cs",
    "content": "using MailKit.Security;\nusing Mailozaurr;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ApplicationSmtpSessionFactoryTests {\n    [Fact]\n    public async Task FactoryBuildsBasicAuthSessionRequestFromProfileAndSecrets() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"work-smtp\", MailSecretNames.Password, \"super-secret\");\n\n        SmtpSessionRequest? captured = null;\n        var factory = new SmtpSessionFactory(secretStore, (request, _) => {\n            captured = request;\n            return Task.FromResult(new Smtp());\n        });\n\n        await factory.ConnectAsync(new MailProfile {\n            Id = \"work-smtp\",\n            DisplayName = \"Work SMTP\",\n            Kind = MailProfileKind.Smtp,\n            DefaultSender = \"sender@example.com\",\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\",\n                [MailProfileSettingsKeys.Port] = \"1587\",\n                [MailProfileSettingsKeys.SecureSocketOptions] = SecureSocketOptions.StartTls.ToString(),\n                [MailProfileSettingsKeys.UseSsl] = \"true\"\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(\"smtp.example.com\", captured!.Server);\n        Assert.Equal(1587, captured.Port);\n        Assert.Equal(SecureSocketOptions.StartTls, captured.SecureSocketOptions);\n        Assert.True(captured.UseSsl);\n        Assert.Equal(\"sender@example.com\", captured.UserName);\n        Assert.Equal(\"super-secret\", captured.Password);\n        Assert.Equal(ProtocolAuthMode.Basic, captured.AuthMode);\n    }\n\n    [Fact]\n    public async Task FactoryUsesOAuthAccessTokenWhenConfigured() {\n        var secretStore = new InMemorySecretStore();\n        await secretStore.SetSecretAsync(\"gmail-smtp\", MailSecretNames.AccessToken, \"oauth-token\");\n\n        SmtpSessionRequest? captured = null;\n        var factory = new SmtpSessionFactory(secretStore, (request, _) => {\n            captured = request;\n            return Task.FromResult(new Smtp());\n        });\n\n        await factory.ConnectAsync(new MailProfile {\n            Id = \"gmail-smtp\",\n            DisplayName = \"Gmail SMTP\",\n            Kind = MailProfileKind.Smtp,\n            Settings = new Dictionary<string, string> {\n                [MailProfileSettingsKeys.Server] = \"smtp.gmail.com\",\n                [MailProfileSettingsKeys.UserName] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.AuthMode] = \"oauth2\"\n            }\n        });\n\n        Assert.NotNull(captured);\n        Assert.Equal(ProtocolAuthMode.OAuth2, captured!.AuthMode);\n        Assert.Equal(\"oauth-token\", captured.Password);\n        Assert.Equal(\"user@gmail.com\", captured.UserName);\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _values.TryGetValue($\"{profileId}::{secretName}\", out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_values.Remove($\"{profileId}::{secretName}\"));\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _values[$\"{profileId}::{secretName}\"] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/CliRunnerTests.cs",
    "content": "#if NET8_0_OR_GREATER\nusing System.Text.Json;\nusing Mailozaurr.Application;\nusing Mailozaurr.Cli;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class CliRunnerTests {\n    [Fact]\n    public async Task HelpIsShownWhenNoArgumentsAreProvided() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(Array.Empty<string>(), stdout, stderr, _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"Mailozaurr CLI\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileListUsesApplicationProfilesService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"list\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"work-imap\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileListSummaryUsesSharedOverviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"list\", \"--summary\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"work-imap\", fixture.ProfileAuthService.LastStatusProfileId);\n        Assert.Contains(\"\\\"SupportsRead\\\": true\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Summary\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileListSummaryCompactUsesSharedCompactProjection() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"list\", \"--summary\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Id\\\": \\\"work-imap\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"Profile\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileListSummarySupportsSharedFilters() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"list\", \"--summary\", \"--kind\", \"imap\", \"--can-read\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Profile\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Kind\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"SupportsSend\\\": true\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileListSummarySupportsSharedSorting() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"z-smtp\",\n            DisplayName = \"SMTP Z\",\n            Kind = MailProfileKind.Smtp,\n            DefaultSender = \"alerts@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"list\", \"--summary\", \"--sort\", \"id\", \"--desc\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Id\\\": \\\"z-smtp\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.True(stdout.ToString().IndexOf(\"\\\"Id\\\": \\\"z-smtp\\\"\", StringComparison.Ordinal) <\n                    stdout.ToString().IndexOf(\"\\\"Id\\\": \\\"work-imap\\\"\", StringComparison.Ordinal));\n    }\n\n    [Fact]\n    public async Task ProfileCreateSavesProfileThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"create\",\n                \"--profile\", \"alerts-imap\",\n                \"--kind\", \"imap\",\n                \"--name\", \"Alerts IMAP\",\n                \"--default-mailbox\", \"alerts@example.com\",\n                \"--setting\", \"server=imap.alerts.example.com\",\n                \"--setting\", \"port=993\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var created = await fixture.ProfileStore.GetByIdAsync(\"alerts-imap\");\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(created);\n        Assert.Equal(MailProfileKind.Imap, created!.Kind);\n        Assert.Equal(\"Alerts IMAP\", created.DisplayName);\n        Assert.Equal(\"alerts@example.com\", created.DefaultMailbox);\n        Assert.Equal(\"imap.alerts.example.com\", created.Settings[MailProfileSettingsKeys.Server]);\n        Assert.Equal(\"993\", created.Settings[MailProfileSettingsKeys.Port]);\n    }\n\n    [Fact]\n    public async Task ProfileGraphBootstrapSavesProfileAndSecretsThroughApplicationServices() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"graph-bootstrap\",\n                \"--profile\", \"graph-work\",\n                \"--name\", \"Work Graph\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--client-id\", \"client-id\",\n                \"--tenant-id\", \"tenant-id\",\n                \"--client-secret\", \"client-secret\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"graph-work\");\n        var clientSecret = await fixture.SecretStore.GetSecretAsync(\"graph-work\", MailSecretNames.ClientSecret);\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(profile);\n        Assert.Equal(MailProfileKind.Graph, profile!.Kind);\n        Assert.Equal(\"shared@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"shared@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"tenant-id\", profile.Settings[MailProfileSettingsKeys.TenantId]);\n        Assert.Equal(\"client-secret\", clientSecret);\n    }\n\n    [Fact]\n    public async Task ProfileGraphBootstrapSupportsSecretValuesFromEnvironmentVariables() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        const string envName = \"MAILOZAURR_TEST_GRAPH_SECRET\";\n        Environment.SetEnvironmentVariable(envName, \"env-client-secret\");\n\n        try {\n            var exitCode = await CliRunner.RunAsync(\n                new[] {\n                    \"profile\", \"graph-bootstrap\",\n                    \"--profile\", \"graph-env\",\n                    \"--name\", \"Graph Env\",\n                    \"--mailbox\", \"shared@example.com\",\n                    \"--client-id\", \"client-id\",\n                    \"--tenant-id\", \"tenant-id\",\n                    \"--client-secret-env\", envName,\n                    \"--json\"\n                },\n                stdout,\n                stderr,\n                _ => fixture.CreateBuilder());\n\n            var clientSecret = await fixture.SecretStore.GetSecretAsync(\"graph-env\", MailSecretNames.ClientSecret);\n\n            Assert.Equal(0, exitCode);\n            Assert.Equal(\"env-client-secret\", clientSecret);\n        } finally {\n            Environment.SetEnvironmentVariable(envName, null);\n        }\n    }\n\n    [Fact]\n    public async Task ProfileGraphBootstrapSupportsSecretReferences() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"shared-secrets\",\n            DisplayName = \"Shared Secrets\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"shared-client\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.ClientSecret, \"shared-client-secret\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"graph-bootstrap\",\n                \"--profile\", \"graph-ref\",\n                \"--name\", \"Graph Ref\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--client-id\", \"client-id\",\n                \"--tenant-id\", \"tenant-id\",\n                \"--client-secret-ref\", $\"shared-secrets:{MailSecretNames.ClientSecret}\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var clientSecret = await fixture.SecretStore.GetSecretAsync(\"graph-ref\", MailSecretNames.ClientSecret);\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"shared-client-secret\", clientSecret);\n    }\n\n    [Fact]\n    public async Task ProfileGmailBootstrapSavesProfileAndSecretsThroughApplicationServices() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"gmail-bootstrap\",\n                \"--profile\", \"gmail-work\",\n                \"--name\", \"Work Gmail\",\n                \"--mailbox\", \"me@example.com\",\n                \"--client-id\", \"client-id\",\n                \"--client-secret\", \"client-secret\",\n                \"--refresh-token\", \"refresh-token\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"gmail-work\");\n        var clientSecret = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret);\n        var refreshToken = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken);\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(profile);\n        Assert.Equal(MailProfileKind.Gmail, profile!.Kind);\n        Assert.Equal(\"me@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"me@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"client-secret\", clientSecret);\n        Assert.Equal(\"refresh-token\", refreshToken);\n    }\n\n    [Fact]\n    public async Task ProfileSetSecretSupportsReadingValueFromStandardInput() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        using var stdin = new StringReader(\"stdin-secret\\r\\n\");\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"stdin-profile\",\n            DisplayName = \"stdin\",\n            Kind = MailProfileKind.Imap\n        });\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"set-secret\",\n                \"--profile\", \"stdin-profile\",\n                \"--name\", \"password\",\n                \"--value-stdin\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder(),\n            stdin);\n\n        var secret = await fixture.SecretStore.GetSecretAsync(\"stdin-profile\", \"password\");\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"stdin-secret\", secret);\n    }\n\n    [Fact]\n    public async Task JsonErrorsAreWrittenAsStructuredPayloads() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"set-secret\",\n                \"--profile\", \"missing-value\",\n                \"--name\", \"password\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        using var document = JsonDocument.Parse(stderr.ToString());\n\n        Assert.Equal(1, exitCode);\n        Assert.Equal(\"InvalidOperationException\", document.RootElement.GetProperty(\"Error\").GetProperty(\"Type\").GetString());\n        Assert.Contains(\"--value\", document.RootElement.GetProperty(\"Error\").GetProperty(\"Message\").GetString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileDoctorReportsMissingGmailAuthenticationMaterial() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-broken\",\n            DisplayName = \"Broken Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"me@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"doctor\", \"--profile\", \"gmail-broken\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(1, exitCode);\n        Assert.Contains(\"profile_not_ready\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"Gmail profiles need an access token\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileGmailLoginPersistsTokensThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-login\",\n            DisplayName = \"Gmail Login\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-login\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"gmail-login\", \"--profile\", \"gmail-login\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var accessToken = await fixture.SecretStore.GetSecretAsync(\"gmail-login\", MailSecretNames.AccessToken);\n        var refreshToken = await fixture.SecretStore.GetSecretAsync(\"gmail-login\", MailSecretNames.RefreshToken);\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"gmail-access-token\", accessToken);\n        Assert.Equal(\"gmail-refresh-token\", refreshToken);\n        Assert.NotNull(fixture.ProfileAuthService.LastGmailRequest);\n        Assert.Equal(\"user@gmail.com\", fixture.ProfileAuthService.LastGmailRequest!.GmailAccount);\n    }\n\n    [Fact]\n    public async Task ProfileGmailLoginSupportsClientSecretReference() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-login-ref\",\n            DisplayName = \"Gmail Login Ref\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.ClientSecret, \"client-secret-from-reference\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"gmail-login\",\n                \"--profile\", \"gmail-login-ref\",\n                \"--client-secret-ref\", $\"shared-secrets:{MailSecretNames.ClientSecret}\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ProfileAuthService.LastGmailRequest);\n        Assert.Equal(\"client-secret-from-reference\", fixture.ProfileAuthService.LastGmailRequest!.ClientSecret);\n    }\n\n    [Fact]\n    public async Task ProfileGraphLoginPersistsAccessTokenThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"graph-login\",\n            DisplayName = \"Graph Login\",\n            Kind = MailProfileKind.Graph,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\"\n            }\n        });\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"graph-login\", \"--profile\", \"graph-login\", \"--login\", \"user@example.com\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var accessToken = await fixture.SecretStore.GetSecretAsync(\"graph-login\", MailSecretNames.AccessToken);\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"graph-login\");\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"graph-access-token\", accessToken);\n        Assert.NotNull(profile);\n        Assert.Equal(\"user@example.com\", profile!.DefaultMailbox);\n        Assert.Equal(\"user@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"https://login.microsoftonline.com/common/oauth2/nativeclient\", profile.Settings[MailProfileSettingsKeys.RedirectUri]);\n        Assert.NotNull(fixture.ProfileAuthService.LastGraphRequest);\n        Assert.Equal(\"user@example.com\", fixture.ProfileAuthService.LastGraphRequest!.Login);\n    }\n\n    [Fact]\n    public async Task ProfileRefreshAuthDelegatesToSharedAuthService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-refresh\",\n            DisplayName = \"Gmail Refresh\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"refresh@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-refresh\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"refresh-auth\", \"--profile\", \"gmail-refresh\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(1, fixture.ProfileAuthService.RefreshCalls);\n        Assert.Equal(\"gmail-refresh\", fixture.ProfileAuthService.LastRefreshProfileId);\n        Assert.Contains(\"ProfileId\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileAuthStatusDelegatesToSharedAuthService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"auth-status\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"work-imap\", fixture.ProfileAuthService.LastStatusProfileId);\n        Assert.Contains(\"\\\"Mode\\\": \\\"basic\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileSummaryUsesSharedOverviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"summary\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"work-imap\", fixture.ProfileAuthService.LastStatusProfileId);\n        Assert.Contains(\"\\\"SupportsRead\\\": true\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"IsReady\\\": true\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileSummaryCompactUsesSharedCompactProjection() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"summary\", \"--profile\", \"work-imap\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Id\\\": \\\"work-imap\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"Profile\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileTestDelegatesToSharedConnectionService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"test\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"work-imap\", fixture.ProfileConnectionService.LastProfileId);\n        Assert.Contains(\"\\\"Probe\\\": \\\"connect\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileTestParsesRequestedScope() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"test\", \"--profile\", \"work-imap\", \"--scope\", \"send\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(MailProfileConnectionTestScope.Send, fixture.ProfileConnectionService.LastScope);\n        Assert.Contains(\"\\\"RequestedScope\\\": 3\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileCapabilitiesUsesApplicationProfileService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"capabilities\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Kind\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Capabilities\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task ProfileSetSecretPersistsSecretThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"set-secret\",\n                \"--profile\", \"work-imap\",\n                \"--name\", MailSecretNames.Password,\n                \"--value\", \"super-secret\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var secretValue = await fixture.SecretStore.GetSecretAsync(\"work-imap\", MailSecretNames.Password);\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"super-secret\", secretValue);\n    }\n\n    [Fact]\n    public async Task ProfileSetSecretSupportsReferenceCopy() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"shared-secrets\",\n            DisplayName = \"Shared Secrets\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"shared-client\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.Password, \"copied-secret\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"set-secret\",\n                \"--profile\", \"work-imap\",\n                \"--name\", MailSecretNames.Password,\n                \"--value-ref\", $\"shared-secrets:{MailSecretNames.Password}\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var secretValue = await fixture.SecretStore.GetSecretAsync(\"work-imap\", MailSecretNames.Password);\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"copied-secret\", secretValue);\n    }\n\n    [Fact]\n    public async Task ProfileRemoveSecretDeletesSecretThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        await fixture.SecretStore.SetSecretAsync(\"work-imap\", MailSecretNames.Password, \"super-secret\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"profile\", \"remove-secret\",\n                \"--profile\", \"work-imap\",\n                \"--name\", MailSecretNames.Password,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var secretValue = await fixture.SecretStore.GetSecretAsync(\"work-imap\", MailSecretNames.Password);\n\n        Assert.Equal(0, exitCode);\n        Assert.Null(secretValue);\n    }\n\n    [Fact]\n    public async Task ProfileDeleteRemovesProfileThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"delete\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"work-imap\");\n\n        Assert.Equal(0, exitCode);\n        Assert.Null(profile);\n    }\n\n    [Fact]\n    public async Task ProfileSetDefaultUpdatesProfileThroughApplicationService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"profile\", \"set-default\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"work-imap\");\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(profile);\n        Assert.True(profile!.IsDefault);\n    }\n\n    [Fact]\n    public async Task MailFoldersUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"folders\", \"--profile\", \"work-imap\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Inbox\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailFoldersCompactUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"folders\", \"--profile\", \"work-imap\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastFolderCompactQuery);\n        Assert.Equal(\"work-imap\", fixture.ReadService.LastFolderCompactQuery!.ProfileId);\n        Assert.Contains(\"\\\"Summary\\\": \\\"inbox Inbox\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailFolderAliasesUseSharedFolderAliasService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"folder-aliases\", \"--profile\", \"work-imap\", \"--mailbox\", \"shared@example.com\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastFolderQuery);\n        Assert.Equal(\"work-imap\", fixture.ReadService.LastFolderQuery!.ProfileId);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastFolderQuery.MailboxId);\n        Assert.Contains(\"\\\"Alias\\\": \\\"Archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"IsResolved\\\": true\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailResolveFolderUsesSharedFolderAliasService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"resolve-folder\", \"--profile\", \"work-imap\", \"--mailbox\", \"shared@example.com\", \"--target-folder\", \"archive\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastFolderQuery);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastFolderQuery!.MailboxId);\n        Assert.Contains(\"\\\"Alias\\\": \\\"Archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"EffectiveFolderId\\\": \\\"archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPreviewMoveUsesSharedPreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-move\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--target-folder\", \"archive\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"UniqueMessageCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"EffectiveFolderId\\\": \\\"archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"ConfirmationToken\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPreviewActionsUsesSharedPreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-actions\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"IncludedActionCount\\\": 4\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"SucceededActionCount\\\": 4\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"move\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"EffectiveFolderId\\\": \\\"projects/2026\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"ConfirmationToken\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPreviewDeleteUsesSharedPreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-delete\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"UniqueMessageCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"RequestedCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailSearchUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"search\", \"--profile\", \"work-imap\", \"--query\", \"reports\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"msg-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailSearchCompactUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"search\", \"--profile\", \"work-imap\", \"--query\", \"reports\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastSearchCompactRequest);\n        Assert.Equal(\"work-imap\", fixture.ReadService.LastSearchCompactRequest!.ProfileId);\n        Assert.Equal(\"reports\", fixture.ReadService.LastSearchCompactRequest.QueryText);\n        Assert.Contains(\"\\\"Summary\\\": \\\"msg-1 reports\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailGetPassesMailboxToApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"get\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--message-id\", \"msg-42\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastGetRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastGetRequest!.MailboxId);\n        Assert.Equal(\"msg-42\", fixture.ReadService.LastGetRequest.MessageId);\n    }\n\n    [Fact]\n    public async Task MailGetCompactUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"get\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--message-id\", \"msg-42\",\n                \"--compact\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastGetCompactRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastGetCompactRequest!.MailboxId);\n        Assert.Equal(\"msg-42\", fixture.ReadService.LastGetCompactRequest.MessageId);\n        Assert.Contains(\"\\\"SummaryText\\\": \\\"msg-42 Subject\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailGetManyCompactUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"get-many\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"msg-84\",\n                \"--compact\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastGetManyCompactRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastGetManyCompactRequest!.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastGetManyCompactRequest.FolderId);\n        Assert.Equal(new[] { \"msg-42\", \"msg-84\" }, fixture.ReadService.LastGetManyCompactRequest.MessageIds);\n        Assert.Contains(\"\\\"Id\\\": \\\"msg-42\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Id\\\": \\\"msg-84\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailMarkReadUsesApplicationMessageActionService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_mark\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"mark-read\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"msg-84\",\n                \"--unread\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastSetReadStateRequest);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastSetReadStateRequest!.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionService.LastSetReadStateRequest.FolderId);\n        Assert.False(fixture.MessageActionService.LastSetReadStateRequest.IsRead);\n        Assert.Equal(new[] { \"msg-42\", \"msg-84\" }, fixture.MessageActionService.LastSetReadStateRequest.MessageIds);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastSetReadStateRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailFlagUsesApplicationMessageActionService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_flag\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"flag\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"msg-84\",\n                \"--unflag\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastSetFlaggedStateRequest);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastSetFlaggedStateRequest!.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionService.LastSetFlaggedStateRequest.FolderId);\n        Assert.False(fixture.MessageActionService.LastSetFlaggedStateRequest.IsFlagged);\n        Assert.Equal(new[] { \"msg-42\", \"msg-84\" }, fixture.MessageActionService.LastSetFlaggedStateRequest.MessageIds);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastSetFlaggedStateRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailPreviewMarkReadUsesSharedPreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-mark-read\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--unread\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Action\\\": \\\"read-state\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"DesiredState\\\": false\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"ConfirmationToken\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPreviewFlagUsesSharedPreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-flag\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--unflag\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Action\\\": \\\"flagged-state\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"DesiredState\\\": false\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"ConfirmationToken\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPreviewAllUsesSharedBundlePreviewService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-all\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"IncludedActionCount\\\": 8\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"mark-read\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"flag\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"archive\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Action\\\": \\\"move\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailPlanActionUsesSharedPlanningService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"plan-action\",\n                \"--action\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"ExecutionKind\\\": \\\"Move\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"UniqueMessageCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"RequestedDestinationFolderId\\\": \\\"projects/2026\\\"\", stdout.ToString(), StringComparison.OrdinalIgnoreCase);\n        Assert.Contains(\"\\\"ConfirmationToken\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailExecutePlanUsesSharedPlanningAndBatchServices() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"execute-plan\",\n                \"--action\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"MSG-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastMoveRequest!.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionService.LastMoveRequest.FolderId);\n        Assert.Equal(\"projects/2026\", fixture.MessageActionService.LastMoveRequest.DestinationFolderId);\n        Assert.Contains(\"\\\"RequestedPlanCount\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"SucceededPlanCount\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailExportPlanUsesSharedPlanExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"export-plan\",\n                \"--action\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--message-id\", \"msg-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--path\", @\"C:\\Temp\\plan.json\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(@\"C:\\Temp\\plan.json\", fixture.MessageActionPlanExchangeService.LastSavedPath);\n        Assert.NotNull(fixture.MessageActionPlanExchangeService.LastSavedPlan);\n        Assert.Equal(\"move\", fixture.MessageActionPlanExchangeService.LastSavedPlan!.Action);\n        Assert.Contains(\"Action plan exported\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailShowPlanUsesSharedPlanExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        fixture.MessageActionPlanExchangeService.NextPlan = new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"delete\",\n            ExecutionKind = \"Delete\",\n            ProfileId = \"work-imap\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            MessageIds = { \"msg-42\" }\n        };\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"show-plan\", \"--path\", @\"C:\\Temp\\plan.json\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(@\"C:\\Temp\\plan.json\", fixture.MessageActionPlanExchangeService.LastLoadedPath);\n        Assert.Contains(\"\\\"Action\\\": \\\"delete\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailExecutePlanFileUsesSharedPlanExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        fixture.MessageActionPlanExchangeService.NextPlan = new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"move\",\n            ExecutionKind = \"Move\",\n            ProfileId = \"work-imap\",\n            MailboxId = \"shared@example.com\",\n            FolderId = \"Inbox\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            RequestedDestinationFolderId = \"projects/2026\",\n            MessageIds = { \"msg-42\" }\n        };\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"execute-plan-file\", \"--path\", @\"C:\\Temp\\plan.json\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(@\"C:\\Temp\\plan.json\", fixture.MessageActionPlanExchangeService.LastLoadedPath);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"projects/2026\", fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Contains(\"\\\"SucceededPlanCount\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(1, fixture.MessageActionPlanRegistryService.ListCompactCalls);\n        Assert.Contains(\"\\\"Id\\\": \\\"cleanup\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"PlanNames\\\": [\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"Delete spam\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesSummaryUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--summary\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(1, fixture.MessageActionPlanRegistryService.ListSummaryCalls);\n        Assert.Contains(\"\\\"ActionCounts\\\": {\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"delete\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesPassesPlanNameFilterToSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--compact\", \"--plan-name\", \"Delete spam\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"Delete spam\" }, fixture.MessageActionPlanRegistryService.LastBatchQuery!.PlanNames);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesPassesProfileFilterToSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--compact\", \"--profile\", \"gmail-work\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"gmail-work\" }, fixture.MessageActionPlanRegistryService.LastBatchQuery!.ProfileIds);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesPassesActionFilterToSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--compact\", \"--action\", \"delete\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"delete\" }, fixture.MessageActionPlanRegistryService.LastBatchQuery!.Actions);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesPassesSortToSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--summary\", \"--sort\", \"plans\", \"--desc\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastBatchQuery);\n        Assert.Equal(MailMessageActionPlanBatchSortBy.PlanCount, fixture.MessageActionPlanRegistryService.LastBatchQuery!.SortBy);\n        Assert.True(fixture.MessageActionPlanRegistryService.LastBatchQuery.Descending);\n    }\n\n    [Fact]\n    public async Task MailListPlanBatchesPassesExplicitIdSortToSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"list-plan-batches\", \"--summary\", \"--sort\", \"id\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastBatchQuery);\n        Assert.Equal(MailMessageActionPlanBatchSortBy.Id, fixture.MessageActionPlanRegistryService.LastBatchQuery!.SortBy);\n        Assert.False(fixture.MessageActionPlanRegistryService.LastBatchQuery.Descending);\n    }\n\n    [Fact]\n    public async Task MailImportPlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"import-plan-batch\",\n                \"--batch\", \"cleanup\",\n                \"--name\", \"Cleanup batch\",\n                \"--path\", @\"C:\\Temp\\plans.json\",\n                \"--description\", \"Quarterly cleanup\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastImportedBatchId);\n        Assert.Equal(\"Cleanup batch\", fixture.MessageActionPlanRegistryService.LastImportedName);\n        Assert.Equal(@\"C:\\Temp\\plans.json\", fixture.MessageActionPlanRegistryService.LastImportedPath);\n    }\n\n    [Fact]\n    public async Task MailCreateCommonPlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"create-common-plan-batch\",\n                \"--batch\", \"cleanup\",\n                \"--name\", \"Cleanup batch\",\n                \"--profile\", \"work-imap\",\n                \"--message-id\", \"msg-1\",\n                \"--message-id\", \"MSG-1\",\n                \"--action\", \"archive\",\n                \"--action\", \"delete\",\n                \"--target-folder\", \"Archive\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--description\", \"Common action batch\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastCreatedCommonBatchId);\n        Assert.Equal(\"Cleanup batch\", fixture.MessageActionPlanRegistryService.LastCreatedCommonName);\n        Assert.Equal(\"Common action batch\", fixture.MessageActionPlanRegistryService.LastCreatedCommonDescription);\n        Assert.Equal(new[] { \"archive\", \"delete\" }, fixture.MessageActionPlanRegistryService.LastCreatedCommonActions);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest);\n        Assert.Equal(\"work-imap\", fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest!.ProfileId);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest.FolderId);\n        Assert.Equal(\"Archive\", fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest.DestinationFolderId);\n        Assert.Equal(new[] { \"msg-1\", \"MSG-1\" }, fixture.MessageActionPlanRegistryService.LastCreatedCommonRequest.MessageIds);\n    }\n\n    [Fact]\n    public async Task MailExecuteStoredPlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"execute-plan-batch-stored\", \"--batch\", \"cleanup\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastExecutedBatchId);\n        Assert.Contains(\"\\\"SucceededPlanCount\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailAddPlanToBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"add-plan-to-batch\",\n                \"--batch\", \"cleanup\",\n                \"--action\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--message-id\", \"msg-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastAppendedBatchId);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastAppendedPlan);\n        Assert.Equal(\"move\", fixture.MessageActionPlanRegistryService.LastAppendedPlan!.Action);\n    }\n\n    [Fact]\n    public async Task MailRemovePlanFromBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"remove-plan-from-batch\", \"--batch\", \"cleanup\", \"--index\", \"1\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastRemovedBatchId);\n        Assert.Equal(1, fixture.MessageActionPlanRegistryService.LastRemovedIndex);\n    }\n\n    [Fact]\n    public async Task MailClonePlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"clone-plan-batch\",\n                \"--source-batch\", \"cleanup\",\n                \"--target-batch\", \"cleanup-copy\",\n                \"--name\", \"Cleanup copy\",\n                \"--description\", \"Cloned batch\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastClonedSourceBatchId);\n        Assert.Equal(\"cleanup-copy\", fixture.MessageActionPlanRegistryService.LastClonedTargetBatchId);\n    }\n\n    [Fact]\n    public async Task MailTransformPlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"transform-plan-batch\",\n                \"--source-batch\", \"cleanup\",\n                \"--target-batch\", \"cleanup-target\",\n                \"--name\", \"Cleanup Target\",\n                \"--index\", \"1\",\n                \"--index\", \"2\",\n                \"--plan-name\", \"Archive newsletter\",\n                \"--target-profile\", \"work-imap-target\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Projects\",\n                \"--target-folder\", \"Projects/Archive\",\n                \"--description\", \"Remapped batch\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastTransformedSourceBatchId);\n        Assert.Equal(\"cleanup-target\", fixture.MessageActionPlanRegistryService.LastTransformedTargetBatchId);\n        Assert.Equal(\"Cleanup Target\", fixture.MessageActionPlanRegistryService.LastTransformedName);\n        Assert.Equal(\"Remapped batch\", fixture.MessageActionPlanRegistryService.LastTransformedDescription);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastTransformRequest);\n        Assert.Equal(\"work-imap-target\", fixture.MessageActionPlanRegistryService.LastTransformRequest!.ProfileId);\n        Assert.Equal(new[] { 1, 2 }, fixture.MessageActionPlanRegistryService.LastTransformRequest.PlanIndexes);\n        Assert.Equal(new[] { \"Archive newsletter\" }, fixture.MessageActionPlanRegistryService.LastTransformRequest.PlanNames);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionPlanRegistryService.LastTransformRequest.MailboxId);\n        Assert.Equal(\"Projects\", fixture.MessageActionPlanRegistryService.LastTransformRequest.FolderId);\n        Assert.Equal(\"Projects/Archive\", fixture.MessageActionPlanRegistryService.LastTransformRequest.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task MailPreviewTransformPlanBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"preview-transform-plan-batch\",\n                \"--source-batch\", \"cleanup\",\n                \"--index\", \"1\",\n                \"--plan-name\", \"Archive newsletter\",\n                \"--target-profile\", \"work-imap-target\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Projects\",\n                \"--target-folder\", \"Projects/Archive\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastPreviewedTransformSourceBatchId);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest);\n        Assert.Equal(\"work-imap-target\", fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest!.ProfileId);\n        Assert.Equal(new[] { 1 }, fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest.PlanIndexes);\n        Assert.Equal(new[] { \"Archive newsletter\" }, fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest.PlanNames);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest.MailboxId);\n        Assert.Equal(\"Projects\", fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest.FolderId);\n        Assert.Equal(\"Projects/Archive\", fixture.MessageActionPlanRegistryService.LastPreviewedTransformRequest.DestinationFolderId);\n        Assert.Contains(\"\\\"ChangedPlanCount\\\": 1\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailReplacePlanInBatchUsesSharedRegistryService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"replace-plan-in-batch\",\n                \"--batch\", \"cleanup\",\n                \"--index\", \"0\",\n                \"--action\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--message-id\", \"msg-42\",\n                \"--target-folder\", \"projects/2026\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"cleanup\", fixture.MessageActionPlanRegistryService.LastReplacedBatchId);\n        Assert.Equal(0, fixture.MessageActionPlanRegistryService.LastReplacedIndex);\n        Assert.NotNull(fixture.MessageActionPlanRegistryService.LastReplacedPlan);\n        Assert.Equal(\"move\", fixture.MessageActionPlanRegistryService.LastReplacedPlan!.Action);\n    }\n\n    [Fact]\n    public async Task MailExecutePlanBatchUsesSharedBatchService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        fixture.MessageActionPlanExchangeService.NextBatchPlans = new[] {\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"mark-read\",\n                ExecutionKind = \"SetReadState\",\n                ProfileId = \"work-imap\",\n                MailboxId = \"shared@example.com\",\n                FolderId = \"Inbox\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                DesiredState = true,\n                MessageIds = { \"msg-1\" }\n            },\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"move\",\n                ExecutionKind = \"Move\",\n                ProfileId = \"work-imap\",\n                MailboxId = \"shared@example.com\",\n                FolderId = \"Inbox\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                RequestedDestinationFolderId = \"projects/2026\",\n                MessageIds = { \"msg-2\" }\n            }\n        };\n        var path = @\"C:\\Temp\\action-plans.json\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"mail\", \"execute-plan-batch\", \"--path\", path, \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(path, fixture.MessageActionPlanExchangeService.LastLoadedBatchPath);\n        Assert.NotNull(fixture.MessageActionService.LastSetReadStateRequest);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"projects/2026\", fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Contains(\"\\\"AttemptedPlanCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.Contains(\"\\\"SucceededPlanCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailArchiveUsesSharedArchiveAlias() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_archive\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"archive\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(MailFolderAliases.Archive, fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailTrashUsesSharedTrashAlias() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_trash\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"trash\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(MailFolderAliases.Trash, fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailMoveUsesApplicationMessageActionService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_move\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"move\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--target-folder\", \"Archive\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"Archive\", fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailDeleteUsesApplicationMessageActionService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var confirmationToken = \"mact_v1_delete\";\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"delete\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"msg-84\",\n                \"--confirm-token\", confirmationToken,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.MessageActionService.LastDeleteRequest);\n        Assert.Equal(\"shared@example.com\", fixture.MessageActionService.LastDeleteRequest!.MailboxId);\n        Assert.Equal(new[] { \"msg-42\", \"msg-84\" }, fixture.MessageActionService.LastDeleteRequest.MessageIds);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastDeleteRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailAttachmentsUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"attachments\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--message-id\", \"msg-42\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastListAttachmentsRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastListAttachmentsRequest!.MailboxId);\n        Assert.Equal(\"msg-42\", fixture.ReadService.LastListAttachmentsRequest.MessageId);\n        Assert.Contains(\"\\\"report.pdf\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailSaveAttachmentsManyUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"save-attachments-many\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--folder\", \"Inbox\",\n                \"--message-id\", \"msg-42\",\n                \"--message-id\", \"msg-84\",\n                \"--path\", @\"C:\\Temp\",\n                \"--attachment-id\", \"att-1\",\n                \"--name-contains\", \"report\",\n                \"--content-type\", \"pdf\",\n                \"--overwrite\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastSaveAttachmentsManyRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastSaveAttachmentsManyRequest!.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastSaveAttachmentsManyRequest.FolderId);\n        Assert.Equal(@\"C:\\Temp\", fixture.ReadService.LastSaveAttachmentsManyRequest.DestinationPath);\n        Assert.Equal(new[] { \"msg-42\", \"msg-84\" }, fixture.ReadService.LastSaveAttachmentsManyRequest.MessageIds);\n        Assert.Equal(new[] { \"att-1\" }, fixture.ReadService.LastSaveAttachmentsManyRequest.AttachmentIds);\n        Assert.Equal(\"report\", fixture.ReadService.LastSaveAttachmentsManyRequest.FileNameContains);\n        Assert.Equal(\"pdf\", fixture.ReadService.LastSaveAttachmentsManyRequest.ContentTypeContains);\n        Assert.True(fixture.ReadService.LastSaveAttachmentsManyRequest.Overwrite);\n        Assert.Contains(\"\\\"SavedCount\\\": 2\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task MailSaveAttachmentPassesMailboxToApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"save-attachment\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--message-id\", \"msg-42\",\n                \"--attachment-id\", \"1\",\n                \"--path\", \"C:\\\\Temp\\\\report.pdf\",\n                \"--overwrite\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastSaveAttachmentRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastSaveAttachmentRequest!.MailboxId);\n        Assert.Equal(\"1\", fixture.ReadService.LastSaveAttachmentRequest.AttachmentId);\n        Assert.True(fixture.ReadService.LastSaveAttachmentRequest.Overwrite);\n    }\n\n    [Fact]\n    public async Task MailSaveAttachmentsUsesApplicationReadService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"mail\", \"save-attachments\",\n                \"--profile\", \"work-imap\",\n                \"--mailbox\", \"shared@example.com\",\n                \"--message-id\", \"msg-42\",\n                \"--path\", \"C:\\\\Temp\",\n                \"--attachment-id\", \"att-1\",\n                \"--name-contains\", \"report\",\n                \"--content-type\", \"pdf\",\n                \"--overwrite\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.ReadService.LastSaveAttachmentsRequest);\n        Assert.Equal(\"shared@example.com\", fixture.ReadService.LastSaveAttachmentsRequest!.MailboxId);\n        Assert.Equal(\"msg-42\", fixture.ReadService.LastSaveAttachmentsRequest.MessageId);\n        Assert.Equal(\"C:\\\\Temp\", fixture.ReadService.LastSaveAttachmentsRequest.DestinationPath);\n        Assert.Contains(\"att-1\", fixture.ReadService.LastSaveAttachmentsRequest.AttachmentIds);\n        Assert.Equal(\"report\", fixture.ReadService.LastSaveAttachmentsRequest.FileNameContains);\n        Assert.Equal(\"pdf\", fixture.ReadService.LastSaveAttachmentsRequest.ContentTypeContains);\n        Assert.True(fixture.ReadService.LastSaveAttachmentsRequest.Overwrite);\n    }\n\n    [Fact]\n    public async Task QueueListUsesApplicationQueueService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"queue\", \"list\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"queued-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task QueueListCompactUsesApplicationQueueService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"queue\", \"list\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"MessageId\\\": \\\"queued-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"QueuedAt\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task QueueGetUsesApplicationQueueService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"queue\", \"get\", \"--message-id\", \"queued-1\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"queued-1\", fixture.QueueService.LastGetMessageId);\n    }\n\n    [Fact]\n    public async Task QueueGetCompactUsesApplicationQueueService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"queue\", \"get\", \"--message-id\", \"queued-1\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"MessageId\\\": \\\"queued-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"QueuedAt\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task QueueProcessUsesApplicationQueueService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"queue\", \"process\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(1, fixture.QueueService.ProcessCalls);\n    }\n\n    [Fact]\n    public async Task DraftSaveUsesApplicationDraftService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"draft\", \"save\",\n                \"--draft\", \"draft-1\",\n                \"--name\", \"Quarterly report\",\n                \"--profile\", \"work-imap\",\n                \"--to\", \"alice@example.com\",\n                \"--subject\", \"Quarterly report\",\n                \"--text\", \"Body\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.DraftService.LastSavedDraft);\n        Assert.Equal(\"draft-1\", fixture.DraftService.LastSavedDraft!.Id);\n        Assert.Equal(\"work-imap\", fixture.DraftService.LastSavedDraft.Message.ProfileId);\n    }\n\n    [Fact]\n    public async Task DraftListCompactUsesApplicationDraftService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"draft\", \"list\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Id\\\": \\\"draft-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"Message\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task DraftGetCompactUsesApplicationDraftService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] { \"draft\", \"get\", \"--draft\", \"draft-1\", \"--compact\", \"--json\" },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Contains(\"\\\"Id\\\": \\\"draft-1\\\"\", stdout.ToString(), StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"Message\\\":\", stdout.ToString(), StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task DraftSaveFromFileUsesDraftExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var importPath = CreateTemporaryFilePath(\"import-draft.json\");\n        await File.WriteAllTextAsync(importPath, JsonSerializer.Serialize(new MailDraft {\n            Id = \"imported-draft\",\n            Name = \"Imported draft\",\n            Message = new DraftMessage {\n                ProfileId = \"work-imap\",\n                Subject = \"Imported subject\",\n                To = {\n                    new MessageRecipient { Address = \"imported@example.com\" }\n                }\n            }\n        }));\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"draft\", \"save\",\n                \"--file\", importPath,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(importPath, fixture.DraftExchangeService.LastLoadedPath);\n        Assert.NotNull(fixture.DraftService.LastSavedDraft);\n        Assert.Equal(\"imported-draft\", fixture.DraftService.LastSavedDraft!.Id);\n    }\n\n    [Fact]\n    public async Task DraftExportUsesDraftExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var exportPath = CreateTemporaryFilePath(\"export-draft.json\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"draft\", \"export\",\n                \"--draft\", \"draft-1\",\n                \"--path\", exportPath,\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"draft-1\", fixture.DraftService.LastRequestedDraftId);\n        Assert.Equal(exportPath, fixture.DraftExchangeService.LastSavedPath);\n    }\n\n    [Fact]\n    public async Task SendUsingDraftUsesApplicationDraftService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"send\",\n                \"--draft\", \"draft-1\",\n                \"--send-now\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(\"draft-1\", fixture.DraftService.LastRequestedDraftId);\n        Assert.NotNull(fixture.SendService.LastRequest);\n        Assert.Equal(\"work-imap\", fixture.SendService.LastRequest!.ProfileId);\n        Assert.Equal(\"saved@example.com\", fixture.SendService.LastRequest.Message.To[0].Address);\n    }\n\n    [Fact]\n    public async Task SendUsingFileUsesDraftExchangeService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n        var importPath = CreateTemporaryFilePath(\"send-draft.json\");\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"send\",\n                \"--file\", importPath,\n                \"--send-now\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.Equal(importPath, fixture.DraftExchangeService.LastLoadedPath);\n        Assert.NotNull(fixture.SendService.LastRequest);\n        Assert.Equal(\"work-imap\", fixture.SendService.LastRequest!.ProfileId);\n        Assert.True(fixture.SendService.LastRequest.RequireImmediateSend);\n        Assert.False(fixture.SendService.LastRequest.PreferQueue);\n        Assert.Equal(\"imported@example.com\", fixture.SendService.LastRequest.Message.To[0].Address);\n        Assert.Equal(\"Imported subject\", fixture.SendService.LastRequest.Message.Subject);\n    }\n\n    [Fact]\n    public async Task SendUsesApplicationSendService() {\n        using var stdout = new StringWriter();\n        using var stderr = new StringWriter();\n        var fixture = CreateFixture();\n\n        var exitCode = await CliRunner.RunAsync(\n            new[] {\n                \"send\",\n                \"--profile\", \"work-imap\",\n                \"--from\", \"sender@example.com\",\n                \"--to\", \"alice@example.com\",\n                \"--to\", \"bob@example.com\",\n                \"--cc\", \"carol@example.com\",\n                \"--reply-to\", \"reply@example.com\",\n                \"--subject\", \"Quarterly report\",\n                \"--text\", \"Plain text body\",\n                \"--html\", \"<b>HTML body</b>\",\n                \"--header\", \"X-Test=value\",\n                \"--attachment\", \"C:\\\\Temp\\\\report.pdf\",\n                \"--send-now\",\n                \"--json\"\n            },\n            stdout,\n            stderr,\n            _ => fixture.CreateBuilder());\n\n        Assert.Equal(0, exitCode);\n        Assert.NotNull(fixture.SendService.LastRequest);\n        Assert.Equal(\"work-imap\", fixture.SendService.LastRequest!.ProfileId);\n        Assert.True(fixture.SendService.LastRequest.RequireImmediateSend);\n        Assert.False(fixture.SendService.LastRequest.PreferQueue);\n        Assert.Equal(\"sender@example.com\", fixture.SendService.LastRequest.Message.From!.Address);\n        Assert.Equal(2, fixture.SendService.LastRequest.Message.To.Count);\n        Assert.Equal(\"alice@example.com\", fixture.SendService.LastRequest.Message.To[0].Address);\n        Assert.Equal(\"carol@example.com\", fixture.SendService.LastRequest.Message.Cc[0].Address);\n        Assert.Equal(\"reply@example.com\", fixture.SendService.LastRequest.Message.ReplyTo[0].Address);\n        Assert.Equal(\"Quarterly report\", fixture.SendService.LastRequest.Message.Subject);\n        Assert.Equal(\"Plain text body\", fixture.SendService.LastRequest.Message.TextBody);\n        Assert.Equal(\"<b>HTML body</b>\", fixture.SendService.LastRequest.Message.HtmlBody);\n        Assert.Equal(\"value\", fixture.SendService.LastRequest.Message.Headers[\"X-Test\"]);\n        Assert.Equal(\"C:\\\\Temp\\\\report.pdf\", fixture.SendService.LastRequest.Message.Attachments[0].Path);\n    }\n\n    private static TestApplicationFixture CreateFixture() => new();\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles;\n\n        public InMemoryProfileStore(IEnumerable<MailProfile> profiles) {\n            _profiles = profiles.ToDictionary(profile => profile.Id, StringComparer.OrdinalIgnoreCase);\n        }\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult(profile);\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = profile;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, Dictionary<string, string>> _secrets = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            if (_secrets.TryGetValue(profileId, out var profileSecrets) &&\n                profileSecrets.TryGetValue(secretName, out var secretValue)) {\n                return Task.FromResult<string?>(secretValue);\n            }\n\n            return Task.FromResult<string?>(null);\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            var removed = _secrets.TryGetValue(profileId, out var profileSecrets) &&\n                          profileSecrets.Remove(secretName);\n            return Task.FromResult(removed);\n        }\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            if (!_secrets.TryGetValue(profileId, out var profileSecrets)) {\n                profileSecrets = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n                _secrets[profileId] = profileSecrets;\n            }\n\n            profileSecrets[secretName] = secretValue;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class FakeReadService : IMailReadService {\n        public MailFolderQuery? LastFolderQuery { get; private set; }\n\n        public GetMessageRequest? LastGetCompactRequest { get; private set; }\n\n        public GetMessagesRequest? LastGetManyCompactRequest { get; private set; }\n\n        public GetMessagesRequest? LastGetManyRequest { get; private set; }\n\n        public SaveAttachmentsManyRequest? LastSaveAttachmentsManyRequest { get; private set; }\n\n        public SaveAttachmentsRequest? LastSaveAttachmentsRequest { get; private set; }\n\n        public ListAttachmentsRequest? LastListAttachmentsRequest { get; private set; }\n\n        public MailFolderQuery? LastFolderCompactQuery { get; private set; }\n\n        public MailSearchRequest? LastSearchCompactRequest { get; private set; }\n\n        public GetMessageRequest? LastGetRequest { get; private set; }\n\n        public SaveAttachmentRequest? LastSaveAttachmentRequest { get; private set; }\n\n        public Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(CaptureGetRequest(request));\n\n        public Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default) {\n            LastGetCompactRequest = request;\n            return Task.FromResult<MessageDetailCompact?>(new MessageDetailCompact {\n                ProfileId = request.ProfileId,\n                Id = request.MessageId,\n                Summary = new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = request.MessageId,\n                    Subject = \"Subject\",\n                    Summary = $\"{request.MessageId} Subject\"\n                },\n                TextBodyPreview = \"Body\",\n                SummaryText = $\"{request.MessageId} Subject\"\n            });\n        }\n\n        public Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastGetManyCompactRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageDetailCompact>>(request.MessageIds.Select(messageId => new MessageDetailCompact {\n                ProfileId = request.ProfileId,\n                Id = messageId,\n                Summary = new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = messageId,\n                    Subject = \"Subject\",\n                    Summary = $\"{messageId} Subject\"\n                },\n                TextBodyPreview = \"Body\",\n                SummaryText = $\"{messageId} Subject\"\n            }).ToArray());\n        }\n\n        private MessageDetail CaptureGetRequest(GetMessageRequest request) {\n            LastGetRequest = request;\n            return new MessageDetail {\n                ProfileId = request.ProfileId,\n                Id = request.MessageId,\n                Summary = new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = request.MessageId,\n                    Subject = \"Subject\"\n                },\n                TextBody = \"Body\"\n            };\n        }\n\n        public Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastGetManyRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageDetail>>(request.MessageIds.Select(messageId => new MessageDetail {\n                ProfileId = request.ProfileId,\n                Id = messageId,\n                Summary = new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = messageId,\n                    Subject = \"Subject\"\n                },\n                TextBody = \"Body\"\n            }).ToArray());\n        }\n\n        public Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n            LastFolderCompactQuery = query;\n            return Task.FromResult<IReadOnlyList<FolderRefCompact>>(new[] {\n                new FolderRefCompact {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\",\n                    Summary = \"inbox Inbox\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n            LastFolderQuery = query;\n            return Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\",\n                    SpecialUse = \"inbox\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"archive\",\n                    DisplayName = \"Archive\",\n                    Path = \"Archive\",\n                    SpecialUse = \"archive\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"trash\",\n                    DisplayName = \"Trash\",\n                    Path = \"Trash\",\n                    SpecialUse = \"trash\"\n                }\n            });\n        }\n\n        public Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default) {\n            LastSaveAttachmentRequest = request;\n            return Task.FromResult(OperationResult.Success(\"Attachment saved.\"));\n        }\n\n        public Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default) {\n            LastSaveAttachmentsRequest = request;\n            return Task.FromResult(new SaveAttachmentsResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                MessageId = request.MessageId,\n                MatchedCount = 1,\n                AttemptedCount = 1,\n                SavedCount = 1,\n                FailedCount = 0,\n                Message = \"Saved 1 attachment(s).\",\n                Results = {\n                    new SavedAttachmentResult {\n                        Succeeded = true,\n                        AttachmentId = \"att-1\",\n                        FileName = \"report.pdf\",\n                        ContentType = \"application/pdf\"\n                    }\n                }\n            });\n        }\n\n        public Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default) {\n            LastSaveAttachmentsManyRequest = request;\n            return Task.FromResult(new SaveAttachmentsManyResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedMessageCount = request.MessageIds.Count,\n                AttemptedMessageCount = request.MessageIds.Count,\n                SucceededMessageCount = request.MessageIds.Count,\n                MatchedCount = request.MessageIds.Count,\n                AttemptedCount = request.MessageIds.Count,\n                SavedCount = request.MessageIds.Count,\n                FailedCount = 0,\n                Message = $\"Saved {request.MessageIds.Count} attachment(s) across {request.MessageIds.Count} message(s).\",\n                MessageResults = request.MessageIds.Select(messageId => new SaveAttachmentsResult {\n                    Succeeded = true,\n                    ProfileId = request.ProfileId,\n                    MessageId = messageId,\n                    MatchedCount = 1,\n                    AttemptedCount = 1,\n                    SavedCount = 1,\n                    FailedCount = 0,\n                    Message = \"Saved 1 attachment(s).\"\n                }).ToList()\n            });\n        }\n\n        public Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default) {\n            LastSearchCompactRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageSummaryCompact>>(new[] {\n                new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = \"msg-1\",\n                    Subject = request.QueryText,\n                    Summary = $\"msg-1 {request.QueryText}\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default) {\n            LastListAttachmentsRequest = request;\n            return Task.FromResult<IReadOnlyList<AttachmentSummary>>(new[] {\n                new AttachmentSummary {\n                    MessageId = request.MessageId,\n                    Id = \"att-1\",\n                    FileName = \"report.pdf\",\n                    SizeInBytes = 2048\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MessageSummary>>(new[] {\n                new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = \"msg-1\",\n                    Subject = request.QueryText\n                }\n            });\n    }\n\n    private sealed class FakeSendService : IMailSendService {\n        public SendMessageRequest? LastRequest { get; private set; }\n\n        public Task<SendResult> SendAsync(SendMessageRequest request, CancellationToken cancellationToken = default) {\n            LastRequest = request;\n            return Task.FromResult(new SendResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Imap,\n                ProviderMessageId = \"provider-1\",\n                Message = \"Message sent successfully.\"\n            });\n        }\n    }\n\n    private sealed class FakeMessageActionService : IMailMessageActionService {\n        public SetReadStateRequest? LastSetReadStateRequest { get; private set; }\n\n        public SetFlaggedStateRequest? LastSetFlaggedStateRequest { get; private set; }\n\n        public MoveMessagesRequest? LastMoveRequest { get; private set; }\n\n        public DeleteMessagesRequest? LastDeleteRequest { get; private set; }\n\n        public Task<MessageActionResult> SetReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) {\n            LastSetReadStateRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, request.IsRead ? \"Marked messages as read.\" : \"Marked messages as unread.\"));\n        }\n\n        public Task<MessageActionResult> SetFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n            LastSetFlaggedStateRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, request.IsFlagged ? \"Flagged messages.\" : \"Unflagged messages.\"));\n        }\n\n        public Task<MessageActionResult> MoveAsync(MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastMoveRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, $\"Moved messages to '{request.DestinationFolderId}'.\"));\n        }\n\n        public Task<MessageActionResult> DeleteAsync(DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastDeleteRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, \"Deleted messages.\"));\n        }\n\n        private static MessageActionResult CreateResult(string profileId, IReadOnlyList<string> messageIds, string message) => new() {\n            Succeeded = true,\n            ProfileId = profileId,\n            RequestedCount = messageIds.Count,\n            SucceededCount = messageIds.Count,\n            FailedCount = 0,\n            Message = message,\n            Results = messageIds.Select(id => new MessageActionItemResult {\n                MessageId = id,\n                Succeeded = true\n            }).ToList()\n        };\n    }\n\n    private sealed class FakeMessageActionPlanExchangeService : IMailMessageActionPlanExchangeService {\n        public string? LastLoadedPath { get; private set; }\n\n        public string? LastLoadedBatchPath { get; private set; }\n\n        public string? LastSavedPath { get; private set; }\n\n        public string? LastSavedBatchPath { get; private set; }\n\n        public MessageActionExecutionPlan? LastSavedPlan { get; private set; }\n\n        public IReadOnlyList<MessageActionExecutionPlan>? LastSavedBatch { get; private set; }\n\n        public MessageActionExecutionPlan NextPlan { get; set; } = new() {\n            Succeeded = true,\n            Action = \"mark-read\",\n            ExecutionKind = \"SetReadState\",\n            ProfileId = \"work-imap\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            DesiredState = true,\n            MessageIds = { \"msg-1\" }\n        };\n\n        public IReadOnlyList<MessageActionExecutionPlan> NextBatchPlans { get; set; } = Array.Empty<MessageActionExecutionPlan>();\n\n        public Task<MessageActionExecutionPlan> LoadAsync(string path, CancellationToken cancellationToken = default) {\n            LastLoadedPath = path;\n            return Task.FromResult(NextPlan);\n        }\n\n        public Task<IReadOnlyList<MessageActionExecutionPlan>> LoadBatchAsync(string path, CancellationToken cancellationToken = default) {\n            LastLoadedBatchPath = path;\n            return Task.FromResult(NextBatchPlans);\n        }\n\n        public Task SaveAsync(string path, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastSavedPath = path;\n            LastSavedPlan = plan;\n            return Task.CompletedTask;\n        }\n\n        public Task SaveBatchAsync(string path, IReadOnlyList<MessageActionExecutionPlan> plans, CancellationToken cancellationToken = default) {\n            LastSavedBatchPath = path;\n            LastSavedBatch = plans;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class FakeMessageActionPlanRegistryService : IMailMessageActionPlanRegistryService {\n        public int ListCompactCalls { get; private set; }\n        public int ListSummaryCalls { get; private set; }\n        public MailMessageActionPlanBatchQuery? LastBatchQuery { get; private set; }\n\n        public string? LastAppendedBatchId { get; private set; }\n\n        public MessageActionExecutionPlan? LastAppendedPlan { get; private set; }\n\n        public string? LastClonedSourceBatchId { get; private set; }\n\n        public string? LastClonedTargetBatchId { get; private set; }\n\n        public string? LastImportedBatchId { get; private set; }\n\n        public string? LastImportedName { get; private set; }\n\n        public string? LastImportedPath { get; private set; }\n\n        public string? LastCreatedCommonBatchId { get; private set; }\n\n        public string? LastCreatedCommonName { get; private set; }\n\n        public string? LastCreatedCommonDescription { get; private set; }\n\n        public CommonMessageActionsPreviewRequest? LastCreatedCommonRequest { get; private set; }\n\n        public IReadOnlyList<string>? LastCreatedCommonActions { get; private set; }\n\n        public string? LastCreatedFromPreviewBatchId { get; private set; }\n\n        public string? LastCreatedFromPreviewName { get; private set; }\n\n        public string? LastCreatedFromPreviewDescription { get; private set; }\n\n        public CommonMessageActionsPreview? LastCreatedFromPreview { get; private set; }\n\n        public IReadOnlyList<string>? LastCreatedFromPreviewActions { get; private set; }\n\n        public string? LastTransformedSourceBatchId { get; private set; }\n\n        public string? LastTransformedTargetBatchId { get; private set; }\n\n        public string? LastTransformedName { get; private set; }\n\n        public string? LastTransformedDescription { get; private set; }\n\n        public MessageActionPlanBatchTransformRequest? LastTransformRequest { get; private set; }\n\n        public string? LastPreviewedTransformSourceBatchId { get; private set; }\n\n        public MessageActionPlanBatchTransformRequest? LastPreviewedTransformRequest { get; private set; }\n\n        public string? LastExecutedBatchId { get; private set; }\n\n        public string? LastRemovedBatchId { get; private set; }\n\n        public int? LastRemovedIndex { get; private set; }\n\n        public string? LastReplacedBatchId { get; private set; }\n\n        public int? LastReplacedIndex { get; private set; }\n\n        public MessageActionExecutionPlan? LastReplacedPlan { get; private set; }\n\n        public Task<OperationResult> AppendImportedPlanAsync(string batchId, string path, CancellationToken cancellationToken = default) {\n            LastAppendedBatchId = batchId;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> AppendPlanAsync(string batchId, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastAppendedBatchId = batchId;\n            LastAppendedPlan = plan;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CloneAsync(string sourceBatchId, string targetBatchId, string name, string? description = null, CancellationToken cancellationToken = default) {\n            LastClonedSourceBatchId = sourceBatchId;\n            LastClonedTargetBatchId = targetBatchId;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{targetBatchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CreateCommonBatchAsync(\n            string batchId,\n            string name,\n            CommonMessageActionsPreviewRequest request,\n            IReadOnlyList<string>? actions = null,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastCreatedCommonBatchId = batchId;\n            LastCreatedCommonName = name;\n            LastCreatedCommonDescription = description;\n            LastCreatedCommonRequest = new CommonMessageActionsPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                DestinationFolderId = request.DestinationFolderId,\n                MessageIds = request.MessageIds.ToList()\n            };\n            LastCreatedCommonActions = actions?.ToArray();\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CreateCommonBatchFromPreviewAsync(\n            string batchId,\n            string name,\n            CommonMessageActionsPreview preview,\n            IReadOnlyList<string>? actions = null,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastCreatedFromPreviewBatchId = batchId;\n            LastCreatedFromPreviewName = name;\n            LastCreatedFromPreviewDescription = description;\n            LastCreatedFromPreview = new CommonMessageActionsPreview {\n                ProfileId = preview.ProfileId,\n                MailboxId = preview.MailboxId,\n                FolderId = preview.FolderId,\n                RequestedDestinationFolderId = preview.RequestedDestinationFolderId,\n                RequestedCount = preview.RequestedCount,\n                UniqueMessageCount = preview.UniqueMessageCount,\n                DuplicateOrEmptyCount = preview.DuplicateOrEmptyCount,\n                MessageIds = preview.MessageIds.ToList(),\n                Actions = preview.Actions.Select(action => new MessageActionPreviewItem {\n                    Action = action.Action,\n                    DisplayName = action.DisplayName,\n                    Succeeded = action.Succeeded,\n                    Code = action.Code,\n                    Message = action.Message,\n                    RequestedDestinationFolderId = action.RequestedDestinationFolderId,\n                    DesiredState = action.DesiredState,\n                    ConfirmationToken = action.ConfirmationToken\n                }).ToList()\n            };\n            LastCreatedFromPreviewActions = actions?.ToArray();\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> TransformCloneAsync(\n            string sourceBatchId,\n            string targetBatchId,\n            string name,\n            MessageActionPlanBatchTransformRequest transform,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastTransformedSourceBatchId = sourceBatchId;\n            LastTransformedTargetBatchId = targetBatchId;\n            LastTransformedName = name;\n            LastTransformedDescription = description;\n            LastTransformRequest = new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = transform.PlanIndexes.ToList(),\n                PlanNames = transform.PlanNames.ToList(),\n                ProfileId = transform.ProfileId,\n                MailboxId = transform.MailboxId,\n                FolderId = transform.FolderId,\n                DestinationFolderId = transform.DestinationFolderId\n            };\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{targetBatchId}' saved.\"));\n        }\n\n        public Task<MailMessageActionPlanBatchTransformPreview> PreviewTransformCloneAsync(\n            string sourceBatchId,\n            MessageActionPlanBatchTransformRequest transform,\n            CancellationToken cancellationToken = default) {\n            LastPreviewedTransformSourceBatchId = sourceBatchId;\n            LastPreviewedTransformRequest = new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = transform.PlanIndexes.ToList(),\n                PlanNames = transform.PlanNames.ToList(),\n                ProfileId = transform.ProfileId,\n                MailboxId = transform.MailboxId,\n                FolderId = transform.FolderId,\n                DestinationFolderId = transform.DestinationFolderId\n            };\n            return Task.FromResult(new MailMessageActionPlanBatchTransformPreview {\n                Succeeded = true,\n                SourceBatchId = sourceBatchId,\n                SourceBatchName = \"Cleanup batch\",\n                PlanCount = 1,\n                ChangedPlanCount = 1,\n                ConfirmationTokenChangedCount = 1,\n                TargetProfileExists = true,\n                Plans = {\n                    new MessageActionPlanBatchTransformPreviewItem {\n                        Index = 0,\n                        Action = \"move\",\n                        ExecutionKind = \"Move\",\n                        SourceProfileId = \"work-imap\",\n                        TargetProfileId = transform.ProfileId ?? \"work-imap\",\n                        SourceMailboxId = \"source@example.com\",\n                        TargetMailboxId = transform.MailboxId,\n                        SourceFolderId = \"Inbox\",\n                        TargetFolderId = transform.FolderId,\n                        SourceDestinationFolderId = \"Archive\",\n                        TargetDestinationFolderId = transform.DestinationFolderId,\n                        WillChange = true,\n                        ConfirmationTokenWillChange = true,\n                        Summary = \"move: preview\"\n                    }\n                },\n                Message = \"Previewed transform.\"\n            });\n        }\n\n        public Task<OperationResult> DeleteAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' deleted.\"));\n\n        public Task<MessageActionBatchExecutionResult> ExecuteAsync(string batchId, bool continueOnError = true, CancellationToken cancellationToken = default) {\n            LastExecutedBatchId = batchId;\n            return Task.FromResult(new MessageActionBatchExecutionResult {\n                Succeeded = true,\n                RequestedPlanCount = 1,\n                AttemptedPlanCount = 1,\n                SucceededPlanCount = 1,\n                Message = \"Stored batch executed.\"\n            });\n        }\n\n        public Task<OperationResult> ExportAsync(string batchId, string path, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' exported.\"));\n\n        public Task<MailMessageActionPlanBatch?> GetBatchAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatch?>(new MailMessageActionPlanBatch {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                Plans = {\n                    new MessageActionExecutionPlan {\n                        Succeeded = true,\n                        Action = \"delete\",\n                        ExecutionKind = \"Delete\",\n                        ProfileId = \"work-imap\",\n                        RequestedCount = 1,\n                        UniqueMessageCount = 1,\n                        MessageIds = { \"msg-1\" }\n                    }\n                }\n            });\n\n        public Task<MailMessageActionPlanBatchCompact?> GetBatchCompactAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatchCompact?>(new MailMessageActionPlanBatchCompact {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                PlanCount = 1,\n                ReadyPlanCount = 1,\n                ProfileCount = 1,\n                PlanNames = { \"Delete spam\" },\n                Summary = $\"{batchId} (1 plan(s), 1 ready)\"\n            });\n\n        public Task<MailMessageActionPlanBatchSummary?> GetBatchSummaryAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatchSummary?>(new MailMessageActionPlanBatchSummary {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                PlanCount = 1,\n                ReadyPlanCount = 1,\n                ProfileIds = { \"gmail-work\" },\n                ActionCounts = {\n                    [\"delete\"] = 1\n                },\n                PlanNames = { \"Delete spam\" },\n                Summary = $\"{batchId} (1 plan(s), 1 ready, 1 action type(s))\"\n            });\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatch>> GetBatchesAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatch>>(new[] {\n                new MailMessageActionPlanBatch {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    Plans = {\n                        new MessageActionExecutionPlan {\n                            Succeeded = true,\n                            Action = \"delete\",\n                            ExecutionKind = \"Delete\",\n                            ProfileId = \"work-imap\",\n                            RequestedCount = 1,\n                            UniqueMessageCount = 1,\n                            MessageIds = { \"msg-1\" }\n                        }\n                    }\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatchCompact>> GetBatchesCompactAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            ListCompactCalls++;\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatchCompact>>(new[] {\n                new MailMessageActionPlanBatchCompact {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    PlanCount = 1,\n                    ReadyPlanCount = 1,\n                    ProfileCount = 1,\n                    PlanNames = { \"Delete spam\" },\n                    Summary = \"cleanup (1 plan(s), 1 ready)\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatchSummary>> GetBatchesSummaryAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            ListSummaryCalls++;\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatchSummary>>(new[] {\n                new MailMessageActionPlanBatchSummary {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    PlanCount = 1,\n                    ReadyPlanCount = 1,\n                    ProfileIds = { \"gmail-work\" },\n                    ActionCounts = {\n                        [\"delete\"] = 1\n                    },\n                    PlanNames = { \"Delete spam\" },\n                    Summary = \"cleanup (1 plan(s), 1 ready, 1 action type(s))\"\n                }\n            });\n        }\n\n        public Task<OperationResult> ImportAsync(string batchId, string name, string path, string? description = null, CancellationToken cancellationToken = default) {\n            LastImportedBatchId = batchId;\n            LastImportedName = name;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> ReplaceImportedPlanAtAsync(string batchId, int index, string path, CancellationToken cancellationToken = default) {\n            LastReplacedBatchId = batchId;\n            LastReplacedIndex = index;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> ReplacePlanAtAsync(string batchId, int index, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastReplacedBatchId = batchId;\n            LastReplacedIndex = index;\n            LastReplacedPlan = plan;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> RemovePlanAtAsync(string batchId, int index, CancellationToken cancellationToken = default) {\n            LastRemovedBatchId = batchId;\n            LastRemovedIndex = index;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batch.Id}' saved.\"));\n    }\n\n    private sealed class FakeQueueService : IMailQueueService {\n        public string? LastGetMessageId { get; private set; }\n\n        public string? LastRemoveMessageId { get; private set; }\n\n        public int ProcessCalls { get; private set; }\n\n        public Task<QueuedMessageCompact?> GetCompactAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<QueuedMessageCompact?>(new QueuedMessageCompact {\n                MessageId = messageId,\n                Provider = \"Gmail\",\n                ProfileKind = MailProfileKind.Gmail,\n                NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1),\n                AttemptCount = 0,\n                IsDue = false,\n                HasProviderData = false,\n                Summary = $\"{messageId} [Gmail] attempts=0\"\n            });\n\n        public Task<QueuedMessageSummary?> GetAsync(string messageId, CancellationToken cancellationToken = default) {\n            LastGetMessageId = messageId;\n            return Task.FromResult<QueuedMessageSummary?>(new QueuedMessageSummary {\n                MessageId = messageId,\n                Provider = \"Gmail\",\n                ProfileKind = MailProfileKind.Gmail,\n                QueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5),\n                NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1)\n            });\n        }\n\n        public Task<IReadOnlyList<QueuedMessageCompact>> ListCompactAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<QueuedMessageCompact>>(new[] {\n                new QueuedMessageCompact {\n                    MessageId = \"queued-1\",\n                    Provider = \"Gmail\",\n                    ProfileKind = MailProfileKind.Gmail,\n                    NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1),\n                    AttemptCount = 1,\n                    IsDue = false,\n                    HasProviderData = true,\n                    Summary = \"queued-1 [Gmail] attempts=1\"\n                }\n            });\n\n        public Task<IReadOnlyList<QueuedMessageSummary>> ListAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<QueuedMessageSummary>>(new[] {\n                new QueuedMessageSummary {\n                    MessageId = \"queued-1\",\n                    Provider = \"Gmail\",\n                    ProfileKind = MailProfileKind.Gmail,\n                    QueuedAt = DateTimeOffset.UtcNow.AddMinutes(-10),\n                    NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1),\n                    AttemptCount = 1,\n                    HasProviderData = true\n                }\n            });\n\n        public Task<QueueProcessResult> ProcessAsync(CancellationToken cancellationToken = default) {\n            ProcessCalls++;\n            return Task.FromResult(new QueueProcessResult {\n                Succeeded = true,\n                AttemptedCount = 1,\n                SentCount = 1,\n                Message = \"Queue processing completed.\"\n            });\n        }\n\n        public Task<OperationResult> RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            LastRemoveMessageId = messageId;\n            return Task.FromResult(OperationResult.Success(\"Queued message removed.\"));\n        }\n    }\n\n    private sealed class FakeDraftService : IMailDraftService {\n        public string? LastRequestedDraftId { get; private set; }\n\n        public MailDraft? LastSavedDraft { get; private set; }\n\n        public Task<MailDraftCompact?> GetDraftCompactAsync(string draftId, CancellationToken cancellationToken = default) {\n            LastRequestedDraftId = draftId;\n            return Task.FromResult<MailDraftCompact?>(new MailDraftCompact {\n                Id = draftId,\n                Name = \"Saved draft\",\n                ProfileId = \"work-imap\",\n                Subject = \"Saved subject\",\n                ToCount = 1,\n                AttachmentCount = 0,\n                UpdatedAt = DateTimeOffset.UtcNow,\n                Summary = $\"{draftId} [work-imap] Saved draft\"\n            });\n        }\n\n        public Task<MailDraft?> GetDraftAsync(string draftId, CancellationToken cancellationToken = default) {\n            LastRequestedDraftId = draftId;\n            return Task.FromResult<MailDraft?>(new MailDraft {\n                Id = draftId,\n                Name = \"Saved draft\",\n                Message = new DraftMessage {\n                    ProfileId = \"work-imap\",\n                    Subject = \"Saved subject\",\n                    To = {\n                        new MessageRecipient { Address = \"saved@example.com\" }\n                    }\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MailDraftCompact>> GetDraftsCompactAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailDraftCompact>>(new[] {\n                new MailDraftCompact {\n                    Id = \"draft-1\",\n                    Name = \"Saved draft\",\n                    ProfileId = \"work-imap\",\n                    Subject = \"Saved subject\",\n                    ToCount = 0,\n                    AttachmentCount = 0,\n                    UpdatedAt = DateTimeOffset.UtcNow,\n                    Summary = \"draft-1 [work-imap] Saved draft\"\n                }\n            });\n\n        public Task<IReadOnlyList<MailDraft>> GetDraftsAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailDraft>>(new[] {\n                new MailDraft {\n                    Id = \"draft-1\",\n                    Name = \"Saved draft\",\n                    Message = new DraftMessage {\n                        ProfileId = \"work-imap\",\n                        Subject = \"Saved subject\"\n                    }\n                }\n            });\n\n        public Task<OperationResult> SaveAsync(MailDraft draft, CancellationToken cancellationToken = default) {\n            LastSavedDraft = draft;\n            return Task.FromResult(OperationResult.Success(\"Draft saved.\"));\n        }\n\n        public Task<OperationResult> DeleteAsync(string draftId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success(\"Draft deleted.\"));\n    }\n\n    private sealed class FakeDraftExchangeService : IMailDraftExchangeService {\n        public string? LastLoadedPath { get; private set; }\n\n        public string? LastSavedPath { get; private set; }\n\n        public Task<MailDraft> LoadAsync(string path, CancellationToken cancellationToken = default) {\n            LastLoadedPath = path;\n            return Task.FromResult(new MailDraft {\n                Id = \"imported-draft\",\n                Name = \"Imported draft\",\n                Message = new DraftMessage {\n                    ProfileId = \"work-imap\",\n                    Subject = \"Imported subject\",\n                    To = {\n                        new MessageRecipient { Address = \"imported@example.com\" }\n                    }\n                }\n            });\n        }\n\n        public Task SaveAsync(string path, MailDraft draft, CancellationToken cancellationToken = default) {\n            LastSavedPath = path;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class FakeProfileAuthService : IMailProfileAuthService {\n        private readonly InMemoryProfileStore _profileStore;\n        private readonly InMemorySecretStore _secretStore;\n\n        public FakeProfileAuthService(InMemoryProfileStore profileStore, InMemorySecretStore secretStore) {\n            _profileStore = profileStore;\n            _secretStore = secretStore;\n        }\n\n        public GmailProfileLoginRequest? LastGmailRequest { get; private set; }\n\n        public GraphProfileLoginRequest? LastGraphRequest { get; private set; }\n\n        public string? LastRefreshProfileId { get; private set; }\n\n        public string? LastStatusProfileId { get; private set; }\n\n        public int RefreshCalls { get; private set; }\n\n        public async Task<MailProfileAuthStatus?> GetStatusAsync(string profileId, CancellationToken cancellationToken = default) {\n            LastStatusProfileId = profileId;\n            var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken);\n            if (profile == null) {\n                return null;\n            }\n\n            var accessToken = await _secretStore.GetSecretAsync(profileId, MailSecretNames.AccessToken, cancellationToken);\n            var refreshToken = await _secretStore.GetSecretAsync(profileId, MailSecretNames.RefreshToken, cancellationToken);\n            var clientSecret = await _secretStore.GetSecretAsync(profileId, MailSecretNames.ClientSecret, cancellationToken);\n            var password = await _secretStore.GetSecretAsync(profileId, MailSecretNames.Password, cancellationToken);\n\n            return new MailProfileAuthStatus {\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind,\n                AuthFlow = profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthFlow, out var authFlow) ? authFlow : null,\n                Mode = profile.Kind == MailProfileKind.Imap && !string.IsNullOrWhiteSpace(password) ? \"basic\" : \"interactive\",\n                Mailbox = profile.DefaultMailbox,\n                HasAccessToken = !string.IsNullOrWhiteSpace(accessToken),\n                HasRefreshToken = !string.IsNullOrWhiteSpace(refreshToken),\n                HasClientSecret = !string.IsNullOrWhiteSpace(clientSecret),\n                HasPassword = !string.IsNullOrWhiteSpace(password),\n                CanRefresh = profile.Kind is MailProfileKind.Gmail or MailProfileKind.Graph,\n                CanLoginInteractively = profile.Kind is MailProfileKind.Gmail or MailProfileKind.Graph,\n                Summary = $\"{profile.Id} [{profile.Kind}] auth status available.\"\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> LoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken = default) {\n            var profile = await _profileStore.GetByIdAsync(request.ProfileId, cancellationToken);\n            Assert.NotNull(profile);\n            var savedProfile = profile!;\n            var clientSecret = request.ClientSecret;\n            if (string.IsNullOrWhiteSpace(clientSecret) && !string.IsNullOrWhiteSpace(request.ClientSecretReference)) {\n                var (sourceProfileId, sourceSecretName) = ParseSecretReference(request.ClientSecretReference!, request.ProfileId);\n                clientSecret = await _secretStore.GetSecretAsync(sourceProfileId, sourceSecretName, cancellationToken);\n            }\n            var account = request.GmailAccount\n                ?? (savedProfile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) ? mailbox : null)\n                ?? \"user@gmail.com\";\n            LastGmailRequest = new GmailProfileLoginRequest {\n                ProfileId = request.ProfileId,\n                GmailAccount = account,\n                ClientId = request.ClientId ?? (savedProfile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ? clientId : null),\n                ClientSecret = clientSecret,\n                Scopes = request.Scopes\n            };\n            savedProfile.Settings[MailProfileSettingsKeys.Mailbox] = account;\n            savedProfile.Settings[MailProfileSettingsKeys.ClientId] = request.ClientId ?? savedProfile.Settings[MailProfileSettingsKeys.ClientId];\n            savedProfile.DefaultMailbox = account;\n            await _profileStore.SaveAsync(savedProfile, cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.AccessToken, \"gmail-access-token\", cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.RefreshToken, \"gmail-refresh-token\", cancellationToken);\n            return new MailProfileAuthenticationResult {\n                Succeeded = true,\n                Message = \"Gmail login completed.\",\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Gmail,\n                UserName = account\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> LoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken = default) {\n            LastGraphRequest = request;\n            var profile = await _profileStore.GetByIdAsync(request.ProfileId, cancellationToken);\n            var mailbox = request.Mailbox ?? request.Login ?? \"user@example.com\";\n            profile!.Settings[MailProfileSettingsKeys.Mailbox] = mailbox;\n            profile.Settings[MailProfileSettingsKeys.RedirectUri] = request.RedirectUri ?? \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n            profile.DefaultMailbox = mailbox;\n            await _profileStore.SaveAsync(profile, cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.AccessToken, \"graph-access-token\", cancellationToken);\n            return new MailProfileAuthenticationResult {\n                Succeeded = true,\n                Message = \"Graph login completed.\",\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Graph,\n                UserName = request.Login ?? mailbox\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> RefreshAsync(string profileId, CancellationToken cancellationToken = default) {\n            RefreshCalls++;\n            LastRefreshProfileId = profileId;\n            var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken);\n            Assert.NotNull(profile);\n            return profile!.Kind switch {\n                MailProfileKind.Gmail => await LoginGmailAsync(new GmailProfileLoginRequest { ProfileId = profileId }, cancellationToken),\n                MailProfileKind.Graph => await LoginGraphAsync(new GraphProfileLoginRequest { ProfileId = profileId }, cancellationToken),\n                _ => new MailProfileAuthenticationResult {\n                    Succeeded = false,\n                    Code = \"refresh_not_supported\",\n                    Message = \"Refresh not supported.\",\n                    ProfileId = profileId,\n                    ProfileKind = profile.Kind\n                }\n            };\n        }\n\n        private static (string ProfileId, string SecretName) ParseSecretReference(string secretReference, string defaultProfileId) {\n            var normalized = secretReference.Trim();\n            var colonIndex = normalized.IndexOf(':');\n            var slashIndex = normalized.IndexOf('/');\n            var separatorIndex = colonIndex >= 0 && slashIndex >= 0\n                ? Math.Min(colonIndex, slashIndex)\n                : Math.Max(colonIndex, slashIndex);\n\n            if (separatorIndex < 0) {\n                return (defaultProfileId, normalized);\n            }\n\n            return (normalized[..separatorIndex], normalized[(separatorIndex + 1)..]);\n        }\n    }\n\n    private sealed class FakeProfileConnectionService : IMailProfileConnectionService {\n        public string? LastProfileId { get; private set; }\n\n        public MailProfileConnectionTestScope LastScope { get; private set; }\n\n        public Task<MailProfileConnectionTestResult> TestAsync(\n            string profileId,\n            MailProfileConnectionTestScope scope = MailProfileConnectionTestScope.Auto,\n            CancellationToken cancellationToken = default) {\n            LastProfileId = profileId;\n            LastScope = scope;\n            return Task.FromResult(new MailProfileConnectionTestResult {\n                Succeeded = true,\n                Message = \"Profile connection succeeded.\",\n                ProfileId = profileId,\n                ProfileKind = MailProfileKind.Imap,\n                Probe = \"connect\",\n                Target = \"work@example.com\",\n                RequestedScope = scope,\n                ExecutedScope = scope == MailProfileConnectionTestScope.Auto ? MailProfileConnectionTestScope.Auth : scope\n            });\n        }\n    }\n\n    private sealed class TestApplicationFixture {\n        public TestApplicationFixture() {\n            ProfileStore = new InMemoryProfileStore(new[] {\n                new MailProfile {\n                    Id = \"work-imap\",\n                    DisplayName = \"Work IMAP\",\n                    Kind = MailProfileKind.Imap,\n                    Settings = new Dictionary<string, string> {\n                        [MailProfileSettingsKeys.Server] = \"imap.example.com\"\n                    }\n                }\n            });\n            SecretStore = new InMemorySecretStore();\n            SecretStore.SetSecretAsync(\"work-imap\", MailSecretNames.Password, \"secret\").GetAwaiter().GetResult();\n            ProfileAuthService = new FakeProfileAuthService(ProfileStore, SecretStore);\n        }\n\n        public InMemoryProfileStore ProfileStore { get; }\n\n        public InMemorySecretStore SecretStore { get; }\n\n        public FakeReadService ReadService { get; } = new();\n\n        public FakeQueueService QueueService { get; } = new();\n\n        public FakeSendService SendService { get; } = new();\n\n        public FakeMessageActionService MessageActionService { get; } = new();\n\n        public FakeMessageActionPlanExchangeService MessageActionPlanExchangeService { get; } = new();\n\n        public FakeMessageActionPlanRegistryService MessageActionPlanRegistryService { get; } = new();\n\n        public FakeDraftService DraftService { get; } = new();\n\n        public FakeDraftExchangeService DraftExchangeService { get; } = new();\n\n        public FakeProfileAuthService ProfileAuthService { get; }\n\n        public FakeProfileConnectionService ProfileConnectionService { get; } = new();\n\n        public MailApplicationBuilder CreateBuilder() =>\n            new MailApplicationBuilder()\n                .UseProfileStore(ProfileStore)\n                .UseSecretStore(SecretStore)\n                .UseDraftStore(new FileMailDraftStore(CreateTemporaryFilePath(\"drafts.json\")))\n                .UseProfileService(new MailProfileService(ProfileStore, SecretStore))\n                .UseProfileConnectionService(ProfileConnectionService)\n                .UseProfileSecretService(new MailProfileSecretService(ProfileStore, SecretStore))\n                .UseProfileAuthService(ProfileAuthService)\n                .UseDraftService(DraftService)\n                .UseDraftExchangeService(DraftExchangeService)\n                .UseReadService(ReadService)\n                .UseMessageActionService(MessageActionService)\n                .UseMessageActionPlanExchangeService(MessageActionPlanExchangeService)\n                .UseMessageActionPlanRegistryService(MessageActionPlanRegistryService)\n                .UseQueueService(QueueService)\n                .UseSendService(SendService);\n\n        private static string CreateTemporaryFilePath(string fileName) {\n            var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n            Directory.CreateDirectory(directory);\n            return Path.Combine(directory, fileName);\n        }\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ClientSmtpDisposeTests.cs",
    "content": "using System.Threading;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ClientSmtpDisposeTests\n{\n    private class FakeClient : ClientSmtp\n    {\n        public bool DisconnectCalled;\n        private bool _connected;\n        public override bool IsConnected => _connected;\n        public void SetConnected(bool value) => _connected = value;\n        public override void Disconnect(bool quit, CancellationToken cancellationToken = default)\n        {\n            DisconnectCalled = true;\n        }\n    }\n\n    [Fact]\n    public void Dispose_DisconnectsWhenConnected()\n    {\n        var client = new FakeClient();\n        client.SetConnected(true);\n        client.Dispose();\n        Assert.True(client.DisconnectCalled);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ClientSmtpTests.cs",
    "content": "using System.Collections.Generic;\nusing System;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\npublic class ClientSmtpTests\n{\n    [Fact]\n    public void Ctor_DefaultsPriorityToNormal()\n    {\n        var client = new ClientSmtp();\n        Assert.Equal(MessagePriority.Normal, client.Priority);\n    }\n    [Fact]\n    public void ConvertToMailboxAddress_InvalidType_IncludesValueInException()\n    {\n        var client = new ClientSmtp();\n        MethodInfo? method = typeof(ClientSmtp).GetMethod(\n            \"ConvertToMailboxAddress\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var enumerable = (IEnumerable<MailboxAddress>)method!.Invoke(client, new object[] { 42 })!;\n        using var enumerator = enumerable.GetEnumerator();\n        var ex = Assert.Throws<ArgumentException>(() => enumerator.MoveNext());\n        Assert.Contains(\"42\", ex.Message, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public void ConvertStringToMailboxAddresses_InvalidInput_LogsWarning()\n    {\n        var client = new ClientSmtp();\n        MethodInfo? method = typeof(ClientSmtp).GetMethod(\n            \"ConvertStringToMailboxAddresses\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        var enumerable = (IEnumerable<MailboxAddress>)method!.Invoke(client, new object[] { \"invalid@\" })!;\n        var result = enumerable.ToList();\n\n        LoggingMessages.Logger.OnWarningMessage -= Handler;\n        Assert.Empty(result);\n        Assert.Contains(messages, static m => m.Contains(\"invalid@\"));\n    }\n\n    [Fact]\n    public async Task CreateMessage_SyncAndAsyncProduceEquivalentMessages()\n    {\n        const string url = \"https://example.com/img.png\";\n        var html = $\"<img src=\\\"{url}\\\">\";\n        var imageContent = new byte[] { 1, 2, 3 };\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new ByteArrayContent(imageContent)\n                {\n                    Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n                }\n            },\n            new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new ByteArrayContent(imageContent)\n                {\n                    Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n                }\n            });\n\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n\n        try\n        {\n            var syncClient = CreateClientForTest(html);\n            syncClient.CreateMessage();\n\n            var asyncClient = CreateClientForTest(html);\n            await asyncClient.CreateMessageAsync();\n\n            Assert.Equal(syncClient.Message.HtmlBody, asyncClient.Message.HtmlBody);\n            Assert.Equal(syncClient.Message.TextBody, asyncClient.Message.TextBody);\n            Assert.Contains(\"cid:\", syncClient.Message.HtmlBody);\n\n            var syncInline = GetInlineAttachments(syncClient.Message).OrderBy(p => p.ContentId).ToList();\n            var asyncInline = GetInlineAttachments(asyncClient.Message).OrderBy(p => p.ContentId).ToList();\n\n            Assert.Equal(syncInline.Count, asyncInline.Count);\n            for (var i = 0; i < syncInline.Count; i++)\n            {\n                Assert.Equal(syncInline[i].ContentId, asyncInline[i].ContentId);\n                Assert.Equal(syncInline[i].MediaType, asyncInline[i].MediaType);\n            }\n        }\n        finally\n        {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task CreateMessage_CancellationPropagatesToBothOverloads()\n    {\n        const string url = \"https://example.com/slow.png\";\n        var html = $\"<img src=\\\"{url}\\\">\";\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n\n        try\n        {\n            handlerField.SetValue(client, new DelayedHandler());\n            var asyncClient = CreateClientForTest(html);\n            using (var asyncCts = new CancellationTokenSource())\n            {\n                var asyncOperation = asyncClient.CreateMessageAsync(asyncCts.Token);\n                asyncCts.CancelAfter(TimeSpan.FromMilliseconds(100));\n                await Assert.ThrowsAsync<OperationCanceledException>(async () => await asyncOperation);\n            }\n\n            handlerField.SetValue(client, new DelayedHandler());\n            var syncClient = CreateClientForTest(html);\n            using var syncCts = new CancellationTokenSource();\n            var syncTask = Task.Run(() => syncClient.CreateMessage(syncCts.Token));\n            syncCts.CancelAfter(TimeSpan.FromMilliseconds(100));\n            await Assert.ThrowsAsync<OperationCanceledException>(async () => await syncTask);\n        }\n        finally\n        {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    private static ClientSmtp CreateClientForTest(string html)\n    {\n        return new ClientSmtp\n        {\n            AutoEmbedRemoteImages = true,\n            HtmlBody = html,\n            Subject = \"Hello\",\n            From = \"sender@example.com\",\n            To = new object[] { \"recipient@example.com\" }\n        };\n    }\n\n    private static List<InlineAttachmentInfo> GetInlineAttachments(MimeMessage message)\n    {\n        var attachments = new List<InlineAttachmentInfo>();\n        foreach (var part in message.BodyParts.OfType<MimePart>())\n        {\n            if (!string.Equals(part.ContentDisposition?.Disposition, ContentDisposition.Inline, StringComparison.OrdinalIgnoreCase))\n            {\n                continue;\n            }\n\n            attachments.Add(new InlineAttachmentInfo(part.ContentId ?? string.Empty, part.ContentType.MimeType));\n        }\n\n        return attachments;\n    }\n\n    private sealed class InlineAttachmentInfo\n    {\n        public InlineAttachmentInfo(string contentId, string mediaType)\n        {\n            ContentId = contentId;\n            MediaType = mediaType;\n        }\n\n        public string ContentId { get; }\n\n        public string MediaType { get; }\n    }\n\n    private sealed class DelayedHandler : HttpMessageHandler\n    {\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);\n            return new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new ByteArrayContent(new byte[] { 1, 2, 3 })\n                {\n                    Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n                }\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/CmdletImportMailFileTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Mailozaurr.PowerShell;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class CmdletImportMailFileTests {\n    [Fact]\n    public void ProcessRecordAsync_EmptyPath_Warns() {\n        var cmdlet = new CmdletImportMailFile { InputPath = \" \" };\n\n        var (outputs, warnings) = InvokeAndCapture(cmdlet);\n\n        Assert.Empty(outputs);\n        Assert.Single(warnings);\n        Assert.Contains(\"File path is empty\", warnings[0], StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void ProcessRecordAsync_UnsupportedExtension_Warns() {\n        var tempDir = CreateTempDirectory();\n        try {\n            var txtPath = Path.Combine(tempDir, \"sample.txt\");\n            File.WriteAllText(txtPath, \"data\");\n            var cmdlet = new CmdletImportMailFile { InputPath = txtPath };\n\n            var (outputs, warnings) = InvokeAndCapture(cmdlet);\n\n            Assert.Empty(outputs);\n            Assert.Single(warnings);\n            Assert.Contains(\"not a .msg or .eml\", warnings[0], StringComparison.OrdinalIgnoreCase);\n        } finally {\n            Directory.Delete(tempDir, true);\n        }\n    }\n\n    [Fact]\n    public void ProcessRecordAsync_ValidEml_OutputsMessage() {\n        var tempDir = CreateTempDirectory();\n        try {\n            var emlPath = CreateEmlFile(tempDir, \"sample.eml\", \"Cmdlet subject\", \"Hello\");\n            var cmdlet = new CmdletImportMailFile { InputPath = emlPath };\n\n            var (outputs, warnings) = InvokeAndCapture(cmdlet);\n\n            Assert.Empty(warnings);\n            Assert.Single(outputs);\n            var message = Assert.IsType<MailFileMessage>(outputs[0]);\n            Assert.Equal(MailFileFormat.Eml, message.Format);\n            Assert.Equal(\"Cmdlet subject\", message.Subject);\n        } finally {\n            Directory.Delete(tempDir, true);\n        }\n    }\n\n    private static (List<object?> Outputs, List<string> Warnings) InvokeAndCapture(CmdletImportMailFile cmdlet) {\n        var asyncType = typeof(AsyncPSCmdlet);\n        var pipelineType = asyncType.GetNestedType(\"PipelineType\", BindingFlags.NonPublic)!;\n        var tupleType = typeof(ValueTuple<,>).MakeGenericType(typeof(object), pipelineType);\n        var outPipeType = typeof(BlockingCollection<>).MakeGenericType(tupleType);\n\n        var outPipe = Activator.CreateInstance(outPipeType)!;\n        var replyPipe = new BlockingCollection<object?>();\n\n        var outPipeField = asyncType.GetField(\"_currentOutPipe\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var replyPipeField = asyncType.GetField(\"_currentReplyPipe\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        outPipeField.SetValue(cmdlet, outPipe);\n        replyPipeField.SetValue(cmdlet, replyPipe);\n\n        var method = typeof(CmdletImportMailFile).GetMethod(\"ProcessRecordAsync\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var task = (Task)method.Invoke(cmdlet, null)!;\n        task.GetAwaiter().GetResult();\n\n        var outputs = new List<object?>();\n        var warnings = new List<string>();\n        var items = (Array)outPipeType.GetMethod(\"ToArray\")!.Invoke(outPipe, null)!;\n        var item1Field = tupleType.GetField(\"Item1\")!;\n        var item2Field = tupleType.GetField(\"Item2\")!;\n\n        foreach (var item in items) {\n            var data = item1Field.GetValue(item);\n            var pipeline = item2Field.GetValue(item)?.ToString();\n            if (string.Equals(pipeline, \"Output\", StringComparison.Ordinal) || string.Equals(pipeline, \"OutputEnumerate\", StringComparison.Ordinal)) {\n                outputs.Add(data);\n            } else if (string.Equals(pipeline, \"Warning\", StringComparison.Ordinal)) {\n                warnings.Add(data?.ToString() ?? string.Empty);\n            }\n        }\n\n        outPipeField.SetValue(cmdlet, null);\n        replyPipeField.SetValue(cmdlet, null);\n\n        return (outputs, warnings);\n    }\n\n    private static string CreateTempDirectory() {\n        var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(path);\n        return path;\n    }\n\n    private static string CreateEmlFile(string directory, string fileName, string subject, string body) {\n        var content = string.Join(\"\\r\\n\", new[] {\n            \"From: Alice <alice@example.com>\",\n            \"To: Bob <bob@example.com>\",\n            $\"Subject: {subject}\",\n            \"Date: Mon, 21 Jun 2021 10:00:00 +0000\",\n            \"Message-ID: <test@example.com>\",\n            \"MIME-Version: 1.0\",\n            \"Content-Type: text/plain; charset=utf-8\",\n            string.Empty,\n            body\n        });\n\n        var path = Path.Combine(directory, fileName);\n        File.WriteAllText(path, content);\n        return path;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/CmdletWaitImapMessageTests.cs",
    "content": "using MailKit.Net.Imap;\nusing System;\nusing System.Reflection;\nusing Xunit;\nusing Mailozaurr.PowerShell;\n\nnamespace Mailozaurr.Tests;\n\npublic class CmdletWaitImapMessageTests\n{\n    [Fact]\n    public void EndProcessing_DisposesListenerAndDetachesEvents()\n    {\n        var cmd = new CmdletWaitIMAPMessage();\n        var listener = new ImapIdleListener(new ImapClient());\n        var field = typeof(CmdletWaitIMAPMessage).GetField(\"_listener\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(cmd, listener);\n\n        var method = typeof(CmdletWaitIMAPMessage).GetMethod(\"OnMessageArrived\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var handler = (EventHandler<ImapEmailMessage>)Delegate.CreateDelegate(typeof(EventHandler<ImapEmailMessage>), cmd, method);\n        listener.MessageArrived += handler;\n\n        typeof(CmdletWaitIMAPMessage).GetMethod(\"EndProcessing\", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(cmd, null);\n\n        var eventField = typeof(ImapIdleListener).GetField(\"MessageArrived\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        Assert.Null(eventField.GetValue(listener));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ComposeProfileUtilitiesTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ComposeProfileUtilitiesTests {\n    [Fact]\n    public void NormalizeProfiles_AppliesFallbackValues_AndKeepsOneDefault() {\n        var profiles = new[] {\n            new MailComposeProfile {\n                Id = \"sales\",\n                Name = \"Sales\",\n                From = \"sales@example.com\",\n                IsDefault = true\n            },\n            new MailComposeProfile {\n                Name = \"Billing\",\n                From = \"billing@example.com\",\n                ReplyTo = \"billing-replies@example.com\"\n            }\n        };\n\n        var fallback = new MailComposeProfile {\n            ReplyTo = \"reply@example.com\",\n            SignatureText = \"Regards\",\n            IsDefault = true\n        };\n\n        var normalized = ComposeProfileUtilities.NormalizeProfiles(profiles, fallback);\n\n        Assert.Equal(2, normalized.Count);\n        Assert.Equal(\"sales\", normalized[0].Id);\n        Assert.Equal(\"reply@example.com\", normalized[0].ReplyTo);\n        Assert.Equal(\"Regards\", normalized[0].SignatureText);\n        Assert.True(normalized[0].IsDefault);\n        Assert.Equal(\"billing\", normalized[1].Id);\n        Assert.Equal(\"billing-replies@example.com\", normalized[1].ReplyTo);\n        Assert.False(normalized[1].IsDefault);\n    }\n\n    [Fact]\n    public void NormalizeProfiles_SynthesizesFallbackProfile_WhenConfiguredProfilesMissing() {\n        var normalized = ComposeProfileUtilities.NormalizeProfiles(\n            profiles: null,\n            fallbackProfile: new MailComposeProfile {\n                From = \"default@example.com\",\n                ReplyTo = \"reply@example.com\",\n                SignatureText = \"Thanks\"\n            });\n\n        Assert.Single(normalized);\n        Assert.Equal(\"default\", normalized[0].Id);\n        Assert.Equal(\"default@example.com\", normalized[0].From);\n        Assert.True(normalized[0].IsDefault);\n    }\n\n    [Fact]\n    public void GetDefaultProfile_ReturnsFirstMarkedDefault() {\n        var profiles = new List<MailComposeProfile> {\n            new MailComposeProfile { Id = \"one\", IsDefault = false },\n            new MailComposeProfile { Id = \"two\", IsDefault = true }\n        };\n\n        var selected = ComposeProfileUtilities.GetDefaultProfile(profiles);\n\n        Assert.NotNull(selected);\n        Assert.Equal(\"two\", selected!.Id);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ConnectorTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Imap;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ConnectorTests\n{\n    private class FakeImapClient : ImapClient\n    {\n        public int FailuresBeforeSuccess { get; set; }\n        public int ConnectCalls { get; private set; }\n        public string? LastHost { get; private set; }\n        public int LastPort { get; private set; }\n        public SecureSocketOptions LastSecureSocketOptions { get; private set; }\n        public new bool Authenticated { get; set; }\n        public override bool IsAuthenticated => Authenticated;\n        private bool _connected;\n        private int _timeout;\n        public string? AuthMechanism { get; private set; }\n        public override bool IsConnected => _connected;\n        public override int Timeout { get => _timeout; set => _timeout = value; }\n        public bool Disposed { get; private set; }\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default)\n        {\n            ConnectCalls++;\n            LastHost = host;\n            LastPort = port;\n            LastSecureSocketOptions = options;\n            if (ConnectCalls <= FailuresBeforeSuccess)\n            {\n                throw new HttpRequestException(\"fail\");\n            }\n            _connected = true;\n            return Task.CompletedTask;\n        }\n        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default)\n        {\n            _connected = false;\n            return Task.CompletedTask;\n        }\n        public override Task AuthenticateAsync(SaslMechanism mechanism, CancellationToken cancellationToken = default) {\n            AuthMechanism = mechanism.GetType().Name;\n            Authenticated = true;\n            return Task.CompletedTask;\n        }\n        protected override void Dispose(bool disposing)\n        {\n            Disposed = true;\n            base.Dispose(disposing);\n        }\n    }\n\n    private class FakePop3Client : Pop3Client\n    {\n        public int FailuresBeforeSuccess { get; set; }\n        public int ConnectCalls { get; private set; }\n        public new bool Authenticated { get; set; }\n        public override bool IsAuthenticated => Authenticated;\n        private bool _connected;\n        private int _timeout;\n        public override bool IsConnected => _connected;\n        public override int Timeout { get => _timeout; set => _timeout = value; }\n        public bool Disposed { get; private set; }\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default)\n        {\n            ConnectCalls++;\n            if (ConnectCalls <= FailuresBeforeSuccess)\n            {\n                throw new HttpRequestException(\"fail\");\n            }\n            _connected = true;\n            return Task.CompletedTask;\n        }\n        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default)\n        {\n            _connected = false;\n            return Task.CompletedTask;\n        }\n        protected override void Dispose(bool disposing)\n        {\n            Disposed = true;\n            base.Dispose(disposing);\n        }\n    }\n\n    private class FakeImapDisconnectFailClient : FakeImapClient\n    {\n        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default)\n            => Task.FromException(new InvalidOperationException(\"disconnect\"));\n    }\n\n    private class FakePop3DisconnectFailClient : FakePop3Client\n    {\n        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default)\n            => Task.FromException(new InvalidOperationException(\"disconnect\"));\n    }\n\n    [Fact]\n    public async Task ImapConnector_RetriesUntilSuccess()\n    {\n        var fake = new FakeImapClient { FailuresBeforeSuccess = 2 };\n        var delays = new List<int>();\n        ImapConnector.ClientFactory = () => fake;\n        ImapConnector.DelayAsync = (d, ct) => { delays.Add(d); return Task.CompletedTask; };\n        var client = await ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            3, 10, 2);\n        ImapConnector.ClientFactory = () => new ImapClient();\n        ImapConnector.DelayAsync = null;\n        Assert.Same(fake, client);\n        Assert.Equal(3, fake.ConnectCalls);\n        Assert.Equal(new[] { 10, 20 }, delays);\n    }\n\n    [Fact]\n    public async Task ImapConnector_ThrowsAfterRetries()\n    {\n        var fake = new FakeImapClient { FailuresBeforeSuccess = 5 };\n        var delays = new List<int>();\n        ImapConnector.ClientFactory = () => fake;\n        ImapConnector.DelayAsync = (d, ct) => { delays.Add(d); return Task.CompletedTask; };\n        await Assert.ThrowsAsync<HttpRequestException>(() => ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            2, 10, 2));\n        ImapConnector.ClientFactory = () => new ImapClient();\n        ImapConnector.DelayAsync = null;\n        Assert.Equal(3, fake.ConnectCalls);\n        Assert.Equal(new[] { 10, 20 }, delays);\n    }\n\n    [Fact]\n    public async Task ImapConnector_RequestOverload_UsesRequestSettings()\n    {\n        var fake = new FakeImapClient();\n        ImapConnector.ClientFactory = () => fake;\n        var request = new ImapConnectionRequest(\n            \"imap.example.test\",\n            1993,\n            SecureSocketOptions.SslOnConnect,\n            timeout: 4321,\n            skipCertificateRevocation: true,\n            skipCertificateValidation: true,\n            retryCount: 0,\n            retryDelayMilliseconds: 10,\n            retryDelayBackoff: 2.0);\n\n        var client = await ImapConnector.ConnectAsync(\n            request,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; });\n\n        ImapConnector.ClientFactory = () => new ImapClient();\n\n        Assert.Same(fake, client);\n        Assert.Equal(\"imap.example.test\", fake.LastHost);\n        Assert.Equal(1993, fake.LastPort);\n        Assert.Equal(SecureSocketOptions.SslOnConnect, fake.LastSecureSocketOptions);\n        Assert.Equal(4321, fake.Timeout);\n    }\n\n    [Fact]\n    public async Task ImapConnector_ConnectAuthenticatedAsync_UsesOAuthAuthentication() {\n        var fake = new FakeImapClient();\n        ImapConnector.ClientFactory = () => fake;\n        var request = new ImapConnectionRequest(\"imap.example.test\", 993);\n\n        var client = await ImapConnector.ConnectAuthenticatedAsync(\n            request,\n            userName: \"user@example.com\",\n            secret: \"oauth-token\",\n            mode: ProtocolAuthMode.OAuth2);\n\n        ImapConnector.ClientFactory = () => new ImapClient();\n\n        Assert.Same(fake, client);\n        Assert.True(fake.IsAuthenticated);\n        Assert.Equal(nameof(SaslMechanismOAuth2), fake.AuthMechanism);\n    }\n\n    [Fact]\n    public async Task ImapConnector_RequestOverload_ValidatesArguments()\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            ImapConnector.ConnectAsync(\n                null!,\n                (_, _) => Task.CompletedTask));\n\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            ImapConnector.ConnectAsync(\n                new ImapConnectionRequest(\"imap.example.test\", 993),\n                null!));\n    }\n\n    [Fact]\n    public async Task Pop3Connector_RetriesUntilSuccess()\n    {\n        var fake = new FakePop3Client { FailuresBeforeSuccess = 1 };\n        var delays = new List<int>();\n        Pop3Connector.ClientFactory = () => fake;\n        Pop3Connector.DelayAsync = (d, ct) => { delays.Add(d); return Task.CompletedTask; };\n        var client = await Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            2, 10, 2);\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Pop3Connector.DelayAsync = null;\n        Assert.Same(fake, client);\n        Assert.Equal(2, fake.ConnectCalls);\n        Assert.Equal(new[] { 10 }, delays);\n    }\n\n    [Fact]\n    public async Task Pop3Connector_ThrowsAfterRetries()\n    {\n        var fake = new FakePop3Client { FailuresBeforeSuccess = 4 };\n        var delays = new List<int>();\n        Pop3Connector.ClientFactory = () => fake;\n        Pop3Connector.DelayAsync = (d, ct) => { delays.Add(d); return Task.CompletedTask; };\n        await Assert.ThrowsAsync<HttpRequestException>(() => Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            2, 10, 2));\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Pop3Connector.DelayAsync = null;\n        Assert.Equal(3, fake.ConnectCalls);\n        Assert.Equal(new[] { 10, 20 }, delays);\n    }\n\n    [Fact]\n    public async Task ImapConnector_NoRetriesThrowsOriginalException()\n    {\n        var fake = new FakeImapClient { FailuresBeforeSuccess = 1 };\n        ImapConnector.ClientFactory = () => fake;\n        await Assert.ThrowsAsync<HttpRequestException>(() => ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            0, 10, 2));\n        ImapConnector.ClientFactory = () => new ImapClient();\n        Assert.Equal(1, fake.ConnectCalls);\n    }\n\n    [Fact]\n    public async Task Pop3Connector_NoRetriesThrowsOriginalException()\n    {\n        var fake = new FakePop3Client { FailuresBeforeSuccess = 1 };\n        Pop3Connector.ClientFactory = () => fake;\n        await Assert.ThrowsAsync<HttpRequestException>(() => Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            0, 10, 2));\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Assert.Equal(1, fake.ConnectCalls);\n    }\n\n    [Fact]\n    public async Task ImapConnector_CancellationStopsRetries()\n    {\n        var fake = new FakeImapClient { FailuresBeforeSuccess = 5 };\n        var cts = new CancellationTokenSource();\n        ImapConnector.ClientFactory = () => fake;\n        ImapConnector.DelayAsync = (d, ct) => { cts.Cancel(); return Task.Delay(d, ct); };\n        await Assert.ThrowsAsync<TaskCanceledException>(() => ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            3, 10, 2, cts.Token));\n        ImapConnector.ClientFactory = () => new ImapClient();\n        ImapConnector.DelayAsync = null;\n        Assert.Equal(1, fake.ConnectCalls);\n    }\n\n    [Fact]\n    public async Task Pop3Connector_CancellationStopsRetries()\n    {\n        var fake = new FakePop3Client { FailuresBeforeSuccess = 5 };\n        var cts = new CancellationTokenSource();\n        Pop3Connector.ClientFactory = () => fake;\n        Pop3Connector.DelayAsync = (d, ct) => { cts.Cancel(); return Task.Delay(d, ct); };\n        await Assert.ThrowsAsync<TaskCanceledException>(() => Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            3, 10, 2, cts.Token));\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Pop3Connector.DelayAsync = null;\n        Assert.Equal(1, fake.ConnectCalls);\n    }\n\n    [Fact]\n    public async Task ImapConnector_LogsDisconnectException()\n    {\n        var fake = new FakeImapDisconnectFailClient();\n        ImapConnector.ClientFactory = () => fake;\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (_, _) => throw new InvalidOperationException(\"auth\"),\n            0, 0, 1));\n\n        LoggingMessages.Logger.OnWarningMessage -= Handler;\n        ImapConnector.ClientFactory = () => new ImapClient();\n        Assert.Contains(messages, static m => m.Contains(\"disconnect\"));\n    }\n\n    [Fact]\n    public async Task ImapConnector_DisposesClientBeforeRetry()\n    {\n        var first = new FakeImapClient { FailuresBeforeSuccess = 1 };\n        var second = new FakeImapClient();\n        var call = 0;\n        ImapConnector.ClientFactory = () =>\n        {\n            call++;\n            if (call == 2)\n            {\n                Assert.True(first.Disposed);\n                return second;\n            }\n            return first;\n        };\n        var client = await ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            1, 0, 1);\n        ImapConnector.ClientFactory = () => new ImapClient();\n        Assert.Same(second, client);\n    }\n\n    [Fact]\n    public async Task ImapConnector_DisposesClientAfterFinalFailure()\n    {\n        var fake = new FakeImapClient { FailuresBeforeSuccess = 1 };\n        ImapConnector.ClientFactory = () => fake;\n        await Assert.ThrowsAsync<HttpRequestException>(() => ImapConnector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakeImapClient)c).Authenticated = true; return Task.CompletedTask; },\n            0, 0, 1));\n        ImapConnector.ClientFactory = () => new ImapClient();\n        Assert.True(fake.Disposed);\n    }\n\n    [Fact]\n    public async Task Pop3Connector_LogsDisconnectException()\n    {\n        var fake = new FakePop3DisconnectFailClient();\n        Pop3Connector.ClientFactory = () => fake;\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (_, _) => throw new InvalidOperationException(\"auth\"),\n            0, 0, 1));\n\n        LoggingMessages.Logger.OnWarningMessage -= Handler;\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Assert.Contains(messages, static m => m.Contains(\"disconnect\"));\n    }\n\n    [Fact]\n    public async Task Pop3Connector_DisposesClientBeforeRetry()\n    {\n        var first = new FakePop3Client { FailuresBeforeSuccess = 1 };\n        var second = new FakePop3Client();\n        var call = 0;\n        Pop3Connector.ClientFactory = () =>\n        {\n            call++;\n            if (call == 2)\n            {\n                Assert.True(first.Disposed);\n                return second;\n            }\n            return first;\n        };\n        var client = await Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            1, 0, 1);\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Assert.Same(second, client);\n    }\n\n    [Fact]\n    public async Task Pop3Connector_DisposesClientAfterFinalFailure()\n    {\n        var fake = new FakePop3Client { FailuresBeforeSuccess = 1 };\n        Pop3Connector.ClientFactory = () => fake;\n        await Assert.ThrowsAsync<HttpRequestException>(() => Pop3Connector.ConnectAsync(\n            \"s\", 1, SecureSocketOptions.Auto, 0, false, false,\n            (c, ct) => { ((FakePop3Client)c).Authenticated = true; return Task.CompletedTask; },\n            0, 0, 1));\n        Pop3Connector.ClientFactory = () => new Pop3Client();\n        Assert.True(fake.Disposed);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ConvertFromGraphCredentialTests.cs",
    "content": "using System;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\npublic class ConvertFromGraphCredentialTests\n{\n    [Fact]\n    public void ThrowsOnNullUsername()\n    {\n        Assert.Throws<ArgumentNullException>(() => MicrosoftGraphUtils.ConvertFromGraphCredential(null!, \"secret\"));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"   \")]\n    public void ThrowsOnWhitespaceUsername(string username)\n    {\n        Assert.Throws<ArgumentException>(() => MicrosoftGraphUtils.ConvertFromGraphCredential(username, \"secret\"));\n    }\n\n    [Fact]\n    public void ThrowsOnNullPassword()\n    {\n        Assert.Throws<ArgumentNullException>(() => MicrosoftGraphUtils.ConvertFromGraphCredential(\"client@tenant\", null!));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"   \")]\n    public void ThrowsOnWhitespacePassword(string password)\n    {\n        Assert.Throws<ArgumentException>(() => MicrosoftGraphUtils.ConvertFromGraphCredential(\"client@tenant\", password));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/CredentialHelpersTests.cs",
    "content": "using System.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class CredentialHelpersTests {\n    [Fact]\n    public void ToSecureString_ReturnsEmpty_WhenInputIsNull() {\n        SecureString result = Mailozaurr.PowerShell.CredentialHelpers.ToSecureString(null);\n        Assert.NotNull(result);\n        Assert.Equal(0, result.Length);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Cryptography/AesCredentialProtectorTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests.Cryptography;\n\npublic sealed class AesCredentialProtectorTests {\n    [Fact]\n    public async Task ConcurrentInstancesShareKeyMaterial() {\n        using var scope = new KeyDirectoryScope();\n\n        var keyDirectory = CredentialProtectionPaths.ResolveKeyDirectory();\n        if (Directory.Exists(keyDirectory)) {\n            Directory.Delete(keyDirectory, true);\n        }\n\n        var startEvent = new ManualResetEventSlim(false);\n        var concurrencyLevel = Math.Max(4, Environment.ProcessorCount);\n        var tasks = new List<Task<(byte[] Key, string CipherText, string PlainText)>>(concurrencyLevel);\n\n        for (var i = 0; i < concurrencyLevel; i++) {\n            var secret = $\"secret-{i}\";\n            tasks.Add(Task.Run(() => {\n                startEvent.Wait();\n                var protector = new AesCredentialProtector();\n                var cipher = protector.Protect(secret);\n                var keyMaterial = GetKeyMaterial(protector);\n                return (keyMaterial, cipher, secret);\n            }));\n        }\n\n        startEvent.Set();\n\n        var results = await Task.WhenAll(tasks);\n\n        var verifier = new AesCredentialProtector();\n        foreach (var (_, cipher, plain) in results) {\n            var roundtrip = verifier.Unprotect(cipher);\n            Assert.Equal(plain, roundtrip);\n        }\n\n        var keyPath = Path.Combine(keyDirectory, \"credential.key\");\n        Assert.True(File.Exists(keyPath));\n        var keyBytes = File.ReadAllBytes(keyPath);\n        Assert.Equal(32, keyBytes.Length);\n\n        foreach (var (key, _, _) in results) {\n            Assert.Equal(keyBytes, key);\n        }\n    }\n\n    private static byte[] GetKeyMaterial(AesCredentialProtector protector) {\n        var field = typeof(AesCredentialProtector).GetField(\"key\", BindingFlags.Instance | BindingFlags.NonPublic);\n        Assert.NotNull(field);\n        var value = field!.GetValue(protector) as byte[];\n        Assert.NotNull(value);\n        return ((byte[])value!).ToArray();\n    }\n\n    private sealed class KeyDirectoryScope : IDisposable {\n        private const string OverrideVariable = \"MAILOZAURR_KEY_DIRECTORY\";\n\n        private static readonly string[] BasePathVariables = new[] {\n            \"LOCALAPPDATA\",\n            \"APPDATA\",\n            \"HOME\",\n            \"USERPROFILE\",\n            \"XDG_DATA_HOME\"\n        };\n\n        private readonly Dictionary<string, string?> previousValues = new();\n        private readonly string root;\n\n        public KeyDirectoryScope() {\n            root = Path.Combine(Path.GetTempPath(), \"Mailozaurr\", \"KeyTests\", Guid.NewGuid().ToString(\"N\"));\n            Directory.CreateDirectory(root);\n\n            SetVariable(OverrideVariable, Path.Combine(root, \"keys\"));\n\n            foreach (var variable in BasePathVariables) {\n                SetVariable(variable, root);\n            }\n        }\n\n        public void Dispose() {\n            foreach (var pair in previousValues) {\n                Environment.SetEnvironmentVariable(pair.Key, pair.Value);\n            }\n\n            try {\n                if (Directory.Exists(root)) {\n                    Directory.Delete(root, true);\n                }\n            } catch {\n                // Best-effort cleanup.\n            }\n        }\n\n        private void SetVariable(string name, string value) {\n            previousValues[name] = Environment.GetEnvironmentVariable(name);\n            Environment.SetEnvironmentVariable(name, value);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Cryptography/CredentialProtectionTests.cs",
    "content": "using System;\nusing System.Text;\n\nnamespace Mailozaurr.Tests.Cryptography;\n\npublic sealed class CredentialProtectionTests {\n    [Fact]\n    public void ProtectRoundTripsSecret() {\n        const string secret = \"P@ssw0rd!\";\n\n        var protector = CredentialProtection.Default;\n        var encrypted = protector.Protect(secret);\n        var decrypted = protector.Unprotect(encrypted);\n\n        Assert.Equal(secret, decrypted);\n    }\n\n    [Fact]\n    public void ProtectDoesNotReturnPlainBase64() {\n        const string secret = \"AnotherSecret\";\n\n        var protector = CredentialProtection.Default;\n        var encrypted = protector.Protect(secret);\n        var plainBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));\n\n        Assert.NotEqual(plainBase64, encrypted);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/DraftMimeMessageFactoryTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class DraftMimeMessageFactoryTests {\n    [Fact]\n    public async Task CreateAsyncBuildsMimeMessageFromDraftAndProfileDefaults() {\n        var attachmentPath = CreateTemporaryFilePath(\"hello.txt\");\n        File.WriteAllText(attachmentPath, \"hello world\");\n\n        var factory = new DraftMimeMessageFactory();\n        var message = await factory.CreateAsync(\n            new MailProfile {\n                Id = \"gmail-personal\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultSender = \"Sender Name <sender@example.com>\"\n            },\n            new DraftMessage {\n                Subject = \"Hello\",\n                TextBody = \"plain text\",\n                HtmlBody = \"<b>html</b>\",\n                To = {\n                    new MessageRecipient { Name = \"Alice\", Address = \"alice@example.com\" }\n                },\n                ReplyTo = {\n                    new MessageRecipient { Address = \"reply@example.com\" }\n                },\n                Headers = {\n                    [\"X-Test\"] = \"value\"\n                },\n                Attachments = {\n                    new DraftAttachment {\n                        Path = attachmentPath\n                    }\n                }\n            });\n\n        Assert.Equal(\"sender@example.com\", message.From.Mailboxes.First().Address);\n        Assert.Equal(\"alice@example.com\", message.To.Mailboxes.First().Address);\n        Assert.Equal(\"reply@example.com\", message.ReplyTo.Mailboxes.First().Address);\n        Assert.Equal(\"Hello\", message.Subject);\n        Assert.Equal(\"plain text\", message.TextBody);\n        Assert.Equal(\"<b>html</b>\", message.HtmlBody);\n        Assert.Equal(\"value\", message.Headers[\"X-Test\"]);\n        Assert.Single(message.Attachments);\n    }\n\n    [Fact]\n    public async Task CreateAsyncRejectsDraftWithoutRecipients() {\n        var factory = new DraftMimeMessageFactory();\n\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => factory.CreateAsync(\n            new MailProfile {\n                Id = \"gmail-personal\",\n                DisplayName = \"Personal Gmail\",\n                Kind = MailProfileKind.Gmail,\n                DefaultSender = \"sender@example.com\"\n            },\n            new DraftMessage {\n                Subject = \"Hello\"\n            }));\n\n        Assert.Contains(\"recipient\", exception.Message, StringComparison.OrdinalIgnoreCase);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/EmailMessageConversionTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class EmailMessageConversionTests {\n    [Fact]\n    public void ConvertEmlToMsg_CreatesOutputDirectory() {\n        var emlDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(emlDir);\n        var emlPath = Path.Combine(emlDir, \"sample.eml\");\n        File.WriteAllText(emlPath, \"From: a@example.com\\r\\nTo: a@example.com\\r\\nSubject: Test\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nHello\");\n\n        var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var results = EmailMessage.ConvertEmlToMsg(new[] { emlPath }, outputDir, true).ToList();\n        Assert.True(results[0].Status);\n        Assert.True(Directory.Exists(outputDir));\n        Assert.True(File.Exists(Path.Combine(outputDir, \"sample.msg\")));\n\n        Directory.Delete(emlDir, true);\n        Directory.Delete(outputDir, true);\n    }\n\n    [Fact]\n    public void ConvertEmlToMsg_MultipleFiles() {\n        var tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(tmpDir);\n\n        var eml1 = Path.Combine(tmpDir, \"a.eml\");\n        File.WriteAllText(eml1, \"From: a@example.com\\r\\nTo: a@example.com\\r\\nSubject: A\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nHello\");\n        var eml2 = Path.Combine(tmpDir, \"b.eml\");\n        File.WriteAllText(eml2, \"From: b@example.com\\r\\nTo: b@example.com\\r\\nSubject: B\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nWorld\");\n\n        var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var results = EmailMessage.ConvertEmlToMsg(new[] { eml1, eml2 }, outputDir, true).ToList();\n\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.True(r.Status));\n        Assert.True(File.Exists(Path.Combine(outputDir, \"a.msg\")));\n        Assert.True(File.Exists(Path.Combine(outputDir, \"b.msg\")));\n\n        Directory.Delete(tmpDir, true);\n        Directory.Delete(outputDir, true);\n    }\n\n    [Fact]\n    public void ConvertMsgToEml_CreatesOutputDirectory() {\n        var tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(tmpDir);\n        var emlPath = Path.Combine(tmpDir, \"sample.eml\");\n        File.WriteAllText(emlPath, \"From: a@example.com\\r\\nTo: a@example.com\\r\\nSubject: Test\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nHello\");\n\n        var msgDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(msgDir);\n        EmailMessage.ConvertEmlToMsg(new[] { emlPath }, msgDir, true).ToList();\n        var msgPath = Path.Combine(msgDir, \"sample.msg\");\n\n        var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Assert.Throws<NotImplementedException>(() => EmailMessage.ConvertMsgToEml(new[] { msgPath }, outputDir, true).ToList());\n        Assert.True(Directory.Exists(outputDir));\n\n\n        Directory.Delete(tmpDir, true);\n        Directory.Delete(msgDir, true);\n        Directory.Delete(outputDir, true);\n    }\n\n    [Fact]\n    public void ConvertEmlToMsg_DoesNotOverwriteWhenForceFalse() {\n        var tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(tmpDir);\n        Directory.CreateDirectory(outputDir);\n        try {\n            var emlPath = Path.Combine(tmpDir, \"sample.eml\");\n            File.WriteAllText(emlPath, \"From: a@example.com\\r\\nTo: a@example.com\\r\\nSubject: Test\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nHello\");\n            var msgPath = Path.Combine(outputDir, \"sample.msg\");\n            File.WriteAllText(msgPath, \"sentinel\");\n\n            var result = EmailMessage.ConvertEmlToMsg(new FileInfo(emlPath), new FileInfo(msgPath), false);\n\n            Assert.False(result.Status);\n            Assert.NotNull(result.Error);\n            Assert.Contains(\"already exists\", result.Error!, StringComparison.OrdinalIgnoreCase);\n            Assert.Equal(\"sentinel\", File.ReadAllText(msgPath));\n        } finally {\n            Directory.Delete(tmpDir, true);\n            Directory.Delete(outputDir, true);\n        }\n    }\n\n    [Fact]\n    public void ConvertEmlToMsg_OverwritesWhenForceTrue() {\n        var tmpDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(tmpDir);\n        Directory.CreateDirectory(outputDir);\n        try {\n            var emlPath = Path.Combine(tmpDir, \"sample.eml\");\n            File.WriteAllText(emlPath, \"From: a@example.com\\r\\nTo: a@example.com\\r\\nSubject: Test\\r\\nDate: Mon, 21 Jun 2021 10:00:00 +0000\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nHello\");\n            var msgPath = Path.Combine(outputDir, \"sample.msg\");\n            File.WriteAllText(msgPath, \"sentinel\");\n            var sentinelLength = new FileInfo(msgPath).Length;\n\n            var result = EmailMessage.ConvertEmlToMsg(new FileInfo(emlPath), new FileInfo(msgPath), true);\n\n            Assert.True(result.Status);\n            Assert.True(File.Exists(msgPath));\n            Assert.True(new FileInfo(msgPath).Length > sentinelLength);\n        } finally {\n            Directory.Delete(tmpDir, true);\n            Directory.Delete(outputDir, true);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/EphemeralOpenPgpContextTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class EphemeralOpenPgpContextTests\n{\n    [Fact]\n    public async Task CreateTempDirectories_AreUniqueAcrossThreads()\n    {\n        const int count = 20;\n        var bag = new ConcurrentBag<string>();\n        var field = typeof(EphemeralOpenPgpContext).GetField(\"_tempDirectory\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n\n        var tasks = Enumerable.Range(0, count).Select(_ => Task.Run(() =>\n        {\n            using var ctx = new EphemeralOpenPgpContext();\n            var dir = (string)field.GetValue(ctx)!;\n            bag.Add(dir);\n        })).ToArray();\n\n        await Task.WhenAll(tasks);\n\n        Assert.Equal(count, bag.Distinct(StringComparer.Ordinal).Count());\n        foreach (var dir in bag)\n        {\n            Assert.False(Directory.Exists(dir));\n        }\n    }\n\n    [Fact]\n    public void Dispose_DoesNotThrow_WhenDirectoryAlreadyDeleted()\n    {\n        var field = typeof(EphemeralOpenPgpContext).GetField(\"_tempDirectory\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var ctx = new EphemeralOpenPgpContext();\n        var dir = (string)field.GetValue(ctx)!;\n\n        Assert.True(Directory.Exists(dir));\n        Directory.Delete(dir, true);\n        Assert.False(Directory.Exists(dir));\n\n        var ex = Record.Exception(() => ctx.Dispose());\n        Assert.Null(ex);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/FetchExamplesTests.cs",
    "content": "using System.Threading.Tasks;\nusing System.Collections.Generic;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests {\n#if NET8_0\n    public class FetchExamplesTests {\n        private class FakeImapFolder\n        {\n            public bool OpenCalled;\n            public bool ExpungeCalled;\n            public List<int> Fetched { get; } = new();\n\n            public Task OpenAsync(FolderAccess access)\n            {\n                OpenCalled = true;\n                return Task.CompletedTask;\n            }\n\n            public Task<IList<int>> SearchAsync(object? query)\n                => Task.FromResult<IList<int>>(new List<int> { 1, 2 });\n\n            public Task<MimeMessage> GetMessageAsync(int id)\n            {\n                Fetched.Add(id);\n                return Task.FromResult(new MimeMessage());\n            }\n\n            public Task AddFlagsAsync(int id, MessageFlags flags, bool silent)\n                => Task.CompletedTask;\n\n            public Task ExpungeAsync()\n            {\n                ExpungeCalled = true;\n                return Task.CompletedTask;\n            }\n        }\n\n        private class FakeImapClient\n        {\n            public bool ConnectCalled;\n            public bool AuthCalled;\n            public bool DisconnectCalled;\n            public FakeImapFolder Folder { get; } = new();\n\n            public Task ConnectAsync(string host, int port, SecureSocketOptions options)\n            {\n                ConnectCalled = true;\n                return Task.CompletedTask;\n            }\n\n            public Task AuthenticateAsync(string user, string pass)\n            {\n                AuthCalled = true;\n                return Task.CompletedTask;\n            }\n\n            public FakeImapFolder GetFolder(string name) => Folder;\n\n            public Task DisconnectAsync(bool quit)\n            {\n                DisconnectCalled = true;\n                return Task.CompletedTask;\n            }\n        }\n\n        private class FakePop3Client\n        {\n            public bool ConnectCalled;\n            public bool AuthCalled;\n            public bool DisconnectCalled;\n            public int Count { get; set; } = 2;\n            public List<int> Deleted { get; } = new();\n\n            public Task ConnectAsync(string host, int port, SecureSocketOptions options)\n            {\n                ConnectCalled = true;\n                return Task.CompletedTask;\n            }\n\n            public Task AuthenticateAsync(string user, string pass)\n            {\n                AuthCalled = true;\n                return Task.CompletedTask;\n            }\n\n            public Task<MimeMessage> GetMessageAsync(int index)\n                => Task.FromResult(new MimeMessage());\n\n            public Task DeleteMessageAsync(int index)\n            {\n                Deleted.Add(index);\n                return Task.CompletedTask;\n            }\n\n            public Task DisconnectAsync(bool quit)\n            {\n                DisconnectCalled = true;\n                return Task.CompletedTask;\n            }\n        }\n\n        [Fact]\n        public async Task FetchImapExample_Runs() {\n            var client = new FakeImapClient();\n            await client.ConnectAsync(\"imap.example.com\", 993, SecureSocketOptions.SslOnConnect);\n            await client.AuthenticateAsync(\"user@example.com\", \"Pa55w0rd\");\n            var folder = client.GetFolder(\"Inbox/Reports\");\n            Assert.NotNull(folder);\n            await folder.OpenAsync(FolderAccess.ReadWrite);\n            var uids = await folder.SearchAsync(null);\n            foreach (var uid in uids) {\n                _ = await folder.GetMessageAsync(uid);\n                await folder.AddFlagsAsync(uid, MessageFlags.Deleted, true);\n            }\n            if (uids.Count > 0) {\n                await folder.ExpungeAsync();\n            }\n            await client.DisconnectAsync(true);\n\n            Assert.True(client.ConnectCalled);\n            Assert.True(client.AuthCalled);\n            Assert.True(client.DisconnectCalled);\n            Assert.Equal(new[] { 1, 2 }, folder.Fetched);\n            Assert.True(folder.ExpungeCalled);\n        }\n\n        [Fact]\n        public async Task FetchPopExample_Runs() {\n            var client = new FakePop3Client();\n            await client.ConnectAsync(\"pop.example.com\", 995, SecureSocketOptions.SslOnConnect);\n            await client.AuthenticateAsync(\"user@example.com\", \"Pa55w0rd\");\n            for (int i = 0; i < client.Count; i++) {\n                _ = await client.GetMessageAsync(i);\n                await client.DeleteMessageAsync(i);\n            }\n            await client.DisconnectAsync(true);\n\n            Assert.True(client.ConnectCalled);\n            Assert.True(client.AuthCalled);\n            Assert.True(client.DisconnectCalled);\n            Assert.Equal(new[] { 0, 1 }, client.Deleted);\n        }\n    }\n#endif\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/FilePendingMessageRepositoryTests.cs",
    "content": "using System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class FilePendingMessageRepositoryTests {\n    [Fact]\n    public async Task SaveRetrieveAndRemove() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        try {\n            var repo = new FilePendingMessageRepository(options);\n            var message = new MimeMessage();\n            message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n            message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n            message.Subject = \"Pending\";\n            message.Body = new TextPart(\"plain\") { Text = \"Hello\" };\n            using var ms = new MemoryStream();\n            await message.WriteToAsync(ms);\n            var record = new PendingMessageRecord {\n                MessageId = message.MessageId ?? MimeKit.Utils.MimeUtils.GenerateMessageId(),\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.SendGrid\n            };\n            record.ProviderData[\"ApiKeyId\"] = \"sendgrid-key\";\n            await repo.SaveAsync(record);\n\n            var loaded = await repo.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(loaded);\n            Assert.True(loaded!.NextAttemptAt <= DateTimeOffset.UtcNow);\n            Assert.Equal(EmailProvider.SendGrid, loaded.Provider);\n            Assert.Equal(\"sendgrid-key\", loaded.ProviderData[\"ApiKeyId\"]);\n            using var ms2 = new MemoryStream(Convert.FromBase64String(loaded!.MimeMessage));\n            var restored = await MimeMessage.LoadAsync(ms2);\n            Assert.Equal(\"Pending\", restored.Subject);\n\n            await repo.RemoveAsync(record.MessageId);\n            var removed = await repo.GetByMessageIdAsync(record.MessageId);\n            Assert.Null(removed);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public void DefaultsToTempPath() {\n        var repo = new FilePendingMessageRepository();\n        var field = typeof(FilePendingMessageRepository).GetField(\"filePath\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var path = (string)field.GetValue(repo)!;\n        Assert.StartsWith(Path.GetTempPath(), path, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void DirectoryPath_MustNotBeNullOrEmpty() {\n        var options = new PendingMessageRepositoryOptions();\n        Assert.Throws<ArgumentException>(() => options.DirectoryPath = null!);\n        Assert.Throws<ArgumentException>(() => options.DirectoryPath = \"\");\n    }\n\n    [Fact]\n    public void FileNamingScheme_ExceptionWrapped() {\n        var options = new PendingMessageRepositoryOptions {\n            FileNamingScheme = () => throw new InvalidOperationException(\"boom\")\n        };\n        var ex = Assert.Throws<InvalidOperationException>(() => new FilePendingMessageRepository(options));\n        Assert.Contains(\"FileNamingScheme\", ex.Message);\n        Assert.IsType<InvalidOperationException>(ex.InnerException);\n    }\n\n    [Fact]\n    public async Task SaveAsync_ReplacesExistingMessageWithSameId() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var repo = new FilePendingMessageRepository(options);\n            var messageId = Guid.NewGuid().ToString(\"N\");\n            var initial = new PendingMessageRecord {\n                MessageId = messageId,\n                Timestamp = DateTimeOffset.UtcNow,\n                NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(-1),\n                Provider = EmailProvider.SendGrid,\n                AttemptCount = 1\n            };\n\n            await repo.SaveAsync(initial);\n\n            var updated = new PendingMessageRecord {\n                MessageId = messageId,\n                Timestamp = initial.Timestamp.AddMinutes(1),\n                NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(10),\n                Provider = EmailProvider.Mailgun,\n                AttemptCount = 3\n            };\n\n            await repo.SaveAsync(updated);\n\n            var loaded = await repo.GetByMessageIdAsync(messageId);\n            Assert.NotNull(loaded);\n            Assert.Equal(updated.AttemptCount, loaded!.AttemptCount);\n            Assert.Equal(updated.Provider, loaded.Provider);\n            Assert.Equal(updated.NextAttemptAt, loaded.NextAttemptAt);\n\n            var lines = File.ReadAllLines(filePath).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();\n\n            Assert.Equal(2, lines.Count);\n\n            using (var json = JsonDocument.Parse(lines[1])) {\n                Assert.True(TryGetProperty(json.RootElement, \"EntryType\", out var entryTypeElement));\n                Assert.Equal(\"upsert\", entryTypeElement.GetString(), StringComparer.OrdinalIgnoreCase);\n            }\n\n            var records = await ReadAllAsync(repo);\n            Assert.Single(records);\n            Assert.Equal(messageId, records[0].MessageId);\n            Assert.Equal(updated.AttemptCount, records[0].AttemptCount);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task SaveAsync_PerformsCompactionWhenThresholdReached() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var repo = new FilePendingMessageRepository(options);\n            var thresholdField = typeof(FilePendingMessageRepository).GetField(\"DefaultCompactionThreshold\", BindingFlags.NonPublic | BindingFlags.Static);\n            Assert.NotNull(thresholdField);\n            var threshold = (int)thresholdField!.GetValue(null)!;\n            var totalOperations = threshold + 5;\n\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.SendGrid\n            };\n\n            for (var i = 0; i < totalOperations; i++) {\n                record.AttemptCount = i;\n                record.NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(i);\n                await repo.SaveAsync(record);\n            }\n\n            var lines = File.ReadAllLines(filePath).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();\n            Assert.True(lines.Count < totalOperations, $\"Expected compaction to reduce log size. Entries: {lines.Count}, operations: {totalOperations}\");\n\n            var loaded = await repo.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(loaded);\n            Assert.Equal(record.AttemptCount, loaded!.AttemptCount);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task RemoveAsync_AppendsTombstoneEntry() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var repo = new FilePendingMessageRepository(options);\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.Mailgun\n            };\n\n            await repo.SaveAsync(record);\n\n            await repo.RemoveAsync(record.MessageId);\n\n            var lines = File.ReadAllLines(filePath).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();\n            Assert.Equal(2, lines.Count);\n\n            var lastLine = lines[lines.Count - 1];\n\n            using (var json = JsonDocument.Parse(lastLine)) {\n                Assert.True(TryGetProperty(json.RootElement, \"EntryType\", out var entryTypeElement));\n                Assert.Equal(\"tombstone\", entryTypeElement.GetString(), StringComparer.OrdinalIgnoreCase);\n                Assert.True(TryGetProperty(json.RootElement, \"MessageId\", out var messageIdElement));\n                Assert.Equal(record.MessageId, messageIdElement.GetString());\n            }\n\n            var loaded = await repo.GetByMessageIdAsync(record.MessageId);\n            Assert.Null(loaded);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task Constructor_RebuildsLfIndexedLogWithoutOffsetDrift() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var first = new PendingMessageRecord {\n                MessageId = \"msg-1\",\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.SendGrid\n            };\n            var second = new PendingMessageRecord {\n                MessageId = \"msg-2\",\n                Timestamp = DateTimeOffset.UtcNow.AddMinutes(1),\n                Provider = EmailProvider.Mailgun\n            };\n\n            var payload = string.Join(\"\\n\", new[] {\n                JsonSerializer.Serialize(new PendingMessageLogEnvelope { EntryType = \"upsert\", MessageId = first.MessageId, Record = first }, MailozaurrJsonContext.Default.PendingMessageLogEnvelope),\n                JsonSerializer.Serialize(new PendingMessageLogEnvelope { EntryType = \"upsert\", MessageId = second.MessageId, Record = second }, MailozaurrJsonContext.Default.PendingMessageLogEnvelope)\n            }) + \"\\n\";\n            File.WriteAllText(filePath, payload, Encoding.UTF8);\n\n            var repository = new FilePendingMessageRepository(filePath);\n            var loaded = await repository.GetByMessageIdAsync(second.MessageId);\n\n            Assert.NotNull(loaded);\n            Assert.Equal(second.MessageId, loaded!.MessageId);\n            Assert.Equal(second.Provider, loaded.Provider);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task GetByMessageIdAsync_ReturnsNullWhenFileIsDeletedAfterIndexing() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var repository = new FilePendingMessageRepository(options);\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.SendGrid\n            };\n\n            await repository.SaveAsync(record);\n            File.Delete(filePath);\n\n            var loaded = await repository.GetByMessageIdAsync(record.MessageId);\n\n            Assert.Null(loaded);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task GetAllAsync_ReturnsEmptyWhenFileIsDeletedAfterIndexing() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions { DirectoryPath = dir, FileNamingScheme = () => \"pending.log\" };\n        var filePath = Path.Combine(dir, \"pending.log\");\n        Directory.CreateDirectory(dir);\n\n        try {\n            var repository = new FilePendingMessageRepository(options);\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = DateTimeOffset.UtcNow,\n                Provider = EmailProvider.SendGrid\n            };\n\n            await repository.SaveAsync(record);\n            File.Delete(filePath);\n\n            var records = await ReadAllAsync(repository);\n\n            Assert.Empty(records);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(dir)) {\n                Directory.Delete(dir, true);\n            }\n        }\n    }\n\n    private static async Task<List<PendingMessageRecord>> ReadAllAsync(IPendingMessageRepository repository) {\n        var records = new List<PendingMessageRecord>();\n        await foreach (var record in repository.GetAllAsync()) {\n            records.Add(record);\n        }\n\n        return records;\n    }\n\n    private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement value) {\n        foreach (var property in element.EnumerateObject()) {\n            if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) {\n                value = property.Value;\n                return true;\n            }\n        }\n\n        value = default;\n        return false;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/FolderOperationsTests.cs",
    "content": "using System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing Moq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class FolderOperationsTests {\n    [Fact]\n    public async Task MoveFolderAsync_ClosesFolders() {\n        var source = new Mock<IMailFolder>();\n        var dest = new Mock<IMailFolder>();\n\n        source.SetupGet(f => f.FullName).Returns(\"source\");\n        source.SetupGet(f => f.Name).Returns(\"source\");\n        source.SetupGet(f => f.IsOpen).Returns(true);\n        source.Setup(f => f.Open(It.IsAny<FolderAccess>(), It.IsAny<CancellationToken>()));\n        source.Setup(f => f.RenameAsync(dest.Object, \"source\", It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);\n        source.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        dest.SetupGet(f => f.FullName).Returns(\"dest\");\n        dest.SetupGet(f => f.IsOpen).Returns(true);\n        dest.Setup(f => f.Open(It.IsAny<FolderAccess>(), It.IsAny<CancellationToken>()));\n        dest.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(source.Object);\n        client.Setup(c => c.GetFolder(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(dest.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        await FolderOperations.MoveFolderAsync(client.Object, \"source\", \"dest\");\n\n        source.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n        dest.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n    }\n\n    [Fact]\n    public async Task MoveFolderAsync_ClosesFolders_OnFailure() {\n        var source = new Mock<IMailFolder>();\n        var dest = new Mock<IMailFolder>();\n\n        source.SetupGet(f => f.FullName).Returns(\"source\");\n        source.SetupGet(f => f.Name).Returns(\"source\");\n        source.SetupGet(f => f.IsOpen).Returns(true);\n        source.Setup(f => f.Open(It.IsAny<FolderAccess>(), It.IsAny<CancellationToken>()));\n        source.Setup(f => f.RenameAsync(dest.Object, \"source\", It.IsAny<CancellationToken>())).ThrowsAsync(new ImapProtocolException(\"fail\"));\n        source.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        dest.SetupGet(f => f.FullName).Returns(\"dest\");\n        dest.SetupGet(f => f.IsOpen).Returns(true);\n        dest.Setup(f => f.Open(It.IsAny<FolderAccess>(), It.IsAny<CancellationToken>()));\n        dest.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(source.Object);\n        client.Setup(c => c.GetFolder(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(dest.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        await Assert.ThrowsAsync<ImapProtocolException>(() => FolderOperations.MoveFolderAsync(client.Object, \"source\", \"dest\"));\n\n        source.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n        dest.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n    }\n\n    [Fact]\n    public async Task RenameFolderAsync_ClosesFolder_OnFailure() {\n        var folder = new Mock<IMailFolder>();\n\n        folder.SetupGet(f => f.FullName).Returns(\"source\");\n        folder.SetupGet(f => f.ParentFolder).Returns(Mock.Of<IMailFolder>());\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.Setup(f => f.RenameAsync(It.IsAny<IMailFolder>(), \"new\", It.IsAny<CancellationToken>())).ThrowsAsync(new ImapProtocolException(\"fail\"));\n        folder.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == \"inbox\"));\n        client.Setup(c => c.GetFolder(\"source\", It.IsAny<CancellationToken>())).Returns(folder.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        await Assert.ThrowsAsync<ImapProtocolException>(() => FolderOperations.RenameFolderAsync(client.Object, \"source\", \"new\"));\n\n        folder.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n    }\n\n    [Fact]\n    public async Task RemoveFolderAsync_ClosesFolder_OnFailure() {\n        var folder = new Mock<IMailFolder>();\n\n        folder.SetupGet(f => f.FullName).Returns(\"source\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.Setup(f => f.DeleteAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new ImapProtocolException(\"fail\"));\n        folder.Setup(f => f.CloseAsync(false, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == \"inbox\"));\n        client.Setup(c => c.GetFolder(\"source\", It.IsAny<CancellationToken>())).Returns(folder.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        await Assert.ThrowsAsync<ImapProtocolException>(() => FolderOperations.RemoveFolderAsync(client.Object, \"source\"));\n\n        folder.Verify(f => f.CloseAsync(false, It.IsAny<CancellationToken>()), Times.Once());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GmailApiClientTests.cs",
    "content": "using System.Reflection;\nusing System.Net.Http;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GmailApiClientTests {\n    private static Dictionary<string, List<string>> ParseQueryParams(Uri uri) {\n        // Uri.ToString()/Query behave differently across runtimes (notably net472 vs net8).\n        // Parse and compare decoded query params so tests are stable across TFMs.\n        var dict = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);\n        var q = uri.Query;\n        if (string.IsNullOrEmpty(q) || q == \"?\") {\n            return dict;\n        }\n        if (q[0] == '?') {\n            q = q.Substring(1);\n        }\n\n        foreach (var part in q.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) {\n            var idx = part.IndexOf('=');\n            string rawKey;\n            string rawValue;\n            if (idx < 0) {\n                rawKey = part;\n                rawValue = string.Empty;\n            } else {\n                rawKey = part.Substring(0, idx);\n                rawValue = part.Substring(idx + 1);\n            }\n\n            // Query encoding may use '+' for space; normalize before unescaping.\n            var key = Uri.UnescapeDataString(rawKey.Replace(\"+\", \" \"));\n            var value = Uri.UnescapeDataString(rawValue.Replace(\"+\", \" \"));\n\n            if (!dict.TryGetValue(key, out var list)) {\n                list = new List<string>();\n                dict[key] = list;\n            }\n            list.Add(value);\n        }\n\n        return dict;\n    }\n\n    private sealed class CancelAwareHandler : HttpMessageHandler {\n        private readonly HttpResponseMessage _response;\n        public CancelAwareHandler(HttpResponseMessage response) => _response = response;\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            cancellationToken.ThrowIfCancellationRequested();\n            return Task.FromResult(_response);\n        }\n    }\n\n    private sealed class DisposingHandler : HttpMessageHandler {\n        public bool Disposed { get; private set; }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK));\n\n        protected override void Dispose(bool disposing) {\n            if (disposing) {\n                Disposed = true;\n            }\n            base.Dispose(disposing);\n        }\n    }\n\n    private sealed class PendingRepositoryStub : IPendingMessageRepository {\n        public List<PendingMessageRecord> Saved { get; } = new();\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            Saved.Add(record);\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var record = Saved.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.OrdinalIgnoreCase));\n            if (record == null || record.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            record.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<PendingMessageRecord?>(Saved.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.OrdinalIgnoreCase)));\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            foreach (var record in Saved) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return record;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            Saved.RemoveAll(r => string.Equals(r.MessageId, messageId, StringComparison.OrdinalIgnoreCase));\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class DelayedCancellationHandler : HttpMessageHandler {\n        private readonly TaskCompletionSource<bool> _started = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        public Task WaitForStartAsync() => _started.Task;\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            _started.TrySetResult(true);\n            await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);\n            throw new InvalidOperationException(\"Unreachable\");\n        }\n    }\n    [Fact]\n    public void Constructor_SetsAuthorizationHeader() {\n        var cred = new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue };\n        var client = new GmailApiClient(cred);\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var httpClient = (System.Net.Http.HttpClient)field.GetValue(client)!;\n        Assert.Equal(\"Bearer\", httpClient.DefaultRequestHeaders.Authorization?.Scheme);\n        Assert.Equal(\"t\", httpClient.DefaultRequestHeaders.Authorization?.Parameter);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteAsync_SendsDeleteRequest() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK));\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        await client.DeleteAsync(\"me\", \"123\");\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Delete, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/123\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetProfileAsync_ReturnsProfile() {\n        var json = \"{\\\"emailAddress\\\":\\\"a@b.com\\\",\\\"messagesTotal\\\":1,\\\"threadsTotal\\\":2,\\\"historyId\\\":\\\"10\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var profile = await client.GetProfileAsync(\"me\");\n        Assert.Equal(\"a@b.com\", profile.EmailAddress);\n        Assert.Equal(1, profile.MessagesTotal);\n        Assert.Equal(2, profile.ThreadsTotal);\n        Assert.Equal(\"10\", profile.HistoryId);\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Get, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/profile\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task WatchAsync_SendsRequestAndParsesResponse() {\n        var json = \"{\\\"historyId\\\":\\\"1\\\",\\\"expiration\\\":\\\"12345\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var result = await client.WatchAsync(\"me\", \"projects/p/topics/t\", new[] { \"INBOX\", \"SENT\" });\n        Assert.Equal(\"1\", result.HistoryId);\n        Assert.Equal(12345, result.Expiration);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/watch\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"topicName\\\":\\\"projects/p/topics/t\\\"\", body);\n        Assert.Contains(\"\\\"labelIds\\\":[\\\"INBOX\\\",\\\"SENT\\\"]\", body);\n        Assert.Contains(\"\\\"labelFilterAction\\\":\\\"include\\\"\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task StopWatchAsync_SendsStopRequest() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        await client.StopWatchAsync(\"me\");\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/stop\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Equal(\"{}\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListHistoryAsync_BuildsQueryAndParsesResponse() {\n        var json = \"{\\\"history\\\":[{\\\"id\\\":\\\"1\\\",\\\"messagesAdded\\\":[{\\\"message\\\":{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}}]}],\\\"historyId\\\":\\\"10\\\",\\\"nextPageToken\\\":\\\"pt\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var result = await client.ListHistoryAsync(\"me\", \"5\", labelId: \"INBOX\", historyTypes: new[] { \"messageAdded\", \"labelAdded\" }, maxResults: 3, pageToken: \"tok\");\n        Assert.Equal(\"10\", result.HistoryId);\n        Assert.Equal(\"pt\", result.NextPageToken);\n        Assert.Single(result.History!);\n        Assert.Equal(\"1\", result.History![0].Id);\n        Assert.Equal(\"m1\", result.History![0].MessagesAdded![0].Message!.Id);\n        Assert.Equal(\"t1\", result.History![0].MessagesAdded![0].Message!.ThreadId);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!.ToString();\n        Assert.Contains(\"https://gmail.googleapis.com/gmail/v1/users/me/history?\", uri);\n        Assert.Contains(\"startHistoryId=5\", uri);\n        Assert.Contains(\"labelId=INBOX\", uri);\n        Assert.Contains(\"maxResults=3\", uri);\n        Assert.Contains(\"pageToken=tok\", uri);\n        Assert.Contains(\"historyTypes=messageAdded\", uri);\n        Assert.Contains(\"historyTypes=labelAdded\", uri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DownloadAttachmentAsync_ReturnsBytes() {\n        var json = \"{\\\"data\\\":\\\"dGVzdA\\\"}\"; // \"test\" base64url\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var data = await client.DownloadAttachmentAsync(\"me\", \"123\", \"att\");\n        Assert.Equal(4, data.Length);\n        Assert.Equal(new byte[] { 116, 101, 115, 116 }, data);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/123/attachments/att\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DownloadAttachmentAsync_NoData_ReturnsEmptyArray() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var data = await client.DownloadAttachmentAsync(\"me\", \"123\", \"att\");\n        Assert.Empty(data);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DownloadAttachmentAsync_InvalidData_Throws() {\n        var json = \"{\\\"data\\\":\\\"abcde\\\"}\"; // invalid length base64url\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        await Assert.ThrowsAsync<System.IO.InvalidDataException>(() => client.DownloadAttachmentAsync(\"me\", \"123\", \"att\"));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListLabelsAsync_SendsRequestAndParsesResponse() {\n        var json = \"{\\\"labels\\\":[{\\\"id\\\":\\\"INBOX\\\",\\\"name\\\":\\\"Inbox\\\",\\\"type\\\":\\\"system\\\"},{\\\"id\\\":\\\"Label_1\\\",\\\"name\\\":\\\"X\\\",\\\"type\\\":\\\"user\\\"}]}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var labels = await client.ListLabelsAsync(\"me\");\n        Assert.Equal(2, labels.Count);\n        Assert.Equal(\"INBOX\", labels[0].Id);\n        Assert.Equal(\"Inbox\", labels[0].Name);\n        Assert.Equal(\"system\", labels[0].Type);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Get, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/labels?fields=labels(id,name,type)\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListPageAsync_BuildsQueryAndParsesResponse() {\n        var json = \"{\\\"messages\\\":[{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"},{\\\"id\\\":\\\"m2\\\",\\\"threadId\\\":\\\"t2\\\"}],\\\"nextPageToken\\\":\\\"pt\\\",\\\"resultSizeEstimate\\\":\\\"123\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var page = await client.ListPageAsync(\n            \"me\",\n            query: \"from:test@example.com\",\n            labelIds: new[] { \"INBOX\", \"Label_1\" },\n            includeSpamTrash: true,\n            maxResults: 999,\n            pageToken: \"tok\",\n            fields: \"messages(id,threadId),nextPageToken,resultSizeEstimate\");\n\n        Assert.NotNull(page.Messages);\n        Assert.Equal(2, page.Messages!.Count);\n        Assert.Equal(\"m1\", page.Messages[0].Id);\n        Assert.Equal(\"t1\", page.Messages[0].ThreadId);\n        Assert.Equal(\"pt\", page.NextPageToken);\n        Assert.Equal(123, page.ResultSizeEstimate);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!;\n        Assert.StartsWith(\"https://gmail.googleapis.com/gmail/v1/users/me/messages\", uri.ToString());\n\n        var qp = ParseQueryParams(uri);\n        Assert.Equal(\"from:test@example.com\", Assert.Single(qp[\"q\"]));\n        Assert.Equal(\"500\", Assert.Single(qp[\"maxResults\"])); // clamped\n        Assert.Equal(\"tok\", Assert.Single(qp[\"pageToken\"]));\n        Assert.Equal(\"true\", Assert.Single(qp[\"includeSpamTrash\"]));\n        Assert.Contains(\"INBOX\", qp[\"labelIds\"]);\n        Assert.Contains(\"Label_1\", qp[\"labelIds\"]);\n        Assert.Equal(\"messages(id,threadId),nextPageToken,resultSizeEstimate\", Assert.Single(qp[\"fields\"]));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetRawAsync_BuildsQueryAndParsesResponse() {\n        var json = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\",\\\"internalDate\\\":\\\"1700000\\\",\\\"labelIds\\\":[\\\"UNREAD\\\"],\\\"raw\\\":\\\"dGVzdA\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var msg = await client.GetRawAsync(\"me\", \"m1\", fields: \"id,threadId,internalDate,labelIds,raw\");\n        Assert.Equal(\"m1\", msg.Id);\n        Assert.Equal(\"t1\", msg.ThreadId);\n        Assert.Equal(1700000, msg.InternalDate);\n        Assert.Single(msg.LabelIds!);\n        Assert.Equal(\"UNREAD\", msg.LabelIds![0]);\n        Assert.Equal(\"dGVzdA\", msg.Raw);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!.ToString();\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/m1?format=raw&fields=id%2CthreadId%2CinternalDate%2ClabelIds%2Craw\", uri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadAsync_WithFormatAndFields_BuildsQuery() {\n        var json = \"{\\\"id\\\":\\\"t1\\\",\\\"messages\\\":[{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}]}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var thread = await client.GetThreadWithOptionsAsync(\"me\", \"t1\", format: \"full\", fields: \"id,messages(id,threadId)\");\n        Assert.Equal(\"t1\", thread.Id);\n        Assert.NotNull(thread.Messages);\n        Assert.Single(thread.Messages!);\n        Assert.Equal(\"m1\", thread.Messages![0].Id);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!;\n        Assert.StartsWith(\"https://gmail.googleapis.com/gmail/v1/users/me/threads/t1\", uri.ToString());\n        var qp = ParseQueryParams(uri);\n        Assert.Equal(\"full\", Assert.Single(qp[\"format\"]));\n        Assert.Equal(\"id,messages(id,threadId)\", Assert.Single(qp[\"fields\"]));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetMessageWithOptionsAsync_MetadataFormat_BuildsQuery() {\n        var json = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\",\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Message-ID\\\",\\\"value\\\":\\\"<x>\\\"}]}}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var msg = await client.GetMessageWithOptionsAsync(\n            \"me\",\n            \"m1\",\n            format: \"metadata\",\n            metadataHeaders: new[] { \"Message-ID\", \"References\" },\n            fields: \"id,payload(headers)\");\n\n        Assert.Equal(\"m1\", msg.Id);\n        Assert.NotNull(msg.Payload);\n        Assert.NotNull(msg.Payload!.Headers);\n        Assert.Single(msg.Payload!.Headers!);\n        Assert.Equal(\"Message-ID\", msg.Payload!.Headers![0].Name);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!;\n        Assert.StartsWith(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/m1\", uri.ToString());\n        var qp = ParseQueryParams(uri);\n        Assert.Equal(\"metadata\", Assert.Single(qp[\"format\"]));\n        Assert.Equal(\"id,payload(headers)\", Assert.Single(qp[\"fields\"]));\n        Assert.Contains(\"Message-ID\", qp[\"metadataHeaders\"]);\n        Assert.Contains(\"References\", qp[\"metadataHeaders\"]);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ImportAsync_BuildsQueryAndSendsBody() {\n        var json = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var msg = await client.ImportAsync(\"me\", raw: \"dGVzdA\", labelIds: new[] { \"SENT\" });\n        Assert.Equal(\"m1\", msg.Id);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        var uri = handler.Requests[0].RequestUri!;\n        Assert.StartsWith(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/import\", uri.ToString());\n        var qp = ParseQueryParams(uri);\n        Assert.Equal(\"dateHeader\", Assert.Single(qp[\"internalDateSource\"]));\n        Assert.Equal(\"true\", Assert.Single(qp[\"neverMarkSpam\"]));\n\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"raw\\\":\\\"dGVzdA\\\"\", body);\n        Assert.Contains(\"\\\"labelIds\\\":[\\\"SENT\\\"]\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ModifyMessageLabelsAsync_SendsModifyRequest() {\n        var json = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var message = await client.ModifyMessageLabelsAsync(\"me\", \"m1\", addLabelIds: new[] { \"INBOX\" }, removeLabelIds: new[] { \"UNREAD\" });\n        Assert.Equal(\"m1\", message.Id);\n        Assert.Equal(\"t1\", message.ThreadId);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/m1/modify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"INBOX\\\"]\", body);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"UNREAD\\\"]\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task TrashMessageAsync_SendsTrashRequest() {\n        var json = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var message = await client.TrashMessageAsync(\"me\", \"m1\");\n        Assert.Equal(\"m1\", message.Id);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/m1/trash\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task BatchModifyMessagesAsync_SendsBatchModifyRequest() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        await client.BatchModifyMessagesAsync(\"me\", new[] { \"m1\", \"m2\" }, addLabelIds: new[] { \"INBOX\" }, removeLabelIds: new[] { \"TRASH\" });\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/batchModify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"ids\\\":[\\\"m1\\\",\\\"m2\\\"]\", body);\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"INBOX\\\"]\", body);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"TRASH\\\"]\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task BatchDeleteMessagesAsync_SendsBatchDeleteRequest() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        await client.BatchDeleteMessagesAsync(\"me\", new[] { \"m1\", \"m2\" });\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/batchDelete\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"ids\\\":[\\\"m1\\\",\\\"m2\\\"]\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ModifyThreadLabelsAsync_SendsModifyRequest() {\n        var json = \"{\\\"id\\\":\\\"t1\\\",\\\"messages\\\":[{\\\"id\\\":\\\"m1\\\"}]}\"; \n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var thread = await client.ModifyThreadLabelsAsync(\"me\", \"t1\", addLabelIds: new[] { \"INBOX\" }, removeLabelIds: new[] { \"TRASH\" });\n        Assert.Equal(\"t1\", thread.Id);\n        Assert.Single(thread.Messages!);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/threads/t1/modify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"INBOX\\\"]\", body);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"TRASH\\\"]\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task TrashThreadAsync_SendsTrashRequest() {\n        var json = \"{\\\"id\\\":\\\"t1\\\",\\\"messages\\\":[]}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var thread = await client.TrashThreadAsync(\"me\", \"t1\");\n        Assert.Equal(\"t1\", thread.Id);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/threads/t1/trash\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteThreadAsync_SendsDeleteRequest() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK));\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        await client.DeleteThreadAsync(\"me\", \"t1\");\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(System.Net.Http.HttpMethod.Delete, handler.Requests[0].Method);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/threads/t1\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Theory]\n    [InlineData(System.Net.HttpStatusCode.Unauthorized)]\n    [InlineData(System.Net.HttpStatusCode.Forbidden)]\n    public async System.Threading.Tasks.Task SendAsync_AuthError_ThrowsGmailAuthenticationException(System.Net.HttpStatusCode status) {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(status) { Content = new System.Net.Http.StringContent(\"error\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var message = new MimeKit.MimeMessage();\n        await Assert.ThrowsAsync<GmailAuthenticationException>(() => client.SendAsync(\"me\", message));\n        Assert.Single(handler.Requests);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendAsync_ExpiredToken_InvokesRefresh() {\n        var handler = new RecordingHandler(\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new System.Net.Http.StringContent(\"error\") },\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{\\\"id\\\":\\\"1\\\"}\") });\n        int refreshes = 0;\n        System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<string>> refresher = _ => {\n            refreshes++;\n            return System.Threading.Tasks.Task.FromResult(\"new\");\n        };\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"old\", ExpiresOn = System.DateTimeOffset.MaxValue }, refresher);\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var httpClient = new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(\"Bearer\", \"old\");\n        field.SetValue(client, httpClient);\n        var message = new MimeKit.MimeMessage();\n        await Assert.ThrowsAsync<GmailAuthenticationException>(() => client.SendAsync(\"me\", message));\n        Assert.Equal(1, refreshes);\n        await client.SendAsync(\"me\", message);\n        Assert.Equal(\"Bearer old\", handler.Requests[0].Headers.Authorization!.ToString());\n        Assert.Equal(\"Bearer new\", handler.Requests[1].Headers.Authorization!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListAsync_PaginatesUntilTokenNull() {\n        var page1 = \"{\\\"messages\\\":[{\\\"id\\\":\\\"1\\\"}],\\\"nextPageToken\\\":\\\"tok\\\"}\";\n        var page2 = \"{\\\"messages\\\":[{\\\"id\\\":\\\"2\\\"}]}\";\n        var handler = new RecordingHandler(\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page1) },\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page2) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var list = await client.ListAsync(\"me\");\n        Assert.Equal(2, list.Count);\n        Assert.Equal(\"1\", list[0].Id);\n        Assert.Equal(\"2\", list[1].Id);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Equal(string.Empty, handler.Requests[0].RequestUri!.Query);\n        Assert.Equal(\"?pageToken=tok\", handler.Requests[1].RequestUri!.Query);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListAsync_ExactMaxResults_StopsEarly() {\n        var page1 = \"{\\\"messages\\\":[{\\\"id\\\":\\\"1\\\"},{\\\"id\\\":\\\"2\\\"}],\\\"nextPageToken\\\":\\\"tok\\\"}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page1) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var list = await client.ListAsync(\"me\", maxResults: 2);\n        Assert.Equal(2, list.Count);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"?maxResults=2\", handler.Requests[0].RequestUri!.Query);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListAsync_PartialPageLimit_AdjustsRequests() {\n        var page1 = \"{\\\"messages\\\":[{\\\"id\\\":\\\"1\\\"},{\\\"id\\\":\\\"2\\\"}],\\\"nextPageToken\\\":\\\"tok\\\"}\";\n        var page2 = \"{\\\"messages\\\":[{\\\"id\\\":\\\"3\\\"}],\\\"nextPageToken\\\":\\\"tok2\\\"}\";\n        var handler = new RecordingHandler(\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page1) },\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page2) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var list = await client.ListAsync(\"me\", maxResults: 3);\n        Assert.Equal(3, list.Count);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Equal(\"?maxResults=3\", handler.Requests[0].RequestUri!.Query);\n        Assert.Equal(\"?maxResults=1&pageToken=tok\", handler.Requests[1].RequestUri!.Query);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendAsync_CanBeCancelled() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK));\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        var message = new MimeKit.MimeMessage();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.SendAsync(\"me\", message, cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendAsync_CallerCancellation_DoesNotQueuePendingMessage() {\n        var handler = new DelayedCancellationHandler();\n        var repository = new PendingRepositoryStub();\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue }) {\n            PendingMessageRepository = repository\n        };\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n\n        var message = new MimeKit.MimeMessage();\n        message.From.Add(MimeKit.MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MimeKit.MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Body = new MimeKit.TextPart(\"plain\") { Text = \"body\" };\n\n        using var cts = new System.Threading.CancellationTokenSource();\n        var task = client.SendAsync(\"me\", message, cts.Token);\n        await handler.WaitForStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(async () => await task);\n        Assert.Empty(repository.Saved);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListAsync_CanBeCancelled() {\n        var handler = new CancelAwareHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.ListAsync(\"me\", cancellationToken: cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetAsync_CanBeCancelled() {\n        var handler = new CancelAwareHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.GetAsync(\"me\", \"id\", cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DownloadAttachmentAsync_CanBeCancelled() {\n        var handler = new CancelAwareHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{\\\"data\\\":\\\"dGVzdA\\\"}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.DownloadAttachmentAsync(\"me\", \"m\", \"a\", cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendAsync_NullResponse_Throws() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"null\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var message = new MimeKit.MimeMessage();\n        await Assert.ThrowsAsync<System.IO.InvalidDataException>(() => client.SendAsync(\"me\", message));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetAsync_NullResponse_Throws() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"null\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        await Assert.ThrowsAsync<System.IO.InvalidDataException>(() => client.GetAsync(\"me\", \"id\"));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendAsync_InvalidJson_ThrowsGmailApiException() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"not json\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var message = new MimeKit.MimeMessage();\n        var ex = await Assert.ThrowsAsync<GmailApiException>(() => client.SendAsync(\"me\", message));\n        Assert.Equal(\"not json\", ex.ResponseContent);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetAsync_InvalidJson_ThrowsGmailApiException() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"not json\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var ex = await Assert.ThrowsAsync<GmailApiException>(() => client.GetAsync(\"me\", \"id\"));\n        Assert.Equal(\"not json\", ex.ResponseContent);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListThreadsAsync_PaginatesUntilTokenNull() {\n        var page1 = \"{\\\"threads\\\":[{\\\"id\\\":\\\"1\\\"}],\\\"nextPageToken\\\":\\\"tok\\\"}\";\n        var page2 = \"{\\\"threads\\\":[{\\\"id\\\":\\\"2\\\"}]}\";\n        var handler = new RecordingHandler(\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page1) },\n            new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(page2) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var list = await client.ListThreadsAsync(\"me\");\n        Assert.Equal(2, list.Count);\n        Assert.Equal(\"1\", list[0].Id);\n        Assert.Equal(\"2\", list[1].Id);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Equal(string.Empty, handler.Requests[0].RequestUri!.Query);\n        Assert.Equal(\"?pageToken=tok\", handler.Requests[1].RequestUri!.Query);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadAsync_ReturnsThread() {\n        var json = \"{\\\"id\\\":\\\"t1\\\",\\\"messages\\\":[{\\\"id\\\":\\\"m1\\\"}]}\";\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var thread = await client.GetThreadAsync(\"me\", \"t1\");\n        Assert.Equal(\"t1\", thread.Id);\n        Assert.Single(thread.Messages!);\n        Assert.Equal(\"m1\", thread.Messages![0].Id);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListThreadsAsync_CanBeCancelled() {\n        var handler = new CancelAwareHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.ListThreadsAsync(\"me\", cancellationToken: cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadAsync_CanBeCancelled() {\n        var handler = new CancelAwareHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        using var cts = new System.Threading.CancellationTokenSource();\n        cts.Cancel();\n        await Assert.ThrowsAnyAsync<System.OperationCanceledException>(() => client.GetThreadAsync(\"me\", \"id\", cts.Token));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadAsync_NullResponse_Throws() {\n        var handler = new RecordingHandler(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"null\") });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        await Assert.ThrowsAsync<System.IO.InvalidDataException>(() => client.GetThreadAsync(\"me\", \"id\"));\n    }\n     \n    [Fact]\n    public void Dispose_DisposesHttpClient() {\n        var cred = new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue };\n        var client = new GmailApiClient(cred);\n        var handler = new DisposingHandler();\n        var httpClient = new System.Net.Http.HttpClient(handler) { BaseAddress = new System.Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, httpClient);\n        client.Dispose();\n        Assert.True(handler.Disposed);\n    }\n\n    public static IEnumerable<object[]> DisposedMethods() {\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.SendAsync(\"u\", new MimeKit.MimeMessage())) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ListAsync(\"u\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.GetAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.GetMimeMessageAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.DeleteAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.TrashMessageAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ModifyMessageLabelsAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.BatchModifyMessagesAsync(\"u\", new[] { \"id\" })) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.BatchDeleteMessagesAsync(\"u\", new[] { \"id\" })) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ListLabelsAsync(\"u\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ModifyThreadLabelsAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.TrashThreadAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.DeleteThreadAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.GetProfileAsync(\"u\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.WatchAsync(\"u\", \"topic\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.StopWatchAsync(\"u\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ListHistoryAsync(\"u\", \"1\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ListThreadsAsync(\"u\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.GetThreadAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.ListAttachmentsAsync(\"u\", \"id\")) };\n        yield return new object[] { (Func<GmailApiClient, Task>)(c => c.DownloadAttachmentAsync(\"u\", \"mid\", \"aid\")) };\n    }\n\n    [Theory]\n    [MemberData(nameof(DisposedMethods))]\n    public async Task Methods_AfterDispose_ThrowObjectDisposedException(Func<GmailApiClient, Task> action) {\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        client.Dispose();\n        await Assert.ThrowsAsync<ObjectDisposedException>(() => action(client));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GmailMailboxBrowserTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class GmailMailboxBrowserTests {\n    [Fact]\n    public async System.Threading.Tasks.Task ResolveLabelIdAsync_MapsSystemAliases_AndResolvesCustomLabel() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_1\\\",\\\"name\\\":\\\"Project\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) });\n        var browser = CreateBrowser(handler);\n\n        Assert.Equal(\"INBOX\", await browser.ResolveLabelIdAsync(\"INBOX\"));\n        Assert.Equal(\"SENT\", await browser.ResolveLabelIdAsync(\"Sent Items\"));\n        Assert.Equal(\"Label_1\", await browser.ResolveLabelIdAsync(\"Project\"));\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/labels?fields=labels(id,name,type)\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListFoldersAsync_ReturnsSortedLabelFolders() {\n        var labelsJson = \"{\" +\n                         \"\\\"labels\\\":[\" +\n                         \"{\\\"id\\\":\\\"Label_2\\\",\\\"name\\\":\\\"Zeta\\\",\\\"type\\\":\\\"user\\\"},\" +\n                         \"{\\\"id\\\":\\\"INBOX\\\",\\\"name\\\":\\\"Inbox\\\",\\\"type\\\":\\\"system\\\"},\" +\n                         \"{\\\"id\\\":\\\"Label_1\\\",\\\"name\\\":\\\"Alpha\\\",\\\"type\\\":\\\"user\\\"}\" +\n                         \"]\" +\n                         \"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) });\n        var browser = CreateBrowser(handler);\n\n        var folders = await browser.ListFoldersAsync();\n\n        Assert.Equal(3, folders.Count);\n        Assert.Equal(\"Alpha\", folders[0].Name);\n        Assert.Equal(\"Inbox\", folders[1].Name);\n        Assert.Equal(\"Zeta\", folders[2].Name);\n        Assert.Equal(\"INBOX\", folders[1].Id);\n        Assert.Equal(\"system\", folders[1].Type);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/labels?fields=labels(id,name,type)\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListMessagesAsync_UsesListThenLoadsSummaries() {\n        var listJson = \"{\\\"messages\\\":[{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"},{\\\"id\\\":\\\"m2\\\",\\\"threadId\\\":\\\"t1\\\"}],\\\"resultSizeEstimate\\\":\\\"9\\\"}\";\n        var m2Json = \"{\" +\n                     \"\\\"id\\\":\\\"m2\\\",\\\"threadId\\\":\\\"t1\\\",\\\"internalDate\\\":\\\"1739577600000\\\",\" +\n                     \"\\\"labelIds\\\":[\\\"INBOX\\\"],\" +\n                     \"\\\"payload\\\":{\\\"filename\\\":\\\"\\\",\\\"headers\\\":[\" +\n                     \"{\\\"name\\\":\\\"From\\\",\\\"value\\\":\\\"a@example.test\\\"},\" +\n                     \"{\\\"name\\\":\\\"To\\\",\\\"value\\\":\\\"b@example.test\\\"},\" +\n                     \"{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"second\\\"},\" +\n                     \"{\\\"name\\\":\\\"Message-Id\\\",\\\"value\\\":\\\"<m2@example.test>\\\"}]}}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(m2Json) });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.ListMessagesAsync(\"INBOX\", limit: 1, offset: 1);\n\n        Assert.Equal(\"INBOX\", result.ResolvedLabelId);\n        Assert.Equal(9, result.TotalCount);\n        Assert.Single(result.Messages);\n        Assert.Equal(\"m2\", result.Messages[0].NativeId);\n        Assert.Equal(\"m2@example.test\", result.Messages[0].MessageId);\n        Assert.Equal(\"a@example.test\", result.Messages[0].From);\n\n        Assert.Equal(2, handler.Requests.Count);\n        var listUri = handler.Requests[0].RequestUri!;\n        Assert.Contains(\"/users/me/messages\", listUri.ToString());\n        var listQuery = ParseQueryParams(listUri);\n        Assert.Equal(\"messages(id,threadId),nextPageToken,resultSizeEstimate\", Assert.Single(listQuery[\"fields\"]));\n        Assert.Equal(\"INBOX\", Assert.Single(listQuery[\"labelIds\"]));\n\n        var summaryUri = handler.Requests[1].RequestUri!.ToString();\n        Assert.Contains(\"/users/me/messages/m2?\", summaryUri);\n        Assert.Contains(\"format=full\", summaryUri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SearchMessagesAsync_UsesBuiltQuery_AndSortsByDateDesc() {\n        var listJson = \"{\\\"messages\\\":[{\\\"id\\\":\\\"m-old\\\"},{\\\"id\\\":\\\"m-new\\\"}],\\\"nextPageToken\\\":null}\";\n        var oldJson = \"{\" +\n                      \"\\\"id\\\":\\\"m-old\\\",\\\"threadId\\\":\\\"t1\\\",\\\"internalDate\\\":\\\"1739491200000\\\",\" +\n                      \"\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"old\\\"}]}}\";\n        var newJson = \"{\" +\n                      \"\\\"id\\\":\\\"m-new\\\",\\\"threadId\\\":\\\"t1\\\",\\\"internalDate\\\":\\\"1739577600000\\\",\" +\n                      \"\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"new\\\"}]}}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(oldJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(newJson) });\n        var browser = CreateBrowser(handler);\n\n        var search = await browser.SearchMessagesAsync(new GmailMailboxBrowser.GmailMailboxSearchRequest {\n            Folder = \"INBOX\",\n            Query = \"urgent\",\n            SubjectContains = \"invoice\",\n            UnseenOnly = true\n        }, max: 20);\n\n        Assert.Equal(\"INBOX\", search.ResolvedLabelId);\n        Assert.Equal(2, search.Messages.Count);\n        Assert.Equal(\"m-new\", search.Messages[0].NativeId);\n        Assert.Equal(\"m-old\", search.Messages[1].NativeId);\n\n        Assert.Equal(3, handler.Requests.Count);\n        var uri = handler.Requests[0].RequestUri!;\n        var query = ParseQueryParams(uri);\n        Assert.Equal(\"INBOX\", Assert.Single(query[\"labelIds\"]));\n        var q = Assert.Single(query[\"q\"]);\n        Assert.Contains(\"is:unread\", q);\n        Assert.Contains(\"subject:(invoice)\", q);\n        Assert.Contains(\"urgent\", q);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListThreadMessagesAsync_ReturnsSortedSummaries() {\n        var threadJson = \"{\" +\n                         \"\\\"id\\\":\\\"thr-1\\\",\\\"messages\\\":[\" +\n                         \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"thr-1\\\",\\\"internalDate\\\":\\\"1739491200000\\\",\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"first\\\"}]}},\" +\n                         \"{\\\"id\\\":\\\"m2\\\",\\\"threadId\\\":\\\"thr-1\\\",\\\"internalDate\\\":\\\"1739577600000\\\",\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"second\\\"}]}}\" +\n                         \"]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(threadJson) });\n        var browser = CreateBrowser(handler);\n\n        var messages = await browser.ListThreadMessagesAsync(\"thr-1\");\n\n        Assert.Equal(2, messages.Count);\n        Assert.Equal(\"m2\", messages[0].NativeId);\n        Assert.Equal(\"m1\", messages[1].NativeId);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/threads/thr-1\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListThreadMessagesPageAsync_ReturnsPagedSortedSummaries() {\n        var threadJson = \"{\" +\n                         \"\\\"id\\\":\\\"thr-1\\\",\\\"messages\\\":[\" +\n                         \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"thr-1\\\",\\\"internalDate\\\":\\\"1739491200000\\\",\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"first\\\"}]}},\" +\n                         \"{\\\"id\\\":\\\"m2\\\",\\\"threadId\\\":\\\"thr-1\\\",\\\"internalDate\\\":\\\"1739577600000\\\",\\\"payload\\\":{\\\"headers\\\":[{\\\"name\\\":\\\"Subject\\\",\\\"value\\\":\\\"second\\\"}]}}\" +\n                         \"]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(threadJson) });\n        var browser = CreateBrowser(handler);\n\n        var page = await browser.ListThreadMessagesPageAsync(\"thr-1\", limit: 1, offset: 1);\n\n        Assert.Equal(\"thr-1\", page.ThreadId);\n        Assert.Equal(2, page.TotalCount);\n        Assert.Single(page.Messages);\n        Assert.Equal(\"m1\", page.Messages[0].NativeId);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/threads/thr-1\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetMessageContentAsync_ReturnsMimeAndFlags() {\n        var mime = \"From: a@example.test\\r\\nTo: b@example.test\\r\\nSubject: Sample\\r\\nMessage-Id: <m1@example.test>\\r\\n\\r\\nhello\";\n        var raw = Convert.ToBase64String(Encoding.UTF8.GetBytes(mime)).Replace('+', '-').Replace('/', '_').TrimEnd('=');\n        var rawJson = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"thr-1\\\",\\\"labelIds\\\":[\\\"UNREAD\\\",\\\"STARRED\\\"],\\\"raw\\\":\\\"\" + raw + \"\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(rawJson) });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.GetMessageContentAsync(\"m1\");\n\n        Assert.NotNull(result.Message);\n        Assert.Equal(\"Sample\", result.Message.Subject);\n        Assert.False(result.Seen);\n        Assert.True(result.Flagged);\n        Assert.Equal(\"thr-1\", result.NativeThreadId);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendMessageAsync_UsesGmailSendEndpoint() {\n        var sentJson = \"{\\\"id\\\":\\\"gmail-sent-id\\\",\\\"threadId\\\":\\\"gmail-thread-id\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(sentJson) });\n        var browser = CreateBrowser(handler);\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.test\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.test\"));\n        message.Subject = \"Send me\";\n        message.Body = new TextPart(\"plain\") { Text = \"hello\" };\n\n        var result = await browser.SendMessageAsync(message);\n\n        Assert.Equal(\"gmail-sent-id\", result.NativeId);\n        Assert.Equal(\"gmail-thread-id\", result.NativeThreadId);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/messages/send\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"raw\\\":\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadingMetadataAsync_ParsesSelectedHeaders() {\n        var json = \"{\" +\n                   \"\\\"id\\\":\\\"m1\\\",\" +\n                   \"\\\"payload\\\":{\\\"headers\\\":[\" +\n                   \"{\\\"name\\\":\\\"Message-ID\\\",\\\"value\\\":\\\"<thread-child@example.test>\\\"},\" +\n                   \"{\\\"name\\\":\\\"In-Reply-To\\\",\\\"value\\\":\\\"<thread-parent@example.test>\\\"},\" +\n                   \"{\\\"name\\\":\\\"References\\\",\\\"value\\\":\\\"<thread-root@example.test> <thread-parent@example.test> <thread-root@example.test>\\\"},\" +\n                   \"{\\\"name\\\":\\\"Reply-To\\\",\\\"value\\\":\\\"replies@example.test\\\"},\" +\n                   \"{\\\"name\\\":\\\"Cc\\\",\\\"value\\\":\\\"cc@example.test\\\"}\" +\n                   \"]}}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.GetThreadingMetadataAsync(\"m1\");\n\n        Assert.Equal(\"thread-child@example.test\", result.MessageId);\n        Assert.Equal(\"thread-parent@example.test\", result.InReplyTo);\n        Assert.Equal(\"replies@example.test\", result.ReplyTo);\n        Assert.Equal(\"cc@example.test\", result.Cc);\n        Assert.Equal(2, result.References.Count);\n        Assert.Equal(\"thread-root@example.test\", result.References[0]);\n        Assert.Equal(\"thread-parent@example.test\", result.References[1]);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!;\n        var query = ParseQueryParams(uri);\n        Assert.Equal(\"metadata\", Assert.Single(query[\"format\"]));\n        Assert.Equal(\"id,payload(headers)\", Assert.Single(query[\"fields\"]));\n        Assert.Contains(\"Message-ID\", query[\"metadataHeaders\"]);\n        Assert.Contains(\"In-Reply-To\", query[\"metadataHeaders\"]);\n        Assert.Contains(\"References\", query[\"metadataHeaders\"]);\n        Assert.Contains(\"Reply-To\", query[\"metadataHeaders\"]);\n        Assert.Contains(\"Cc\", query[\"metadataHeaders\"]);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task WatchAsync_ResolvesLabels_AndParsesExpiration() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_1\\\",\\\"name\\\":\\\"Project\\\"}]}\";\n        var watchJson = \"{\\\"historyId\\\":\\\"77\\\",\\\"expiration\\\":\\\"1739577600000\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(watchJson) });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.WatchAsync(\"projects/p/topics/t\", new[] { \"INBOX\", \"Project\" });\n\n        Assert.Equal(\"77\", result.HistoryId);\n        Assert.NotNull(result.ExpirationUtc);\n        Assert.Contains(\"INBOX\", result.LabelIds);\n        Assert.Contains(\"Label_1\", result.LabelIds);\n\n        Assert.Equal(2, handler.Requests.Count);\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"topicName\\\":\\\"projects/p/topics/t\\\"\", body);\n        Assert.Contains(\"INBOX\", body);\n        Assert.Contains(\"Label_1\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task StopWatchAsync_ReturnsAlreadyStopped_WhenMissingAndConfigured() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent(\"{\\\"error\\\":\\\"missing\\\"}\") });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.StopWatchAsync(treatMissingAsSuccess: true);\n\n        Assert.True(result.Stopped);\n        Assert.True(result.AlreadyStopped);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/stop\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task StopWatchAsync_Throws_WhenMissingAndStrictMode() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent(\"{\\\"error\\\":\\\"missing\\\"}\") });\n        var browser = CreateBrowser(handler);\n\n        await Assert.ThrowsAsync<GmailApiException>(() => browser.StopWatchAsync(treatMissingAsSuccess: false));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetHistoryAsync_MapsUpsertsDeletes_AndDropsDeletedFromUpserts() {\n        var historyJson = \"{\" +\n                          \"\\\"historyId\\\":\\\"200\\\",\" +\n                          \"\\\"history\\\":[\" +\n                          \"{\" +\n                          \"\\\"id\\\":\\\"10\\\",\" +\n                          \"\\\"messagesAdded\\\":[{\\\"message\\\":{\\\"id\\\":\\\"m1\\\"}},{\\\"message\\\":{\\\"id\\\":\\\"m2\\\"}}],\" +\n                          \"\\\"messagesDeleted\\\":[{\\\"message\\\":{\\\"id\\\":\\\"m2\\\"}}],\" +\n                          \"\\\"labelsAdded\\\":[{\\\"message\\\":{\\\"id\\\":\\\"m3\\\"}}],\" +\n                          \"\\\"labelsRemoved\\\":[{\\\"message\\\":{\\\"id\\\":\\\"m3\\\"}}]\" +\n                          \"}\" +\n                          \"]\" +\n                          \"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(historyJson) });\n        var browser = CreateBrowser(handler);\n\n        var result = await browser.GetHistoryAsync(\"INBOX\", \"5\", maxChanges: 100);\n\n        Assert.Equal(\"INBOX\", result.ResolvedLabelId);\n        Assert.Equal(\"200\", result.NewHistoryId);\n        Assert.Equal(new[] { \"m1\" }, result.UpsertNativeIds);\n        Assert.Equal(new[] { \"m2\", \"m3\" }, result.DeletedNativeIds);\n\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!;\n        var query = ParseQueryParams(uri);\n        Assert.Equal(\"5\", Assert.Single(query[\"startHistoryId\"]));\n        Assert.Equal(\"INBOX\", Assert.Single(query[\"labelId\"]));\n        Assert.Contains(\"messageAdded\", query[\"historyTypes\"]);\n        Assert.Contains(\"messageDeleted\", query[\"historyTypes\"]);\n        Assert.Contains(\"labelAdded\", query[\"historyTypes\"]);\n        Assert.Contains(\"labelRemoved\", query[\"historyTypes\"]);\n    }\n\n    [Fact]\n    public void BuildSearchQuery_ComposesExpectedTokens() {\n        var query = GmailMailboxBrowser.BuildSearchQuery(new GmailMailboxBrowser.GmailMailboxSearchRequest {\n            Query = \"urgent\",\n            SubjectContains = \"invoice\",\n            FromContains = \"boss@example.test\",\n            ToContains = \"team@example.test\",\n            BodyContains = \"overdue\",\n            UnseenOnly = true,\n            HasAttachment = true,\n            SinceUtc = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),\n            BeforeUtc = new DateTime(2024, 1, 2, 0, 0, 0, DateTimeKind.Utc)\n        });\n\n        Assert.Contains(\"is:unread\", query);\n        Assert.Contains(\"has:attachment\", query);\n        Assert.Contains(\"after:1704067200\", query);\n        Assert.Contains(\"before:1704153600\", query);\n        Assert.Contains(\"subject:(invoice)\", query);\n        Assert.Contains(\"from:(boss@example.test)\", query);\n        Assert.Contains(\"to:(team@example.test)\", query);\n        Assert.Contains(\"overdue\", query);\n        Assert.Contains(\"urgent\", query);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetMessageSeenAsync_ModifiesMessageLabels() {\n        var modifyJson = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(modifyJson) });\n        var browser = CreateBrowser(handler);\n\n        await browser.SetMessageSeenAsync(\"m1\", seen: true);\n\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/messages/m1/modify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"UNREAD\\\"]\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessageAsync_ResolvesLabels_AndModifiesMessage() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_Target\\\",\\\"name\\\":\\\"Archive\\\"}]}\";\n        var modifyJson = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(modifyJson) });\n        var browser = CreateBrowser(handler);\n\n        await browser.MoveMessageAsync(\"m1\", sourceFolder: \"INBOX\", targetFolder: \"Archive\");\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/users/me/labels\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/users/me/messages/m1/modify\", handler.Requests[1].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"Label_Target\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"INBOX\", body, StringComparison.Ordinal);\n        Assert.Contains(\"TRASH\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessageAsync_ToTrash_UsesTrashEndpoint() {\n        var trashJson = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(trashJson) });\n        var browser = CreateBrowser(handler);\n\n        await browser.MoveMessageAsync(\"m1\", sourceFolder: \"INBOX\", targetFolder: \"TRASH\");\n\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/messages/m1/trash\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessageAsync_WithNullSourceFolder_DoesNotRemoveInbox() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_Target\\\",\\\"name\\\":\\\"Archive\\\"}]}\";\n        var modifyJson = \"{\\\"id\\\":\\\"m1\\\",\\\"threadId\\\":\\\"t1\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(modifyJson) });\n        var browser = CreateBrowser(handler);\n\n        await browser.MoveMessageAsync(\"m1\", sourceFolder: null, targetFolder: \"Archive\");\n\n        Assert.Equal(2, handler.Requests.Count);\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"Label_Target\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"TRASH\\\"]\", body, StringComparison.Ordinal);\n        Assert.DoesNotContain(\"INBOX\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ArchiveMessagesAsync_UsesBatchModify_AndReturnsPerMessageResults() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.ArchiveMessagesAsync(new[] { \"m1\", \"m2\" });\n\n        Assert.Equal(2, results.Count);\n        Assert.All(results, x => Assert.True(x.Ok, x.Error));\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/messages/batchModify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"ids\\\":[\\\"m1\\\",\\\"m2\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"INBOX\\\",\\\"TRASH\\\"]\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessagesAsync_WithNullSourceFolder_DoesNotRemoveInbox() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_Target\\\",\\\"name\\\":\\\"Archive\\\"}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.MoveMessagesAsync(new[] { \"m1\", \"m2\" }, sourceFolder: null, targetFolder: \"Archive\");\n\n        Assert.Equal(2, results.Count);\n        Assert.All(results, x => Assert.True(x.Ok, x.Error));\n        Assert.Equal(2, handler.Requests.Count);\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"ids\\\":[\\\"m1\\\",\\\"m2\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"Label_Target\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"TRASH\\\"]\", body, StringComparison.Ordinal);\n        Assert.DoesNotContain(\"INBOX\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteThreadsAsync_MapsPerThreadFailures() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new StringContent(string.Empty) },\n            new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(\"boom\") });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.DeleteThreadsAsync(new[] { \"t1\", \"t2\" });\n\n        Assert.Equal(2, results.Count);\n        Assert.Equal(\"t1\", results[0].Id);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Equal(\"t2\", results[1].Id);\n        Assert.False(results[1].Ok);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/users/me/threads/t1\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/users/me/threads/t2\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveThreadsAsync_UsesThreadModifyLabels() {\n        var labelsJson = \"{\\\"labels\\\":[{\\\"id\\\":\\\"Label_1\\\",\\\"name\\\":\\\"Project\\\"}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(labelsJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"id\\\":\\\"t1\\\"}\") });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.MoveThreadsAsync(new[] { \"t1\" }, sourceFolder: \"INBOX\", targetFolder: \"Project\");\n\n        Assert.Single(results);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/users/me/labels\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/users/me/threads/t1/modify\", handler.Requests[1].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"addLabelIds\\\":[\\\"Label_1\\\"]\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"INBOX\\\",\\\"TRASH\\\"]\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetThreadsSeenAsync_UsesThreadModifyLabels() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"id\\\":\\\"t1\\\"}\") });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.SetThreadsSeenAsync(new[] { \"t1\" }, seen: true);\n\n        Assert.Single(results);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/threads/t1/modify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"UNREAD\\\"]\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetThreadsFlaggedAsync_UsesThreadModifyLabels() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"id\\\":\\\"t1\\\"}\") });\n        var browser = CreateBrowser(handler);\n\n        var results = await browser.SetThreadsFlaggedAsync(new[] { \"t1\" }, flagged: false);\n\n        Assert.Single(results);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/users/me/threads/t1/modify\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"removeLabelIds\\\":[\\\"STARRED\\\"]\", body, StringComparison.Ordinal);\n    }\n\n    private static Dictionary<string, List<string>> ParseQueryParams(Uri uri) {\n        var dict = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);\n        var q = uri.Query;\n        if (string.IsNullOrEmpty(q) || q == \"?\") {\n            return dict;\n        }\n        if (q[0] == '?') {\n            q = q.Substring(1);\n        }\n\n        foreach (var part in q.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) {\n            var idx = part.IndexOf('=');\n            string rawKey;\n            string rawValue;\n            if (idx < 0) {\n                rawKey = part;\n                rawValue = string.Empty;\n            } else {\n                rawKey = part.Substring(0, idx);\n                rawValue = part.Substring(idx + 1);\n            }\n\n            var key = Uri.UnescapeDataString(rawKey.Replace(\"+\", \" \"));\n            var value = Uri.UnescapeDataString(rawValue.Replace(\"+\", \" \"));\n\n            if (!dict.TryGetValue(key, out var list)) {\n                list = new List<string>();\n                dict[key] = list;\n            }\n            list.Add(value);\n        }\n\n        return dict;\n    }\n\n    private static GmailMailboxBrowser CreateBrowser(HttpMessageHandler handler) {\n        var api = new GmailApiClient(new OAuthCredential { UserName = \"me\", AccessToken = \"token\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        return new GmailMailboxBrowser(api);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GmailNonDeliveryReportsTests.cs",
    "content": "using Mailozaurr;\nusing MimeKit;\nusing System;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GmailNonDeliveryReportsTests {\n    private static MimeMessage CreateNdr(string recipient, string messageId, DateTimeOffset date) {\n        string raw = $\"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\r\\n\\r\\n--XXX\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\ntext\\r\\n\\r\\n--XXX\\r\\nContent-Type: message/delivery-status\\r\\n\\r\\nOriginal-Recipient: rfc822; {recipient}\\r\\nFinal-Recipient: rfc822; {recipient}\\r\\nOriginal-Message-ID: {messageId}\\r\\nReporting-MTA: dns; mx.example.com\\r\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\r\\nStatus: 5.1.1\\r\\nArrival-Date: {date:R}\\r\\n\\r\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        return MimeMessage.Load(stream);\n    }\n\n    private static string Encode(MimeMessage message) {\n        using var ms = new MemoryStream();\n        message.WriteTo(ms);\n        return Convert.ToBase64String(ms.ToArray()).Replace('+', '-').Replace('/', '_').Replace(\"=\", string.Empty);\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_GmailApi_ReturnsReports() {\n        var now = DateTimeOffset.UtcNow;\n        var ndr = CreateNdr(\"user@example.com\", \"<id1>\", now);\n        ndr.Subject = \"Undeliverable: Delivery has failed\";\n        var normal = new MimeMessage();\n        normal.Subject = \"hello\";\n        var listJson = \"{\\\"messages\\\":[{\\\"id\\\":\\\"1\\\"},{\\\"id\\\":\\\"2\\\"}]}\";\n        var msg1Json = $\"{{\\\"raw\\\":\\\"{Encode(ndr)}\\\"}}\";\n        var msg2Json = $\"{{\\\"raw\\\":\\\"{Encode(normal)}\\\"}}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(msg1Json) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(msg2Json) });\n        var client = new GmailApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GmailApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") });\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(client, \"me\", parallelDownloadLimit: 1, cancellationToken: CancellationToken.None);\n        Assert.NotEmpty(handler.Requests);\n        var listRequest = handler.Requests[0];\n        var uri = listRequest.RequestUri!;\n        string? queryParam = null;\n        var query = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries);\n        foreach (var part in query) {\n            var kvp = part.Split(new[] { '=' }, 2);\n            if (kvp.Length == 2 && kvp[0] == \"q\") {\n                queryParam = Uri.UnescapeDataString(kvp[1]);\n                break;\n            }\n        }\n        Assert.NotNull(queryParam);\n        Assert.Contains(\"subject:\\\"Undeliverable:\\\"\", queryParam, StringComparison.Ordinal);\n        Assert.Single(reports);\n        Assert.Equal(\"id1\", reports[0].OriginalMessageId);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphApiClientMailboxTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphApiClientMailboxTests {\n    [Fact]\n    public async System.Threading.Tasks.Task ListMessagesAsync_AddsConsistencyHeaders_WhenSearchIsUsed() {\n        var json = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"subject\\\":\\\"s\\\",\\\"receivedDateTime\\\":\\\"2026-02-15T00:00:00Z\\\",\\\"internetMessageId\\\":\\\"<x>\\\",\\\"hasAttachments\\\":false,\\\"isRead\\\":true,\\\"conversationId\\\":\\\"c1\\\",\\\"from\\\":{\\\"emailAddress\\\":{\\\"address\\\":\\\"a@b.com\\\"}},\\\"toRecipients\\\":[{\\\"emailAddress\\\":{\\\"address\\\":\\\"c@d.com\\\"}}],\\\"flag\\\":{\\\"flagStatus\\\":\\\"flagged\\\"}}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var page = await api.ListMessagesAsync(\"inbox\", search: \"hello\");\n        Assert.Single(page.Items);\n        Assert.Equal(\"m1\", page.Items[0].Id);\n        Assert.Equal(\"a@b.com\", page.Items[0].From!.Email.Address);\n\n        Assert.Single(handler.Requests);\n        Assert.True(handler.Requests[0].Headers.Contains(\"ConsistencyLevel\"));\n        Assert.True(handler.Requests[0].Headers.Contains(\"Prefer\"));\n        Assert.Contains(\"/me/mailFolders/inbox/messages?\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeltaMessagesAsync_ParsesDeletes_AndCursor() {\n        var json = \"{\\\"@odata.deltaLink\\\":\\\"https://graph.microsoft.com/v1.0/delta\\\",\\\"value\\\":[{\\\"id\\\":\\\"d1\\\",\\\"@removed\\\":{}},{\\\"id\\\":\\\"u1\\\",\\\"subject\\\":\\\"x\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var delta = await api.DeltaMessagesAsync(\"inbox\", cursor: null, top: 5);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/delta\", delta.Cursor);\n        Assert.Single(delta.DeletedIds);\n        Assert.Equal(\"d1\", delta.DeletedIds[0]);\n        Assert.Single(delta.Items);\n        Assert.Equal(\"u1\", delta.Items[0].Id);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListConversationMessagesAsync_BuildsFilterQuery() {\n        var json = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"subject\\\":\\\"s\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var msgs = await api.ListConversationMessagesAsync(\"conv-1\");\n        Assert.Single(msgs);\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!.ToString();\n        Assert.Contains(\"/me/messages?\", uri);\n        Assert.Contains(\"$filter=\", uri);\n        Assert.Contains(\"conversationId\", uri);\n        Assert.Contains(\"conv-1\", uri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendBatchAsync_UsesBatchEndpoint_AndSerializesRelativeUrls() {\n        var json = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":204,\\\"headers\\\":{},\\\"body\\\":{}}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var r = new GraphBatchRequest { Id = \"\", Method = GraphHttpMethod.DELETE, Url = \"/me/messages/123\" };\n        var results = await api.SendBatchAsync(new[] { r });\n        Assert.Single(results);\n        Assert.Equal(204, results[0].Status);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/$batch\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"url\\\":\\\"me/messages/123\\\"\", body);\n        Assert.Contains(\"\\\"method\\\":\\\"DELETE\\\"\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task CreateMessageAsync_CreatesDraftInFolder() {\n        var json = \"{\\\"id\\\":\\\"m-created\\\",\\\"subject\\\":\\\"s\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(json)\n        });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var created = await api.CreateMessageAsync(\n            new GraphMessage {\n                Subject = \"s\",\n                Body = new GraphContent { Type = \"Text\", Content = \"body\" }\n            },\n            folderIdOrWellKnownName: \"sentitems\");\n\n        Assert.Equal(\"m-created\", created.Id);\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Contains(\"/me/mailFolders/sentitems/messages\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendDraftMessageAsync_PostsToSendEndpoint() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted) {\n            Content = new StringContent(string.Empty)\n        });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        await api.SendDraftMessageAsync(\"m123\");\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Contains(\"/me/messages/m123/send\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task CreateAttachmentUploadSessionAsync_UsesAttachmentItemEnvelope() {\n        var json = \"{\\\"uploadUrl\\\":\\\"https://upload.example/session\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(json)\n        });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var session = await api.CreateAttachmentUploadSessionAsync(\n            \"m123\",\n            new GraphAttachmentItem(\"file\", \"a.txt\", 10));\n\n        Assert.Equal(\"https://upload.example/session\", session.UploadUrl);\n        Assert.Single(handler.Requests);\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"attachmentItem\\\"\", body);\n        Assert.Contains(\"\\\"attachmentType\\\":\\\"file\\\"\", body);\n        Assert.Contains(\"\\\"name\\\":\\\"a.txt\\\"\", body);\n        Assert.Contains(\"\\\"size\\\":10\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task BatchSetMessagesIsReadAsync_ReturnsPerMessageStatus() {\n        var batchResponse = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":200,\\\"headers\\\":{},\\\"body\\\":{}},{\\\"id\\\":\\\"2\\\",\\\"status\\\":400,\\\"headers\\\":{},\\\"body\\\":{\\\"error\\\":{\\\"message\\\":\\\"bad-request\\\"}}}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(batchResponse)\n        });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var results = await api.BatchSetMessagesIsReadAsync(new[] { \"m-1\", \"m-2\" }, isRead: true);\n\n        Assert.Equal(2, results.Count);\n        Assert.True(results[0].Ok);\n        Assert.Equal(\"m-1\", results[0].Id);\n        Assert.False(results[1].Ok);\n        Assert.Equal(\"m-2\", results[1].Id);\n        Assert.Contains(\"bad-request\", results[1].Error);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Contains(\"/$batch\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"url\\\":\\\"me/messages/m-1\\\"\", body);\n        Assert.Contains(\"\\\"isRead\\\":true\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task BatchMoveConversationsAsync_ListsConversationMessages_ThenMovesInBatch() {\n        var listResponse = \"{\\\"value\\\":[{\\\"id\\\":\\\"m-1\\\"},{\\\"id\\\":\\\"m-2\\\"}]}\";\n        var batchResponse = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":201,\\\"headers\\\":{},\\\"body\\\":{}},{\\\"id\\\":\\\"2\\\",\\\"status\\\":201,\\\"headers\\\":{},\\\"body\\\":{}}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listResponse) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(batchResponse) });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var results = await api.BatchMoveConversationsAsync(new[] { \"conv-1\" }, destinationFolderId: \"archive\");\n\n        Assert.Single(results);\n        Assert.Equal(\"conv-1\", results[0].Id);\n        Assert.True(results[0].Ok);\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Equal(HttpMethod.Get, handler.Requests[0].Method);\n        Assert.Contains(\"conversationId\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Equal(HttpMethod.Post, handler.Requests[1].Method);\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"url\\\":\\\"me/messages/m-1/move\\\"\", body);\n        Assert.Contains(\"\\\"url\\\":\\\"me/messages/m-2/move\\\"\", body);\n        Assert.Contains(\"\\\"destinationId\\\":\\\"archive\\\"\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task BatchDeleteMessagesAsync_WhenBatchFails_ReturnsFailurePerMessage() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.InternalServerError) {\n            Content = new StringContent(\"{\\\"error\\\":{\\\"message\\\":\\\"batch-down\\\"}}\")\n        });\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var results = await api.BatchDeleteMessagesAsync(new[] { \"m-1\", \"m-2\" });\n\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.False(r.Ok));\n        Assert.Contains(\"Graph batch failed (500).\", results[0].Error);\n        Assert.Contains(\"Graph batch failed (500).\", results[1].Error);\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Contains(\"/$batch\", handler.Requests[0].RequestUri!.ToString());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphApiClientTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphApiClientTests {\n    [Fact]\n    public async System.Threading.Tasks.Task CreateSubscriptionAsync_NullRequest_Throws() {\n        var client = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        await Assert.ThrowsAsync<ArgumentNullException>(() => client.CreateSubscriptionAsync(null!));\n    }\n\n    [Theory]\n    [InlineData(\"\", \"created\", \"https://example.com\", \"resource\")]\n    [InlineData(\"resource\", \"\", \"https://example.com\", \"changeType\")]\n    [InlineData(\"resource\", \"created\", \"\", \"notificationUrl\")]\n    public async System.Threading.Tasks.Task CreateSubscriptionAsync_MissingRequired_Throws(string resource, string changeType, string notificationUrl, string _) {\n        var client = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var req = new GraphApiClient.GraphCreateSubscriptionRequest {\n            Resource = resource,\n            ChangeType = changeType,\n            NotificationUrl = notificationUrl,\n            ExpirationDateTime = System.DateTimeOffset.UtcNow.AddHours(1)\n        };\n        await Assert.ThrowsAsync<ArgumentException>(() => client.CreateSubscriptionAsync(req));\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task CreateSubscriptionAsync_SendsPostAndParses() {\n        var json = \"{\\\"id\\\":\\\"s1\\\",\\\"resource\\\":\\\"me/messages\\\",\\\"changeType\\\":\\\"created\\\",\\\"notificationUrl\\\":\\\"https://example.com\\\",\\\"expirationDateTime\\\":\\\"2026-02-15T00:00:00Z\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        var req = new GraphApiClient.GraphCreateSubscriptionRequest {\n            Resource = \"me/messages\",\n            ChangeType = \"created\",\n            NotificationUrl = \"https://example.com\",\n            ExpirationDateTime = System.DateTimeOffset.Parse(\"2026-02-15T00:00:00Z\")\n        };\n        var sub = await client.CreateSubscriptionAsync(req);\n        Assert.Equal(\"s1\", sub.Id);\n        Assert.Equal(\"me/messages\", sub.Resource);\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/subscriptions\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"resource\\\":\\\"me/messages\\\"\", body);\n        Assert.Contains(\"\\\"changeType\\\":\\\"created\\\"\", body);\n        Assert.Contains(\"\\\"notificationUrl\\\":\\\"https://example.com\\\"\", body);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task RenewSubscriptionAsync_EncodesSubscriptionId_UsesPatch() {\n        var json = \"{\\\"id\\\":\\\"a b\\\",\\\"resource\\\":\\\"me/messages\\\",\\\"changeType\\\":\\\"created\\\",\\\"notificationUrl\\\":\\\"https://example.com\\\",\\\"expirationDateTime\\\":\\\"2026-02-15T00:00:00Z\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(json) });\n        var client = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        await client.RenewSubscriptionAsync(\"a b\", System.DateTimeOffset.Parse(\"2026-02-15T00:00:00Z\"));\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"PATCH\", handler.Requests[0].Method.Method);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/subscriptions/a%20b\", handler.Requests[0].RequestUri!.AbsoluteUri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteSubscriptionAsync_EncodesSubscriptionId() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent(\"{}\") });\n        var client = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = System.DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(client, new HttpClient(handler) { BaseAddress = new System.Uri(\"https://graph.microsoft.com/v1.0/\") });\n\n        await client.DeleteSubscriptionAsync(\"a b\");\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Delete, handler.Requests[0].Method);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/subscriptions/a%20b\", handler.Requests[0].RequestUri!.AbsoluteUri);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphApiErrorParserTests.cs",
    "content": "using System.Net;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphApiErrorParserTests {\n    [Fact]\n    public void Parse_ShouldExtractInformation() {\n        var sample = \"POST https://graph.microsoft.com/v1.0/users/przemyslaw.klys@company.pl/sendMail\\n\" +\n                     \"HTTP/2.0 404 Not Found\\n\" +\n                     \"request-id: 2ff18766-1395-4fb9-abd1-162774d4b063\\n\" +\n                     \"client-request-id: 6c57f9e6-3cad-48ee-8f7a-d566dc92aca3\\n\" +\n                     \"x-ms-ags-diagnostic: {\\\"ServerInfo\\\":{\\\"DataCenter\\\":\\\"Poland Central\\\",\\\"Slice\\\":\\\"E\\\",\\\"Ring\\\":\\\"2\\\",\\\"ScaleUnit\\\":\\\"002\\\",\\\"RoleInstance\\\":\\\"WA3PEPF000004A2\\\"}}\\n\" +\n                     \"Date: Sun, 24 Aug 2025 12:55:18 GMT\\n\" +\n                     \"Content-Type: application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8\\n\\n\" +\n                     \"{\\\"error\\\":{\\\"code\\\":\\\"ErrorInvalidUser\\\",\\\"message\\\":\\\"The requested user 'przemyslaw.klys@company.pl' is invalid.\\\"}}\";\n\n        var parsed = GraphApiErrorParser.Parse(sample);\n        Assert.NotNull(parsed);\n        Assert.Equal(HttpStatusCode.NotFound, parsed!.StatusCode);\n        Assert.Equal(GraphHttpMethod.POST, parsed.Method);\n        Assert.Equal(\"2ff18766-1395-4fb9-abd1-162774d4b063\", parsed.Headers.RequestId);\n        Assert.Equal(\"Poland Central\", parsed.Headers.Diagnostic?.ServerInfo.DataCenter);\n        Assert.Equal(\"ErrorInvalidUser\", parsed.Error?.Code);\n    }\n\n    [Fact]\n    public void Parse_InvalidInput_ReturnsRaw() {\n        const string sample = \"not a graph error\";\n        var parsed = GraphApiErrorParser.Parse(sample);\n        Assert.NotNull(parsed);\n        Assert.Equal(sample, parsed!.Raw);\n        Assert.Null(parsed.Error);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphAuthenticateTests.cs",
    "content": "using System;\nusing System.Net;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphAuthenticateTests\n{\n    [Fact]\n    public void Authenticate_WithNonNetworkCredential_Throws()\n    {\n        using var graph = new Graph();\n        ICredentials credentials = new DummyCredentials();\n\n        var exception = Assert.Throws<ArgumentException>(() => graph.Authenticate(credentials));\n        Assert.Contains(\"NetworkCredential\", exception.Message);\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"client\")]\n    [InlineData(\"client@\")]\n    [InlineData(\"@tenant\")]\n    public void Authenticate_WithMalformedUserName_Throws(string username)\n    {\n        using var graph = new Graph();\n        var credential = new NetworkCredential(username, \"secret\");\n\n        var exception = Assert.Throws<ArgumentException>(() => graph.Authenticate(credential));\n        Assert.Contains(\"clientid@directoryid\", exception.Message);\n    }\n\n    private sealed class DummyCredentials : ICredentials\n    {\n        public NetworkCredential? GetCredential(Uri uri, string authType) => null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphBatchAndRetryTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class GraphBatchAndRetryTests {\n    private class BatchHandler : HttpMessageHandler {\n        public HttpRequestMessage? BatchRequest;\n        public string? BatchPayload;\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            if (request.RequestUri!.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                };\n            }\n            if (request.RequestUri!.AbsoluteUri.Contains(\"$batch\")) {\n                BatchRequest = request;\n                if (request.Content is not null) {\n                    BatchPayload = await request.Content.ReadAsStringAsync().ConfigureAwait(false);\n                }\n                var json = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":202}]}\";\n                return new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                };\n            }\n            return new HttpResponseMessage(HttpStatusCode.NotFound);\n        }\n    }\n\n    private static FieldInfo GetHandlerField()\n        => typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    [Fact]\n    public async Task SendMessageBatchAsync_BuildsBatchPayload() {\n        var handler = new BatchHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            using var graph = new Graph {\n                From = \"sender@example.com\",\n                To = new object[] { \"recipient@example.com\" },\n                Subject = \"sub\",\n                HTML = \"body\",\n                ContentType = \"HTML\"\n            };\n            graph.Authenticate(new System.Net.NetworkCredential(\"id@tenant\", \"secret\"));\n            var result = await graph.SendMessageBatchAsync();\n            Assert.True(result.Status);\n\n            string payload = handler.BatchPayload!;\n            using var doc = JsonDocument.Parse(payload);\n            var req = doc.RootElement.GetProperty(\"requests\")[0];\n            Assert.Equal(\"1\", req.GetProperty(\"id\").GetString());\n            Assert.Equal(\"POST\", req.GetProperty(\"method\").GetString());\n            var msg = graph.MessageContainer?.Message;\n            Assert.NotNull(msg);\n            Assert.Equal($\"users/{msg!.From!.Email!.Address!}/sendMail\", req.GetProperty(\"url\").GetString());\n            Assert.Equal(\"application/json\", req.GetProperty(\"headers\").GetProperty(\"Content-Type\").GetString());\n            Assert.Equal(\"sub\", req.GetProperty(\"body\").GetProperty(\"message\").GetProperty(\"subject\").GetString());\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task SendMessageBatchAsync_Canceled_ThrowsOperationCanceledException() {\n        var handler = new BatchHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            using var graph = new Graph {\n                From = \"sender@example.com\",\n                To = new object[] { \"recipient@example.com\" },\n                Subject = \"sub\",\n                HTML = \"body\",\n                ContentType = \"HTML\"\n            };\n            graph.Authenticate(new System.Net.NetworkCredential(\"id@tenant\", \"secret\"));\n            using var cts = new CancellationTokenSource();\n            cts.Cancel();\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => graph.SendMessageBatchAsync(cts.Token));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    private class RetryHandler : HttpMessageHandler {\n        public int CallCount;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            CallCount++;\n            if (CallCount < 3) {\n                throw new HttpRequestException(\"fail\");\n            }\n            var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(json)\n            });\n        }\n    }\n\n    private class RetryAfterHandler : HttpMessageHandler {\n        public int CallCount;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            CallCount++;\n            if (request.RequestUri!.AbsoluteUri.Contains(\"oauth2\")) {\n                if (CallCount == 1) {\n                    var resp = new HttpResponseMessage((HttpStatusCode)429);\n                    resp.Headers.Add(\"Retry-After\", \"0\");\n                    return Task.FromResult(resp);\n                }\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private class AlwaysFailHandler : HttpMessageHandler {\n        public int CallCount;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            CallCount++;\n            var json = \"{\\\"error\\\":\\\"fail\\\"}\";\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(json) });\n        }\n    }\n\n    private class HangingHandler : HttpMessageHandler {\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false);\n            throw new InvalidOperationException(\"Unreachable\");\n        }\n    }\n\n    private class TransientFailHandler : HttpMessageHandler {\n        public int CallCount;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            CallCount++;\n            throw new HttpRequestException(\"fail\");\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphWithRetryAsync_RetriesUntilSuccess() {\n        var handler = new RetryHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            string token = await MicrosoftGraphUtils.ConnectO365GraphWithRetryAsync(credential, \"tenant\", 2, 0, 1);\n            Assert.Equal(\"Bearer token\", token);\n            Assert.Equal(3, handler.CallCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_UsesProvidedAccessTokenWithoutHttpCall() {\n        var handler = new RetryHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential {\n                ClientId = \"id\",\n                DirectoryId = \"tenant\",\n                AccessToken = \"delegated-token\"\n            };\n\n            string token = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, \"tenant\");\n\n            Assert.Equal(\"Bearer delegated-token\", token);\n            Assert.Equal(0, handler.CallCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphWithRetryAsync_NoRetriesThrowsException() {\n        var handler = new AlwaysFailHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            await Assert.ThrowsAsync<GraphApiException>(() => MicrosoftGraphUtils.ConnectO365GraphWithRetryAsync(credential, \"tenant\", 0, 0, 1));\n            Assert.Equal(1, handler.CallCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_RetriesAfter429() {\n        var handler = new RetryAfterHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            string token = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, \"tenant\", \"https://graph.microsoft.com\");\n            Assert.Equal(\"Bearer token\", token);\n            Assert.Equal(2, handler.CallCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_CancellationRequested_Throws() {\n        var handler = new HangingHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        var cts = new CancellationTokenSource(100);\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => MicrosoftGraphUtils.ConnectO365GraphAsync(credential, \"tenant\", \"https://graph.microsoft.com\", cts.Token));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphWithRetryAsync_CancelledDuringDelay_Throws() {\n        var handler = new TransientFailHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        var cts = new CancellationTokenSource(100);\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(\n                () => MicrosoftGraphUtils.ConnectO365GraphWithRetryAsync(credential, \"tenant\", 3, 10000, 1, \"https://graph.microsoft.com\", cts.Token));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphChunkSizeLimitTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphChunkSizeLimitTests\n{\n    [Fact]\n    public void ChunkSize_SetAboveLimit_IsCapped()\n    {\n        using Graph graph = new();\n        graph.ChunkSize = Graph.MaxChunkSize * 2;\n        Assert.Equal(Graph.MaxChunkSize, graph.ChunkSize);\n    }\n\n    [Fact]\n    public async Task PrepareByteArrayContentForUpload_EnforcesMaxChunkSize()\n    {\n        string tmp = Path.GetTempFileName();\n        int fileSize = Graph.MaxChunkSize + 1024;\n        File.WriteAllBytes(tmp, new byte[fileSize]);\n        using Graph graph = new();\n        MethodInfo? method = typeof(Graph).GetMethod(\n            \"PrepareByteArrayContentForUpload\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        List<StreamContent> chunks = (List<StreamContent>)method!.Invoke(\n            graph,\n            new object[] { tmp, graph.ChunkSize * 2, default(System.Threading.CancellationToken) })!;\n        File.Delete(tmp);\n        Assert.Equal(2, chunks.Count);\n        byte[] chunk0 = await chunks[0].ReadAsByteArrayAsync();\n        byte[] chunk1 = await chunks[1].ReadAsByteArrayAsync();\n        Assert.Equal(Graph.MaxChunkSize, chunk0.Length);\n        Assert.Equal(fileSize - Graph.MaxChunkSize, chunk1.Length);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphCreateMessageTests.cs",
    "content": "using System;\nusing System.IO;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Tests creation of Graph API messages.\n/// </summary>\npublic class GraphCreateMessageTests\n{\n    [Fact]\n    public void CreateMessage_WithValidData_BuildsJson()\n    {\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = \"body\",\n            ContentType = \"HTML\"\n        };\n        graph.CreateMessage();\n        Assert.Contains(\"subject\", graph.MessageJson, StringComparison.OrdinalIgnoreCase);\n        Assert.Contains(\"to@example.com\", graph.MessageJson, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void CreateMessage_WithHeaders_IncludesHeaders()\n    {\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            Headers = new System.Collections.Generic.Dictionary<string, string> { [\"X-Test\"] = \"123\" }\n        };\n        graph.CreateMessage();\n        Assert.Contains(\"X-Test\", graph.MessageJson, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void CreateMessage_WithoutExplicitContentType_DefaultsToHtml()\n    {\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = \"body\"\n        };\n\n        graph.CreateMessage();\n\n        Assert.Equal(\"HTML\", graph.MessageContainer.Message.Body?.Type);\n    }\n\n    [Fact]\n    public void ContentType_SetToInvalidValue_ThrowsArgumentException()\n    {\n        using var graph = new Graph();\n\n        Assert.Throws<ArgumentException>(() => graph.ContentType = \"Markdown\");\n    }\n\n    [Fact]\n    public void CreateAttachments_WithMissingFile_SkipsAttachment()\n    {\n        using var graph = new Graph\n        {\n            Attachments = new object[] { \"missing.file\" }\n        };\n        graph.CreateAttachments();\n\n        Assert.Empty(graph.ConvertedAttachments);\n    }\n\n    [Fact]\n    public void CreateMessage_WithLargeAttachment_DoesNotIncludeAttachment()\n    {\n        string tmp = Path.GetTempFileName();\n        File.WriteAllBytes(tmp, new byte[4000001]);\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            Attachments = new object[] { tmp }\n        };\n        graph.CreateAttachments();\n        graph.CreateMessage();\n        File.Delete(tmp);\n        Assert.True(graph.IsLargerAttachment);\n        Assert.Null(graph.MessageContainer.Message.Attachments);\n    }\n\n    [Fact]\n    public void CreateMessage_WithoutFrom_ThrowsInvalidOperationException()\n    {\n        using var graph = new Graph\n        {\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = \"body\",\n            ContentType = \"HTML\"\n        };\n\n        Assert.Throws<InvalidOperationException>(() => graph.CreateMessage());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphDraftTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphDraftTests\n{\n    [Fact]\n    public void CreateDraft_LargeAttachments_ExcludesAttachments()\n    {\n        string tmp = Path.GetTempFileName();\n        File.WriteAllBytes(tmp, new byte[4_100_000]);\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"sub\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            Attachments = new object[] { tmp }\n        };\n\n        string json = graph.CreateDraft();\n        File.Delete(tmp);\n        Assert.True(graph.IsLargerAttachment);\n        Assert.DoesNotContain(\"\\\"attachments\\\"\", json, StringComparison.OrdinalIgnoreCase);\n        Assert.DoesNotContain(\"saveToSentItems\", json, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public async Task PrepareAttachments_LargeFiles_CreatePlaceholders()\n    {\n        string tmp1 = Path.GetTempFileName();\n        string tmp2 = Path.GetTempFileName();\n        File.WriteAllBytes(tmp1, new byte[3_000_000]);\n        File.WriteAllBytes(tmp2, new byte[2_000_000]);\n        using var graph = new Graph { Attachments = new object[] { tmp1, tmp2 } };\n        graph.CreateAttachments();\n        await graph.PrepareAttachments();\n        File.Delete(tmp1);\n        File.Delete(tmp2);\n        Assert.True(graph.IsLargerAttachment);\n        Assert.Equal(2, graph.AttachmentsPlaceHolders.Count);\n        Assert.All(graph.AttachmentsPlaceHolders, p => Assert.False(string.IsNullOrWhiteSpace(p.FileName)));\n    }\n\n    [Fact]\n    public async Task CreateGraphAttachment_MissingFile_ThrowsAndLogsWarning()\n    {\n        string missing = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(\"N\") + \".txt\");\n        using var graph = new Graph();\n\n        var exception = await Assert.ThrowsAsync<FileNotFoundException>(() => graph.CreateGraphAttachment(missing));\n        Assert.Contains(\"Attachment file not found\", exception.Message);\n        Assert.Equal(missing, exception.FileName);\n\n        var warnings = graph.LogCollector.Logs.ToArray();\n        Assert.Contains(warnings, entry => entry.Type == LogType.Warning && entry.Message.IndexOf(missing, StringComparison.OrdinalIgnoreCase) >= 0);\n    }\n\n    [Fact]\n    public async Task CreateGraphAttachment_Canceled_ThrowsOperationCanceledException()\n    {\n        string tmp = Path.GetTempFileName();\n        File.WriteAllBytes(tmp, new byte[1024]);\n        using var graph = new Graph();\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        try\n        {\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => graph.CreateGraphAttachment(tmp, cts.Token));\n        }\n        finally\n        {\n            File.Delete(tmp);\n        }\n    }\n\n    [Fact]\n    public async Task PrepareAttachments_MissingFile_SkipsPlaceholderAndLogs()\n    {\n        string missing = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(\"N\") + \".txt\");\n        using var graph = new Graph { Attachments = new object[] { missing } };\n\n        await graph.PrepareAttachments();\n\n        Assert.Empty(graph.AttachmentsPlaceHolders);\n        var warnings = graph.LogCollector.Logs.ToArray();\n        Assert.True(warnings.Count(entry => entry.Type == LogType.Warning && entry.Message.IndexOf(missing, StringComparison.OrdinalIgnoreCase) >= 0) >= 1);\n    }\n\n    [Fact]\n    public void CreateDraft_SetsImportanceFromPriority()\n    {\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"sub\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            Priority = MessagePriority.High\n        };\n\n        string json = graph.CreateDraft();\n        Assert.Contains(\"\\\"importance\\\":\\\"high\\\"\", json, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void CreateDraftForMg_SetsImportanceFromPriority()\n    {\n        using var graph = new Graph\n        {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"sub\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            Priority = MessagePriority.Low\n        };\n\n        string json = graph.CreateDraftForMg();\n        Assert.Contains(\"\\\"importance\\\":\\\"low\\\"\", json, StringComparison.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphEventBuilderTests.cs",
    "content": "using System;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphEventBuilderTests {\n    [Fact]\n    public void Builder_CreatesEvent() {\n        GraphEvent ev = new GraphEventBuilder()\n            .Subject(\"Test\")\n            .Start(new DateTime(2024,1,1,12,0,0,DateTimeKind.Utc))\n            .End(new DateTime(2024,1,1,13,0,0,DateTimeKind.Utc))\n            .Attendee(\"user@example.com\", \"User\");\n        Assert.Equal(\"Test\", ev.Subject);\n        Assert.NotNull(ev.Start);\n        Assert.NotNull(ev.End);\n        Assert.NotNull(ev.Attendees);\n        Assert.Contains(ev.Attendees!, a => a.EmailAddress.Email.Address == \"user@example.com\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphExceptionStackTraceTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing System.Management.Automation;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphExceptionStackTraceTests {\n    [Fact]\n    public async Task SendMessageAsync_Failure_PreservesStackTrace() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.BadRequest) {\n            Content = new StringContent(\"{\\\"error\\\":{\\\"code\\\":\\\"BadRequest\\\",\\\"message\\\":\\\"Invalid\\\",\\\"innerError\\\":{\\\"requestId\\\":\\\"1\\\",\\\"date\\\":\\\"2020-01-01\\\"}}}\")\n        });\n\n        using var graph = new Graph {\n            AccessToken = \"token\",\n            TokenType = \"Bearer\",\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"sub\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            ErrorAction = ActionPreference.Stop\n        };\n        var field = typeof(Graph).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(graph, new HttpClient(handler));\n\n        var ex = await Assert.ThrowsAsync<GraphApiException>(() => graph.SendMessageAsync());\n        Assert.Contains(nameof(Graph.SendMessageAsync), ex.StackTrace);\n    }\n\n    [Fact]\n    public async Task SendDraftMessage_Failure_PreservesStackTrace() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.BadRequest) {\n            Content = new StringContent(\"{\\\"error\\\":{\\\"code\\\":\\\"BadRequest\\\",\\\"message\\\":\\\"Invalid\\\",\\\"innerError\\\":{\\\"requestId\\\":\\\"1\\\",\\\"date\\\":\\\"2020-01-01\\\"}}}\")\n        });\n\n        using var graph = new Graph {\n            AccessToken = \"token\",\n            TokenType = \"Bearer\",\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"sub\",\n            HTML = \"body\",\n            ContentType = \"HTML\",\n            MessageContainer = new GraphMessageContainer {\n                Message = new GraphMessage {\n                    From = new GraphEmailAddress { Email = new GraphEmail { Address = \"from@example.com\" } }\n                }\n            }\n        };\n        var field = typeof(Graph).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(graph, new HttpClient(handler));\n\n        var draft = new GraphMessage { Id = \"id\" };\n        var ex = await Assert.ThrowsAsync<GraphApiException>(() => graph.SendDraftMessage(draft));\n        Assert.Contains(nameof(Graph.SendDraftMessage), ex.StackTrace);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphInboxRulesTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Unit tests for <see cref=\"GraphInboxRuleBuilder\"/> and related helpers.\n/// </summary>\npublic class GraphInboxRulesTests {\n    [Fact]\n    public void JoinUriQuery_BuildsRulesUri() {\n        string uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            \"/users/user@example.com/mailFolders/inbox/messageRules\");\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/users/user@example.com/mailFolders/inbox/messageRules\", uri);\n    }\n\n    [Fact]\n    public void JoinUriQuery_BuildsRuleIdUri() {\n        string uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            \"/users/user@example.com/mailFolders/inbox/messageRules/1\");\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/users/user@example.com/mailFolders/inbox/messageRules/1\", uri);\n    }\n\n    [Fact]\n    public void JoinUriQuery_BuildsFilteredUri() {\n        string uri = MicrosoftGraphUtils.JoinUriQuery(\n            GraphEndpoint.V1,\n            \"/users/user@example.com/mailFolders/inbox/messageRules\",\n            new Dictionary<string, object> { [\"$filter\"] = \"displayName eq 'A'\" });\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/users/user@example.com/mailFolders/inbox/messageRules?%24filter=displayName%20eq%20%27A%27\", uri);\n    }\n\n    [Fact]\n    public void Builder_CreatesRule() {\n        var rule = new GraphInboxRuleBuilder()\n            .DisplayName(\"Test\")\n            .Sequence(1)\n            .SenderContains(\"a@example.com\")\n            .Build();\n        Assert.Equal(\"Test\", rule.DisplayName);\n        Assert.Equal(1, rule.Sequence);\n        Assert.NotNull(rule.Conditions);\n        Assert.Contains(\"a@example.com\", rule.Conditions!.SenderContains!);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphMailboxBrowserTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphMailboxBrowserTests {\n    [Theory]\n    [InlineData(null, \"inbox\")]\n    [InlineData(\"\", \"inbox\")]\n    [InlineData(\"INBOX\", \"inbox\")]\n    [InlineData(\"Sent Items\", \"sentitems\")]\n    [InlineData(\"Drafts\", \"drafts\")]\n    [InlineData(\"Archive\", \"archive\")]\n    [InlineData(\"Junk\", \"junkemail\")]\n    [InlineData(\"Spam\", \"junkemail\")]\n    [InlineData(\"Trash\", \"deleteditems\")]\n    [InlineData(\"my-folder-id\", \"my-folder-id\")]\n    public void ResolveFolderSelector_MapsKnownAliases(string? input, string expected) {\n        var actual = GraphMailboxBrowser.ResolveFolderSelector(input);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListFoldersAsync_BuildsHierarchicalNames() {\n        var topLevelJson = \"{\" +\n                           \"\\\"value\\\":[\" +\n                           \"{\\\"id\\\":\\\"inbox-id\\\",\\\"displayName\\\":\\\"Inbox\\\",\\\"childFolderCount\\\":1,\\\"wellKnownName\\\":\\\"inbox\\\",\\\"totalItemCount\\\":10,\\\"unreadItemCount\\\":2},\" +\n                           \"{\\\"id\\\":\\\"archive-id\\\",\\\"displayName\\\":\\\"Archive\\\",\\\"childFolderCount\\\":0,\\\"wellKnownName\\\":\\\"archive\\\"}\" +\n                           \"]\" +\n                           \"}\";\n        var childJson = \"{\" +\n                        \"\\\"value\\\":[\" +\n                        \"{\\\"id\\\":\\\"projects-id\\\",\\\"displayName\\\":\\\"Projects\\\",\\\"parentFolderId\\\":\\\"inbox-id\\\",\\\"childFolderCount\\\":0}\" +\n                        \"]\" +\n                        \"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(topLevelJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(childJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var folders = await browser.ListFoldersAsync();\n\n        Assert.Equal(3, folders.Count);\n        Assert.Equal(\"Archive\", folders[0].Name);\n        Assert.Equal(\"Inbox\", folders[1].Name);\n        Assert.Equal(\"Inbox/Projects\", folders[2].Name);\n        Assert.Equal(\"inbox\", folders[1].WellKnownName);\n        Assert.Equal(10, folders[1].TotalItemCount);\n        Assert.Equal(2, folders[1].UnreadItemCount);\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders?$top=200\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/mailFolders/inbox-id/childFolders?$top=200\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListMessagesAsync_ReturnsTotalCount_AndMappedSummaries() {\n        var folderJson = \"{\\\"id\\\":\\\"inbox\\\",\\\"totalItemCount\\\":12}\";\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"subject\\\":\\\"s\\\",\\\"receivedDateTime\\\":\\\"2026-02-15T00:00:00Z\\\",\\\"internetMessageId\\\":\\\"<msg@example.test>\\\",\\\"hasAttachments\\\":true,\\\"isRead\\\":false,\\\"conversationId\\\":\\\"conv-1\\\",\\\"from\\\":{\\\"emailAddress\\\":{\\\"address\\\":\\\"a@example.test\\\"}},\\\"toRecipients\\\":[{\\\"emailAddress\\\":{\\\"address\\\":\\\"b@example.test\\\"}}],\\\"flag\\\":{\\\"flagStatus\\\":\\\"flagged\\\"}}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.ListMessagesAsync(\"INBOX\", limit: 50, offset: 10);\n\n        Assert.Equal(\"inbox\", result.FolderSelector);\n        Assert.Equal(12, result.TotalCount);\n        Assert.Single(result.Messages);\n        Assert.Equal(\"m1\", result.Messages[0].NativeId);\n        Assert.Equal(\"msg@example.test\", result.Messages[0].MessageId);\n        Assert.True(result.Messages[0].HasAttachments);\n        Assert.False(result.Messages[0].Seen);\n        Assert.True(result.Messages[0].Flagged);\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/inbox?$select=totalItemCount\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/mailFolders/inbox/messages?\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ListConversationMessagesPageAsync_ReturnsPagedSortedSummaries() {\n        var listJson = \"{\\\"value\\\":[\" +\n                       \"{\\\"id\\\":\\\"m1\\\",\\\"subject\\\":\\\"first\\\",\\\"receivedDateTime\\\":\\\"2026-02-14T00:00:00Z\\\",\\\"conversationId\\\":\\\"conv-1\\\"},\" +\n                       \"{\\\"id\\\":\\\"m2\\\",\\\"subject\\\":\\\"second\\\",\\\"receivedDateTime\\\":\\\"2026-02-15T00:00:00Z\\\",\\\"conversationId\\\":\\\"conv-1\\\"}\" +\n                       \"]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var page = await browser.ListConversationMessagesPageAsync(\"conv-1\", limit: 1, offset: 1);\n\n        Assert.Equal(\"conv-1\", page.ConversationId);\n        Assert.Equal(2, page.TotalCount);\n        Assert.Single(page.Messages);\n        Assert.Equal(\"m1\", page.Messages[0].NativeId);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/me/messages?\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"conversationId\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ImportMessageAsync_CreatesMessageInResolvedFolder_AndReturnsResult() {\n        var createdJson = \"{\\\"id\\\":\\\"created-id\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(createdJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.test\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.test\"));\n        message.Subject = \"Imported\";\n        message.MessageId = \"<imported@example.test>\";\n        message.Headers.Replace(\"X-Idempotency-Key\", \"idem-123\");\n        message.Body = new TextPart(\"plain\") { Text = \"hello\" };\n\n        var result = await browser.ImportMessageAsync(\n            message,\n            folder: \"Sent Items\",\n            idempotencyHeaderName: \"X-Idempotency-Key\");\n\n        Assert.Equal(\"sentitems\", result.FolderSelector);\n        Assert.Equal(\"created-id\", result.NativeId);\n        Assert.Equal(\"imported@example.test\", result.MessageId);\n\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/me/mailFolders/sentitems/messages\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"subject\\\":\\\"Imported\\\"\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"name\\\":\\\"X-Idempotency-Key\\\"\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"value\\\":\\\"idem-123\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ImportMessageAsync_UploadsLargeAttachmentsThroughUploadSession() {\n        var createJson = \"{\\\"id\\\":\\\"created-id\\\"}\";\n        var uploadSessionJson = \"{\\\"uploadUrl\\\":\\\"https://upload.test/session\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(createJson) },\n            new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(uploadSessionJson) },\n            new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(string.Empty) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.test\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.test\"));\n        message.Subject = \"With attachment\";\n        message.MessageId = \"<upload@example.test>\";\n        var builder = new BodyBuilder { TextBody = \"hello\" };\n        var attachmentBytes = Encoding.UTF8.GetBytes(\"hello world\");\n        builder.Attachments.Add(\"notes.txt\", attachmentBytes);\n        message.Body = builder.ToMessageBody();\n\n        var result = await browser.ImportMessageAsync(\n            message,\n            folder: \"Sent Items\",\n            maxInlineAttachmentBytes: 0);\n\n        Assert.Equal(\"sentitems\", result.FolderSelector);\n        Assert.Equal(\"created-id\", result.NativeId);\n        Assert.Equal(\"upload@example.test\", result.MessageId);\n\n        Assert.Equal(3, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/sentitems/messages\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages/created-id/attachments/createUploadSession\", handler.Requests[1].RequestUri!.ToString());\n        Assert.Equal(\"https://upload.test/session\", handler.Requests[2].RequestUri!.ToString());\n        var uploadBody = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"name\\\":\\\"notes.txt\\\"\", uploadBody, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"size\\\":11\", uploadBody, StringComparison.Ordinal);\n        var range = handler.Requests[2].Content!.Headers.ContentRange;\n        Assert.NotNull(range);\n        Assert.Equal(0L, range!.From);\n        Assert.Equal(10L, range.To);\n        Assert.Equal(11L, range.Length);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SendMessageAsync_CreatesDraftAndSendsIt() {\n        var createJson = \"{\\\"id\\\":\\\"draft-id\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(createJson) },\n            new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent(string.Empty) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.test\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.test\"));\n        message.Subject = \"Send me\";\n        message.MessageId = \"<send@example.test>\";\n        message.Headers.Replace(\"X-Idempotency-Key\", \"idem-send\");\n        message.Body = new TextPart(\"plain\") { Text = \"hello\" };\n\n        var result = await browser.SendMessageAsync(\n            message,\n            idempotencyHeaderName: \"X-Idempotency-Key\");\n\n        Assert.Equal(\"draft-id\", result.DraftId);\n        Assert.Equal(\"send@example.test\", result.MessageId);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/drafts/messages\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages/draft-id/send\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task FindMessageByInternetMessageIdAsync_UsesBracketedFilterAndReturnsMatch() {\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"internetMessageId\\\":\\\"<msg-123@example.test>\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.FindMessageByInternetMessageIdAsync(\"msg-123@example.test\", folder: \"Sent Items\");\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"sentitems\", result.FolderSelector);\n        Assert.Equal(\"m1\", result.NativeId);\n        Assert.Equal(\"msg-123@example.test\", result.MessageId);\n        Assert.Single(handler.Requests);\n        var requestUri = handler.Requests[0].RequestUri!;\n        Assert.Contains(\"/me/mailFolders/sentitems/messages?\", requestUri.ToString());\n        var decodedQuery = Uri.UnescapeDataString(requestUri.Query);\n        Assert.Contains(\"internetMessageId eq '<msg-123@example.test>'\", decodedQuery, StringComparison.OrdinalIgnoreCase);\n        Assert.Contains(\"internetMessageId eq 'msg-123@example.test'\", decodedQuery, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task FindMessageByInternetMessageIdAsync_ReturnsNoMatch_WhenResponseContainsDifferentMessageId() {\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"internetMessageId\\\":\\\"<other@example.test>\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.FindMessageByInternetMessageIdAsync(\"msg-123@example.test\", folder: \"Sent Items\");\n\n        Assert.False(result.IsMatch);\n        Assert.Equal(\"sentitems\", result.FolderSelector);\n        Assert.Null(result.NativeId);\n        Assert.Null(result.MessageId);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SearchMessagesAsync_UsesGraphSearchWhenTextIsProvided() {\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\",\\\"subject\\\":\\\"hello\\\"}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.SearchMessagesAsync(new GraphMailboxBrowser.GraphMailboxSearchRequest {\n            Folder = \"Archive\",\n            Query = \"urgent report\",\n            UnseenOnly = true,\n            HasAttachment = true\n        }, max: 25);\n\n        Assert.Equal(\"archive\", result.FolderSelector);\n        Assert.Single(result.Messages);\n        Assert.Single(handler.Requests);\n        var uri = handler.Requests[0].RequestUri!.ToString();\n        Assert.Contains(\"/me/mailFolders/archive/messages?\", uri);\n        Assert.Contains(\"$search=\", uri);\n        Assert.DoesNotContain(\"$filter=\", uri);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeltaMessagesAsync_MapsUpsertsDeletesAndCursor() {\n        var json = \"{\\\"@odata.deltaLink\\\":\\\"https://graph.microsoft.com/v1.0/delta\\\",\\\"value\\\":[{\\\"id\\\":\\\"m-up\\\",\\\"subject\\\":\\\"hello\\\"},{\\\"id\\\":\\\"m-del\\\",\\\"@removed\\\":{}}]}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.DeltaMessagesAsync(\"INBOX\", cursor: null, max: 100);\n\n        Assert.Equal(\"inbox\", result.FolderSelector);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/delta\", result.Cursor);\n        Assert.Single(result.Upserts);\n        Assert.Single(result.DeletedNativeIds);\n        Assert.Equal(\"m-up\", result.Upserts[0].NativeId);\n        Assert.Equal(\"m-del\", result.DeletedNativeIds[0]);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetMessageContentAsync_ReturnsMimeAndFlags() {\n        var metaJson = \"{\\\"id\\\":\\\"m1\\\",\\\"isRead\\\":true,\\\"flag\\\":{\\\"flagStatus\\\":\\\"flagged\\\"},\\\"conversationId\\\":\\\"conv-1\\\"}\";\n        var mime = \"From: a@example.test\\r\\nTo: b@example.test\\r\\nSubject: Sample\\r\\nMessage-Id: <m1@example.test>\\r\\n\\r\\nhello\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(metaJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Encoding.UTF8.GetBytes(mime)) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.GetMessageContentAsync(\"m1\");\n\n        Assert.True(result.Seen);\n        Assert.True(result.Flagged);\n        Assert.Equal(\"conv-1\", result.NativeThreadId);\n        Assert.Equal(\"Sample\", result.Message.Subject);\n        Assert.Equal(2, handler.Requests.Count);\n        var metaUri = handler.Requests[0].RequestUri!.ToString();\n        Assert.Contains(\"/me/messages/m1?\", metaUri);\n        Assert.Contains(\"$select=\", metaUri);\n        Assert.Contains(\"isRead\", metaUri);\n        Assert.Contains(\"flag\", metaUri);\n        Assert.Contains(\"conversationId\", metaUri);\n        Assert.Contains(\"/me/messages/m1/$value\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task GetThreadingMetadataAsync_ParsesHeaderFieldsFromMime() {\n        var mime = \"From: a@example.test\\r\\n\" +\n                   \"To: b@example.test\\r\\n\" +\n                   \"Reply-To: replies@example.test\\r\\n\" +\n                   \"Cc: c@example.test\\r\\n\" +\n                   \"Message-Id: <thread-child@example.test>\\r\\n\" +\n                   \"In-Reply-To: <thread-parent@example.test>\\r\\n\" +\n                   \"References: <thread-root@example.test> <thread-parent@example.test> <thread-root@example.test>\\r\\n\" +\n                   \"\\r\\nhello\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Encoding.UTF8.GetBytes(mime)) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.GetThreadingMetadataAsync(\"m1\");\n\n        Assert.Equal(\"thread-child@example.test\", result.MessageId);\n        Assert.Equal(\"thread-parent@example.test\", result.InReplyTo);\n        Assert.Equal(\"replies@example.test\", result.ReplyTo);\n        Assert.Equal(\"c@example.test\", result.Cc);\n        Assert.Equal(2, result.References.Count);\n        Assert.Equal(\"thread-root@example.test\", result.References[0]);\n        Assert.Equal(\"thread-parent@example.test\", result.References[1]);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/me/messages/m1/$value\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Theory]\n    [InlineData(\"Archive\", \"me/mailFolders('archive')/messages\")]\n    [InlineData(\"AAMkADk0Y2Qx\", \"me/mailFolders('AAMkADk0Y2Qx')/messages\")]\n    [InlineData(\"A'B\", \"me/mailFolders('A''B')/messages\")]\n    public void BuildMessageSubscriptionResource_MapsAndEscapesFolderSelector(string folder, string expected) {\n        var actual = GraphMailboxBrowser.BuildMessageSubscriptionResource(folder);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task CreateMessageSubscriptionAsync_PostsSubscriptionPayloadAndMapsResult() {\n        var json = \"{\\\"id\\\":\\\"sub-1\\\",\\\"resource\\\":\\\"me/mailFolders('archive')/messages\\\",\\\"clientState\\\":\\\"state-1\\\",\\\"expirationDateTime\\\":\\\"2026-02-16T01:00:00Z\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent(json) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n        var expiration = new DateTimeOffset(new DateTime(2026, 2, 16, 0, 0, 0, DateTimeKind.Utc));\n\n        var result = await browser.CreateMessageSubscriptionAsync(\n            notificationUrl: \"https://example.test/webhook\",\n            folder: \"Archive\",\n            expirationDateTime: expiration,\n            changeType: \"created,updated,deleted\",\n            clientState: \"state-1\");\n\n        Assert.Equal(\"sub-1\", result.SubscriptionId);\n        Assert.Equal(\"me/mailFolders('archive')/messages\", result.Resource);\n        Assert.Equal(\"state-1\", result.ClientState);\n        Assert.Equal(new DateTimeOffset(new DateTime(2026, 2, 16, 1, 0, 0, DateTimeKind.Utc)), result.ExpirationDateTime);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/subscriptions\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[0].Content!.ReadAsStringAsync();\n        using var doc = System.Text.Json.JsonDocument.Parse(body);\n        Assert.Equal(\"https://example.test/webhook\", doc.RootElement.GetProperty(\"notificationUrl\").GetString());\n        Assert.Equal(\"me/mailFolders('archive')/messages\", doc.RootElement.GetProperty(\"resource\").GetString());\n        Assert.Equal(\"created,updated,deleted\", doc.RootElement.GetProperty(\"changeType\").GetString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task RenewSubscriptionAsync_PatchesSubscriptionAndMapsResult() {\n        var json = \"{\\\"id\\\":\\\"sub-renew\\\",\\\"resource\\\":\\\"me/mailFolders('inbox')/messages\\\",\\\"clientState\\\":\\\"state-2\\\",\\\"expirationDateTime\\\":\\\"2026-02-16T01:00:00Z\\\"}\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n        var expiration = new DateTimeOffset(new DateTime(2026, 2, 16, 0, 0, 0, DateTimeKind.Utc));\n\n        var result = await browser.RenewSubscriptionAsync(\"sub-renew\", expiration);\n\n        Assert.Equal(\"sub-renew\", result.SubscriptionId);\n        Assert.Equal(new DateTimeOffset(new DateTime(2026, 2, 16, 1, 0, 0, DateTimeKind.Utc)), result.ExpirationDateTime);\n        Assert.Single(handler.Requests);\n        Assert.Equal(new HttpMethod(\"PATCH\"), handler.Requests[0].Method);\n        Assert.Contains(\"/subscriptions/sub-renew\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task RenewSubscriptionSafeAsync_ReturnsMissing_WhenSubscriptionIsGone() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent(\"{\\\"error\\\":\\\"missing\\\"}\") });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n        var expiration = new DateTimeOffset(new DateTime(2026, 2, 16, 0, 0, 0, DateTimeKind.Utc));\n\n        var result = await browser.RenewSubscriptionSafeAsync(\"sub-renew-missing\", expiration, treatMissingAsStale: true);\n\n        Assert.False(result.Renewed);\n        Assert.True(result.Missing);\n        Assert.Null(result.Subscription);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteSubscriptionAsync_ReturnsAlreadyDeleted_WhenMissingAndConfigured() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent(\"{\\\"error\\\":\\\"missing\\\"}\") });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var result = await browser.DeleteSubscriptionAsync(\"sub-missing\", treatMissingAsSuccess: true);\n\n        Assert.True(result.Deleted);\n        Assert.True(result.AlreadyDeleted);\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Delete, handler.Requests[0].Method);\n        Assert.Contains(\"/subscriptions/sub-missing\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetMessageSeenAsync_PatchesReadState() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.SetMessageSeenAsync(\"m1\", seen: true);\n\n        Assert.Single(handler.Requests);\n        var request = handler.Requests[0];\n        Assert.Equal(new HttpMethod(\"PATCH\"), request.Method);\n        Assert.Contains(\"/me/messages/m1\", request.RequestUri!.ToString());\n        var body = await request.Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"isRead\\\":true\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetMessageFlaggedAsync_PatchesFlagState() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.SetMessageFlaggedAsync(\"m1\", flagged: true);\n\n        Assert.Single(handler.Requests);\n        var request = handler.Requests[0];\n        Assert.Equal(new HttpMethod(\"PATCH\"), request.Method);\n        Assert.Contains(\"/me/messages/m1\", request.RequestUri!.ToString());\n        var body = await request.Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"flagStatus\\\":\\\"flagged\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessageAsync_ResolvesFolderAliasAndUsesDestinationId() {\n        var folderJson = \"{\\\"id\\\":\\\"archive-id\\\"}\";\n        var movedJson = \"{\\\"id\\\":\\\"m1\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(movedJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.MoveMessageAsync(\"m1\", \"Archive\");\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/archive?$select=id\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages/m1/move\", handler.Requests[1].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"\\\"destinationId\\\":\\\"archive-id\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ArchiveMessageAsync_UsesArchiveDestinationAlias() {\n        var folderJson = \"{\\\"id\\\":\\\"archive-id\\\"}\";\n        var movedJson = \"{\\\"id\\\":\\\"m1\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(movedJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.ArchiveMessageAsync(\"m1\");\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/archive?$select=id\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages/m1/move\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task TrashMessageAsync_UsesTrashDestinationAlias() {\n        var folderJson = \"{\\\"id\\\":\\\"trash-id\\\"}\";\n        var movedJson = \"{\\\"id\\\":\\\"m1\\\"}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(movedJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.TrashMessageAsync(\"m1\");\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/deleteditems?$select=id\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages/m1/move\", handler.Requests[1].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteMessageAsync_UsesDeleteEndpoint() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new StringContent(string.Empty) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        await browser.DeleteMessageAsync(\"m1\");\n\n        Assert.Single(handler.Requests);\n        Assert.Equal(HttpMethod.Delete, handler.Requests[0].Method);\n        Assert.Contains(\"/me/messages/m1\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessagesAsync_ResolvesFolderAliasAndBatchesMoveRequests() {\n        var folderJson = \"{\\\"id\\\":\\\"archive-id\\\"}\";\n        var batchJson = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":201},{\\\"id\\\":\\\"2\\\",\\\"status\\\":201}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(batchJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.MoveMessagesAsync(new[] { \"m1\", \"m2\" }, \"Archive\");\n\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.True(r.Ok, r.Error));\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/archive?$select=id\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/$batch\", handler.Requests[1].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"me/messages/m1/move\", body, StringComparison.Ordinal);\n        Assert.Contains(\"me/messages/m2/move\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"destinationId\\\":\\\"archive-id\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveMessagesAsync_WithNoMessageIds_SkipsFolderResolution() {\n        var handler = new RecordingHandler();\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.MoveMessagesAsync(Array.Empty<string>(), \"Archive\");\n\n        Assert.Empty(results);\n        Assert.Empty(handler.Requests);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task ArchiveConversationsAsync_UsesArchiveDestinationAlias() {\n        var folderJson = \"{\\\"id\\\":\\\"archive-id\\\"}\";\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\"}]}\";\n        var batchJson = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":201}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(folderJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(batchJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.ArchiveConversationsAsync(new[] { \"conv-1\" });\n\n        Assert.Single(results);\n        Assert.All(results, r => Assert.True(r.Ok, r.Error));\n        Assert.Equal(3, handler.Requests.Count);\n        Assert.Contains(\"/me/mailFolders/archive?$select=id\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/me/messages?\", handler.Requests[1].RequestUri!.ToString());\n        Assert.Contains(\"conversationId\", handler.Requests[1].RequestUri!.ToString());\n        Assert.Contains(\"/$batch\", handler.Requests[2].RequestUri!.ToString());\n        var body = await handler.Requests[2].Content!.ReadAsStringAsync();\n        Assert.Contains(\"me/messages/m1/move\", body, StringComparison.Ordinal);\n        Assert.Contains(\"\\\"destinationId\\\":\\\"archive-id\\\"\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task MoveConversationsAsync_WithNoConversationIds_SkipsFolderResolution() {\n        var handler = new RecordingHandler();\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.MoveConversationsAsync(Array.Empty<string>(), \"Archive\");\n\n        Assert.Empty(results);\n        Assert.Empty(handler.Requests);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task DeleteConversationsAsync_ExpandsAndDeletesConversationMessages() {\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\"},{\\\"id\\\":\\\"m2\\\"}]}\";\n        var batchJson = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":204},{\\\"id\\\":\\\"2\\\",\\\"status\\\":204}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(batchJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.DeleteConversationsAsync(new[] { \"conv-1\" });\n\n        Assert.Single(results);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Equal(\"conv-1\", results[0].Id);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/messages?\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"conversationId\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"/$batch\", handler.Requests[1].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"me/messages/m1\", body, StringComparison.Ordinal);\n        Assert.Contains(\"me/messages/m2\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetConversationsSeenAsync_ExpandsAndBatchesReadStateChanges() {\n        var listJson = \"{\\\"value\\\":[{\\\"id\\\":\\\"m1\\\"},{\\\"id\\\":\\\"m2\\\"}]}\";\n        var batchJson = \"{\\\"responses\\\":[{\\\"id\\\":\\\"1\\\",\\\"status\\\":200},{\\\"id\\\":\\\"2\\\",\\\"status\\\":200}]}\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(listJson) },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(batchJson) });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.SetConversationsSeenAsync(new[] { \"conv-1\" }, seen: true);\n\n        Assert.Single(results);\n        Assert.True(results[0].Ok, results[0].Error);\n        Assert.Equal(\"conv-1\", results[0].Id);\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(\"/me/messages?\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Contains(\"conversationId\", handler.Requests[0].RequestUri!.ToString());\n        var body = await handler.Requests[1].Content!.ReadAsStringAsync();\n        Assert.Contains(\"me/messages/m1\", body, StringComparison.Ordinal);\n        Assert.Contains(\"isRead\", body, StringComparison.Ordinal);\n        Assert.Contains(\"true\", body, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async System.Threading.Tasks.Task SetConversationsFlaggedAsync_MapsConversationFailures() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(\"boom\") });\n        var client = CreateClient(handler);\n        var browser = new GraphMailboxBrowser(client);\n\n        var results = await browser.SetConversationsFlaggedAsync(new[] { \"conv-1\" }, flagged: true);\n\n        Assert.Single(results);\n        Assert.Equal(\"conv-1\", results[0].Id);\n        Assert.False(results[0].Ok);\n        Assert.Contains(\"conversation list failed\", results[0].Error ?? string.Empty, StringComparison.OrdinalIgnoreCase);\n        Assert.Single(handler.Requests);\n        Assert.Contains(\"/me/messages?\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    private static GraphApiClient CreateClient(HttpMessageHandler handler) {\n        var api = new GraphApiClient(new OAuthCredential { UserName = \"u\", AccessToken = \"t\", ExpiresOn = DateTimeOffset.MaxValue });\n        var field = typeof(GraphApiClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(api, new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") });\n        return api;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphMailboxPermissionBuilderTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphMailboxPermissionBuilderTests {\n    [Fact]\n    public void Builder_CreatesPermission() {\n        var perm = new GraphMailboxPermissionBuilder()\n            .UserPrincipalName(\"owner@example.com\")\n            .GrantedToUser(\"grantee@example.com\")\n            .Roles(GraphMailboxRole.Owner, GraphMailboxRole.Read)\n            .Build();\n        Assert.Equal(\"owner@example.com\", perm.UserPrincipalName);\n        Assert.Equal(\"grantee@example.com\", perm.GrantedTo!.User);\n        Assert.Contains(GraphMailboxRole.Owner, perm.Roles!);\n        Assert.Contains(GraphMailboxRole.Read, perm.Roles!);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphMailboxPermissionTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphMailboxPermissionTests {\n    [Fact]\n    public void Constructor_ParsesRoles_IgnoringCase() {\n        var raw = new Dictionary<string, object> {\n            [\"roles\"] = new object[] { \"owner\", \"read\", \"write\" }\n        };\n        var perm = new Mailozaurr.GraphMailboxPermission(raw);\n        Assert.NotNull(perm.Roles);\n        Assert.Contains(GraphMailboxRole.Owner, perm.Roles!);\n        Assert.Contains(GraphMailboxRole.Read, perm.Roles!);\n        Assert.Contains(GraphMailboxRole.Write, perm.Roles!);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphMessageListenerTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class GraphMessageListenerTests {\n    private sealed class QueueHandler : HttpMessageHandler {\n        private readonly Queue<HttpResponseMessage> responses;\n\n        public QueueHandler(IEnumerable<HttpResponseMessage> responses) {\n            this.responses = new Queue<HttpResponseMessage>(responses);\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            if (responses.Count > 0) {\n                return Task.FromResult(responses.Dequeue());\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"value\\\":[]}\")\n            });\n        }\n    }\n\n    private sealed class HangingHandler : HttpMessageHandler {\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);\n            throw new InvalidOperationException(\"Unreachable\");\n        }\n    }\n\n    private static FieldInfo GetHandlerField() =>\n        typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance) ??\n        typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance) ??\n        throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    [Fact]\n    public async Task Listener_StartStopMultipleTimes_DoesNotLeakResources() {\n        var responses = new[] {\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"value\\\":[]}\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"value\\\":[]}\") }\n        };\n        var handler = new QueueHandler(responses);\n        var clientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)clientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            var listener = new GraphMessageListener(cred, \"user\", TimeSpan.FromSeconds(1));\n            var cancelField = typeof(GraphMessageListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n            var pollField = typeof(GraphMessageListener).GetField(\"_pollTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n\n            await listener.StartAsync();\n            var first = cancelField.GetValue(listener);\n            var firstPoll = pollField.GetValue(listener);\n            await Task.Delay(20);\n            listener.Dispose();\n            Assert.Null(cancelField.GetValue(listener));\n            Assert.Null(pollField.GetValue(listener));\n\n            await listener.StartAsync();\n            var second = cancelField.GetValue(listener);\n            var secondPoll = pollField.GetValue(listener);\n            await Task.Delay(20);\n            listener.Dispose();\n            Assert.Null(cancelField.GetValue(listener));\n            Assert.Null(pollField.GetValue(listener));\n\n            Assert.NotNull(first);\n            Assert.NotNull(second);\n            Assert.NotSame(first, second);\n            Assert.NotNull(firstPoll);\n            Assert.NotNull(secondPoll);\n            Assert.NotSame(firstPoll, secondPoll);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task StartAsync_CancellationDuringInitialFetch_CleansUpState() {\n        var handler = new HangingHandler();\n        var clientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)clientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            var listener = new GraphMessageListener(cred, \"user\", TimeSpan.FromSeconds(1));\n            var cancelField = typeof(GraphMessageListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n            var pollField = typeof(GraphMessageListener).GetField(\"_pollTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n            using var cts = new CancellationTokenSource(100);\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => listener.StartAsync(cts.Token));\n\n            Assert.Null(cancelField.GetValue(listener));\n            Assert.Null(pollField.GetValue(listener));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphSearchMailboxesTests.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class GraphSearchMailboxesTests {\n    private static FieldInfo GetHandlerField()\n        => typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    [Fact]\n    public async Task SearchMailboxesAsync_SingleUseMailboxEnumerable_IsEnumeratedOnce() {\n        var handler = new SearchMailboxesHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            var mailboxes = new SingleUseEnumerable<string>(\"first@example.com\");\n\n            var results = await MicrosoftGraphUtils.SearchMailboxesAsync(credential, mailboxes, \"subject:test\");\n\n            var message = Assert.Single(results);\n            Assert.Equal(\"first@example.com\", message.UserPrincipalName);\n            Assert.Equal(\"message-1\", message.Id);\n            Assert.Equal(1, mailboxes.EnumerationCount);\n            Assert.Equal(1, handler.SearchRequestCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task SearchMailboxesAsync_EmptyMailboxList_SkipsGraphRequest() {\n        var handler = new SearchMailboxesHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n\n            var results = await MicrosoftGraphUtils.SearchMailboxesAsync(credential, Array.Empty<string>(), \"subject:test\");\n\n            Assert.Empty(results);\n            Assert.Equal(0, handler.AuthRequestCount);\n            Assert.Equal(0, handler.SearchRequestCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    private sealed class SearchMailboxesHandler : HttpMessageHandler {\n        public int AuthRequestCount { get; private set; }\n        public int SearchRequestCount { get; private set; }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.IndexOf(\"oauth2\", StringComparison.Ordinal) >= 0) {\n                AuthRequestCount++;\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/search/query\", StringComparison.Ordinal)) {\n                SearchRequestCount++;\n                const string json = \"{\\\"value\\\":[{\\\"hitsContainers\\\":[{\\\"hits\\\":[{\\\"summary\\\":\\\"match\\\",\\\"resource\\\":{\\\"id\\\":\\\"message-1\\\",\\\"subject\\\":\\\"Test subject\\\"}}]}]}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private sealed class SingleUseEnumerable<T> : IEnumerable<T> {\n        private readonly IReadOnlyList<T> _items;\n\n        public SingleUseEnumerable(params T[] items) {\n            _items = items;\n        }\n\n        public int EnumerationCount { get; private set; }\n\n        public IEnumerator<T> GetEnumerator() {\n            EnumerationCount++;\n            if (EnumerationCount > 1) {\n                throw new InvalidOperationException(\"Sequence was enumerated more than once.\");\n            }\n\n            return _items.GetEnumerator();\n        }\n\n        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphStopwatchTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphStopwatchTests\n{\n    private static void SetHttpClient(Graph graph, HttpMessageHandler handler)\n    {\n        var field = typeof(Graph).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        field.SetValue(graph, new HttpClient(handler));\n    }\n\n    private static HttpResponseMessage CreateJsonResponse(string content)\n    {\n        return new HttpResponseMessage(HttpStatusCode.OK)\n        {\n            Content = new StringContent(content)\n        };\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_SuccessiveCallsHaveIndependentDurations()\n    {\n        var interCallDelay = TimeSpan.FromSeconds(1);\n        var maxExpectedDuration = TimeSpan.FromMilliseconds(800);\n        var responsePayload = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n        var handler = new RecordingHandler(\n            CreateJsonResponse(responsePayload),\n            CreateJsonResponse(responsePayload));\n\n        using var graph = new Graph();\n        SetHttpClient(graph, handler);\n        graph.Authenticate(new NetworkCredential(\"client@tenant\", \"secret\"));\n\n        await Task.Delay(interCallDelay);\n        var first = await graph.ConnectO365GraphAsync();\n        Assert.True(first.Status);\n        Assert.True(\n            first.TimeToExecute < maxExpectedDuration,\n            $\"Expected first elapsed time to be well below the idle delay of {interCallDelay.TotalMilliseconds}ms but was {first.TimeToExecute.TotalMilliseconds}ms\");\n\n        await Task.Delay(interCallDelay);\n        var second = await graph.ConnectO365GraphAsync();\n        Assert.True(second.Status);\n        Assert.True(\n            second.TimeToExecute < maxExpectedDuration,\n            $\"Expected second elapsed time to be well below the idle delay of {interCallDelay.TotalMilliseconds}ms but was {second.TimeToExecute.TotalMilliseconds}ms\");\n    }\n\n    [Fact]\n    public async Task SendMessageAsync_SuccessiveCallsHaveIndependentDurations()\n    {\n        var interCallDelay = TimeSpan.FromSeconds(1);\n        var maxExpectedDuration = TimeSpan.FromMilliseconds(800);\n        var handler = new RecordingHandler(\n            CreateJsonResponse(\"{}\"),\n            CreateJsonResponse(\"{}\"));\n\n        using var graph = new Graph();\n        SetHttpClient(graph, handler);\n        graph.From = \"sender@example.com\";\n        graph.To = new object[] { \"recipient@example.com\" };\n        graph.Subject = \"Test\";\n        graph.HTML = \"<p>Hello</p>\";\n        graph.ContentType = \"HTML\";\n        graph.AccessToken = \"token\";\n        graph.TokenType = \"Bearer\";\n\n        await Task.Delay(interCallDelay);\n        var first = await graph.SendMessageAsync();\n        Assert.True(first.Status);\n        Assert.True(\n            first.TimeToExecute < maxExpectedDuration,\n            $\"Expected first elapsed time to be well below the idle delay of {interCallDelay.TotalMilliseconds}ms but was {first.TimeToExecute.TotalMilliseconds}ms\");\n\n        await Task.Delay(interCallDelay);\n        var second = await graph.SendMessageAsync();\n        Assert.True(second.Status);\n        Assert.True(\n            second.TimeToExecute < maxExpectedDuration,\n            $\"Expected second elapsed time to be well below the idle delay of {interCallDelay.TotalMilliseconds}ms but was {second.TimeToExecute.TotalMilliseconds}ms\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphTestCollection.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Collection used for tests that mutate shared static state.\n/// Parallelization is disabled to avoid interference.\n/// </summary>\n[CollectionDefinition(\"GraphCollection\", DisableParallelization = true)]\npublic class GraphTestCollection { }\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphUploadRangeTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphUploadRangeTests\n{\n    [Fact]\n    public void PrepareByteArrayContentForUpload_ComputesRangesCorrectly()\n    {\n        string tmp = Path.GetTempFileName();\n        File.WriteAllBytes(tmp, Enumerable.Range(0, 25).Select(b => (byte)b).ToArray());\n        using Graph graph = new Graph();\n        MethodInfo? method = typeof(Graph).GetMethod(\n            \"PrepareByteArrayContentForUpload\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var nonNullMethod = method!;\n        List<StreamContent> chunks = (List<StreamContent>)nonNullMethod.Invoke(graph, new object[] { tmp, 10, CancellationToken.None })!;\n        File.Delete(tmp);\n        Assert.Equal(3, chunks.Count);\n        Assert.Equal(\"bytes 0-9/25\", chunks[0].Headers.GetValues(\"Content-Range\").First());\n        Assert.Equal(\"bytes 10-19/25\", chunks[1].Headers.GetValues(\"Content-Range\").First());\n        Assert.Equal(\"bytes 20-24/25\", chunks[2].Headers.GetValues(\"Content-Range\").First());\n    }\n\n    [Fact]\n    public async Task PrepareByteArrayContentForUpload_CreatesIndependentBuffers()\n    {\n        string tmp = Path.GetTempFileName();\n        byte[] allBytes = Enumerable.Range(0, 25).Select(b => (byte)b).ToArray();\n        File.WriteAllBytes(tmp, allBytes);\n        using Graph graph = new Graph();\n        MethodInfo? method = typeof(Graph).GetMethod(\n            \"PrepareByteArrayContentForUpload\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var nonNullMethod = method!;\n        List<StreamContent> chunks = (List<StreamContent>)nonNullMethod.Invoke(\n            graph,\n            new object[] { tmp, 10, CancellationToken.None })!;\n        File.Delete(tmp);\n        byte[] chunk0 = await chunks[0].ReadAsByteArrayAsync();\n        byte[] chunk1 = await chunks[1].ReadAsByteArrayAsync();\n        byte[] chunk2 = await chunks[2].ReadAsByteArrayAsync();\n        Assert.Equal(allBytes.Take(10), chunk0);\n        Assert.Equal(allBytes.Skip(10).Take(10), chunk1);\n        Assert.Equal(allBytes.Skip(20).Take(5), chunk2);\n    }\n\n    [Fact]\n    public void PrepareByteArrayContentForUpload_ClampsChunkSizeToMax()\n    {\n        string tmp = Path.GetTempFileName();\n        var fileSize = Graph.MaxChunkSize + 10;\n        var bytes = new byte[fileSize];\n        for (var i = 0; i < bytes.Length; i++) {\n            bytes[i] = (byte)(i % 256);\n        }\n        File.WriteAllBytes(tmp, bytes);\n        using Graph graph = new Graph();\n        MethodInfo? method = typeof(Graph).GetMethod(\n            \"PrepareByteArrayContentForUpload\",\n            BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var nonNullMethod = method!;\n        List<StreamContent> chunks = (List<StreamContent>)nonNullMethod.Invoke(graph, new object[] { tmp, Graph.MaxChunkSize * 2, CancellationToken.None })!;\n        File.Delete(tmp);\n        Assert.Equal(2, chunks.Count);\n        Assert.Equal($\"bytes 0-{Graph.MaxChunkSize - 1}/{fileSize}\", chunks[0].Headers.GetValues(\"Content-Range\").First());\n        Assert.Equal($\"bytes {Graph.MaxChunkSize}-{fileSize - 1}/{fileSize}\", chunks[1].Headers.GetValues(\"Content-Range\").First());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/GraphUploadSessionParsingTests.cs",
    "content": "using System;\nusing System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class GraphUploadSessionParsingTests\n{\n    [Fact]\n    public void ParseUploadSessionResult_WithoutUploadUrl_Throws()\n    {\n        MethodInfo? method = typeof(Graph).GetMethod(\n            \"ParseUploadSessionResult\",\n            BindingFlags.NonPublic | BindingFlags.Static);\n        Assert.NotNull(method);\n        var ex = Assert.Throws<TargetInvocationException>(() => method!.Invoke(null, new object[] { \"{}\" }));\n        Assert.IsType<InvalidOperationException>(ex.InnerException);\n        Assert.Contains(\"Upload URL not found\", ex.InnerException!.Message, StringComparison.Ordinal);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/HelpersTests.cs",
    "content": "using Mailozaurr;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Tests for miscellaneous helper methods.\n/// </summary>\npublic class HelpersTests {\n    [Fact]\n    public void GetEmailAddress_ReturnsInputString_WhenStringProvided() {\n        var result = Mailozaurr.Helpers.GetEmailAddress(\"test@example.com\");\n        Assert.Equal(\"test@example.com\", result);\n    }\n\n    [Fact]\n    public void GetEmailAddress_ReturnsEmailFromDictionary_WhenEmailKeyPresent() {\n        var dict = new Dictionary<string, object> { { \"Email\", \"dict@example.com\" } };\n        var result = Mailozaurr.Helpers.GetEmailAddress(dict);\n        Assert.Equal(\"dict@example.com\", result);\n    }\n\n    [Fact]\n    public void GetEmailAddress_ReturnsEmpty_WhenEmailKeyMissing() {\n        var dict = new Dictionary<string, object> { { \"Name\", \"John\" } };\n        var result = Mailozaurr.Helpers.GetEmailAddress(dict);\n        Assert.Equal(string.Empty, result);\n    }\n\n    private class CustomObject {\n        public override string ToString() => \"Custom\";\n    }\n\n    [Fact]\n    public void GetEmailAddress_ReturnsObjectToString_WhenNotStringOrDictionary() {\n        var obj = new CustomObject();\n        var result = Mailozaurr.Helpers.GetEmailAddress(obj);\n        Assert.Equal(\"Custom\", result);\n    }\n\n    [Fact]\n    public void ConvertFromOAuth2Credential_ThrowsArgumentNullException_WhenCredentialIsNull() {\n        Assert.Throws<ArgumentNullException>(() => Mailozaurr.Helpers.ConvertFromOAuth2Credential(null!));\n    }\n\n    [Fact]\n    public void ConvertFromPlainText_ReturnsNetworkCredentialWithSecurePassword() {\n        var cred = Mailozaurr.Helpers.ConvertFromPlainText(\"user\", \"secret\");\n\n        Assert.Equal(\"user\", cred.UserName);\n        Assert.Equal(6, cred.SecurePassword.Length);\n        Assert.Equal(\"secret\", cred.Password);\n    }\n\n    [Fact]\n    public void ConvertFromPlainText_ThrowsArgumentNullException_WhenUserNameIsNull() {\n        Assert.Throws<ArgumentNullException>(() => Mailozaurr.Helpers.ConvertFromPlainText(null!, \"secret\"));\n    }\n\n    [Fact]\n    public void ConvertFromPlainText_ThrowsArgumentNullException_WhenPasswordIsNull() {\n        Assert.Throws<ArgumentNullException>(() => Mailozaurr.Helpers.ConvertFromPlainText(\"user\", null!));\n    }\n\n    [Fact]\n    public void ConvertFromPlainText_ThrowsArgumentException_WhenUserNameIsEmpty() {\n        Assert.Throws<ArgumentException>(() => Mailozaurr.Helpers.ConvertFromPlainText(string.Empty, \"secret\"));\n    }\n\n    [Fact]\n    public void ConvertFromPlainText_ThrowsArgumentException_WhenPasswordIsEmpty() {\n        Assert.Throws<ArgumentException>(() => Mailozaurr.Helpers.ConvertFromPlainText(\"user\", string.Empty));\n    }\n\n    [Fact]\n    public void CredentialToApiKey_ReturnsPassword_WhenNetworkCredential() {\n        var cred = new NetworkCredential(\"apikey\", \"theKey\");\n\n        string result = Mailozaurr.Helpers.CredentialToApiKey(cred);\n\n        Assert.Equal(\"theKey\", result);\n    }\n\n    private class DummyCredentials : ICredentials {\n        public NetworkCredential GetCredential(Uri uri, string authType) => new NetworkCredential();\n    }\n\n    [Fact]\n    public void CredentialToApiKey_ThrowsArgumentException_WhenNotNetworkCredential() {\n        ICredentials creds = new DummyCredentials();\n\n        Assert.Throws<ArgumentException>(() => Mailozaurr.Helpers.CredentialToApiKey(creds));\n    }\n\n    [Fact]\n    public void GetEmailAndName_ReturnsTuple_WhenDictionaryProvided() {\n        var input = new Dictionary<string, object> { { \"Email\", \"a@b.com\" }, { \"Name\", \"Alice\" } };\n\n        var (email, name) = Mailozaurr.Helpers.GetEmailAndName(input);\n\n        Assert.Equal(\"a@b.com\", email);\n        Assert.Equal(\"Alice\", name);\n    }\n\n    [Fact]\n    public void GetEmailAndName_ReturnsEmailAndNullName_WhenStringProvided() {\n        var (email, name) = Mailozaurr.Helpers.GetEmailAndName(\"c@d.com\");\n\n        Assert.Equal(\"c@d.com\", email);\n        Assert.Null(name);\n    }\n\n    [Fact]\n    public void UniqueAddresses_RemovesDuplicates_IgnoringCaseAndWhitespace() {\n        var addresses = new object[]\n        {\n            \" Test@example.com \",\n            \"test@example.com\",\n            \"other@example.com\",\n            \"Other@example.com \"\n        };\n        var seen = new HashSet<string>();\n        var result = Mailozaurr.Helpers\n            .UniqueAddresses(addresses!, seen)\n            .Select(Mailozaurr.Helpers.GetEmailAddress)\n            .ToArray();\n\n        Assert.Equal(new[] { \" Test@example.com \", \"other@example.com\" }, result);\n    }\n\n    [Fact]\n    public void UniqueAddresses_SkipsNullOrInvalidEmails() {\n        var addresses = new object?[]\n        {\n            null,\n            new Dictionary<string, object> { { \"Name\", \"Missing\" } },\n            \" \",\n            \"valid@example.com\"\n        };\n        var seen = new HashSet<string>();\n\n        var result = Mailozaurr.Helpers\n            .UniqueAddresses(addresses!, seen)\n            .Select(Mailozaurr.Helpers.GetEmailAddress)\n            .ToArray();\n\n        Assert.Equal(new[] { \"valid@example.com\" }, result);\n    }\n\n    [Fact]\n    public void UniqueAddresses_ReturnsFirstOccurrence_WhenDictionaryEmailsDuplicate() {\n        var first = new Dictionary<string, object> { { \"Email\", \"d@e.com\" }, { \"Name\", \"One\" } };\n        var second = new Dictionary<string, object> { { \"Email\", \"D@e.com\" }, { \"Name\", \"Two\" } };\n        var addresses = new object[] { first, second };\n        var seen = new HashSet<string>();\n\n        var result = Mailozaurr.Helpers\n            .UniqueAddresses(addresses, seen)\n            .ToArray();\n\n        Assert.Single(result);\n        Assert.Same(first, result[0]);\n    }\n\n    [Fact]\n    public void UniqueAddresses_DefaultsToNewSet_WhenSeenIsNull() {\n        var addresses = new object[] { \"a@example.com\", \"a@example.com\", \"b@example.com\" };\n\n        var result = Mailozaurr.Helpers\n            .UniqueAddresses(addresses, null)\n            .Select(Mailozaurr.Helpers.GetEmailAddress)\n            .ToArray();\n\n        Assert.Equal(new[] { \"a@example.com\", \"b@example.com\" }, result);\n    }\n\n    [Fact]\n    public void UniqueAddresses_UsesProvidedSetAcrossCalls() {\n        var seen = new HashSet<string>();\n        var first = new object[] { \"a@example.com\" };\n        var second = new object[] { \"a@example.com\", \"b@example.com\" };\n\n        Mailozaurr.Helpers\n            .UniqueAddresses(first, seen)\n            .ToArray();\n\n        var result = Mailozaurr.Helpers\n            .UniqueAddresses(second, seen)\n            .Select(Mailozaurr.Helpers.GetEmailAddress)\n            .ToArray();\n\n        Assert.Equal(new[] { \"b@example.com\" }, result);\n    }\n\n    private class CancelHandler : HttpMessageHandler {\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n            => Task.FromCanceled<HttpResponseMessage>(cancellationToken);\n    }\n\n    private class ThrowHandler : HttpMessageHandler {\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n            => throw new HttpRequestException(\"boom\");\n    }\n\n    private class FailStatusHandler : HttpMessageHandler {\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n            => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));\n    }\n\n    private class CountingHandler : HttpMessageHandler {\n        public int Calls;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            Calls++;\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));\n        }\n    }\n\n    [Fact]\n    public async Task PostWebhookAsync_CancellationRequested_ThrowsAsync() {\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n        var client = new HttpClient(new CancelHandler());\n        var result = new SmtpResult(true, EmailAction.Send, string.Empty, string.Empty, string.Empty, 0, TimeSpan.Zero);\n\n        await Assert.ThrowsAsync<TaskCanceledException>(() =>\n            Mailozaurr.Helpers.PostWebhookAsync(\"http://localhost\", result, cts.Token, client));\n    }\n\n    [Fact]\n    public async Task PostWebhookAsync_HttpRequestException_LogsWarning() {\n        var client = new HttpClient(new ThrowHandler());\n        var result = new SmtpResult(true, EmailAction.Send, string.Empty, string.Empty, string.Empty, 0, TimeSpan.Zero);\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        Mailozaurr.LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        await Mailozaurr.Helpers.PostWebhookAsync(\"http://localhost\", result, default, client);\n\n        Mailozaurr.LoggingMessages.Logger.OnWarningMessage -= Handler;\n        Assert.Contains(messages, static m => m.Contains(\"Failed to post webhook\"));\n    }\n\n    [Fact]\n    public async Task PostWebhookAsync_NonSuccessStatus_LogsWarning() {\n        var client = new HttpClient(new FailStatusHandler());\n        var result = new SmtpResult(true, EmailAction.Send, string.Empty, string.Empty, string.Empty, 0, TimeSpan.Zero);\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        Mailozaurr.LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        await Mailozaurr.Helpers.PostWebhookAsync(\"http://localhost\", result, default, client);\n\n        Mailozaurr.LoggingMessages.Logger.OnWarningMessage -= Handler;\n        Assert.Contains(messages, static m => m.Contains(\"Failed to post webhook\"));\n    }\n\n    [Fact]\n    public async Task PostWebhookAsync_UsesSharedClient_WhenClientNotProvided() {\n        var handler = new CountingHandler();\n        var client = new HttpClient(handler);\n        Mailozaurr.Helpers.SharedHttpClient = client;\n\n        try {\n            var result = new SmtpResult(true, EmailAction.Send, string.Empty, string.Empty, string.Empty, 0, TimeSpan.Zero);\n            await Mailozaurr.Helpers.PostWebhookAsync(\"http://localhost\", result);\n            await Mailozaurr.Helpers.PostWebhookAsync(\"http://localhost\", result);\n            Assert.Equal(2, handler.Calls);\n        } finally {\n            Mailozaurr.Helpers.SharedHttpClient = new HttpClient();\n        }\n    }\n\n    private class DisposeTrackingHandler : HttpMessageHandler {\n        public bool Disposed;\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n            => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));\n\n        protected override void Dispose(bool disposing) {\n            if (disposing) {\n                Disposed = true;\n            }\n\n            base.Dispose(disposing);\n        }\n    }\n\n    [Fact]\n    public async Task SharedHttpClient_ReplacesAndDisposesPreviousClient() {\n        var handler1 = new DisposeTrackingHandler();\n        var client1 = new HttpClient(handler1);\n        Mailozaurr.Helpers.SharedHttpClient = client1;\n\n        var handler2 = new CountingHandler();\n        var client2 = new HttpClient(handler2);\n        Mailozaurr.Helpers.SharedHttpClient = client2;\n\n        Assert.True(handler1.Disposed);\n\n        using var response = await Mailozaurr.Helpers.SharedHttpClient.GetAsync(\"http://localhost\");\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        Assert.Equal(1, handler2.Calls);\n\n        Mailozaurr.Helpers.SharedHttpClient = new HttpClient();\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/HtmlAutoEmbedImageTests.cs",
    "content": "using System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Reflection;\nusing System.Threading;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class HtmlAutoEmbedImageTests {\n    [Fact]\n    public void Smtp_CreateMessage_AutoEmbedsImages() {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        var smtp = new Smtp { AutoEmbedImages = true };\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.HtmlBody = $\"<img src=\\\"{tmp}\\\">\";\n        smtp.CreateMessage(CancellationToken.None);\n        var message = smtp.Message;\n        Assert.NotNull(message);\n        var body = Assert.IsType<MultipartRelated>(message!.Body!);\n        var inline = body.OfType<MimePart>().FirstOrDefault(p => p.ContentDisposition?.Disposition == ContentDisposition.Inline);\n        File.Delete(tmp);\n        Assert.NotNull(inline);\n        Assert.Contains(\"cid:\" + Path.GetFileName(tmp), smtp.HtmlBody);\n    }\n\n    [Fact]\n    public void Graph_CreateMessage_AutoEmbedsImages() {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        using var graph = new Graph {\n            From = \"from@example.com\",\n            To = new object[] { \"to@example.com\" },\n            Subject = \"subject\",\n            HTML = $\"<img src=\\\"{tmp}\\\">\",\n            ContentType = \"HTML\",\n            AutoEmbedImages = true\n        };\n        graph.CreateMessage();\n        File.Delete(tmp);\n        var message = graph.MessageContainer?.Message;\n        Assert.NotNull(message);\n        var attachment = Assert.Single(message!.Attachments!);\n        Assert.True(attachment.IsInline);\n        Assert.Contains(\"cid:\" + attachment.ContentId, graph.HTML);\n    }\n\n    [Fact]\n    public void Smtp_CreateMessage_EmbedsRemoteImages() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new ByteArrayContent(new byte[] { 1, 2, 3 }) {\n                Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n            }\n        });\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var smtp = new Smtp { AutoEmbedRemoteImages = true };\n            smtp.From = \"a@b.com\";\n            smtp.To = new object[] { \"c@d.com\" };\n            smtp.Subject = \"test\";\n            smtp.HtmlBody = \"<img src=\\\"https://example.com/img.png\\\">\";\n            smtp.CreateMessage(CancellationToken.None);\n            var message = smtp.Message;\n            Assert.NotNull(message);\n            var body = Assert.IsType<MultipartRelated>(message!.Body!);\n            var inline = body.OfType<MimePart>().FirstOrDefault(p => p.ContentDisposition?.Disposition == ContentDisposition.Inline);\n            Assert.NotNull(inline);\n            Assert.Contains(\"cid:img.png\", smtp.HtmlBody);\n            Assert.Single(handler.Requests);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/HtmlUtilsTests.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Reflection;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class HtmlUtilsTests\n{\n    [Fact]\n    public void ExtractLocalImagePaths_ReplacesOnlySrcValues()\n    {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        var html = $\"<img src=\\\"{tmp}\\\"><p>{tmp}</p>\";\n\n        var (result, paths) = HtmlUtils.ExtractLocalImagePaths(html);\n\n        Assert.Contains($\"cid:{Path.GetFileName(tmp)}\", result);\n        Assert.Contains($\"<p>{tmp}</p>\", result);\n        var path = Assert.Single(paths);\n        Assert.Equal(tmp, path);\n\n        File.Delete(tmp);\n    }\n\n    [Fact]\n    public void ExtractLocalImagePaths_PrecompiledRegex_MatchesInlineImplementation()\n    {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        var html = $\"<IMG SRC=\\\"{tmp}\\\"><p>{tmp}</p>\";\n\n        var expectedPaths = new List<string>();\n        const string pattern = @\"(?<=<img[^>]+src=['\"\"])([^'\"\"]+)(?=['\"\"])\";\n        var expectedHtml = Regex.Replace(html, pattern, match =>\n        {\n            var path = match.Value;\n            if (string.IsNullOrWhiteSpace(path)) return path;\n            if (path.StartsWith(\"http\", StringComparison.OrdinalIgnoreCase) ||\n                path.StartsWith(\"cid:\", StringComparison.OrdinalIgnoreCase) ||\n                path.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase))\n            {\n                return path;\n            }\n            if (File.Exists(path))\n            {\n                var fileName = Path.GetFileName(path);\n                expectedPaths.Add(path);\n                return $\"cid:{fileName}\";\n            }\n            return path;\n        }, RegexOptions.IgnoreCase);\n\n        var (actualHtml, actualPaths) = HtmlUtils.ExtractLocalImagePaths(html);\n\n        Assert.Equal(expectedHtml, actualHtml);\n        Assert.Equal(expectedPaths, actualPaths);\n\n        File.Delete(tmp);\n    }\n\n    [Fact]\n    public async Task DownloadRemoteImagesAsync_ReplacesOnlySrcValuesAsync()\n    {\n        const string url = \"https://example.com/img.png\";\n        var html = $\"<img src=\\\"{url}\\\"><p>{url}</p>\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK)\n        {\n            Content = new ByteArrayContent(new byte[] { 1, 2, 3 })\n            {\n                Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n            }\n        });\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try\n        {\n            var (result, images) = await HtmlUtils.DownloadRemoteImagesAsync(html);\n\n            Assert.Contains(\"cid:img.png\", result);\n            Assert.Contains($\"<p>{url}</p>\", result);\n            var image = Assert.Single(images);\n            Assert.Equal(\"image/png\", image.MediaType);\n        }\n        finally\n        {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task DownloadRemoteImagesAsync_DoesNotCreateExtraHandlers()\n    {\n        const string url = \"https://example.com/img.png\";\n        var html = $\"<img src=\\\"{url}\\\">\";\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK)\n        {\n            Content = new ByteArrayContent(new byte[] { 1, 2, 3 })\n            {\n                Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n            }\n        });\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try\n        {\n            await HtmlUtils.DownloadRemoteImagesAsync(html);\n            await HtmlUtils.DownloadRemoteImagesAsync(html);\n\n            Assert.Equal(2, handler.Requests.Count);\n            Assert.Same(handler, handlerField!.GetValue(HtmlUtils.HttpClient));\n        }\n        finally\n        {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task DownloadRemoteImagesAsync_DetectsMultipleImagesRegardlessOfCaseAsync()\n    {\n        const string url1 = \"https://example.com/img.png\";\n        const string url2 = \"HTTPS://example.com/photo.jpg\";\n        var html = $\"<IMG SRC=\\\"{url1}\\\"><img SRC=\\\"{url2}\\\"><p>{url1}</p>\";\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new ByteArrayContent(new byte[] { 1 })\n                {\n                    Headers = { ContentType = new MediaTypeHeaderValue(\"image/png\") }\n                }\n            },\n            new HttpResponseMessage(HttpStatusCode.OK)\n            {\n                Content = new ByteArrayContent(new byte[] { 2 })\n                {\n                    Headers = { ContentType = new MediaTypeHeaderValue(\"image/jpeg\") }\n                }\n            });\n        var client = HtmlUtils.HttpClient;\n        var handlerField = typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.Instance | BindingFlags.NonPublic)\n            ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.Instance | BindingFlags.NonPublic);\n        var original = (HttpMessageHandler)handlerField!.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try\n        {\n            var (result, images) = await HtmlUtils.DownloadRemoteImagesAsync(html);\n\n            Assert.Contains(\"cid:img.png\", result);\n            Assert.Contains(\"cid:photo.jpg\", result);\n            Assert.Contains($\"<p>{url1}</p>\", result);\n            Assert.Equal(2, images.Count);\n            Assert.Contains(images, i => i.MediaType == \"image/png\");\n            Assert.Contains(images, i => i.MediaType == \"image/jpeg\");\n            Assert.Equal(2, handler.Requests.Count);\n        }\n        finally\n        {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task DownloadRemoteImagesAsync_PropagatesCancellationAsync() {\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>\n            await HtmlUtils.DownloadRemoteImagesAsync(\"<img src=\\\"https://example.com/img.png\\\">\", cts.Token));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapBulkFlagOperationsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapBulkFlagOperationsTests {\n    [Fact]\n    public async Task SetFlagsAsync_UsesBulkPath_WhenBulkSucceeds() {\n        var folder = new FakeFolder();\n\n        var result = await ImapBulkFlagOperations.SetFlagsAsync(\n            folder,\n            new[] { new UniqueId(1), new UniqueId(2), new UniqueId(2) },\n            MessageFlags.Seen,\n            add: true);\n\n        Assert.Equal(2, result.Requested);\n        Assert.Equal(2, result.Updated);\n        Assert.Equal(2, result.SuccessfulUids.Count);\n        Assert.All(result.Results, x => Assert.True(x.Ok));\n        Assert.Equal(1, folder.BulkAddCalls);\n        Assert.Equal(0, folder.SingleAddCalls);\n    }\n\n    [Fact]\n    public async Task SetFlagsAsync_FallsBackPerItem_WhenBulkFails() {\n        var folder = new FakeFolder {\n            ThrowOnBulkRemove = true,\n            FailSingleRemoveFor = new HashSet<uint> { 2 }\n        };\n\n        var result = await ImapBulkFlagOperations.SetFlagsAsync(\n            folder,\n            new[] { new UniqueId(1), new UniqueId(2) },\n            MessageFlags.Flagged,\n            add: false,\n            sanitizeError: message => \"sanitized: \" + message);\n\n        Assert.Equal(2, result.Requested);\n        Assert.Equal(1, result.Updated);\n        Assert.Single(result.SuccessfulUids);\n        Assert.Equal((uint)1, result.SuccessfulUids[0].Id);\n        Assert.Equal(2, result.Results.Count);\n        Assert.True(result.Results[0].Ok);\n        Assert.False(result.Results[1].Ok);\n        Assert.Equal(\"sanitized: uid 2 failed\", result.Results[1].Error);\n        Assert.Equal(1, folder.BulkRemoveCalls);\n        Assert.Equal(2, folder.SingleRemoveCalls);\n    }\n\n    private sealed class FakeFolder : ImapBulkFlagOperations.IImapFolder {\n        internal bool ThrowOnBulkRemove { get; set; }\n        internal HashSet<uint> FailSingleRemoveFor { get; set; } = new();\n        internal int BulkAddCalls { get; private set; }\n        internal int BulkRemoveCalls { get; private set; }\n        internal int SingleAddCalls { get; private set; }\n        internal int SingleRemoveCalls { get; private set; }\n\n        public Task AddFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uids ?? throw new ArgumentNullException(nameof(uids));\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            BulkAddCalls++;\n            return Task.CompletedTask;\n        }\n\n        public Task RemoveFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uids ?? throw new ArgumentNullException(nameof(uids));\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            BulkRemoveCalls++;\n            if (ThrowOnBulkRemove) {\n                throw new InvalidOperationException(\"bulk failed\");\n            }\n            return Task.CompletedTask;\n        }\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uid;\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            SingleAddCalls++;\n            return Task.CompletedTask;\n        }\n\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            SingleRemoveCalls++;\n            if (FailSingleRemoveFor.Contains(uid.Id)) {\n                throw new InvalidOperationException($\"uid {uid.Id} failed\");\n            }\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapConnectionRequestTests.cs",
    "content": "using MailKit.Security;\nusing System;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ImapConnectionRequestTests {\n    [Fact]\n    public void Constructor_SetsValues() {\n        var request = new ImapConnectionRequest(\n            \"imap.example.test\",\n            993,\n            SecureSocketOptions.StartTls,\n            timeout: 1234,\n            skipCertificateRevocation: true,\n            skipCertificateValidation: true,\n            retryCount: 7,\n            retryDelayMilliseconds: 150,\n            retryDelayBackoff: 1.5);\n\n        Assert.Equal(\"imap.example.test\", request.Server);\n        Assert.Equal(993, request.Port);\n        Assert.Equal(SecureSocketOptions.StartTls, request.Options);\n        Assert.Equal(1234, request.Timeout);\n        Assert.True(request.SkipCertificateRevocation);\n        Assert.True(request.SkipCertificateValidation);\n        Assert.Equal(7, request.RetryCount);\n        Assert.Equal(150, request.RetryDelayMilliseconds);\n        Assert.Equal(1.5, request.RetryDelayBackoff);\n    }\n\n    [Fact]\n    public void Constructor_ThrowsForInvalidInput() {\n        Assert.Throws<ArgumentException>(() => new ImapConnectionRequest(\"\", 993));\n        Assert.Throws<ArgumentOutOfRangeException>(() => new ImapConnectionRequest(\"imap.example.test\", 0));\n        Assert.Throws<ArgumentOutOfRangeException>(() => new ImapConnectionRequest(\"imap.example.test\", 993, timeout: -1));\n        Assert.Throws<ArgumentOutOfRangeException>(() => new ImapConnectionRequest(\"imap.example.test\", 993, retryCount: -1));\n        Assert.Throws<ArgumentOutOfRangeException>(() => new ImapConnectionRequest(\"imap.example.test\", 993, retryDelayMilliseconds: -1));\n        Assert.Throws<ArgumentOutOfRangeException>(() => new ImapConnectionRequest(\"imap.example.test\", 993, retryDelayBackoff: 0));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapDeleteOperationsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapDeleteOperationsTests {\n    [Fact]\n    public async Task DeleteAsync_Expunges_WhenRequestedAndAtLeastOneDeleteSucceeds() {\n        var folder = new FakeDeleteFolder {\n            ThrowOnBulkAdd = true,\n            FailSingleAddFor = new HashSet<uint> { 2 }\n        };\n\n        var result = await ImapDeleteOperations.DeleteAsync(\n            folder,\n            new[] { new UniqueId(1), new UniqueId(2) },\n            expunge: true,\n            sanitizeError: message => \"sanitized: \" + message);\n\n        Assert.Equal(2, result.Requested);\n        Assert.Equal(1, result.Deleted);\n        Assert.True(result.Expunged);\n        Assert.Equal(1, folder.ExpungeCalls);\n        Assert.Single(result.DeletedUids);\n        Assert.Equal((uint)1, result.DeletedUids[0].Id);\n        Assert.Equal(2, result.Results.Count);\n        Assert.True(result.Results[0].Ok);\n        Assert.False(result.Results[1].Ok);\n        Assert.Equal(\"sanitized: uid 2 failed\", result.Results[1].Error);\n    }\n\n    [Fact]\n    public async Task DeleteAsync_DoesNotExpunge_WhenNoDeleteSucceeds() {\n        var folder = new FakeDeleteFolder {\n            ThrowOnBulkAdd = true,\n            FailSingleAddFor = new HashSet<uint> { 1 }\n        };\n\n        var result = await ImapDeleteOperations.DeleteAsync(\n            folder,\n            new[] { new UniqueId(1) },\n            expunge: true);\n\n        Assert.Equal(1, result.Requested);\n        Assert.Equal(0, result.Deleted);\n        Assert.False(result.Expunged);\n        Assert.Equal(0, folder.ExpungeCalls);\n    }\n\n    [Fact]\n    public async Task DeleteAsync_DoesNotExpunge_WhenNotRequested() {\n        var folder = new FakeDeleteFolder();\n\n        var result = await ImapDeleteOperations.DeleteAsync(\n            folder,\n            new[] { new UniqueId(1) },\n            expunge: false);\n\n        Assert.Equal(1, result.Requested);\n        Assert.Equal(1, result.Deleted);\n        Assert.False(result.Expunged);\n        Assert.Equal(0, folder.ExpungeCalls);\n    }\n\n    private sealed class FakeDeleteFolder : ImapDeleteOperations.IImapDeleteFolder {\n        internal bool ThrowOnBulkAdd { get; set; }\n        internal HashSet<uint> FailSingleAddFor { get; set; } = new();\n        internal int ExpungeCalls { get; private set; }\n\n        public string FullName => \"INBOX\";\n\n        public Task AddFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uids ?? throw new ArgumentNullException(nameof(uids));\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            if (ThrowOnBulkAdd) {\n                throw new InvalidOperationException(\"bulk failed\");\n            }\n            return Task.CompletedTask;\n        }\n\n        public Task RemoveFlagsAsync(IReadOnlyCollection<UniqueId> uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uids;\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            return Task.CompletedTask;\n        }\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            if (FailSingleAddFor.Contains(uid.Id)) {\n                throw new InvalidOperationException($\"uid {uid.Id} failed\");\n            }\n            return Task.CompletedTask;\n        }\n\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uid;\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            return Task.CompletedTask;\n        }\n\n        public Task ExpungeAsync(CancellationToken cancellationToken = default) {\n            _ = cancellationToken;\n            ExpungeCalls++;\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapFetchTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests {\n    public class ImapFetchTests {\n        private class FakeMessage {\n            public FakeMessage(string subject, bool isRead) {\n                Subject = subject;\n                IsRead = isRead;\n            }\n\n            public string Subject { get; }\n            public bool IsRead { get; }\n        }\n\n        private class FakeImapClient {\n            private readonly Dictionary<string, List<FakeMessage>> _mailboxes = new();\n            public bool ValidCredentials { get; set; } = true;\n\n            public void AddMailbox(string name, params FakeMessage[] messages) =>\n                _mailboxes[name] = messages.ToList();\n\n            public IReadOnlyList<FakeMessage> Fetch(string mailbox, bool unreadOnly = false) {\n                if (!ValidCredentials) {\n                    throw new InvalidOperationException(\"Invalid credentials\");\n                }\n\n                if (!_mailboxes.TryGetValue(mailbox, out var messages)) {\n                    throw new InvalidOperationException(\"Mailbox not found\");\n                }\n\n                return unreadOnly ? messages.Where(m => !m.IsRead).ToList() : messages;\n            }\n        }\n        [Fact]\n        public void Imap_Fetch_WithValidMailbox_Succeeds() {\n            // Arrange\n            var client = new FakeImapClient();\n            client.AddMailbox(\"Inbox\", new FakeMessage(\"hello\", false));\n\n            // Act\n            var messages = client.Fetch(\"Inbox\");\n\n            // Assert\n            Assert.Single(messages);\n        }\n\n        [Fact]\n        public void Imap_Fetch_WithInvalidMailbox_Fails() {\n            // Arrange\n            var client = new FakeImapClient();\n            client.AddMailbox(\"Inbox\");\n\n            // Act & Assert\n            Assert.Throws<InvalidOperationException>(() => client.Fetch(\"Missing\"));\n        }\n\n        [Fact]\n        public void Imap_Fetch_UnreadMessages_Succeeds() {\n            // Arrange\n            var client = new FakeImapClient();\n            client.AddMailbox(\"Inbox\",\n                new FakeMessage(\"read\", true),\n                new FakeMessage(\"unread\", false));\n\n            // Act\n            var unread = client.Fetch(\"Inbox\", unreadOnly: true);\n\n            // Assert\n            Assert.Single(unread);\n            Assert.Equal(\"unread\", unread[0].Subject);\n        }\n\n        [Fact]\n        public void Imap_Fetch_WithInvalidCredentials_Fails() {\n            // Arrange\n            var client = new FakeImapClient { ValidCredentials = false };\n            client.AddMailbox(\"Inbox\", new FakeMessage(\"hello\", false));\n\n            // Act & Assert\n            Assert.Throws<InvalidOperationException>(() => client.Fetch(\"Inbox\"));\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapIdleListenerTests.cs",
    "content": "using System;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\npublic class ImapIdleListenerTests {\n    [Fact]\n    public void Constructor_SetsSearchQuery() {\n        var client = new ImapClient();\n        var query = SearchQuery.SubjectContains(\"Test\");\n\n        var listener = new ImapIdleListener(client, searchQuery: query);\n        var field = typeof(ImapIdleListener).GetField(\"_searchQuery\", BindingFlags.NonPublic | BindingFlags.Instance)!\n            .GetValue(listener);\n\n        Assert.Equal(query, field);\n    }\n\n    [Fact]\n    public async Task StopAsync_CancelsIdleLoopGracefully() {\n        var listener = new ImapIdleListener(new ImapClient());\n        var cancellation = new CancellationTokenSource();\n        var idleCompletion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        SetPrivateField(listener, \"_cancel\", cancellation);\n        SetPrivateField(listener, \"_idleTask\", idleCompletion.Task);\n\n        var stopTask = listener.StopAsync();\n\n        Assert.True(cancellation.IsCancellationRequested);\n        Assert.False(stopTask.IsCompleted, \"StopAsync completed before the idle loop finished.\");\n\n        idleCompletion.SetResult(null);\n\n        await stopTask;\n\n        Assert.Null(GetPrivateField<CancellationTokenSource?>(listener, \"_cancel\"));\n        Assert.Null(GetPrivateField<Task?>(listener, \"_idleTask\"));\n    }\n\n    [Fact]\n    public async Task StartAsync_FailureDuringInitialSetup_CleansUpState() {\n        var listener = new ImapIdleListener(new ImapClient());\n\n        await Assert.ThrowsAnyAsync<Exception>(() => listener.StartAsync());\n\n        Assert.Null(GetPrivateField<CancellationTokenSource?>(listener, \"_cancel\"));\n        Assert.Null(GetPrivateField<Task?>(listener, \"_idleTask\"));\n        Assert.Null(GetPrivateField<IMailFolder?>(listener, \"_folder\"));\n\n        await Assert.ThrowsAnyAsync<Exception>(() => listener.StartAsync());\n    }\n\n    [Fact]\n    public async Task StopAsync_PropagatesExceptionsFromIdleLoop() {\n        var listener = new ImapIdleListener(new ImapClient());\n        var cancellation = new CancellationTokenSource();\n        var idleFailure = new InvalidOperationException(\"Idle loop faulted\");\n\n        SetPrivateField(listener, \"_cancel\", cancellation);\n        SetPrivateField(listener, \"_idleTask\", Task.FromException(idleFailure));\n\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => listener.StopAsync());\n\n        Assert.Same(idleFailure, exception);\n        Assert.True(cancellation.IsCancellationRequested);\n        Assert.Null(GetPrivateField<Task?>(listener, \"_idleTask\"));\n        Assert.Null(GetPrivateField<CancellationTokenSource?>(listener, \"_cancel\"));\n    }\n\n    [Fact]\n    public async Task StopAsync_IgnoresCancellationFromIdleLoopWhenStopping() {\n        var listener = new ImapIdleListener(new ImapClient());\n        var cancellation = new CancellationTokenSource();\n\n        SetPrivateField(listener, \"_cancel\", cancellation);\n        SetPrivateField(listener, \"_idleTask\", Task.FromCanceled(new CancellationToken(canceled: true)));\n\n        await listener.StopAsync();\n\n        Assert.True(cancellation.IsCancellationRequested);\n        Assert.Null(GetPrivateField<Task?>(listener, \"_idleTask\"));\n        Assert.Null(GetPrivateField<CancellationTokenSource?>(listener, \"_cancel\"));\n    }\n\n    [Fact]\n    public void Dispose_IgnoresCancellationFromIdleLoopWhenStopping() {\n        var listener = new ImapIdleListener(new ImapClient());\n        var cancellation = new CancellationTokenSource();\n\n        SetPrivateField(listener, \"_cancel\", cancellation);\n        SetPrivateField(listener, \"_idleTask\", Task.FromCanceled(new CancellationToken(canceled: true)));\n\n        listener.Dispose();\n\n        Assert.Null(GetPrivateField<Task?>(listener, \"_idleTask\"));\n        Assert.Null(GetPrivateField<CancellationTokenSource?>(listener, \"_cancel\"));\n    }\n\n    private static void SetPrivateField<T>(ImapIdleListener listener, string name, T value) =>\n        typeof(ImapIdleListener).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(listener, value);\n\n    private static T? GetPrivateField<T>(ImapIdleListener listener, string name) =>\n        (T?)typeof(ImapIdleListener).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(listener);\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapMailboxSearchQueryBuilderTests.cs",
    "content": "using MailKit.Search;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapMailboxSearchQueryBuilderTests {\n    [Fact]\n    public void Tokenize_ReturnsEmpty_ForNullOrWhitespace() {\n        Assert.Empty(ImapMailboxSearchQueryBuilder.Tokenize(null));\n        Assert.Empty(ImapMailboxSearchQueryBuilder.Tokenize(string.Empty));\n        Assert.Empty(ImapMailboxSearchQueryBuilder.Tokenize(\" \\t \\r\\n \"));\n    }\n\n    [Fact]\n    public void Tokenize_SplitsOnWhitespaceAndTrims() {\n        var tokens = ImapMailboxSearchQueryBuilder.Tokenize(\" alpha\\tbeta \\r\\ngamma  \");\n\n        Assert.Equal(3, tokens.Count);\n        Assert.Equal(\"alpha\", tokens[0]);\n        Assert.Equal(\"beta\", tokens[1]);\n        Assert.Equal(\"gamma\", tokens[2]);\n    }\n\n    [Fact]\n    public void Build_ReturnsSearchQuery() {\n        var query = ImapMailboxSearchQueryBuilder.Build(\n            unseenOnly: true,\n            subjectContains: \"subject\",\n            fromContains: \"sender\",\n            toContains: \"recipient\",\n            bodyContains: \"body\",\n            query: \"token\");\n\n        Assert.NotNull(query);\n        Assert.IsAssignableFrom<SearchQuery>(query);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapMessageReaderTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MimeKit;\nusing Moq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapMessageReaderTests\n{\n    [Fact]\n    public async Task ReadAsync_UsesResolvedFolderAndTruncatesBodies()\n    {\n        var uid = new UniqueId(42);\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"subject\";\n        message.Date = new DateTimeOffset(2026, 3, 17, 10, 0, 0, TimeSpan.Zero);\n        var builder = new BodyBuilder {\n            TextBody = \"1234567890\",\n            HtmlBody = \"<p>abcdefghij</p>\"\n        };\n        builder.Attachments.Add(\"report.txt\", new byte[] { 1, 2, 3 });\n        message.Body = builder.ToMessageBody();\n\n        var folder = new Mock<IMailFolder>();\n        folder.SetupGet(f => f.FullName).Returns(\"Inbox/Sub\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);\n        folder.Setup(f => f.GetMessageAsync(uid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(message);\n\n        var personalRoot = new Mock<IMailFolder>();\n        personalRoot.Setup(f => f.GetSubfolder(\"Sub\", It.IsAny<CancellationToken>()))\n            .Returns(folder.Object);\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == \"Inbox\"));\n        client.Setup(c => c.GetFolder(\"Sub\", It.IsAny<CancellationToken>()))\n            .Throws(new FolderNotFoundException(\"Sub\"));\n        client.Setup(c => c.GetFolder(It.IsAny<string>(), It.IsAny<CancellationToken>()))\n            .Returns((string name, CancellationToken _) => name.Length == 0 ? personalRoot.Object : throw new FolderNotFoundException(name));\n\n        var personalNamespaces = new FolderNamespaceCollection();\n        personalNamespaces.Add(new FolderNamespace('.', \"\"));\n        client.Setup(c => c.GetFolder(It.IsAny<FolderNamespace>()))\n            .Returns(personalRoot.Object);\n        client.Setup(c => c.PersonalNamespaces)\n            .Returns(personalNamespaces);\n\n        var result = await ImapMessageReader.ReadAsync(\n            client.Object,\n            new ImapMessageReadRequest(uid, \"Sub\", 8),\n            CancellationToken.None);\n\n        Assert.Equal(42L, result.Uid);\n        Assert.Equal(\"Inbox/Sub\", result.Folder);\n        Assert.Equal(\"subject\", result.Subject);\n        Assert.True(result.TextTruncated);\n        Assert.True(result.HtmlTruncated);\n        Assert.True(result.HasAttachments);\n        var attachment = Assert.Single(result.Attachments);\n        Assert.Equal(\"report.txt\", attachment.FileName);\n    }\n\n    [Fact]\n    public async Task ReadAsync_UsesInboxWhenFolderOmitted()\n    {\n        var uid = new UniqueId(7);\n        var message = new MimeMessage();\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n\n        var inbox = new Mock<IMailFolder>();\n        inbox.SetupGet(f => f.FullName).Returns(\"Inbox\");\n        inbox.SetupGet(f => f.IsOpen).Returns(true);\n        inbox.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);\n        inbox.Setup(f => f.GetMessageAsync(uid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(message);\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(inbox.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        var result = await ImapMessageReader.ReadAsync(\n            client.Object,\n            new ImapMessageReadRequest(uid, null, 128),\n            CancellationToken.None);\n\n        Assert.Equal(\"Inbox\", result.Folder);\n        Assert.False(result.TextTruncated);\n        Assert.False(result.HasAttachments);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapMoveOperationsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapMoveOperationsTests {\n    [Fact]\n    public async Task MoveAsync_ReturnsNoOp_WhenSourceAndTargetAreSame() {\n        var folder = new FakeFolder(\"INBOX\");\n\n        var result = await ImapMoveOperations.MoveAsync(\n            folder,\n            folder,\n            new[] { new UniqueId(1), new UniqueId(1), new UniqueId(2) });\n\n        Assert.Equal(2, result.Requested);\n        Assert.Equal(2, result.Moved);\n        Assert.Equal(2, result.Results.Count);\n        Assert.All(result.Results, x => Assert.True(x.Ok));\n        Assert.All(result.Results, x => Assert.False(x.UsedCopyFallback));\n        Assert.Equal(0, folder.OpenCalls);\n        Assert.Equal(0, folder.MoveCalls);\n    }\n\n    [Fact]\n    public async Task MoveAsync_UsesCopyFallbackAndUidMapping_WhenMoveNotSupported() {\n        var source = new FakeFolder(\"INBOX\") {\n            NotSupportedMoveUids = new HashSet<uint> { 7 },\n            MessageIdHeaders = new Dictionary<uint, string?> {\n                [7] = \"  <id-7@example>  \"\n            },\n            CopyMap = new Dictionary<uint, uint> {\n                [7] = 701\n            }\n        };\n        var target = new FakeFolder(\"Archive\");\n\n        var result = await ImapMoveOperations.MoveAsync(\n            source,\n            target,\n            new[] { new UniqueId(7) });\n\n        Assert.Equal(1, result.Requested);\n        Assert.Equal(1, result.Moved);\n        var item = Assert.Single(result.Results);\n        Assert.True(item.Ok);\n        Assert.True(item.UsedCopyFallback);\n        Assert.Equal(701, item.TargetUid);\n        Assert.Equal(\"id-7@example\", item.MessageId);\n        Assert.Equal(1, source.AddFlagsCalls);\n        Assert.Equal(1, source.ExpungeCalls);\n    }\n\n    [Fact]\n    public async Task MoveAsync_UsesSearchLookup_WhenFallbackMapMissing() {\n        var source = new FakeFolder(\"INBOX\") {\n            NotSupportedMoveUids = new HashSet<uint> { 5 },\n            ThrowOnCopyWithMap = true,\n            MessageIdHeaders = new Dictionary<uint, string?> {\n                [5] = \"<lookup-5@example>\"\n            }\n        };\n        var target = new FakeFolder(\"Archive\") {\n            SearchByMessageId = new Dictionary<string, List<UniqueId>>(StringComparer.OrdinalIgnoreCase) {\n                [\"lookup-5@example\"] = new List<UniqueId> { new UniqueId(5001) }\n            }\n        };\n\n        var result = await ImapMoveOperations.MoveAsync(\n            source,\n            target,\n            new[] { new UniqueId(5) });\n\n        var item = Assert.Single(result.Results);\n        Assert.True(item.Ok);\n        Assert.True(item.UsedCopyFallback);\n        Assert.Equal(5001, item.TargetUid);\n        Assert.Equal(1, source.CopySingleCalls);\n        Assert.Equal(1, source.ExpungeCalls);\n    }\n\n    [Fact]\n    public async Task MoveAsync_ReturnsPerItemFailures_WithSanitizedErrors() {\n        var source = new FakeFolder(\"INBOX\") {\n            ThrowOnMoveUids = new HashSet<uint> { 2 }\n        };\n        var target = new FakeFolder(\"Archive\");\n\n        var result = await ImapMoveOperations.MoveAsync(\n            source,\n            target,\n            new[] { new UniqueId(1), new UniqueId(2) },\n            sanitizeError: message => \"sanitized: \" + message);\n\n        Assert.Equal(2, result.Requested);\n        Assert.Equal(1, result.Moved);\n        Assert.True(result.Results[0].Ok);\n        Assert.False(result.Results[1].Ok);\n        Assert.Equal(\"sanitized: move failed for uid 2\", result.Results[1].Error);\n    }\n\n    [Fact]\n    public async Task MoveAsync_PropagatesCancellation_WhenOpenIsCanceled() {\n        var source = new FakeFolder(\"INBOX\") {\n            ThrowOnOpenWhenCancellationRequested = true\n        };\n        var target = new FakeFolder(\"Archive\");\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>\n            ImapMoveOperations.MoveAsync(\n                source,\n                target,\n                new[] { new UniqueId(1) },\n                cancellationToken: cts.Token));\n    }\n\n    [Fact]\n    public async Task MoveAsync_RecoversWhenCloseFails_DuringAccessUpgrade() {\n        var source = new FakeFolder(\"INBOX\") {\n            StartOpen = true,\n            StartAccess = FolderAccess.ReadOnly,\n            ThrowOnClose = true\n        };\n        var target = new FakeFolder(\"Archive\");\n\n        var result = await ImapMoveOperations.MoveAsync(\n            source,\n            target,\n            new[] { new UniqueId(1) });\n\n        var item = Assert.Single(result.Results);\n        Assert.True(item.Ok);\n        Assert.Equal(1, source.CloseCalls);\n        Assert.Equal(1, source.OpenCalls);\n    }\n\n    private sealed class FakeFolder : ImapMoveOperations.IImapMoveFolder {\n        internal FakeFolder(string fullName) {\n            FullName = fullName;\n        }\n\n        public string FullName { get; }\n\n        private bool _isOpen;\n        private FolderAccess _access;\n\n        public bool IsOpen => StartOpen || _isOpen;\n\n        public FolderAccess Access => StartOpen ? StartAccess : _access;\n\n        internal int OpenCalls { get; private set; }\n        internal int CloseCalls { get; private set; }\n        internal int MoveCalls { get; private set; }\n        internal int AddFlagsCalls { get; private set; }\n        internal int ExpungeCalls { get; private set; }\n        internal int CopySingleCalls { get; private set; }\n\n        internal bool StartOpen { get; set; }\n        internal FolderAccess StartAccess { get; set; }\n        internal bool ThrowOnOpenWhenCancellationRequested { get; set; }\n        internal bool ThrowOnClose { get; set; }\n        internal HashSet<uint> NotSupportedMoveUids { get; set; } = new();\n        internal HashSet<uint> ThrowOnMoveUids { get; set; } = new();\n        internal bool ThrowOnCopyWithMap { get; set; }\n        internal Dictionary<uint, string?> MessageIdHeaders { get; set; } = new();\n        internal Dictionary<uint, uint> CopyMap { get; set; } = new();\n        internal Dictionary<string, List<UniqueId>> SearchByMessageId { get; set; } = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default) {\n            if (ThrowOnOpenWhenCancellationRequested) {\n                cancellationToken.ThrowIfCancellationRequested();\n            }\n\n            OpenCalls++;\n            StartOpen = false;\n            _isOpen = true;\n            _access = access;\n            return Task.CompletedTask;\n        }\n\n        public Task CloseAsync(bool expunge, CancellationToken cancellationToken = default) {\n            _ = expunge;\n            _ = cancellationToken;\n            CloseCalls++;\n            if (ThrowOnClose) {\n                throw new InvalidOperationException(\"close failed\");\n            }\n\n            StartOpen = false;\n            _isOpen = false;\n            return Task.CompletedTask;\n        }\n\n        public Task<string?> GetMessageIdHeaderAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            _ = cancellationToken;\n            MessageIdHeaders.TryGetValue(uid.Id, out var value);\n            return Task.FromResult(value);\n        }\n\n        public Task MoveToAsync(UniqueId uid, ImapMoveOperations.IImapMoveFolder destination, CancellationToken cancellationToken = default) {\n            _ = destination;\n            _ = cancellationToken;\n            MoveCalls++;\n\n            if (ThrowOnMoveUids.Contains(uid.Id)) {\n                throw new InvalidOperationException($\"move failed for uid {uid.Id}\");\n            }\n            if (NotSupportedMoveUids.Contains(uid.Id)) {\n                throw new NotSupportedException(\"MOVE is not supported\");\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task<IDictionary<UniqueId, UniqueId>?> CopyToWithMapAsync(IReadOnlyCollection<UniqueId> uids, ImapMoveOperations.IImapMoveFolder destination, CancellationToken cancellationToken = default) {\n            _ = destination;\n            _ = cancellationToken;\n            if (ThrowOnCopyWithMap) {\n                throw new InvalidOperationException(\"copy map failed\");\n            }\n\n            var map = new Dictionary<UniqueId, UniqueId>();\n            foreach (var uid in uids) {\n                if (CopyMap.TryGetValue(uid.Id, out var targetUid)) {\n                    map[uid] = new UniqueId(targetUid);\n                }\n            }\n            return Task.FromResult<IDictionary<UniqueId, UniqueId>?>(map);\n        }\n\n        public Task CopyToAsync(UniqueId uid, ImapMoveOperations.IImapMoveFolder destination, CancellationToken cancellationToken = default) {\n            _ = uid;\n            _ = destination;\n            _ = cancellationToken;\n            CopySingleCalls++;\n            return Task.CompletedTask;\n        }\n\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            _ = uid;\n            _ = flags;\n            _ = silent;\n            _ = cancellationToken;\n            AddFlagsCalls++;\n            return Task.CompletedTask;\n        }\n\n        public Task ExpungeAsync(CancellationToken cancellationToken = default) {\n            _ = cancellationToken;\n            ExpungeCalls++;\n            return Task.CompletedTask;\n        }\n\n        public Task<IList<UniqueId>> SearchByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            _ = cancellationToken;\n            if (SearchByMessageId.TryGetValue(messageId, out var hits)) {\n                return Task.FromResult<IList<UniqueId>>(hits);\n            }\n\n            return Task.FromResult<IList<UniqueId>>(Array.Empty<UniqueId>());\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapSentFolderResolverTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapSentFolderResolverTests {\n    [Fact]\n    public void ResolveSentFolderName_UsesRequestedFolder_WhenProvided() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: \" Custom/Sent \",\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent\", Name = \"Sent\", IsSent = true }\n            });\n\n        Assert.Equal(\"Custom/Sent\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSentFolderName_UsesConfiguredFolder_WhenRequestedIsMissing() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: null,\n            configuredFolder: \" Team/Sent \",\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent\", Name = \"Sent\", IsSent = true }\n            });\n\n        Assert.Equal(\"Team/Sent\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSentFolderName_PrefersAttributeTaggedSentFolder() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"INBOX\", Name = \"INBOX\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent Items\", Name = \"Sent Items\", IsSent = true }\n            });\n\n        Assert.Equal(\"Sent Items\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSentFolderName_UsesSentHeuristic_WhenNoAttributeMatch() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"INBOX\", Name = \"INBOX\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent Mail\", Name = \"Sent Mail\" }\n            });\n\n        Assert.Equal(\"Sent Mail\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSentFolderName_IgnoresArchiveAndDraftLikeNames() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Archive\", Name = \"Archive\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Draft Sent Ideas\", Name = \"Draft Sent Ideas\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent\", Name = \"Sent\" }\n            });\n\n        Assert.Equal(\"Sent\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSentFolderName_UsesFallback_WhenNoCandidatesExist() {\n        var resolved = ImapSentFolderResolver.ResolveSentFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new List<ImapSentFolderResolver.ImapFolderInfo>(),\n            fallbackFolder: \"Sent\");\n\n        Assert.Equal(\"Sent\", resolved);\n    }\n\n    [Fact]\n    public void ResolveDraftsFolderName_PrefersConfiguredOverride_WhenProvided() {\n        var resolved = ImapSentFolderResolver.ResolveDraftsFolderName(\n            requestedFolder: null,\n            configuredFolder: \" Team Drafts \",\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Drafts\", Name = \"Drafts\", IsDrafts = true }\n            });\n\n        Assert.Equal(\"Team Drafts\", resolved);\n    }\n\n    [Fact]\n    public void ResolveDraftsFolderName_UsesHeuristic_WhenNoAttributeFlag() {\n        var resolved = ImapSentFolderResolver.ResolveDraftsFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"INBOX\", Name = \"INBOX\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"[Gmail]/Drafts\", Name = \"Drafts\" }\n            });\n\n        Assert.Equal(\"[Gmail]/Drafts\", resolved);\n    }\n\n    [Fact]\n    public void ResolveTrashFolderName_UsesSpecialUseFlag() {\n        var resolved = ImapSentFolderResolver.ResolveTrashFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Deleted Items\", Name = \"Deleted Items\", IsTrash = true },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Junk\", Name = \"Junk\", IsJunk = true }\n            });\n\n        Assert.Equal(\"Deleted Items\", resolved);\n    }\n\n    [Fact]\n    public void ResolveArchiveFolderName_UsesHeuristic() {\n        var resolved = ImapSentFolderResolver.ResolveArchiveFolderName(\n            requestedFolder: null,\n            configuredFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"INBOX\", Name = \"INBOX\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"[Gmail]/All Mail\", Name = \"All Mail\" }\n            });\n\n        Assert.Equal(\"[Gmail]/All Mail\", resolved);\n    }\n\n    [Fact]\n    public void ResolveJunkFolderName_UsesConfiguredOverride_WhenProvided() {\n        var resolved = ImapSentFolderResolver.ResolveJunkFolderName(\n            requestedFolder: null,\n            configuredFolder: \"Spam Box\",\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Junk\", Name = \"Junk\", IsJunk = true }\n            });\n\n        Assert.Equal(\"Spam Box\", resolved);\n    }\n\n    [Fact]\n    public void ResolveSpecialFolderMappings_UsesOverridesBeforeDetection() {\n        var mappings = ImapSentFolderResolver.ResolveSpecialFolderMappings(\n            configuredInboxFolder: \"INBOX.Client\",\n            configuredSentFolder: \"Sent.Custom\",\n            configuredDraftsFolder: \"Drafts.Custom\",\n            configuredTrashFolder: null,\n            configuredArchiveFolder: null,\n            configuredJunkFolder: null,\n            folders: new[] {\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Inbox\", Name = \"Inbox\" },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Sent Items\", Name = \"Sent Items\", IsSent = true },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Drafts\", Name = \"Drafts\", IsDrafts = true },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Deleted Items\", Name = \"Deleted Items\", IsTrash = true },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Archive\", Name = \"Archive\", IsArchive = true },\n                new ImapSentFolderResolver.ImapFolderInfo { FullName = \"Spam\", Name = \"Spam\", IsJunk = true }\n            },\n            configuredImapFolder: \"INBOX\");\n\n        Assert.Equal(\"INBOX.Client\", mappings.Inbox);\n        Assert.Equal(\"Sent.Custom\", mappings.Sent);\n        Assert.Equal(\"Drafts.Custom\", mappings.Drafts);\n        Assert.Equal(\"Deleted Items\", mappings.Trash);\n        Assert.Equal(\"Archive\", mappings.Archive);\n        Assert.Equal(\"Spam\", mappings.Junk);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapSentMessageOperationsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Search;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ImapSentMessageOperationsTests {\n    [Fact]\n    public async Task AppendToSentAsync_EnsuresReadWriteAndAppendsSeen() {\n        var folder = new FakeImapSentFolder {\n            FullNameValue = \"Sent\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        var message = BuildMessage();\n\n        var result = await ImapSentMessageOperations.AppendToSentAsync(folder, message);\n\n        Assert.True(result.Appended);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Equal(1, folder.CloseCount);\n        Assert.Equal(1, folder.OpenCount);\n        Assert.Equal(FolderAccess.ReadWrite, folder.LastOpenAccess);\n        Assert.Equal(1, folder.AppendCount);\n        Assert.Equal(MessageFlags.Seen, folder.LastAppendFlags);\n    }\n\n    [Fact]\n    public async Task FindSentDuplicateAsync_ReturnsMatchFromIdempotencyHeaderSearch() {\n        var uid = new UniqueId(123);\n        var folder = new FakeImapSentFolder {\n            FullNameValue = \"Sent\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folder.SearchResults.Enqueue(new List<UniqueId> { uid });\n        folder.EnvelopeMessageIds[uid] = \"matched@example.test\";\n\n        var result = await ImapSentMessageOperations.FindSentDuplicateAsync(\n            folder,\n            idempotencyHeaderName: \"X-BayManager-Idempotency-Key\",\n            idempotencyKey: \"idem-123\",\n            messageIdToken: \"fallback@example.test\");\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Equal(\"matched@example.test\", result.MessageId);\n        Assert.Single(folder.SearchQueries);\n    }\n\n    [Fact]\n    public async Task FindSentDuplicateAsync_FallsBackToMessageIdSearch() {\n        var uid = new UniqueId(345);\n        var folder = new FakeImapSentFolder {\n            FullNameValue = \"Sent\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folder.SearchResults.Enqueue(new List<UniqueId>());\n        folder.SearchResults.Enqueue(new List<UniqueId> { uid });\n        folder.EnvelopeMessageIds[uid] = \"found-by-message-id@example.test\";\n\n        var result = await ImapSentMessageOperations.FindSentDuplicateAsync(\n            folder,\n            idempotencyHeaderName: \"X-BayManager-Idempotency-Key\",\n            idempotencyKey: \"idem-123\",\n            messageIdToken: \"<msg-123@example.test>\");\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"found-by-message-id@example.test\", result.MessageId);\n        Assert.Equal(2, folder.SearchQueries.Count);\n    }\n\n    [Fact]\n    public async Task FindSentDuplicateAsync_ReturnsNoneWhenNoMatch() {\n        var folder = new FakeImapSentFolder {\n            FullNameValue = \"Sent\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folder.SearchResults.Enqueue(new List<UniqueId>());\n\n        var result = await ImapSentMessageOperations.FindSentDuplicateAsync(\n            folder,\n            idempotencyHeaderName: \"X-BayManager-Idempotency-Key\",\n            idempotencyKey: \"idem-123\",\n            messageIdToken: null);\n\n        Assert.False(result.IsMatch);\n        Assert.Null(result.MessageId);\n        Assert.Single(folder.SearchQueries);\n    }\n\n    [Fact]\n    public async Task FindSentDuplicateAsync_NoMatch_ReturnsDistinctInstances() {\n        var folderA = new FakeImapSentFolder {\n            FullNameValue = \"Sent-A\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folderA.SearchResults.Enqueue(new List<UniqueId>());\n\n        var folderB = new FakeImapSentFolder {\n            FullNameValue = \"Sent-B\",\n            IsOpenValue = true,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folderB.SearchResults.Enqueue(new List<UniqueId>());\n\n        var resultA = await ImapSentMessageOperations.FindSentDuplicateAsync(\n            folderA,\n            idempotencyHeaderName: \"X-BayManager-Idempotency-Key\",\n            idempotencyKey: \"idem-123\",\n            messageIdToken: null);\n        var resultB = await ImapSentMessageOperations.FindSentDuplicateAsync(\n            folderB,\n            idempotencyHeaderName: \"X-BayManager-Idempotency-Key\",\n            idempotencyKey: \"idem-123\",\n            messageIdToken: null);\n\n        Assert.False(resultA.IsMatch);\n        Assert.False(resultB.IsMatch);\n        Assert.NotSame(resultA, resultB);\n        Assert.Null(resultA.Folder);\n        Assert.Null(resultB.Folder);\n        Assert.Null(resultA.MessageId);\n        Assert.Null(resultB.MessageId);\n    }\n\n    [Fact]\n    public async Task GetThreadingMetadataAsync_ReturnsFolderMetadata() {\n        var uid = new UniqueId(777);\n        var folder = new FakeImapSentFolder {\n            FullNameValue = \"INBOX\",\n            IsOpenValue = false,\n            AccessValue = FolderAccess.ReadOnly\n        };\n        folder.ThreadingMetadataByUid[uid] = new ImapSentMessageOperations.ImapThreadingMetadataResult {\n            MessageId = \"child@example.test\",\n            InReplyTo = \"parent@example.test\",\n            ReplyTo = \"reply@example.test\",\n            Cc = \"cc@example.test\",\n            References = new List<string> { \"root@example.test\", \"parent@example.test\" }\n        };\n\n        var result = await ImapSentMessageOperations.GetThreadingMetadataAsync(folder, uid);\n\n        Assert.NotNull(result);\n        Assert.Equal(\"child@example.test\", result!.MessageId);\n        Assert.Equal(\"parent@example.test\", result.InReplyTo);\n        Assert.Equal(\"reply@example.test\", result.ReplyTo);\n        Assert.Equal(\"cc@example.test\", result.Cc);\n        Assert.Equal(2, result.References!.Count);\n        Assert.Equal(1, folder.OpenCount);\n        Assert.Equal(FolderAccess.ReadOnly, folder.LastOpenAccess);\n    }\n\n    [Theory]\n    [InlineData(\"<abc@example.test>\", \"abc@example.test\")]\n    [InlineData(\" abc@example.test \", \"abc@example.test\")]\n    [InlineData(\"\", null)]\n    [InlineData(\"   \", null)]\n    public void NormalizeMessageIdToken_NormalizesExpectedValues(string input, string? expected) {\n        var actual = ImapSentMessageOperations.NormalizeMessageIdToken(input);\n        Assert.Equal(expected, actual);\n    }\n\n    private static MimeMessage BuildMessage() {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.test\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.test\"));\n        message.Subject = \"subject\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        return message;\n    }\n\n    private sealed class FakeImapSentFolder : ImapSentMessageOperations.IImapSentFolder {\n        public string FullNameValue { get; set; } = \"INBOX\";\n        public bool IsOpenValue { get; set; }\n        public FolderAccess AccessValue { get; set; }\n        public int OpenCount { get; private set; }\n        public int CloseCount { get; private set; }\n        public int AppendCount { get; private set; }\n        public FolderAccess LastOpenAccess { get; private set; } = FolderAccess.ReadOnly;\n        public MessageFlags LastAppendFlags { get; private set; } = MessageFlags.None;\n        public Queue<IList<UniqueId>> SearchResults { get; } = new();\n        public List<SearchQuery> SearchQueries { get; } = new();\n        public Dictionary<UniqueId, string?> EnvelopeMessageIds { get; } = new();\n        public Dictionary<UniqueId, ImapSentMessageOperations.ImapThreadingMetadataResult?> ThreadingMetadataByUid { get; } = new();\n\n        public string FullName => FullNameValue;\n        public bool IsOpen => IsOpenValue;\n        public FolderAccess Access => AccessValue;\n\n        public Task OpenAsync(FolderAccess access, CancellationToken cancellationToken = default) {\n            OpenCount++;\n            LastOpenAccess = access;\n            AccessValue = access;\n            IsOpenValue = true;\n            return Task.CompletedTask;\n        }\n\n        public Task CloseAsync(bool expunge, CancellationToken cancellationToken = default) {\n            CloseCount++;\n            IsOpenValue = false;\n            return Task.CompletedTask;\n        }\n\n        public Task AppendAsync(MimeMessage message, MessageFlags flags, CancellationToken cancellationToken = default) {\n            _ = message ?? throw new ArgumentNullException(nameof(message));\n            AppendCount++;\n            LastAppendFlags = flags;\n            return Task.CompletedTask;\n        }\n\n        public Task<IList<UniqueId>> SearchAsync(SearchQuery query, CancellationToken cancellationToken = default) {\n            SearchQueries.Add(query);\n            if (SearchResults.Count > 0) {\n                return Task.FromResult(SearchResults.Dequeue());\n            }\n            return Task.FromResult<IList<UniqueId>>(new List<UniqueId>());\n        }\n\n        public Task<string?> FetchEnvelopeMessageIdAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            EnvelopeMessageIds.TryGetValue(uid, out var value);\n            return Task.FromResult(value);\n        }\n\n        public Task<ImapSentMessageOperations.ImapThreadingMetadataResult?> FetchThreadingMetadataAsync(UniqueId uid, CancellationToken cancellationToken = default) {\n            ThreadingMetadataByUid.TryGetValue(uid, out var value);\n            return Task.FromResult(value);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ImapSessionServiceTests.cs",
    "content": "using System.Threading.Tasks;\nusing MailKit.Net.Imap;\nusing MailKit.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ImapSessionServiceTests {\n    private sealed class FakeImapClient : ImapClient {\n        public bool AuthenticateCalled { get; set; }\n        public new bool Authenticated { get; set; }\n        private int _timeout;\n\n        public override bool IsAuthenticated => Authenticated;\n\n        public override int Timeout {\n            get => _timeout;\n            set => _timeout = value;\n        }\n\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, System.Threading.CancellationToken cancellationToken = default) {\n            return Task.CompletedTask;\n        }\n    }\n\n    [Fact]\n    public async Task ConnectAsync_UsesConnectionAndAuthenticationSettings() {\n        var fakeClient = new FakeImapClient();\n        var previousFactory = ImapConnector.ClientFactory;\n        try {\n            ImapConnector.ClientFactory = () => fakeClient;\n            var request = new ImapSessionRequest {\n                Connection = new ImapConnectionRequest(\n                    \"imap.example.test\",\n                    1993,\n                    SecureSocketOptions.SslOnConnect,\n                    timeout: 4321,\n                    retryCount: 0),\n                UserName = \"user@example.test\",\n                Secret = \"secret\",\n                AuthenticateAsync = (client, _) => {\n                    ((FakeImapClient)client).AuthenticateCalled = true;\n                    ((FakeImapClient)client).Authenticated = true;\n                    return Task.CompletedTask;\n                }\n            };\n\n            var client = await ImapSessionService.ConnectAsync(request);\n\n            Assert.Same(fakeClient, client);\n            Assert.True(fakeClient.AuthenticateCalled);\n            Assert.Equal(4321, fakeClient.Timeout);\n        } finally {\n            ImapConnector.ClientFactory = previousFactory;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/InternalLoggerTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class InternalLoggerTests\n{\n    [Fact]\n    public void WriteProgress_RaisesEventWithCorrectPercentage()\n    {\n        var logger = new Mailozaurr.InternalLogger();\n        int? percentage = null;\n        logger.OnProgressMessage += (_, e) => percentage = e.ProgressPercentage;\n\n        logger.WriteProgress(\"Activity\", \"Operation\", 42);\n\n        Assert.Equal(42, percentage);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/IsDisposableEmailTests.cs",
    "content": "using System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class IsDisposableEmailTests\n{\n    private static readonly MethodInfo Method = typeof(Validator).GetMethod(\"IsDisposableEmail\", BindingFlags.NonPublic | BindingFlags.Static)!;\n\n    [Fact]\n    public void IsDisposableEmail_ValidEmail_ReturnsTrue()\n    {\n        var result = (bool)Method.Invoke(null, new object[] { \"user@example.com\" })!;\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void IsDisposableEmail_MissingAt_ReturnsFalse()\n    {\n        var result = (bool)Method.Invoke(null, new object[] { \"invalidemail.com\" })!;\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsDisposableEmail_MultipleAt_ReturnsFalse()\n    {\n        var result = (bool)Method.Invoke(null, new object[] { \"user@@example.com\" })!;\n        Assert.False(result);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/JsonMailDraftExchangeServiceTests.cs",
    "content": "using System.Text.Json;\nusing Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class JsonMailDraftExchangeServiceTests {\n    [Fact]\n    public async Task SaveAndLoadRoundTripsDraftJson() {\n        var service = new JsonMailDraftExchangeService();\n        var path = CreateTemporaryFilePath(\"draft.json\");\n        var draft = new MailDraft {\n            Id = \"draft-1\",\n            Name = \"Quarterly report\",\n            Message = new DraftMessage {\n                ProfileId = \"work-gmail\",\n                Subject = \"Hello\",\n                To = {\n                    new MessageRecipient { Address = \"alice@example.com\" }\n                }\n            }\n        };\n\n        await service.SaveAsync(path, draft);\n        var loaded = await service.LoadAsync(path);\n\n        Assert.Equal(\"draft-1\", loaded.Id);\n        Assert.Equal(\"Quarterly report\", loaded.Name);\n        Assert.Equal(\"work-gmail\", loaded.Message.ProfileId);\n        Assert.Equal(\"alice@example.com\", loaded.Message.To[0].Address);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/JsonMailMessageActionPlanExchangeServiceTests.cs",
    "content": "using Mailozaurr.Application;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class JsonMailMessageActionPlanExchangeServiceTests {\n    [Fact]\n    public async Task SaveAndLoadRoundTripsSinglePlanJson() {\n        var service = new JsonMailMessageActionPlanExchangeService();\n        var path = CreateTemporaryFilePath(\"plan.json\");\n        var plan = new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"move\",\n            ExecutionKind = \"Move\",\n            ProfileId = \"work-gmail\",\n            MailboxId = \"primary\",\n            FolderId = \"Inbox\",\n            RequestedCount = 2,\n            UniqueMessageCount = 1,\n            RequestedDestinationFolderId = \"Archive\",\n            MessageIds = { \"msg-1\" }\n        };\n\n        await service.SaveAsync(path, plan);\n        var loaded = await service.LoadAsync(path);\n\n        Assert.Equal(\"move\", loaded.Action);\n        Assert.Equal(\"Move\", loaded.ExecutionKind);\n        Assert.Equal(\"work-gmail\", loaded.ProfileId);\n        Assert.Equal(\"Archive\", loaded.RequestedDestinationFolderId);\n        Assert.Equal(new[] { \"msg-1\" }, loaded.MessageIds);\n    }\n\n    [Fact]\n    public async Task SaveAndLoadRoundTripsBatchJson() {\n        var service = new JsonMailMessageActionPlanExchangeService();\n        var path = CreateTemporaryFilePath(\"plans.json\");\n        var plans = new[] {\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"mark-read\",\n                ExecutionKind = \"SetReadState\",\n                ProfileId = \"work-gmail\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                DesiredState = true,\n                MessageIds = { \"msg-1\" }\n            },\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"delete\",\n                ExecutionKind = \"Delete\",\n                ProfileId = \"work-gmail\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                MessageIds = { \"msg-2\" }\n            }\n        };\n\n        await service.SaveBatchAsync(path, plans);\n        var loaded = await service.LoadBatchAsync(path);\n\n        Assert.Equal(2, loaded.Count);\n        Assert.Equal(\"mark-read\", loaded[0].Action);\n        Assert.Equal(\"delete\", loaded[1].Action);\n    }\n\n    private static string CreateTemporaryFilePath(string fileName) {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        return Path.Combine(directory, fileName);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/JunkCleanerTests.cs",
    "content": "using MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\nusing Moq;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class JunkCleanerTests {\n    [Fact]\n    public async Task GetImapJunkAsync_SingleUseSkipEnumerables_AreEnumeratedOnce() {\n        var keepUid = new UniqueId(1);\n        var skipUid = new UniqueId(2);\n        var keepMessage = CreateMessage(\"keep@example.com\", \"keep-recipient@example.com\", \"normal\", \"keep-id\");\n        var filteredMessage = CreateMessage(\"skip-from@example.com\", \"skip-to@example.com\", \"urgent subject\", \"skip-id\");\n\n        var folder = new Mock<IMailFolder>();\n        folder.SetupGet(f => f.FullName).Returns(\"Junk\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);\n        folder.Setup(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(new List<UniqueId> { keepUid, skipUid });\n        folder.Setup(f => f.GetMessageAsync(keepUid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(keepMessage);\n        folder.Setup(f => f.GetMessageAsync(skipUid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(filteredMessage);\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == \"Inbox\"));\n        client.Setup(c => c.GetFolder(\"Junk\", It.IsAny<CancellationToken>())).Returns(folder.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        var skipFrom = new SingleUseEnumerable<string>(\"skip-from@example.com\");\n        var skipTo = new SingleUseEnumerable<string>(\"skip-to@example.com\");\n        var skipSubject = new SingleUseEnumerable<string>(\"urgent\");\n        var skipMessageId = new SingleUseEnumerable<string>(\"skip-id\");\n\n        var results = new List<ImapEmailMessage>();\n        await foreach (var message in JunkCleaner.GetImapJunkAsync(\n            client.Object,\n            folder: \"Junk\",\n            skipFrom: skipFrom,\n            skipTo: skipTo,\n            skipSubjectContains: skipSubject,\n            skipMessageId: skipMessageId)) {\n            results.Add(message);\n        }\n\n        var remaining = Assert.Single(results);\n        Assert.Equal(keepUid, remaining.Uid);\n        Assert.Equal(1, skipFrom.EnumerationCount);\n        Assert.Equal(1, skipTo.EnumerationCount);\n        Assert.Equal(1, skipSubject.EnumerationCount);\n        Assert.Equal(1, skipMessageId.EnumerationCount);\n    }\n\n    [Fact]\n    public async Task GetImapJunkAsync_NormalizesAttachmentExtensionsBeforeFiltering() {\n        var keepUid = new UniqueId(1);\n        var skipUid = new UniqueId(2);\n        var keepMessage = CreateMessage(\"keep@example.com\", \"keep-recipient@example.com\", \"normal\", \"keep-id\");\n        var filteredMessage = CreateMessage(\"sender@example.com\", \"recipient@example.com\", \"subject\", \"skip-id\", \"invoice.pdf\");\n\n        var folder = new Mock<IMailFolder>();\n        folder.SetupGet(f => f.FullName).Returns(\"Junk\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);\n        folder.Setup(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(new List<UniqueId> { keepUid, skipUid });\n        folder.Setup(f => f.GetMessageAsync(keepUid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(keepMessage);\n        folder.Setup(f => f.GetMessageAsync(skipUid, It.IsAny<CancellationToken>(), null))\n            .ReturnsAsync(filteredMessage);\n\n        var client = new Mock<ImapClient> { CallBase = true };\n        client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == \"Inbox\"));\n        client.Setup(c => c.GetFolder(\"Junk\", It.IsAny<CancellationToken>())).Returns(folder.Object);\n        client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());\n\n        var results = new List<ImapEmailMessage>();\n        await foreach (var message in JunkCleaner.GetImapJunkAsync(\n            client.Object,\n            folder: \"Junk\",\n            skipAttachmentExtension: new[] { \".pdf\" })) {\n            results.Add(message);\n        }\n\n        var remaining = Assert.Single(results);\n        Assert.Equal(keepUid, remaining.Uid);\n    }\n\n    private static MimeMessage CreateMessage(\n        string from,\n        string to,\n        string subject,\n        string messageId,\n        string? attachmentName = null) {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(from));\n        message.To.Add(MailboxAddress.Parse(to));\n        message.Subject = subject;\n        message.MessageId = messageId;\n\n        if (attachmentName == null) {\n            message.Body = new TextPart(\"plain\") { Text = \"body\" };\n            return message;\n        }\n\n        var builder = new BodyBuilder { TextBody = \"body\" };\n        builder.Attachments.Add(attachmentName, new byte[] { 1, 2, 3 });\n        message.Body = builder.ToMessageBody();\n        return message;\n    }\n\n    private sealed class SingleUseEnumerable<T> : IEnumerable<T> {\n        private readonly IReadOnlyList<T> items;\n\n        public SingleUseEnumerable(params T[] items) {\n            this.items = items;\n        }\n\n        public int EnumerationCount { get; private set; }\n\n        public IEnumerator<T> GetEnumerator() {\n            EnumerationCount++;\n            if (EnumerationCount > 1) {\n                throw new InvalidOperationException(\"Sequence was enumerated more than once.\");\n            }\n\n            return items.GetEnumerator();\n        }\n\n        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/LoggingConfiguratorDisposeTests.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class LoggingConfiguratorDisposeTests\n{\n    [Fact]\n    public void Dispose_ReleasesResources()\n    {\n        var configurator = new LoggingConfigurator();\n        configurator.ConfigureLogging(string.Empty, false, true, false, false);\n\n        var stream = configurator.LogStream!;\n        var logger = configurator.ProtocolLogger!;\n\n        configurator.Dispose();\n\n        Assert.Null(configurator.LogStream);\n        Assert.Null(configurator.ProtocolLogger);\n        Assert.Throws<ObjectDisposedException>(() => stream.WriteByte(0));\n        Assert.Throws<ObjectDisposedException>(() => logger.LogClient(new byte[] { 0x41 }, 0, 1));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/LoggingConfiguratorTests.cs",
    "content": "using System.IO;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class LoggingConfiguratorTests\n{\n    [Fact]\n    public void ConfigureLogging_NullPath_UsesMemoryStream()\n    {\n        var configurator = new LoggingConfigurator();\n        configurator.ConfigureLogging(null, false, true, false, false);\n\n        Assert.Null(configurator.LogPath);\n        Assert.NotNull(configurator.LogStream);\n        Assert.NotNull(configurator.ProtocolLogger);\n    }\n\n    [Fact]\n    public void ConfigureLogging_EmptyPath_UsesMemoryStream()\n    {\n        var configurator = new LoggingConfigurator();\n        configurator.ConfigureLogging(string.Empty, false, true, false, false);\n\n        Assert.Equal(string.Empty, configurator.LogPath);\n        Assert.NotNull(configurator.LogStream);\n        Assert.NotNull(configurator.ProtocolLogger);\n    }\n\n    [Fact]\n    public void ConfigureLogging_ValidPath_CreatesFileLogger()\n    {\n        var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var configurator = new LoggingConfigurator();\n        try\n        {\n            configurator.ConfigureLogging(path, false, false, false, false);\n\n            Assert.Equal(path, configurator.LogPath);\n            Assert.Null(configurator.LogStream);\n            Assert.NotNull(configurator.ProtocolLogger);\n            Assert.True(File.Exists(path));\n        }\n        finally\n        {\n            configurator.ProtocolLogger?.Dispose();\n            if (File.Exists(path))\n            {\n                File.Delete(path);\n            }\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/LoggingConfiguratorValidationTests.cs",
    "content": "using System;\nusing System.IO;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class LoggingConfiguratorValidationTests {\n    [Fact]\n    public void ConfigureLogging_StripsNewLinesInPrefixes_AndCreatesDirectory() {\n        var warnings = new System.Collections.Generic.List<string>();\n        var logger = new InternalLogger();\n        logger.OnWarningMessage += (_, e) => warnings.Add(e.FullMessage);\n        LoggingMessages.Logger = logger;\n\n        var tempDir = Path.Combine(Path.GetTempPath(), \"mlz-logs\", Guid.NewGuid().ToString(\"N\"));\n        var path = Path.Combine(tempDir, \"log.txt\");\n        var configurator = new LoggingConfigurator();\n        try {\n            configurator.ConfigureLogging(path, logConsole: false, logObject: false, logTimestamps: true, logSecrets: false, logTimestampsFormat: \"O\", logServerPrefix: \"srv\\n\\r\", logClientPrefix: \"cli\\n\");\n\n            Assert.True(File.Exists(path));\n            Assert.Contains(warnings, m => m.IndexOf(\"prefix contains new lines\", StringComparison.OrdinalIgnoreCase) >= 0);\n        } finally {\n            configurator.ProtocolLogger?.Dispose();\n            if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/LoggingMessagesTests.cs",
    "content": "using System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class LoggingMessagesTests\n{\n    [Fact]\n    public async Task Logger_IsAsyncLocalPerTask()\n    {\n        var original = LoggingMessages.Logger;\n        InternalLogger? task1Logger = null;\n        InternalLogger? task2Logger = null;\n\n        await Task.WhenAll(\n            Task.Run(() =>\n            {\n                LoggingMessages.Logger = new InternalLogger();\n                task1Logger = LoggingMessages.Logger;\n            }),\n            Task.Run(() =>\n            {\n                LoggingMessages.Logger = new InternalLogger();\n                task2Logger = LoggingMessages.Logger;\n            })\n        );\n\n        Assert.NotSame(task1Logger, task2Logger);\n        Assert.Same(original, LoggingMessages.Logger);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MailFileReaderTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Linq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class MailFileReaderTests {\n    [Fact]\n    public void ReadEml_MapsBasicFieldsAndHeaders() {\n        var tempDir = CreateTempDirectory();\n        try {\n            var emlPath = CreateEmlFile(tempDir, \"sample.eml\", \"Test subject\", \"Hello\", \"X-Mailozaurr-Test: Value\");\n            var options = new MailFileReaderOptions {\n                IncludeHeaders = true,\n                IncludeAttachments = false,\n                IncludeAttachmentContent = false\n            };\n\n            var message = MailFileReader.Read(emlPath, options);\n\n            Assert.Equal(MailFileFormat.Eml, message.Format);\n            Assert.Equal(\"Test subject\", message.Subject);\n            Assert.Equal(\"alice@example.com\", message.From?.Address);\n            Assert.Single(message.To);\n            Assert.Equal(\"bob@example.com\", message.To[0].Address);\n            Assert.Contains(\"Hello\", message.BodyText ?? string.Empty);\n            Assert.NotNull(message.Headers);\n            Assert.True(message.Headers!.TryGetValue(\"X-Mailozaurr-Test\", out var headerValue));\n            Assert.Contains(\"Value\", headerValue);\n        } finally {\n            Directory.Delete(tempDir, true);\n        }\n    }\n\n    [Fact]\n    public void TryRead_MissingFile_ReturnsFalse() {\n        var tempDir = CreateTempDirectory();\n        try {\n            var missingPath = Path.Combine(tempDir, \"missing.eml\");\n            var result = MailFileReader.TryRead(missingPath, out var message, out var error);\n\n            Assert.False(result);\n            Assert.Null(message);\n            Assert.NotNull(error);\n            Assert.Contains(\"doesn't exist\", error!, StringComparison.OrdinalIgnoreCase);\n        } finally {\n            Directory.Delete(tempDir, true);\n        }\n    }\n\n    [Fact]\n    public void TryRead_UnsupportedExtension_ReturnsFalse() {\n        var tempDir = CreateTempDirectory();\n        try {\n            var txtPath = Path.Combine(tempDir, \"sample.txt\");\n            File.WriteAllText(txtPath, \"data\");\n\n            var result = MailFileReader.TryRead(txtPath, out var message, out var error);\n\n            Assert.False(result);\n            Assert.Null(message);\n            Assert.NotNull(error);\n            Assert.Contains(\"not a .msg or .eml\", error!, StringComparison.OrdinalIgnoreCase);\n        } finally {\n            Directory.Delete(tempDir, true);\n        }\n    }\n\n    [Fact]\n    public void ReadMsg_FromConvertedEml_MapsFields() {\n        var tempDir = CreateTempDirectory();\n        var outputDir = CreateTempDirectory();\n        try {\n            var emlPath = CreateEmlFile(tempDir, \"sample.eml\", \"Msg subject\", \"Hello from msg\");\n            var msgPath = Path.Combine(outputDir, \"sample.msg\");\n\n            var conversion = EmailMessage.ConvertEmlToMsg(new FileInfo(emlPath), new FileInfo(msgPath), true);\n            Assert.True(conversion.Status);\n            Assert.True(File.Exists(msgPath));\n\n            var message = MailFileReader.Read(msgPath);\n\n            Assert.Equal(MailFileFormat.Msg, message.Format);\n            Assert.Equal(\"Msg subject\", message.Subject);\n            Assert.Equal(\"alice@example.com\", message.From?.Address);\n            Assert.Single(message.To);\n            Assert.Equal(\"bob@example.com\", message.To[0].Address);\n        } finally {\n            Directory.Delete(tempDir, true);\n            Directory.Delete(outputDir, true);\n        }\n    }\n\n    private static string CreateTempDirectory() {\n        var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        Directory.CreateDirectory(path);\n        return path;\n    }\n\n    private static string CreateEmlFile(string directory, string fileName, string subject, string body, string? extraHeader = null) {\n        var lines = new[] {\n            \"From: Alice <alice@example.com>\",\n            \"To: Bob <bob@example.com>\",\n            $\"Subject: {subject}\",\n            \"Date: Mon, 21 Jun 2021 10:00:00 +0000\",\n            \"Message-ID: <test@example.com>\",\n            \"MIME-Version: 1.0\",\n            \"Content-Type: text/plain; charset=utf-8\"\n        };\n\n        var headerBlock = string.Join(\"\\r\\n\", lines);\n        if (!string.IsNullOrWhiteSpace(extraHeader)) {\n            headerBlock = string.Concat(headerBlock, \"\\r\\n\", extraHeader);\n        }\n\n        var content = string.Concat(headerBlock, \"\\r\\n\\r\\n\", body);\n        var path = Path.Combine(directory, fileName);\n        File.WriteAllText(path, content);\n        return path;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MailMcpToolsTests.cs",
    "content": "#if NET8_0_OR_GREATER\nusing Mailozaurr.Application;\nusing Mailozaurr.Cli.Mcp;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class MailMcpToolsTests {\n    [Fact]\n    public async Task MailProfilesListReturnsConfiguredProfiles() {\n        using var fixture = new TestFixture();\n\n        var profiles = await fixture.Tools.mail_profiles_list();\n\n        var profile = Assert.Single(profiles);\n        Assert.Equal(\"gmail-work\", profile.Id);\n        Assert.Equal(MailProfileKind.Gmail, profile.Kind);\n    }\n\n    [Fact]\n    public async Task MailProfilesSummaryListReturnsAggregatedOverviews() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultSender = \"user@example.com\",\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"token\");\n\n        var overviews = await fixture.Tools.mail_profiles_summary_list();\n\n        var overview = Assert.Single(overviews);\n        Assert.Equal(\"gmail-work\", overview.Profile.Id);\n        Assert.True(overview.SupportsRead);\n        Assert.True(overview.SupportsSend);\n        Assert.True(overview.IsReady);\n        Assert.Equal(\"gmail-work\", fixture.ProfileAuthService.LastStatusProfileId);\n    }\n\n    [Fact]\n    public async Task MailProfilesSummaryCompactListReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultSender = \"user@example.com\",\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"token\");\n\n        var overviews = await fixture.Tools.mail_profiles_summary_compact_list();\n\n        var overview = Assert.Single(overviews);\n        Assert.Equal(\"gmail-work\", overview.Id);\n        Assert.Equal(\"interactive\", overview.AuthMode);\n        Assert.True(overview.IsReady);\n    }\n\n    [Fact]\n    public async Task MailProfilesSummaryListSupportsSharedFilters() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"smtp-alerts\",\n            DisplayName = \"Alerts SMTP\",\n            Kind = MailProfileKind.Smtp,\n            DefaultSender = \"alerts@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n\n        var overviews = await fixture.Tools.mail_profiles_summary_list(kind: \"smtp\", canSendOnly: true);\n\n        var overview = Assert.Single(overviews);\n        Assert.Equal(\"smtp-alerts\", overview.Profile.Id);\n        Assert.True(overview.SupportsSend);\n        Assert.False(overview.SupportsRead);\n    }\n\n    [Fact]\n    public async Task MailProfilesSummaryListSupportsSharedSorting() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"a-smtp\",\n            DisplayName = \"SMTP A\",\n            Kind = MailProfileKind.Smtp,\n            DefaultSender = \"alerts@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Server] = \"smtp.example.com\"\n            }\n        });\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"z-gmail\",\n            DisplayName = \"Gmail Z\",\n            Kind = MailProfileKind.Gmail,\n            DefaultSender = \"user@example.com\",\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"z-gmail\", MailSecretNames.AccessToken, \"token\");\n\n        var overviews = await fixture.Tools.mail_profiles_summary_list(sortBy: \"kind\", descending: true);\n\n        Assert.Equal(3, overviews.Count);\n        Assert.Equal(\"a-smtp\", overviews[0].Profile.Id);\n    }\n\n    [Fact]\n    public async Task MailCapabilitiesGetReturnsEffectiveCapabilities() {\n        using var fixture = new TestFixture();\n\n        var capabilities = await fixture.Tools.mail_capabilities_get(\"gmail-work\");\n\n        Assert.Equal(MailProfileKind.Gmail, capabilities.Kind);\n        Assert.True(capabilities.Supports(MailCapability.SendMessages));\n        Assert.True(capabilities.Supports(MailCapability.SearchMessages));\n    }\n\n    [Fact]\n    public async Task MailProfileSaveCreatesProfileWithSettings() {\n        using var fixture = new TestFixture();\n\n        var profile = await fixture.Tools.mail_profile_save(\n            profileId: \"graph-work\",\n            kind: \"graph\",\n            displayName: \"Work Graph\",\n            description: \"Microsoft 365 profile\",\n            defaultSender: \"graph@example.com\",\n            defaultMailbox: \"graph@example.com\",\n            settings: new Dictionary<string, string> {\n                [\"tenant-id\"] = \"tenant-1\",\n                [\"client-id\"] = \"client-1\"\n            });\n\n        Assert.Equal(\"graph-work\", profile.Id);\n        Assert.Equal(MailProfileKind.Graph, profile.Kind);\n        Assert.Equal(\"Microsoft 365 profile\", profile.Description);\n        Assert.Equal(\"tenant-1\", profile.Settings[\"tenant-id\"]);\n        Assert.Equal(\"client-1\", profile.Settings[\"client-id\"]);\n    }\n\n    [Fact]\n    public async Task MailProfileGraphBootstrapCreatesProfileAndStoresSecrets() {\n        using var fixture = new TestFixture();\n\n        var profile = await fixture.Tools.mail_profile_graph_bootstrap(\n            profileId: \"graph-work\",\n            displayName: \"Work Graph\",\n            mailbox: \"shared@example.com\",\n            clientId: \"client-id\",\n            tenantId: \"tenant-id\",\n            clientSecret: \"client-secret\");\n        var storedSecret = await fixture.SecretStore.GetSecretAsync(\"graph-work\", MailSecretNames.ClientSecret);\n\n        Assert.Equal(\"graph-work\", profile.Id);\n        Assert.Equal(MailProfileKind.Graph, profile.Kind);\n        Assert.Equal(\"shared@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"shared@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"tenant-id\", profile.Settings[MailProfileSettingsKeys.TenantId]);\n        Assert.Equal(\"client-secret\", storedSecret);\n    }\n\n    [Fact]\n    public async Task MailProfileGraphBootstrapSupportsSecretReferences() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"shared-secrets\",\n            DisplayName = \"Shared Secrets\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"shared-client\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.ClientSecret, \"shared-client-secret\");\n\n        var profile = await fixture.Tools.mail_profile_graph_bootstrap(\n            profileId: \"graph-ref\",\n            displayName: \"Graph Ref\",\n            mailbox: \"shared@example.com\",\n            clientId: \"client-id\",\n            tenantId: \"tenant-id\",\n            clientSecretReference: $\"shared-secrets:{MailSecretNames.ClientSecret}\");\n        var storedSecret = await fixture.SecretStore.GetSecretAsync(\"graph-ref\", MailSecretNames.ClientSecret);\n\n        Assert.Equal(\"graph-ref\", profile.Id);\n        Assert.Equal(\"shared-client-secret\", storedSecret);\n    }\n\n    [Fact]\n    public async Task MailProfileGmailBootstrapCreatesProfileAndStoresSecrets() {\n        using var fixture = new TestFixture();\n\n        var profile = await fixture.Tools.mail_profile_gmail_bootstrap(\n            profileId: \"gmail-work\",\n            displayName: \"Work Gmail\",\n            mailbox: \"me@example.com\",\n            clientId: \"client-id\",\n            clientSecret: \"client-secret\",\n            refreshToken: \"refresh-token\");\n        var clientSecret = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.ClientSecret);\n        var refreshToken = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken);\n\n        Assert.Equal(\"gmail-work\", profile.Id);\n        Assert.Equal(MailProfileKind.Gmail, profile.Kind);\n        Assert.Equal(\"me@example.com\", profile.DefaultMailbox);\n        Assert.Equal(\"me@example.com\", profile.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.Equal(\"client-id\", profile.Settings[MailProfileSettingsKeys.ClientId]);\n        Assert.Equal(\"client-secret\", clientSecret);\n        Assert.Equal(\"refresh-token\", refreshToken);\n    }\n\n    [Fact]\n    public async Task MailProfileDoctorReportsMissingGmailAuthenticationMaterial() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-broken\",\n            DisplayName = \"Broken Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultMailbox = \"me@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"me@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n\n        var result = await fixture.Tools.mail_profile_doctor(\"gmail-broken\");\n\n        Assert.False(result.Succeeded);\n        Assert.Equal(\"profile_not_ready\", result.Code);\n        Assert.Contains(result.Errors, error => error.IndexOf(\"Gmail profiles need an access token\", StringComparison.Ordinal) >= 0);\n    }\n\n    [Fact]\n    public async Task MailProfileValidateRunsStructuralValidationOnly() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-structural\",\n            DisplayName = \"Gmail Structural\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"me@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n\n        var result = await fixture.Tools.mail_profile_validate(\"gmail-structural\");\n\n        Assert.True(result.Succeeded);\n        Assert.Null(result.Code);\n    }\n\n    [Fact]\n    public async Task MailProfileGmailLoginPersistsTokens() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-login\",\n            DisplayName = \"Gmail Login\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-login\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        var result = await fixture.Tools.mail_profile_gmail_login(\"gmail-login\");\n        var accessToken = await fixture.SecretStore.GetSecretAsync(\"gmail-login\", MailSecretNames.AccessToken);\n        var refreshToken = await fixture.SecretStore.GetSecretAsync(\"gmail-login\", MailSecretNames.RefreshToken);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"gmail-login\", result.ProfileId);\n        Assert.Equal(\"gmail-access-token\", accessToken);\n        Assert.Equal(\"gmail-refresh-token\", refreshToken);\n        Assert.NotNull(fixture.ProfileAuthService.LastGmailRequest);\n        Assert.Equal(\"user@gmail.com\", fixture.ProfileAuthService.LastGmailRequest!.GmailAccount);\n    }\n\n    [Fact]\n    public async Task MailProfileGmailLoginSupportsSameProfileSecretReference() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-login-ref\",\n            DisplayName = \"Gmail Login Ref\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@gmail.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-login-ref\", MailSecretNames.ClientSecret, \"client-secret-from-store\");\n\n        var result = await fixture.Tools.mail_profile_gmail_login(\n            \"gmail-login-ref\",\n            clientSecretReference: MailSecretNames.ClientSecret);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.ProfileAuthService.LastGmailRequest);\n        Assert.Equal(\"client-secret-from-store\", fixture.ProfileAuthService.LastGmailRequest!.ClientSecret);\n    }\n\n    [Fact]\n    public async Task MailProfileGraphLoginPersistsAccessToken() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"graph-login\",\n            DisplayName = \"Graph Login\",\n            Kind = MailProfileKind.Graph,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.ClientId] = \"client-id\",\n                [MailProfileSettingsKeys.TenantId] = \"tenant-id\"\n            }\n        });\n\n        var result = await fixture.Tools.mail_profile_graph_login(\"graph-login\", login: \"user@example.com\");\n        var accessToken = await fixture.SecretStore.GetSecretAsync(\"graph-login\", MailSecretNames.AccessToken);\n        var profile = await fixture.ProfileStore.GetByIdAsync(\"graph-login\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"graph-login\", result.ProfileId);\n        Assert.Equal(\"graph-access-token\", accessToken);\n        Assert.NotNull(profile);\n        Assert.Equal(\"user@example.com\", profile!.Settings[MailProfileSettingsKeys.Mailbox]);\n        Assert.NotNull(fixture.ProfileAuthService.LastGraphRequest);\n        Assert.Equal(\"user@example.com\", fixture.ProfileAuthService.LastGraphRequest!.Login);\n    }\n\n    [Fact]\n    public async Task MailProfileRefreshAuthDelegatesToSharedAuthService() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-refresh\",\n            DisplayName = \"Gmail Refresh\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"refresh@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-refresh\", MailSecretNames.ClientSecret, \"client-secret\");\n\n        var result = await fixture.Tools.mail_profile_refresh_auth(\"gmail-refresh\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, fixture.ProfileAuthService.RefreshCalls);\n        Assert.Equal(\"gmail-refresh\", fixture.ProfileAuthService.LastRefreshProfileId);\n    }\n\n    [Fact]\n    public async Task MailProfileAuthStatusDelegatesToSharedAuthService() {\n        using var fixture = new TestFixture();\n\n        var status = await fixture.Tools.mail_profile_auth_status(\"gmail-work\");\n\n        Assert.Equal(\"gmail-work\", status.ProfileId);\n        Assert.Equal(\"gmail-work\", fixture.ProfileAuthService.LastStatusProfileId);\n        Assert.Equal(\"interactive\", status.Mode);\n    }\n\n    [Fact]\n    public async Task MailProfileSummaryAggregatesSharedOverview() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultSender = \"user@example.com\",\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"token\");\n\n        var overview = await fixture.Tools.mail_profile_summary(\"gmail-work\");\n\n        Assert.Equal(\"gmail-work\", overview.Profile.Id);\n        Assert.True(overview.SupportsRead);\n        Assert.True(overview.SupportsSend);\n        Assert.True(overview.IsReady);\n        Assert.Equal(\"gmail-work\", fixture.ProfileAuthService.LastStatusProfileId);\n    }\n\n    [Fact]\n    public async Task MailProfileSummaryCompactReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"gmail-work\",\n            DisplayName = \"Work Gmail\",\n            Kind = MailProfileKind.Gmail,\n            DefaultSender = \"user@example.com\",\n            DefaultMailbox = \"user@example.com\",\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"user@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"client-id\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"gmail-work\", MailSecretNames.AccessToken, \"token\");\n\n        var overview = await fixture.Tools.mail_profile_summary_compact(\"gmail-work\");\n\n        Assert.Equal(\"gmail-work\", overview.Id);\n        Assert.Equal(\"Work Gmail\", overview.DisplayName);\n        Assert.True(overview.SupportsSend);\n        Assert.True(overview.IsReady);\n    }\n\n    [Fact]\n    public async Task MailProfileTestDelegatesToSharedConnectionService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_profile_test(\"gmail-work\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"gmail-work\", fixture.ProfileConnectionService.LastProfileId);\n        Assert.Equal(\"getProfile\", result.Probe);\n    }\n\n    [Fact]\n    public async Task MailProfileTestParsesRequestedScope() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_profile_test(\"gmail-work\", scope: \"send\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(MailProfileConnectionTestScope.Send, fixture.ProfileConnectionService.LastScope);\n        Assert.Equal(MailProfileConnectionTestScope.Send, result.RequestedScope);\n    }\n\n    [Fact]\n    public async Task MailProfileSetDefaultMarksProfileAsDefault() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_profile_save(\n            profileId: \"graph-work\",\n            kind: \"graph\",\n            displayName: \"Work Graph\");\n\n        var result = await fixture.Tools.mail_profile_set_default(\"graph-work\");\n        var profile = await fixture.Tools.mail_profile_get(\"graph-work\");\n\n        Assert.True(result.Succeeded);\n        Assert.True(profile.IsDefault);\n    }\n\n    [Fact]\n    public async Task MailProfileSecretSetAndRemoveUseSharedSecretStore() {\n        using var fixture = new TestFixture();\n\n        var setResult = await fixture.Tools.mail_profile_secret_set(\"gmail-work\", \"refresh-token\", \"secret-value\");\n        var storedSecret = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", \"refresh-token\");\n        var removeResult = await fixture.Tools.mail_profile_secret_remove(\"gmail-work\", \"refresh-token\");\n        var removedSecret = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", \"refresh-token\");\n\n        Assert.True(setResult.Succeeded);\n        Assert.Equal(\"secret-value\", storedSecret);\n        Assert.True(removeResult.Succeeded);\n        Assert.Null(removedSecret);\n    }\n\n    [Fact]\n    public async Task MailProfileSecretSetSupportsReferenceCopy() {\n        using var fixture = new TestFixture();\n        await fixture.ProfileStore.SaveAsync(new MailProfile {\n            Id = \"shared-secrets\",\n            DisplayName = \"Shared Secrets\",\n            Kind = MailProfileKind.Gmail,\n            Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {\n                [MailProfileSettingsKeys.Mailbox] = \"shared@example.com\",\n                [MailProfileSettingsKeys.ClientId] = \"shared-client\"\n            }\n        });\n        await fixture.SecretStore.SetSecretAsync(\"shared-secrets\", MailSecretNames.RefreshToken, \"copied-secret\");\n\n        var setResult = await fixture.Tools.mail_profile_secret_set(\n            profileId: \"gmail-work\",\n            secretName: MailSecretNames.RefreshToken,\n            secretReference: $\"shared-secrets:{MailSecretNames.RefreshToken}\");\n        var storedSecret = await fixture.SecretStore.GetSecretAsync(\"gmail-work\", MailSecretNames.RefreshToken);\n\n        Assert.True(setResult.Succeeded);\n        Assert.Equal(\"copied-secret\", storedSecret);\n    }\n\n    [Fact]\n    public async Task MailProfileDeleteRemovesProfile() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_profile_save(\n            profileId: \"graph-work\",\n            kind: \"graph\",\n            displayName: \"Work Graph\");\n\n        var result = await fixture.Tools.mail_profile_delete(\"graph-work\");\n        var profiles = await fixture.Tools.mail_profiles_list();\n\n        Assert.True(result.Succeeded);\n        Assert.DoesNotContain(profiles, profile => profile.Id == \"graph-work\");\n    }\n\n    [Fact]\n    public async Task MailSearchDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_search(\n            \"gmail-work\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            queryText: \"invoice\",\n            subjectContains: \"Quarterly\",\n            fromContains: \"billing@example.com\",\n            toContains: \"team@example.com\",\n            hasAttachments: true,\n            limit: 5);\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"message-1\", result.Id);\n        Assert.NotNull(fixture.ReadService.LastSearchRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastSearchRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastSearchRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastSearchRequest.FolderId);\n        Assert.Equal(\"invoice\", fixture.ReadService.LastSearchRequest.QueryText);\n        Assert.True(fixture.ReadService.LastSearchRequest.HasAttachments);\n        Assert.Equal(5, fixture.ReadService.LastSearchRequest.Limit);\n    }\n\n    [Fact]\n    public async Task MailFoldersCompactDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_folders_compact_list(\n            \"gmail-work\",\n            mailboxId: \"primary\",\n            parentFolderId: \"root\",\n            rootOnly: true);\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"Inbox\", result.Id);\n        Assert.NotNull(fixture.ReadService.LastFolderCompactQuery);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastFolderCompactQuery!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastFolderCompactQuery.MailboxId);\n        Assert.Equal(\"root\", fixture.ReadService.LastFolderCompactQuery.ParentFolderId);\n        Assert.True(fixture.ReadService.LastFolderCompactQuery.RootOnly);\n    }\n\n    [Fact]\n    public async Task MailFolderAliasesListDelegatesToSharedFolderAliasService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_folder_aliases_list(\"gmail-work\", mailboxId: \"primary\");\n\n        var archive = Assert.Single(results, result => result.Alias == MailFolderAliases.Archive);\n        Assert.True(archive.IsResolved);\n        Assert.Equal(\"archive\", archive.FolderId);\n        Assert.NotNull(fixture.ReadService.LastFolderQuery);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastFolderQuery!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastFolderQuery.MailboxId);\n    }\n\n    [Fact]\n    public async Task MailFolderResolveDelegatesToSharedFolderAliasService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_folder_resolve(\"gmail-work\", \"archive\", mailboxId: \"primary\");\n\n        Assert.True(result.IsAlias);\n        Assert.Equal(MailFolderAliases.Archive, result.Alias);\n        Assert.Equal(\"archive\", result.EffectiveFolderId);\n        Assert.NotNull(fixture.ReadService.LastFolderQuery);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastFolderQuery!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastFolderQuery.MailboxId);\n    }\n\n    [Fact]\n    public async Task MailMovePreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_move_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            \"archive\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, result.RequestedCount);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.NotNull(result.Destination);\n        Assert.Equal(\"archive\", result.Destination!.EffectiveFolderId);\n        Assert.NotNull(result.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailActionsPreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_actions_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(4, result.IncludedActionCount);\n        Assert.Equal(4, result.SucceededActionCount);\n        Assert.Equal(0, result.FailedActionCount);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.Contains(result.Actions, action => action.Action == \"archive\" && action.Destination!.EffectiveFolderId == \"archive\");\n        Assert.Contains(result.Actions, action => action.Action == \"move\" && action.Destination!.EffectiveFolderId == \"Projects/2026\");\n        Assert.Contains(result.Actions, action => action.Action == \"delete\" && action.Succeeded);\n        Assert.All(result.Actions, action => Assert.NotNull(action.ConfirmationToken));\n    }\n\n    [Fact]\n    public async Task MailActionsBundlePreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_actions_bundle_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(8, result.IncludedActionCount);\n        Assert.Equal(8, result.SucceededActionCount);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.Contains(result.Actions, action => action.Action == \"mark-read\" && action.DesiredState == true);\n        Assert.Contains(result.Actions, action => action.Action == \"mark-unread\" && action.DesiredState == false);\n        Assert.Contains(result.Actions, action => action.Action == \"flag\" && action.DesiredState == true);\n        Assert.Contains(result.Actions, action => action.Action == \"unflag\" && action.DesiredState == false);\n        Assert.Contains(result.Actions, action => action.Action == \"move\" && action.Destination!.EffectiveFolderId == \"Projects/2026\");\n    }\n\n    [Fact]\n    public async Task MailActionPlanUsesSharedPlanningService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_plan(\n            action: \"move\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\", \"MESSAGE-1\" },\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"Move\", result.ExecutionKind);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.Equal(\"Projects/2026\", result.RequestedDestinationFolderId);\n        Assert.NotNull(result.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailActionPlanExportUsesSharedPlanExchangeService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_plan_export(\n            action: \"move\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\", \"MESSAGE-1\" },\n            path: @\"C:\\Temp\\plan.json\",\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(@\"C:\\Temp\\plan.json\", fixture.PlanExchangeService.LastSavedPath);\n        Assert.NotNull(fixture.PlanExchangeService.LastSavedPlan);\n        Assert.Equal(\"move\", fixture.PlanExchangeService.LastSavedPlan!.Action);\n    }\n\n    [Fact]\n    public async Task MailActionPlanImportUsesSharedPlanExchangeService() {\n        using var fixture = new TestFixture();\n        fixture.PlanExchangeService.NextPlan = new MessageActionExecutionPlan {\n            Succeeded = true,\n            Action = \"delete\",\n            ExecutionKind = \"Delete\",\n            ProfileId = \"gmail-work\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            MessageIds = { \"message-1\" }\n        };\n\n        var result = await fixture.Tools.mail_action_plan_import(@\"C:\\Temp\\plan.json\");\n\n        Assert.Equal(\"delete\", result.Action);\n        Assert.Equal(@\"C:\\Temp\\plan.json\", fixture.PlanExchangeService.LastLoadedPath);\n    }\n\n    [Fact]\n    public async Task MailActionBatchImportUsesSharedPlanExchangeService() {\n        using var fixture = new TestFixture();\n        fixture.PlanExchangeService.NextBatchPlans = new[] {\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"mark-read\",\n                ExecutionKind = \"SetReadState\",\n                ProfileId = \"gmail-work\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                DesiredState = true,\n                MessageIds = { \"message-1\" }\n            }\n        };\n\n        var result = await fixture.Tools.mail_action_batch_import(@\"C:\\Temp\\plans.json\");\n\n        Assert.Single(result);\n        Assert.Equal(\"mark-read\", result[0].Action);\n        Assert.Equal(@\"C:\\Temp\\plans.json\", fixture.PlanExchangeService.LastLoadedBatchPath);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreListUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_compact_list();\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.Equal(new[] { \"Delete spam\" }, batch.PlanNames);\n        Assert.Equal(1, fixture.PlanRegistryService.ListCompactCalls);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreSummaryListUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_summary_list();\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.Equal(1, batch.ActionCounts[\"delete\"]);\n        Assert.Equal(new[] { \"gmail-work\" }, batch.ProfileIds);\n        Assert.Equal(1, fixture.PlanRegistryService.ListSummaryCalls);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreSummaryListPassesSortToSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_summary_list(sortBy: \"plans\", descending: true);\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.NotNull(fixture.PlanRegistryService.LastBatchQuery);\n        Assert.Equal(MailMessageActionPlanBatchSortBy.PlanCount, fixture.PlanRegistryService.LastBatchQuery!.SortBy);\n        Assert.True(fixture.PlanRegistryService.LastBatchQuery.Descending);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreSummaryListPassesExplicitIdSortToSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_summary_list(sortBy: \"id\");\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.NotNull(fixture.PlanRegistryService.LastBatchQuery);\n        Assert.Equal(MailMessageActionPlanBatchSortBy.Id, fixture.PlanRegistryService.LastBatchQuery!.SortBy);\n        Assert.False(fixture.PlanRegistryService.LastBatchQuery.Descending);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreListPassesPlanNameFilterToSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_compact_list(new[] { \"Delete spam\" });\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.NotNull(fixture.PlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"Delete spam\" }, fixture.PlanRegistryService.LastBatchQuery!.PlanNames);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreListPassesProfileFilterToSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_compact_list(profileIds: new[] { \"gmail-work\" });\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.NotNull(fixture.PlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"gmail-work\" }, fixture.PlanRegistryService.LastBatchQuery!.ProfileIds);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreListPassesActionFilterToSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_compact_list(actions: new[] { \"delete\" });\n\n        var batch = Assert.Single(result);\n        Assert.Equal(\"cleanup\", batch.Id);\n        Assert.NotNull(fixture.PlanRegistryService.LastBatchQuery);\n        Assert.Equal(new[] { \"delete\" }, fixture.PlanRegistryService.LastBatchQuery!.Actions);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreImportUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_import(\"cleanup\", \"Cleanup batch\", @\"C:\\Temp\\plans.json\", \"Quarterly cleanup\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastImportedBatchId);\n        Assert.Equal(@\"C:\\Temp\\plans.json\", fixture.PlanRegistryService.LastImportedPath);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreCreateCommonUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_create_common(\n            batchId: \"cleanup\",\n            name: \"Cleanup batch\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\", \"MESSAGE-1\" },\n            actions: new[] { \"archive\", \"delete\" },\n            destinationFolderId: \"Archive\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            description: \"Common action batch\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastCreatedCommonBatchId);\n        Assert.Equal(\"Cleanup batch\", fixture.PlanRegistryService.LastCreatedCommonName);\n        Assert.Equal(\"Common action batch\", fixture.PlanRegistryService.LastCreatedCommonDescription);\n        Assert.Equal(new[] { \"archive\", \"delete\" }, fixture.PlanRegistryService.LastCreatedCommonActions);\n        Assert.NotNull(fixture.PlanRegistryService.LastCreatedCommonRequest);\n        Assert.Equal(\"gmail-work\", fixture.PlanRegistryService.LastCreatedCommonRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.PlanRegistryService.LastCreatedCommonRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.PlanRegistryService.LastCreatedCommonRequest.FolderId);\n        Assert.Equal(\"Archive\", fixture.PlanRegistryService.LastCreatedCommonRequest.DestinationFolderId);\n        Assert.Equal(new[] { \"message-1\", \"MESSAGE-1\" }, fixture.PlanRegistryService.LastCreatedCommonRequest.MessageIds);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreCreateFromPreviewUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var preview = await fixture.Tools.mail_actions_bundle_preview(\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\", \"MESSAGE-1\" },\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n        var result = await fixture.Tools.mail_action_batch_store_create_from_preview(\n            batchId: \"cleanup-previewed\",\n            name: \"Cleanup Previewed\",\n            preview: preview,\n            actions: new[] { \"move\", \"delete\" },\n            description: \"Built from preview\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup-previewed\", fixture.PlanRegistryService.LastCreatedFromPreviewBatchId);\n        Assert.Equal(\"Cleanup Previewed\", fixture.PlanRegistryService.LastCreatedFromPreviewName);\n        Assert.Equal(\"Built from preview\", fixture.PlanRegistryService.LastCreatedFromPreviewDescription);\n        Assert.Equal(new[] { \"move\", \"delete\" }, fixture.PlanRegistryService.LastCreatedFromPreviewActions);\n        Assert.NotNull(fixture.PlanRegistryService.LastCreatedFromPreview);\n        Assert.Equal(\"gmail-work\", fixture.PlanRegistryService.LastCreatedFromPreview!.ProfileId);\n        Assert.Equal(\"primary\", fixture.PlanRegistryService.LastCreatedFromPreview.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.PlanRegistryService.LastCreatedFromPreview.FolderId);\n        Assert.Equal(\"Projects/2026\", fixture.PlanRegistryService.LastCreatedFromPreview.RequestedDestinationFolderId);\n        Assert.Equal(new[] { \"message-1\", \"MESSAGE-1\" }, fixture.PlanRegistryService.LastCreatedFromPreview.MessageIds);\n        Assert.Contains(fixture.PlanRegistryService.LastCreatedFromPreview.Actions, action => action.Action == \"move\");\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreExecuteUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_execute(\"cleanup\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastExecutedBatchId);\n        Assert.Equal(1, result.SucceededPlanCount);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreAppendPlanUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_append_plan(\n            batchId: \"cleanup\",\n            action: \"move\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\" },\n            destinationFolderId: \"Archive\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastAppendedBatchId);\n        Assert.NotNull(fixture.PlanRegistryService.LastAppendedPlan);\n        Assert.Equal(\"move\", fixture.PlanRegistryService.LastAppendedPlan!.Action);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreRemovePlanUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_remove_plan(\"cleanup\", 1);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastRemovedBatchId);\n        Assert.Equal(1, fixture.PlanRegistryService.LastRemovedIndex);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreCloneUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_clone(\"cleanup\", \"cleanup-copy\", \"Cleanup copy\", \"Cloned batch\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastClonedSourceBatchId);\n        Assert.Equal(\"cleanup-copy\", fixture.PlanRegistryService.LastClonedTargetBatchId);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreTransformCloneUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_transform_clone(\n            sourceBatchId: \"cleanup\",\n            targetBatchId: \"cleanup-target\",\n            name: \"Cleanup Target\",\n            indexes: new[] { 1, 2 },\n            planNames: new[] { \"Archive newsletter\" },\n            profileId: \"gmail-target\",\n            mailboxId: \"shared@example.com\",\n            folderId: \"Projects\",\n            destinationFolderId: \"Projects/Archive\",\n            description: \"Remapped batch\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastTransformedSourceBatchId);\n        Assert.Equal(\"cleanup-target\", fixture.PlanRegistryService.LastTransformedTargetBatchId);\n        Assert.Equal(\"Cleanup Target\", fixture.PlanRegistryService.LastTransformedName);\n        Assert.Equal(\"Remapped batch\", fixture.PlanRegistryService.LastTransformedDescription);\n        Assert.NotNull(fixture.PlanRegistryService.LastTransformRequest);\n        Assert.Equal(\"gmail-target\", fixture.PlanRegistryService.LastTransformRequest!.ProfileId);\n        Assert.Equal(new[] { 1, 2 }, fixture.PlanRegistryService.LastTransformRequest.PlanIndexes);\n        Assert.Equal(new[] { \"Archive newsletter\" }, fixture.PlanRegistryService.LastTransformRequest.PlanNames);\n        Assert.Equal(\"shared@example.com\", fixture.PlanRegistryService.LastTransformRequest.MailboxId);\n        Assert.Equal(\"Projects\", fixture.PlanRegistryService.LastTransformRequest.FolderId);\n        Assert.Equal(\"Projects/Archive\", fixture.PlanRegistryService.LastTransformRequest.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreTransformPreviewUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_transform_preview(\n            sourceBatchId: \"cleanup\",\n            indexes: new[] { 1 },\n            planNames: new[] { \"Archive newsletter\" },\n            profileId: \"gmail-target\",\n            mailboxId: \"shared@example.com\",\n            folderId: \"Projects\",\n            destinationFolderId: \"Projects/Archive\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastPreviewedTransformSourceBatchId);\n        Assert.NotNull(fixture.PlanRegistryService.LastPreviewedTransformRequest);\n        Assert.Equal(\"gmail-target\", fixture.PlanRegistryService.LastPreviewedTransformRequest!.ProfileId);\n        Assert.Equal(new[] { 1 }, fixture.PlanRegistryService.LastPreviewedTransformRequest.PlanIndexes);\n        Assert.Equal(new[] { \"Archive newsletter\" }, fixture.PlanRegistryService.LastPreviewedTransformRequest.PlanNames);\n        Assert.Equal(\"shared@example.com\", fixture.PlanRegistryService.LastPreviewedTransformRequest.MailboxId);\n        Assert.Equal(\"Projects\", fixture.PlanRegistryService.LastPreviewedTransformRequest.FolderId);\n        Assert.Equal(\"Projects/Archive\", fixture.PlanRegistryService.LastPreviewedTransformRequest.DestinationFolderId);\n        Assert.Equal(1, result.ChangedPlanCount);\n    }\n\n    [Fact]\n    public async Task MailActionBatchStoreReplacePlanUsesSharedRegistryService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_store_replace_plan(\n            batchId: \"cleanup\",\n            index: 0,\n            action: \"move\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\" },\n            destinationFolderId: \"Archive\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"cleanup\", fixture.PlanRegistryService.LastReplacedBatchId);\n        Assert.Equal(0, fixture.PlanRegistryService.LastReplacedIndex);\n        Assert.NotNull(fixture.PlanRegistryService.LastReplacedPlan);\n        Assert.Equal(\"move\", fixture.PlanRegistryService.LastReplacedPlan!.Action);\n    }\n\n    [Fact]\n    public async Task MailActionExecuteUsesSharedBatchService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_execute(\n            action: \"move\",\n            profileId: \"gmail-work\",\n            messageIds: new[] { \"message-1\", \"MESSAGE-1\" },\n            destinationFolderId: \"Projects/2026\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, result.RequestedPlanCount);\n        Assert.Equal(1, result.SucceededPlanCount);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastMoveRequest!.MailboxId);\n        Assert.Equal(\"Projects/2026\", fixture.MessageActionService.LastMoveRequest.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task MailActionBatchExecuteUsesSharedBatchService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_action_batch_execute(new[] {\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"mark-read\",\n                ExecutionKind = \"SetReadState\",\n                ProfileId = \"gmail-work\",\n                MailboxId = \"primary\",\n                FolderId = \"Inbox\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                DesiredState = true,\n                MessageIds = { \"message-1\" }\n            },\n            new MessageActionExecutionPlan {\n                Succeeded = true,\n                Action = \"move\",\n                ExecutionKind = \"Move\",\n                ProfileId = \"gmail-work\",\n                MailboxId = \"primary\",\n                FolderId = \"Inbox\",\n                RequestedCount = 1,\n                UniqueMessageCount = 1,\n                RequestedDestinationFolderId = \"Projects/2026\",\n                MessageIds = { \"message-2\" }\n            }\n        });\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, result.AttemptedPlanCount);\n        Assert.Equal(2, result.SucceededPlanCount);\n        Assert.NotNull(fixture.MessageActionService.LastSetReadStateRequest);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"Projects/2026\", fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n    }\n\n    [Fact]\n    public async Task MailDeletePreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_delete_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(2, result.RequestedCount);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.Equal(\"gmail-work\", result.ProfileId);\n        Assert.NotNull(result.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailSearchCompactDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_search_compact(\n            \"gmail-work\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            queryText: \"invoice\",\n            subjectContains: \"Quarterly\",\n            fromContains: \"billing@example.com\",\n            toContains: \"team@example.com\",\n            hasAttachments: true,\n            limit: 5);\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"message-1\", result.Id);\n        Assert.NotNull(fixture.ReadService.LastSearchCompactRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastSearchCompactRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastSearchCompactRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastSearchCompactRequest.FolderId);\n        Assert.Equal(\"invoice\", fixture.ReadService.LastSearchCompactRequest.QueryText);\n        Assert.True(fixture.ReadService.LastSearchCompactRequest.HasAttachments);\n        Assert.Equal(5, fixture.ReadService.LastSearchCompactRequest.Limit);\n    }\n\n    [Fact]\n    public async Task MailAttachmentsListDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_attachments_list(\n            \"gmail-work\",\n            \"message-1\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        var result = Assert.Single(results);\n        Assert.Equal(\"attachment-1\", result.Id);\n        Assert.NotNull(fixture.ReadService.LastListAttachmentsRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastListAttachmentsRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastListAttachmentsRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastListAttachmentsRequest.FolderId);\n        Assert.Equal(\"message-1\", fixture.ReadService.LastListAttachmentsRequest.MessageId);\n    }\n\n    [Fact]\n    public async Task MailAttachmentsSaveDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_attachments_save(\n            \"gmail-work\",\n            \"message-1\",\n            @\"C:\\Temp\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            attachmentIds: new[] { \"attachment-1\" },\n            fileNameContains: \"invoice\",\n            contentTypeContains: \"pdf\",\n            overwrite: true);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.ReadService.LastSaveAttachmentsRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastSaveAttachmentsRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastSaveAttachmentsRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastSaveAttachmentsRequest.FolderId);\n        Assert.Equal(@\"C:\\Temp\", fixture.ReadService.LastSaveAttachmentsRequest.DestinationPath);\n        Assert.Contains(\"attachment-1\", fixture.ReadService.LastSaveAttachmentsRequest.AttachmentIds);\n        Assert.Equal(\"invoice\", fixture.ReadService.LastSaveAttachmentsRequest.FileNameContains);\n        Assert.Equal(\"pdf\", fixture.ReadService.LastSaveAttachmentsRequest.ContentTypeContains);\n        Assert.True(fixture.ReadService.LastSaveAttachmentsRequest.Overwrite);\n    }\n\n    [Fact]\n    public async Task MailAttachmentsSaveManyDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_attachments_save_many(\n            \"gmail-work\",\n            new[] { \"message-1\", \"message-2\" },\n            @\"C:\\Temp\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            attachmentIds: new[] { \"attachment-1\" },\n            fileNameContains: \"invoice\",\n            contentTypeContains: \"pdf\",\n            overwrite: true);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.ReadService.LastSaveAttachmentsManyRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastSaveAttachmentsManyRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastSaveAttachmentsManyRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastSaveAttachmentsManyRequest.FolderId);\n        Assert.Equal(@\"C:\\Temp\", fixture.ReadService.LastSaveAttachmentsManyRequest.DestinationPath);\n        Assert.Equal(new[] { \"message-1\", \"message-2\" }, fixture.ReadService.LastSaveAttachmentsManyRequest.MessageIds);\n        Assert.Equal(new[] { \"attachment-1\" }, fixture.ReadService.LastSaveAttachmentsManyRequest.AttachmentIds);\n        Assert.Equal(\"invoice\", fixture.ReadService.LastSaveAttachmentsManyRequest.FileNameContains);\n        Assert.Equal(\"pdf\", fixture.ReadService.LastSaveAttachmentsManyRequest.ContentTypeContains);\n        Assert.True(fixture.ReadService.LastSaveAttachmentsManyRequest.Overwrite);\n    }\n\n    [Fact]\n    public async Task MailGetCompactDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_get_compact(\n            \"gmail-work\",\n            \"message-1\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            includeRawContent: true);\n\n        Assert.Equal(\"message-1\", result.Id);\n        Assert.NotNull(fixture.ReadService.LastGetCompactRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastGetCompactRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastGetCompactRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastGetCompactRequest.FolderId);\n        Assert.Equal(\"message-1\", fixture.ReadService.LastGetCompactRequest.MessageId);\n        Assert.True(fixture.ReadService.LastGetCompactRequest.IncludeRawContent);\n    }\n\n    [Fact]\n    public async Task MailGetManyCompactDelegatesToApplicationReadService() {\n        using var fixture = new TestFixture();\n\n        var results = await fixture.Tools.mail_get_many_compact(\n            \"gmail-work\",\n            new[] { \"message-1\", \"message-2\" },\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            includeRawContent: true);\n\n        Assert.Equal(2, results.Count);\n        Assert.NotNull(fixture.ReadService.LastGetManyCompactRequest);\n        Assert.Equal(\"gmail-work\", fixture.ReadService.LastGetManyCompactRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.ReadService.LastGetManyCompactRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.ReadService.LastGetManyCompactRequest.FolderId);\n        Assert.Equal(new[] { \"message-1\", \"message-2\" }, fixture.ReadService.LastGetManyCompactRequest.MessageIds);\n        Assert.True(fixture.ReadService.LastGetManyCompactRequest.IncludeRawContent);\n    }\n\n    [Fact]\n    public async Task MailMarkReadDelegatesToApplicationMessageActionService() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_mark\";\n\n        var result = await fixture.Tools.mail_mark_read(\n            \"gmail-work\",\n            new[] { \"message-1\", \"message-2\" },\n            isRead: false,\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastSetReadStateRequest);\n        Assert.Equal(\"gmail-work\", fixture.MessageActionService.LastSetReadStateRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastSetReadStateRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionService.LastSetReadStateRequest.FolderId);\n        Assert.False(fixture.MessageActionService.LastSetReadStateRequest.IsRead);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastSetReadStateRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailFlagDelegatesToApplicationMessageActionService() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_flag\";\n\n        var result = await fixture.Tools.mail_flag(\n            \"gmail-work\",\n            new[] { \"message-1\", \"message-2\" },\n            isFlagged: false,\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastSetFlaggedStateRequest);\n        Assert.Equal(\"gmail-work\", fixture.MessageActionService.LastSetFlaggedStateRequest!.ProfileId);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastSetFlaggedStateRequest.MailboxId);\n        Assert.Equal(\"Inbox\", fixture.MessageActionService.LastSetFlaggedStateRequest.FolderId);\n        Assert.False(fixture.MessageActionService.LastSetFlaggedStateRequest.IsFlagged);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastSetFlaggedStateRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailMarkReadPreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_mark_read_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            isRead: false,\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"read-state\", result.Action);\n        Assert.False(result.DesiredState);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.NotNull(result.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailFlagPreviewUsesSharedPreviewService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_flag_preview(\n            \"gmail-work\",\n            new[] { \"message-1\", \"MESSAGE-1\" },\n            isFlagged: false,\n            mailboxId: \"primary\",\n            folderId: \"Inbox\");\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"flagged-state\", result.Action);\n        Assert.False(result.DesiredState);\n        Assert.Equal(2, result.UniqueMessageCount);\n        Assert.NotNull(result.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailArchiveDelegatesToSharedArchiveAlias() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_archive\";\n\n        var result = await fixture.Tools.mail_archive(\n            \"gmail-work\",\n            new[] { \"message-1\" },\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(MailFolderAliases.Archive, fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailTrashDelegatesToSharedTrashAlias() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_trash\";\n\n        var result = await fixture.Tools.mail_trash(\n            \"gmail-work\",\n            new[] { \"message-1\" },\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(MailFolderAliases.Trash, fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailMoveDelegatesToApplicationMessageActionService() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_move\";\n\n        var result = await fixture.Tools.mail_move(\n            \"gmail-work\",\n            new[] { \"message-1\" },\n            \"Archive\",\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastMoveRequest);\n        Assert.Equal(\"Archive\", fixture.MessageActionService.LastMoveRequest!.DestinationFolderId);\n        Assert.Equal(\"primary\", fixture.MessageActionService.LastMoveRequest.MailboxId);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastMoveRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailDeleteDelegatesToApplicationMessageActionService() {\n        using var fixture = new TestFixture();\n        const string confirmationToken = \"mact_v1_delete\";\n\n        var result = await fixture.Tools.mail_delete(\n            \"gmail-work\",\n            new[] { \"message-1\", \"message-2\" },\n            mailboxId: \"primary\",\n            folderId: \"Inbox\",\n            confirmationToken: confirmationToken);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.MessageActionService.LastDeleteRequest);\n        Assert.Equal(\"gmail-work\", fixture.MessageActionService.LastDeleteRequest!.ProfileId);\n        Assert.Equal(new[] { \"message-1\", \"message-2\" }, fixture.MessageActionService.LastDeleteRequest.MessageIds);\n        Assert.Equal(confirmationToken, fixture.MessageActionService.LastDeleteRequest.ConfirmationToken);\n    }\n\n    [Fact]\n    public async Task MailSendBuildsQueueFirstSendRequest() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_send(\n            \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Status update\",\n            textBody: \"Queued body\",\n            cc: new[] { \"bob@example.com\" },\n            attachmentPaths: new[] { \"C:\\\\Temp\\\\status.txt\" });\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.SendService.LastRequest);\n        Assert.Equal(\"gmail-work\", fixture.SendService.LastRequest!.ProfileId);\n        Assert.True(fixture.SendService.LastRequest.PreferQueue);\n        Assert.False(fixture.SendService.LastRequest.RequireImmediateSend);\n        Assert.Equal(\"alice@example.com\", fixture.SendService.LastRequest.Message.To[0].Address);\n        Assert.Equal(\"bob@example.com\", fixture.SendService.LastRequest.Message.Cc[0].Address);\n        Assert.Equal(\"C:\\\\Temp\\\\status.txt\", fixture.SendService.LastRequest.Message.Attachments[0].Path);\n    }\n\n    [Fact]\n    public async Task MailDraftSaveAndListRoundTripsThroughDraftService() {\n        using var fixture = new TestFixture();\n\n        var saveResult = await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\",\n            textBody: \"Draft body\",\n            cc: new[] { \"bob@example.com\" });\n\n        Assert.True(saveResult.Succeeded);\n\n        var drafts = await fixture.Tools.mail_draft_list();\n\n        var draft = Assert.Single(drafts);\n        Assert.Equal(\"draft-1\", draft.Id);\n        Assert.Equal(\"Weekly update\", draft.Name);\n        Assert.Equal(\"gmail-work\", draft.Message.ProfileId);\n        Assert.Equal(\"alice@example.com\", draft.Message.To[0].Address);\n        Assert.Equal(\"bob@example.com\", draft.Message.Cc[0].Address);\n    }\n\n    [Fact]\n    public async Task MailDraftCompactListReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\");\n\n        var drafts = await fixture.Tools.mail_draft_compact_list();\n\n        var draft = Assert.Single(drafts);\n        Assert.Equal(\"draft-1\", draft.Id);\n        Assert.Equal(\"gmail-work\", draft.ProfileId);\n        Assert.Equal(\"Weekly update\", draft.Subject);\n    }\n\n    [Fact]\n    public async Task MailDraftGetReturnsStoredDraft() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\");\n\n        var draft = await fixture.Tools.mail_draft_get(\"draft-1\");\n\n        Assert.Equal(\"draft-1\", draft.Id);\n        Assert.Equal(\"Weekly update\", draft.Name);\n        Assert.Equal(\"alice@example.com\", draft.Message.To[0].Address);\n    }\n\n    [Fact]\n    public async Task MailDraftCompactGetReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\");\n\n        var draft = await fixture.Tools.mail_draft_compact_get(\"draft-1\");\n\n        Assert.Equal(\"draft-1\", draft.Id);\n        Assert.Equal(\"gmail-work\", draft.ProfileId);\n        Assert.Equal(\"Weekly update\", draft.Subject);\n    }\n\n    [Fact]\n    public async Task MailDraftSendUsesStoredDraft() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\",\n            textBody: \"Draft body\");\n\n        var result = await fixture.Tools.mail_draft_send(\"draft-1\", sendNow: true);\n\n        Assert.True(result.Succeeded);\n        Assert.NotNull(fixture.SendService.LastRequest);\n        Assert.Equal(\"gmail-work\", fixture.SendService.LastRequest!.ProfileId);\n        Assert.True(fixture.SendService.LastRequest.RequireImmediateSend);\n        Assert.False(fixture.SendService.LastRequest.PreferQueue);\n        Assert.Equal(\"alice@example.com\", fixture.SendService.LastRequest.Message.To[0].Address);\n        Assert.Equal(\"Weekly update\", fixture.SendService.LastRequest.Message.Subject);\n    }\n\n    [Fact]\n    public async Task MailDraftImportLoadsDraftFileIntoSharedStore() {\n        using var fixture = new TestFixture();\n        var path = fixture.CreatePath(\"imported-draft.json\");\n        await fixture.Application.DraftExchange.SaveAsync(path, new MailDraft {\n            Id = \"external-draft\",\n            Name = \"Imported draft\",\n            Message = new DraftMessage {\n                ProfileId = \"gmail-work\",\n                Subject = \"Imported subject\",\n                To = {\n                    new MessageRecipient { Address = \"imported@example.com\" }\n                }\n            }\n        });\n\n        var imported = await fixture.Tools.mail_draft_import(path, draftId: \"draft-1\", name: \"Imported into store\");\n        var stored = await fixture.Tools.mail_draft_get(\"draft-1\");\n\n        Assert.Equal(\"draft-1\", imported.Id);\n        Assert.Equal(\"Imported into store\", imported.Name);\n        Assert.Equal(\"draft-1\", stored.Id);\n        Assert.Equal(\"Imported into store\", stored.Name);\n        Assert.Equal(\"imported@example.com\", stored.Message.To[0].Address);\n    }\n\n    [Fact]\n    public async Task MailDraftExportWritesStoredDraftFile() {\n        using var fixture = new TestFixture();\n        var path = fixture.CreatePath(\"exported-draft.json\");\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\");\n\n        var result = await fixture.Tools.mail_draft_export(\"draft-1\", path);\n        var exported = await fixture.Application.DraftExchange.LoadAsync(path);\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(\"draft-1\", exported.Id);\n        Assert.Equal(\"Weekly update\", exported.Name);\n        Assert.Equal(\"alice@example.com\", exported.Message.To[0].Address);\n    }\n\n    [Fact]\n    public async Task MailDraftDeleteRemovesStoredDraft() {\n        using var fixture = new TestFixture();\n        await fixture.Tools.mail_draft_save(\n            draftId: \"draft-1\",\n            name: \"Weekly update\",\n            profileId: \"gmail-work\",\n            to: new[] { \"alice@example.com\" },\n            subject: \"Weekly update\");\n\n        var deleteResult = await fixture.Tools.mail_draft_delete(\"draft-1\");\n        var drafts = await fixture.Tools.mail_draft_list();\n\n        Assert.True(deleteResult.Succeeded);\n        Assert.Empty(drafts);\n    }\n\n    [Fact]\n    public async Task MailQueueProcessDelegatesToQueueService() {\n        using var fixture = new TestFixture();\n\n        var result = await fixture.Tools.mail_queue_process();\n\n        Assert.True(result.Succeeded);\n        Assert.Equal(1, result.AttemptedCount);\n        Assert.Equal(1, result.SentCount);\n        Assert.True(fixture.QueueService.ProcessCalled);\n    }\n\n    [Fact]\n    public async Task MailQueueCompactListReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n\n        var queued = await fixture.Tools.mail_queue_compact_list();\n\n        var message = Assert.Single(queued);\n        Assert.Equal(\"queued-1\", message.MessageId);\n        Assert.Equal(\"gmail\", message.Provider);\n    }\n\n    [Fact]\n    public async Task MailQueueCompactGetReturnsLightweightProjection() {\n        using var fixture = new TestFixture();\n\n        var message = await fixture.Tools.mail_queue_compact_get(\"queued-1\");\n\n        Assert.Equal(\"queued-1\", message.MessageId);\n        Assert.Equal(\"gmail\", message.Provider);\n    }\n\n    private sealed class TestFixture : IDisposable {\n        private readonly string _tempDirectory;\n\n        public TestFixture() {\n            _tempDirectory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.McpTests\", Guid.NewGuid().ToString(\"N\"));\n\n            ProfileStore = new InMemoryProfileStore(new[] {\n                new MailProfile {\n                    Id = \"gmail-work\",\n                    DisplayName = \"Work Gmail\",\n                    Kind = MailProfileKind.Gmail\n                }\n            });\n            SecretStore = new InMemorySecretStore();\n            ReadService = new FakeReadService();\n            SendService = new FakeSendService();\n            MessageActionService = new FakeMessageActionService();\n            PlanExchangeService = new FakeMessageActionPlanExchangeService();\n            PlanRegistryService = new FakeMessageActionPlanRegistryService();\n            QueueService = new FakeQueueService();\n            ProfileAuthService = new FakeProfileAuthService(ProfileStore, SecretStore);\n            ProfileConnectionService = new FakeProfileConnectionService();\n\n            Application = new MailApplicationBuilder()\n                .UseProfileStore(ProfileStore)\n                .UseSecretStore(SecretStore)\n                .UseDraftStore(new FileMailDraftStore(Path.Combine(_tempDirectory, \"drafts.json\")))\n                .UseProfileAuthService(ProfileAuthService)\n                .UseProfileConnectionService(ProfileConnectionService)\n                .UseReadService(ReadService)\n                .UseMessageActionService(MessageActionService)\n                .UseMessageActionPlanExchangeService(PlanExchangeService)\n                .UseMessageActionPlanRegistryService(PlanRegistryService)\n                .UseSendService(SendService)\n                .UseQueueService(QueueService)\n                .Build();\n\n            Tools = new MailMcpTools(Application);\n        }\n\n        public MailApplication Application { get; }\n\n        public MailMcpTools Tools { get; }\n\n        public InMemoryProfileStore ProfileStore { get; }\n\n        public InMemorySecretStore SecretStore { get; }\n\n        public FakeReadService ReadService { get; }\n\n        public FakeSendService SendService { get; }\n\n        public FakeMessageActionService MessageActionService { get; }\n\n        public FakeMessageActionPlanExchangeService PlanExchangeService { get; }\n\n        public FakeMessageActionPlanRegistryService PlanRegistryService { get; }\n\n        public FakeQueueService QueueService { get; }\n\n        public FakeProfileAuthService ProfileAuthService { get; }\n\n        public FakeProfileConnectionService ProfileConnectionService { get; }\n\n        public string CreatePath(string fileName) => Path.Combine(_tempDirectory, fileName);\n\n        public void Dispose() {\n            if (Directory.Exists(_tempDirectory)) {\n                Directory.Delete(_tempDirectory, recursive: true);\n            }\n        }\n    }\n\n    private sealed class FakeMessageActionPlanExchangeService : IMailMessageActionPlanExchangeService {\n        public string? LastLoadedPath { get; private set; }\n\n        public string? LastLoadedBatchPath { get; private set; }\n\n        public string? LastSavedPath { get; private set; }\n\n        public string? LastSavedBatchPath { get; private set; }\n\n        public MessageActionExecutionPlan? LastSavedPlan { get; private set; }\n\n        public IReadOnlyList<MessageActionExecutionPlan>? LastSavedBatch { get; private set; }\n\n        public MessageActionExecutionPlan NextPlan { get; set; } = new() {\n            Succeeded = true,\n            Action = \"move\",\n            ExecutionKind = \"Move\",\n            ProfileId = \"gmail-work\",\n            RequestedCount = 1,\n            UniqueMessageCount = 1,\n            RequestedDestinationFolderId = \"Archive\",\n            MessageIds = { \"message-1\" }\n        };\n\n        public IReadOnlyList<MessageActionExecutionPlan> NextBatchPlans { get; set; } = Array.Empty<MessageActionExecutionPlan>();\n\n        public Task<MessageActionExecutionPlan> LoadAsync(string path, CancellationToken cancellationToken = default) {\n            LastLoadedPath = path;\n            return Task.FromResult(NextPlan);\n        }\n\n        public Task<IReadOnlyList<MessageActionExecutionPlan>> LoadBatchAsync(string path, CancellationToken cancellationToken = default) {\n            LastLoadedBatchPath = path;\n            return Task.FromResult(NextBatchPlans);\n        }\n\n        public Task SaveAsync(string path, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastSavedPath = path;\n            LastSavedPlan = plan;\n            return Task.CompletedTask;\n        }\n\n        public Task SaveBatchAsync(string path, IReadOnlyList<MessageActionExecutionPlan> plans, CancellationToken cancellationToken = default) {\n            LastSavedBatchPath = path;\n            LastSavedBatch = plans;\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class FakeMessageActionPlanRegistryService : IMailMessageActionPlanRegistryService {\n        public int ListCompactCalls { get; private set; }\n        public int ListSummaryCalls { get; private set; }\n        public MailMessageActionPlanBatchQuery? LastBatchQuery { get; private set; }\n\n        public string? LastAppendedBatchId { get; private set; }\n\n        public MessageActionExecutionPlan? LastAppendedPlan { get; private set; }\n\n        public string? LastClonedSourceBatchId { get; private set; }\n\n        public string? LastClonedTargetBatchId { get; private set; }\n\n        public string? LastImportedBatchId { get; private set; }\n\n        public string? LastImportedPath { get; private set; }\n\n        public string? LastCreatedCommonBatchId { get; private set; }\n\n        public string? LastCreatedCommonName { get; private set; }\n\n        public string? LastCreatedCommonDescription { get; private set; }\n\n        public CommonMessageActionsPreviewRequest? LastCreatedCommonRequest { get; private set; }\n\n        public IReadOnlyList<string>? LastCreatedCommonActions { get; private set; }\n\n        public string? LastCreatedFromPreviewBatchId { get; private set; }\n\n        public string? LastCreatedFromPreviewName { get; private set; }\n\n        public string? LastCreatedFromPreviewDescription { get; private set; }\n\n        public CommonMessageActionsPreview? LastCreatedFromPreview { get; private set; }\n\n        public IReadOnlyList<string>? LastCreatedFromPreviewActions { get; private set; }\n\n        public string? LastTransformedSourceBatchId { get; private set; }\n\n        public string? LastTransformedTargetBatchId { get; private set; }\n\n        public string? LastTransformedName { get; private set; }\n\n        public string? LastTransformedDescription { get; private set; }\n\n        public MessageActionPlanBatchTransformRequest? LastTransformRequest { get; private set; }\n\n        public string? LastPreviewedTransformSourceBatchId { get; private set; }\n\n        public MessageActionPlanBatchTransformRequest? LastPreviewedTransformRequest { get; private set; }\n\n        public string? LastExecutedBatchId { get; private set; }\n\n        public string? LastRemovedBatchId { get; private set; }\n\n        public int? LastRemovedIndex { get; private set; }\n\n        public string? LastReplacedBatchId { get; private set; }\n\n        public int? LastReplacedIndex { get; private set; }\n\n        public MessageActionExecutionPlan? LastReplacedPlan { get; private set; }\n\n        public Task<OperationResult> AppendImportedPlanAsync(string batchId, string path, CancellationToken cancellationToken = default) {\n            LastAppendedBatchId = batchId;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> AppendPlanAsync(string batchId, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastAppendedBatchId = batchId;\n            LastAppendedPlan = plan;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CloneAsync(string sourceBatchId, string targetBatchId, string name, string? description = null, CancellationToken cancellationToken = default) {\n            LastClonedSourceBatchId = sourceBatchId;\n            LastClonedTargetBatchId = targetBatchId;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{targetBatchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CreateCommonBatchAsync(\n            string batchId,\n            string name,\n            CommonMessageActionsPreviewRequest request,\n            IReadOnlyList<string>? actions = null,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastCreatedCommonBatchId = batchId;\n            LastCreatedCommonName = name;\n            LastCreatedCommonDescription = description;\n            LastCreatedCommonRequest = new CommonMessageActionsPreviewRequest {\n                ProfileId = request.ProfileId,\n                MailboxId = request.MailboxId,\n                FolderId = request.FolderId,\n                DestinationFolderId = request.DestinationFolderId,\n                MessageIds = request.MessageIds.ToList()\n            };\n            LastCreatedCommonActions = actions?.ToArray();\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> CreateCommonBatchFromPreviewAsync(\n            string batchId,\n            string name,\n            CommonMessageActionsPreview preview,\n            IReadOnlyList<string>? actions = null,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastCreatedFromPreviewBatchId = batchId;\n            LastCreatedFromPreviewName = name;\n            LastCreatedFromPreviewDescription = description;\n            LastCreatedFromPreview = new CommonMessageActionsPreview {\n                ProfileId = preview.ProfileId,\n                MailboxId = preview.MailboxId,\n                FolderId = preview.FolderId,\n                RequestedDestinationFolderId = preview.RequestedDestinationFolderId,\n                RequestedCount = preview.RequestedCount,\n                UniqueMessageCount = preview.UniqueMessageCount,\n                DuplicateOrEmptyCount = preview.DuplicateOrEmptyCount,\n                MessageIds = preview.MessageIds.ToList(),\n                Actions = preview.Actions.Select(action => new MessageActionPreviewItem {\n                    Action = action.Action,\n                    DisplayName = action.DisplayName,\n                    Succeeded = action.Succeeded,\n                    Code = action.Code,\n                    Message = action.Message,\n                    RequestedDestinationFolderId = action.RequestedDestinationFolderId,\n                    DesiredState = action.DesiredState,\n                    ConfirmationToken = action.ConfirmationToken\n                }).ToList()\n            };\n            LastCreatedFromPreviewActions = actions?.ToArray();\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> TransformCloneAsync(\n            string sourceBatchId,\n            string targetBatchId,\n            string name,\n            MessageActionPlanBatchTransformRequest transform,\n            string? description = null,\n            CancellationToken cancellationToken = default) {\n            LastTransformedSourceBatchId = sourceBatchId;\n            LastTransformedTargetBatchId = targetBatchId;\n            LastTransformedName = name;\n            LastTransformedDescription = description;\n            LastTransformRequest = new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = transform.PlanIndexes.ToList(),\n                PlanNames = transform.PlanNames.ToList(),\n                ProfileId = transform.ProfileId,\n                MailboxId = transform.MailboxId,\n                FolderId = transform.FolderId,\n                DestinationFolderId = transform.DestinationFolderId\n            };\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{targetBatchId}' saved.\"));\n        }\n\n        public Task<MailMessageActionPlanBatchTransformPreview> PreviewTransformCloneAsync(\n            string sourceBatchId,\n            MessageActionPlanBatchTransformRequest transform,\n            CancellationToken cancellationToken = default) {\n            LastPreviewedTransformSourceBatchId = sourceBatchId;\n            LastPreviewedTransformRequest = new MessageActionPlanBatchTransformRequest {\n                PlanIndexes = transform.PlanIndexes.ToList(),\n                PlanNames = transform.PlanNames.ToList(),\n                ProfileId = transform.ProfileId,\n                MailboxId = transform.MailboxId,\n                FolderId = transform.FolderId,\n                DestinationFolderId = transform.DestinationFolderId\n            };\n            return Task.FromResult(new MailMessageActionPlanBatchTransformPreview {\n                Succeeded = true,\n                SourceBatchId = sourceBatchId,\n                SourceBatchName = \"Cleanup batch\",\n                PlanCount = 1,\n                ChangedPlanCount = 1,\n                ConfirmationTokenChangedCount = 1,\n                TargetProfileExists = true,\n                Plans = {\n                    new MessageActionPlanBatchTransformPreviewItem {\n                        Index = 0,\n                        Action = \"move\",\n                        ExecutionKind = \"Move\",\n                        SourceProfileId = \"gmail-work\",\n                        TargetProfileId = transform.ProfileId ?? \"gmail-work\",\n                        SourceMailboxId = \"primary\",\n                        TargetMailboxId = transform.MailboxId,\n                        SourceFolderId = \"Inbox\",\n                        TargetFolderId = transform.FolderId,\n                        SourceDestinationFolderId = \"Archive\",\n                        TargetDestinationFolderId = transform.DestinationFolderId,\n                        WillChange = true,\n                        ConfirmationTokenWillChange = true,\n                        Summary = \"move: preview\"\n                    }\n                },\n                Message = \"Previewed transform.\"\n            });\n        }\n\n        public Task<OperationResult> DeleteAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' deleted.\"));\n\n        public Task<MessageActionBatchExecutionResult> ExecuteAsync(string batchId, bool continueOnError = true, CancellationToken cancellationToken = default) {\n            LastExecutedBatchId = batchId;\n            return Task.FromResult(new MessageActionBatchExecutionResult {\n                Succeeded = true,\n                RequestedPlanCount = 1,\n                AttemptedPlanCount = 1,\n                SucceededPlanCount = 1,\n                Message = \"Stored batch executed.\"\n            });\n        }\n\n        public Task<OperationResult> ExportAsync(string batchId, string path, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' exported.\"));\n\n        public Task<MailMessageActionPlanBatch?> GetBatchAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatch?>(new MailMessageActionPlanBatch {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                Plans = {\n                    new MessageActionExecutionPlan {\n                        Succeeded = true,\n                        Action = \"delete\",\n                        ExecutionKind = \"Delete\",\n                        ProfileId = \"gmail-work\",\n                        RequestedCount = 1,\n                        UniqueMessageCount = 1,\n                        MessageIds = { \"message-1\" }\n                    }\n                }\n            });\n\n        public Task<MailMessageActionPlanBatchCompact?> GetBatchCompactAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatchCompact?>(new MailMessageActionPlanBatchCompact {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                PlanCount = 1,\n                ReadyPlanCount = 1,\n                ProfileCount = 1,\n                PlanNames = { \"Delete spam\" },\n                Summary = $\"{batchId} (1 plan(s), 1 ready)\"\n            });\n\n        public Task<MailMessageActionPlanBatchSummary?> GetBatchSummaryAsync(string batchId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MailMessageActionPlanBatchSummary?>(new MailMessageActionPlanBatchSummary {\n                Id = batchId,\n                Name = \"Cleanup batch\",\n                PlanCount = 1,\n                ReadyPlanCount = 1,\n                ProfileIds = { \"gmail-work\" },\n                ActionCounts = {\n                    [\"delete\"] = 1\n                },\n                PlanNames = { \"Delete spam\" },\n                Summary = $\"{batchId} (1 plan(s), 1 ready, 1 action type(s))\"\n            });\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatch>> GetBatchesAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatch>>(new[] {\n                new MailMessageActionPlanBatch {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    Plans = {\n                        new MessageActionExecutionPlan {\n                            Succeeded = true,\n                            Action = \"delete\",\n                            ExecutionKind = \"Delete\",\n                            ProfileId = \"gmail-work\",\n                            RequestedCount = 1,\n                            UniqueMessageCount = 1,\n                            MessageIds = { \"message-1\" }\n                        }\n                    }\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatchCompact>> GetBatchesCompactAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            ListCompactCalls++;\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatchCompact>>(new[] {\n                new MailMessageActionPlanBatchCompact {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    PlanCount = 1,\n                    ReadyPlanCount = 1,\n                    ProfileCount = 1,\n                    PlanNames = { \"Delete spam\" },\n                    Summary = \"cleanup (1 plan(s), 1 ready)\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MailMessageActionPlanBatchSummary>> GetBatchesSummaryAsync(MailMessageActionPlanBatchQuery? query = null, CancellationToken cancellationToken = default) {\n            LastBatchQuery = query == null\n                ? null\n                : new MailMessageActionPlanBatchQuery {\n                    PlanNames = query.PlanNames.ToList(),\n                    ProfileIds = query.ProfileIds.ToList(),\n                    Actions = query.Actions.ToList(),\n                    SortBy = query.SortBy,\n                    Descending = query.Descending\n                };\n            ListSummaryCalls++;\n            return Task.FromResult<IReadOnlyList<MailMessageActionPlanBatchSummary>>(new[] {\n                new MailMessageActionPlanBatchSummary {\n                    Id = \"cleanup\",\n                    Name = \"Cleanup batch\",\n                    PlanCount = 1,\n                    ReadyPlanCount = 1,\n                    ProfileIds = { \"gmail-work\" },\n                    ActionCounts = {\n                        [\"delete\"] = 1\n                    },\n                    PlanNames = { \"Delete spam\" },\n                    Summary = \"cleanup (1 plan(s), 1 ready, 1 action type(s))\"\n                }\n            });\n        }\n\n        public Task<OperationResult> ImportAsync(string batchId, string name, string path, string? description = null, CancellationToken cancellationToken = default) {\n            LastImportedBatchId = batchId;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> ReplaceImportedPlanAtAsync(string batchId, int index, string path, CancellationToken cancellationToken = default) {\n            LastReplacedBatchId = batchId;\n            LastReplacedIndex = index;\n            LastImportedPath = path;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> ReplacePlanAtAsync(string batchId, int index, MessageActionExecutionPlan plan, CancellationToken cancellationToken = default) {\n            LastReplacedBatchId = batchId;\n            LastReplacedIndex = index;\n            LastReplacedPlan = plan;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> RemovePlanAtAsync(string batchId, int index, CancellationToken cancellationToken = default) {\n            LastRemovedBatchId = batchId;\n            LastRemovedIndex = index;\n            return Task.FromResult(OperationResult.Success($\"Action plan batch '{batchId}' saved.\"));\n        }\n\n        public Task<OperationResult> SaveAsync(MailMessageActionPlanBatch batch, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success($\"Action plan batch '{batch.Id}' saved.\"));\n    }\n\n    private sealed class InMemoryProfileStore : IMailProfileStore {\n        private readonly Dictionary<string, MailProfile> _profiles;\n\n        public InMemoryProfileStore(IEnumerable<MailProfile> profiles) {\n            _profiles = profiles.ToDictionary(profile => profile.Id, CloneProfile, StringComparer.OrdinalIgnoreCase);\n        }\n\n        public Task<IReadOnlyList<MailProfile>> GetAllAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<MailProfile>>(_profiles.Values.Select(CloneProfile).ToArray());\n\n        public Task<MailProfile?> GetByIdAsync(string profileId, CancellationToken cancellationToken = default) {\n            _profiles.TryGetValue(profileId, out var profile);\n            return Task.FromResult(profile == null ? null : CloneProfile(profile));\n        }\n\n        public Task SaveAsync(MailProfile profile, CancellationToken cancellationToken = default) {\n            _profiles[profile.Id] = CloneProfile(profile);\n            return Task.CompletedTask;\n        }\n\n        public Task<bool> RemoveAsync(string profileId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_profiles.Remove(profileId));\n\n        private static MailProfile CloneProfile(MailProfile profile) => new() {\n            Id = profile.Id,\n            DisplayName = profile.DisplayName,\n            Description = profile.Description,\n            Kind = profile.Kind,\n            DefaultSender = profile.DefaultSender,\n            DefaultMailbox = profile.DefaultMailbox,\n            IsDefault = profile.IsDefault,\n            Settings = new Dictionary<string, string>(profile.Settings, StringComparer.OrdinalIgnoreCase),\n            Capabilities = profile.Capabilities == null\n                ? null\n                : new ProfileCapabilities(profile.Capabilities.Kind, profile.Capabilities.Capabilities)\n        };\n    }\n\n    private sealed class InMemorySecretStore : IMailSecretStore {\n        private readonly Dictionary<string, string> _secrets = new(StringComparer.OrdinalIgnoreCase);\n\n        public Task<string?> GetSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) {\n            _secrets.TryGetValue(CreateKey(profileId, secretName), out var value);\n            return Task.FromResult<string?>(value);\n        }\n\n        public Task SetSecretAsync(string profileId, string secretName, string secretValue, CancellationToken cancellationToken = default) {\n            _secrets[CreateKey(profileId, secretName)] = secretValue;\n            return Task.CompletedTask;\n        }\n\n        public Task<bool> RemoveSecretAsync(string profileId, string secretName, CancellationToken cancellationToken = default) =>\n            Task.FromResult(_secrets.Remove(CreateKey(profileId, secretName)));\n\n        private static string CreateKey(string profileId, string secretName) => $\"{profileId}::{secretName}\";\n    }\n\n    private sealed class FakeReadService : IMailReadService {\n        public MailFolderQuery? LastFolderQuery { get; private set; }\n\n        public GetMessageRequest? LastGetCompactRequest { get; private set; }\n\n        public GetMessagesRequest? LastGetManyCompactRequest { get; private set; }\n\n        public GetMessagesRequest? LastGetManyRequest { get; private set; }\n\n        public SaveAttachmentsManyRequest? LastSaveAttachmentsManyRequest { get; private set; }\n\n        public SaveAttachmentsRequest? LastSaveAttachmentsRequest { get; private set; }\n\n        public ListAttachmentsRequest? LastListAttachmentsRequest { get; private set; }\n\n        public MailFolderQuery? LastFolderCompactQuery { get; private set; }\n\n        public MailSearchRequest? LastSearchCompactRequest { get; private set; }\n\n        public MailSearchRequest? LastSearchRequest { get; private set; }\n\n        public Task<IReadOnlyList<FolderRefCompact>> GetFoldersCompactAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n            LastFolderCompactQuery = query;\n            return Task.FromResult<IReadOnlyList<FolderRefCompact>>(new[] {\n                new FolderRefCompact {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"Inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\",\n                    Summary = \"Inbox Inbox\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<FolderRef>> GetFoldersAsync(MailFolderQuery query, CancellationToken cancellationToken = default) {\n            LastFolderQuery = query;\n            return Task.FromResult<IReadOnlyList<FolderRef>>(new[] {\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"Inbox\",\n                    DisplayName = \"Inbox\",\n                    Path = \"Inbox\",\n                    SpecialUse = \"inbox\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"archive\",\n                    DisplayName = \"Archive\",\n                    Path = \"Archive\",\n                    SpecialUse = \"archive\"\n                },\n                new FolderRef {\n                    ProfileId = query.ProfileId,\n                    MailboxId = query.MailboxId,\n                    Id = \"trash\",\n                    DisplayName = \"Trash\",\n                    Path = \"Trash\",\n                    SpecialUse = \"trash\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MessageSummary>> SearchAsync(MailSearchRequest request, CancellationToken cancellationToken = default) {\n            LastSearchRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageSummary>>(new[] {\n                new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = \"message-1\",\n                    Subject = \"Quarterly invoice\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<MessageSummaryCompact>> SearchCompactAsync(MailSearchRequest request, CancellationToken cancellationToken = default) {\n            LastSearchCompactRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageSummaryCompact>>(new[] {\n                new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = \"message-1\",\n                    Subject = \"Quarterly invoice\",\n                    Summary = \"message-1 Quarterly invoice\"\n                }\n            });\n        }\n\n        public Task<IReadOnlyList<AttachmentSummary>> GetAttachmentsAsync(ListAttachmentsRequest request, CancellationToken cancellationToken = default) {\n            LastListAttachmentsRequest = request;\n            return Task.FromResult<IReadOnlyList<AttachmentSummary>>(new[] {\n                new AttachmentSummary {\n                    MessageId = request.MessageId,\n                    Id = \"attachment-1\",\n                    FileName = \"invoice.pdf\",\n                    SizeInBytes = 4096\n                }\n            });\n        }\n\n        public Task<SaveAttachmentsResult> SaveAttachmentsAsync(SaveAttachmentsRequest request, CancellationToken cancellationToken = default) {\n            LastSaveAttachmentsRequest = request;\n            return Task.FromResult(new SaveAttachmentsResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                MessageId = request.MessageId,\n                MatchedCount = 1,\n                AttemptedCount = 1,\n                SavedCount = 1,\n                FailedCount = 0,\n                Message = \"Saved 1 attachment(s).\",\n                Results = {\n                    new SavedAttachmentResult {\n                        Succeeded = true,\n                        AttachmentId = \"attachment-1\",\n                        FileName = \"invoice.pdf\",\n                        ContentType = \"application/pdf\"\n                    }\n                }\n            });\n        }\n\n        public Task<SaveAttachmentsManyResult> SaveAttachmentsManyAsync(SaveAttachmentsManyRequest request, CancellationToken cancellationToken = default) {\n            LastSaveAttachmentsManyRequest = request;\n            return Task.FromResult(new SaveAttachmentsManyResult {\n                Succeeded = true,\n                ProfileId = request.ProfileId,\n                RequestedMessageCount = request.MessageIds.Count,\n                AttemptedMessageCount = request.MessageIds.Count,\n                SucceededMessageCount = request.MessageIds.Count,\n                MatchedCount = request.MessageIds.Count,\n                AttemptedCount = request.MessageIds.Count,\n                SavedCount = request.MessageIds.Count,\n                FailedCount = 0,\n                Message = $\"Saved {request.MessageIds.Count} attachment(s) across {request.MessageIds.Count} message(s).\",\n                MessageResults = request.MessageIds.Select(messageId => new SaveAttachmentsResult {\n                    Succeeded = true,\n                    ProfileId = request.ProfileId,\n                    MessageId = messageId,\n                    MatchedCount = 1,\n                    AttemptedCount = 1,\n                    SavedCount = 1,\n                    FailedCount = 0,\n                    Message = \"Saved 1 attachment(s).\"\n                }).ToList()\n            });\n        }\n\n        public Task<MessageDetailCompact?> GetMessageCompactAsync(GetMessageRequest request, CancellationToken cancellationToken = default) {\n            LastGetCompactRequest = request;\n            return Task.FromResult<MessageDetailCompact?>(new MessageDetailCompact {\n                ProfileId = request.ProfileId,\n                Id = request.MessageId,\n                Summary = new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = request.MessageId,\n                    Subject = \"Retrieved message\",\n                    Summary = $\"{request.MessageId} Retrieved message\"\n                },\n                TextBodyPreview = \"Preview\",\n                HtmlBodyPreview = \"<p>Preview</p>\",\n                HasRawContent = request.IncludeRawContent,\n                SummaryText = $\"{request.MessageId} Retrieved message\"\n            });\n        }\n\n        public Task<IReadOnlyList<MessageDetailCompact>> GetMessagesCompactAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastGetManyCompactRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageDetailCompact>>(request.MessageIds.Select(messageId => new MessageDetailCompact {\n                ProfileId = request.ProfileId,\n                Id = messageId,\n                Summary = new MessageSummaryCompact {\n                    ProfileId = request.ProfileId,\n                    Id = messageId,\n                    Subject = \"Retrieved message\",\n                    Summary = $\"{messageId} Retrieved message\"\n                },\n                TextBodyPreview = \"Preview\",\n                HtmlBodyPreview = \"<p>Preview</p>\",\n                HasRawContent = request.IncludeRawContent,\n                SummaryText = $\"{messageId} Retrieved message\"\n            }).ToArray());\n        }\n\n        public Task<MessageDetail?> GetMessageAsync(GetMessageRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult<MessageDetail?>(new MessageDetail {\n                ProfileId = request.ProfileId,\n                Id = request.MessageId,\n                Summary = new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = request.MessageId,\n                    Subject = \"Retrieved message\"\n                }\n            });\n\n        public Task<IReadOnlyList<MessageDetail>> GetMessagesAsync(GetMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastGetManyRequest = request;\n            return Task.FromResult<IReadOnlyList<MessageDetail>>(request.MessageIds.Select(messageId => new MessageDetail {\n                ProfileId = request.ProfileId,\n                Id = messageId,\n                Summary = new MessageSummary {\n                    ProfileId = request.ProfileId,\n                    Id = messageId,\n                    Subject = \"Retrieved message\"\n                }\n            }).ToArray());\n        }\n\n        public Task<OperationResult> SaveAttachmentAsync(SaveAttachmentRequest request, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success(\"Attachment saved.\"));\n    }\n\n    private sealed class FakeSendService : IMailSendService {\n        public SendMessageRequest? LastRequest { get; private set; }\n\n        public Task<SendResult> SendAsync(SendMessageRequest request, CancellationToken cancellationToken = default) {\n            LastRequest = request;\n            return Task.FromResult(new SendResult {\n                Succeeded = true,\n                Queued = request.PreferQueue,\n                QueueMessageId = request.PreferQueue ? \"queued-1\" : null,\n                ProviderMessageId = request.RequireImmediateSend ? \"provider-1\" : null,\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Gmail,\n                Message = \"Send handled.\"\n            });\n        }\n    }\n\n    private sealed class FakeMessageActionService : IMailMessageActionService {\n        public SetReadStateRequest? LastSetReadStateRequest { get; private set; }\n\n        public SetFlaggedStateRequest? LastSetFlaggedStateRequest { get; private set; }\n\n        public MoveMessagesRequest? LastMoveRequest { get; private set; }\n\n        public DeleteMessagesRequest? LastDeleteRequest { get; private set; }\n\n        public Task<MessageActionResult> SetReadStateAsync(SetReadStateRequest request, CancellationToken cancellationToken = default) {\n            LastSetReadStateRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, request.IsRead ? \"Marked messages as read.\" : \"Marked messages as unread.\"));\n        }\n\n        public Task<MessageActionResult> SetFlaggedStateAsync(SetFlaggedStateRequest request, CancellationToken cancellationToken = default) {\n            LastSetFlaggedStateRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, request.IsFlagged ? \"Flagged messages.\" : \"Unflagged messages.\"));\n        }\n\n        public Task<MessageActionResult> MoveAsync(MoveMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastMoveRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, $\"Moved messages to '{request.DestinationFolderId}'.\"));\n        }\n\n        public Task<MessageActionResult> DeleteAsync(DeleteMessagesRequest request, CancellationToken cancellationToken = default) {\n            LastDeleteRequest = request;\n            return Task.FromResult(CreateResult(request.ProfileId, request.MessageIds, \"Deleted messages.\"));\n        }\n\n        private static MessageActionResult CreateResult(string profileId, IReadOnlyList<string> messageIds, string message) => new() {\n            Succeeded = true,\n            ProfileId = profileId,\n            RequestedCount = messageIds.Count,\n            SucceededCount = messageIds.Count,\n            FailedCount = 0,\n            Message = message,\n            Results = messageIds.Select(id => new MessageActionItemResult {\n                MessageId = id,\n                Succeeded = true\n            }).ToList()\n        };\n    }\n\n    private sealed class FakeQueueService : IMailQueueService {\n        public bool ProcessCalled { get; private set; }\n\n        public Task<IReadOnlyList<QueuedMessageCompact>> ListCompactAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<QueuedMessageCompact>>(new[] {\n                new QueuedMessageCompact {\n                    MessageId = \"queued-1\",\n                    Provider = \"gmail\",\n                    ProfileKind = MailProfileKind.Gmail,\n                    NextAttemptAt = DateTimeOffset.UtcNow,\n                    AttemptCount = 0,\n                    IsDue = true,\n                    HasProviderData = false,\n                    Summary = \"queued-1 [gmail] attempts=0\"\n                }\n            });\n\n        public Task<IReadOnlyList<QueuedMessageSummary>> ListAsync(CancellationToken cancellationToken = default) =>\n            Task.FromResult<IReadOnlyList<QueuedMessageSummary>>(new[] {\n                new QueuedMessageSummary {\n                    MessageId = \"queued-1\",\n                    Provider = \"gmail\",\n                    ProfileKind = MailProfileKind.Gmail,\n                    QueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5),\n                    NextAttemptAt = DateTimeOffset.UtcNow,\n                    AttemptCount = 0\n                }\n            });\n\n        public Task<QueuedMessageCompact?> GetCompactAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<QueuedMessageCompact?>(new QueuedMessageCompact {\n                MessageId = messageId,\n                Provider = \"gmail\",\n                ProfileKind = MailProfileKind.Gmail,\n                NextAttemptAt = DateTimeOffset.UtcNow,\n                AttemptCount = 0,\n                IsDue = true,\n                HasProviderData = false,\n                Summary = $\"{messageId} [gmail] attempts=0\"\n            });\n\n        public Task<QueuedMessageSummary?> GetAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<QueuedMessageSummary?>(new QueuedMessageSummary {\n                MessageId = messageId,\n                Provider = \"gmail\",\n                ProfileKind = MailProfileKind.Gmail,\n                QueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5),\n                NextAttemptAt = DateTimeOffset.UtcNow,\n                AttemptCount = 0\n            });\n\n        public Task<OperationResult> RemoveAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(OperationResult.Success(\"Queued message removed.\"));\n\n        public Task<QueueProcessResult> ProcessAsync(CancellationToken cancellationToken = default) {\n            ProcessCalled = true;\n            return Task.FromResult(new QueueProcessResult {\n                Succeeded = true,\n                AttemptedCount = 1,\n                SentCount = 1,\n                FailedCount = 0,\n                SkippedCount = 0,\n                DroppedCount = 0,\n                Message = \"Queue processed.\"\n            });\n        }\n    }\n\n    private sealed class FakeProfileAuthService : IMailProfileAuthService {\n        private readonly InMemoryProfileStore _profileStore;\n        private readonly InMemorySecretStore _secretStore;\n\n        public FakeProfileAuthService(InMemoryProfileStore profileStore, InMemorySecretStore secretStore) {\n            _profileStore = profileStore;\n            _secretStore = secretStore;\n        }\n\n        public GmailProfileLoginRequest? LastGmailRequest { get; private set; }\n\n        public GraphProfileLoginRequest? LastGraphRequest { get; private set; }\n\n        public string? LastRefreshProfileId { get; private set; }\n\n        public string? LastStatusProfileId { get; private set; }\n\n        public int RefreshCalls { get; private set; }\n\n        public async Task<MailProfileAuthStatus?> GetStatusAsync(string profileId, CancellationToken cancellationToken = default) {\n            LastStatusProfileId = profileId;\n            var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken);\n            if (profile == null) {\n                return null;\n            }\n\n            var accessToken = await _secretStore.GetSecretAsync(profileId, MailSecretNames.AccessToken, cancellationToken);\n            var refreshToken = await _secretStore.GetSecretAsync(profileId, MailSecretNames.RefreshToken, cancellationToken);\n            var clientSecret = await _secretStore.GetSecretAsync(profileId, MailSecretNames.ClientSecret, cancellationToken);\n            return new MailProfileAuthStatus {\n                ProfileId = profile.Id,\n                ProfileKind = profile.Kind,\n                AuthFlow = profile.Settings.TryGetValue(MailProfileSettingsKeys.AuthFlow, out var authFlow) ? authFlow : MailProfileAuthFlowNames.Interactive,\n                Mode = profile.Kind == MailProfileKind.Graph ? \"appOnly\" : \"interactive\",\n                Mailbox = profile.DefaultMailbox,\n                HasAccessToken = !string.IsNullOrWhiteSpace(accessToken),\n                HasRefreshToken = !string.IsNullOrWhiteSpace(refreshToken),\n                HasClientSecret = !string.IsNullOrWhiteSpace(clientSecret),\n                CanRefresh = profile.Kind is MailProfileKind.Gmail or MailProfileKind.Graph,\n                CanLoginInteractively = profile.Kind is MailProfileKind.Gmail or MailProfileKind.Graph,\n                Summary = $\"{profile.Id} [{profile.Kind}] auth status available.\"\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> LoginGmailAsync(GmailProfileLoginRequest request, CancellationToken cancellationToken = default) {\n            var profile = await _profileStore.GetByIdAsync(request.ProfileId, cancellationToken);\n            Assert.NotNull(profile);\n            var savedProfile = profile!;\n            var clientSecret = request.ClientSecret;\n            if (string.IsNullOrWhiteSpace(clientSecret) && !string.IsNullOrWhiteSpace(request.ClientSecretReference)) {\n                var (sourceProfileId, sourceSecretName) = ParseSecretReference(request.ClientSecretReference!, request.ProfileId);\n                clientSecret = await _secretStore.GetSecretAsync(sourceProfileId, sourceSecretName, cancellationToken);\n            }\n            var account = request.GmailAccount\n                ?? (savedProfile.Settings.TryGetValue(MailProfileSettingsKeys.Mailbox, out var mailbox) ? mailbox : null)\n                ?? \"user@gmail.com\";\n            LastGmailRequest = new GmailProfileLoginRequest {\n                ProfileId = request.ProfileId,\n                GmailAccount = account,\n                ClientId = request.ClientId ?? (savedProfile.Settings.TryGetValue(MailProfileSettingsKeys.ClientId, out var clientId) ? clientId : null),\n                ClientSecret = clientSecret,\n                Scopes = request.Scopes\n            };\n            savedProfile.Settings[MailProfileSettingsKeys.Mailbox] = account;\n            savedProfile.DefaultMailbox = account;\n            await _profileStore.SaveAsync(savedProfile, cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.AccessToken, \"gmail-access-token\", cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.RefreshToken, \"gmail-refresh-token\", cancellationToken);\n            return new MailProfileAuthenticationResult {\n                Succeeded = true,\n                Message = \"Gmail login completed.\",\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Gmail,\n                UserName = account\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> LoginGraphAsync(GraphProfileLoginRequest request, CancellationToken cancellationToken = default) {\n            LastGraphRequest = request;\n            var profile = await _profileStore.GetByIdAsync(request.ProfileId, cancellationToken);\n            var mailbox = request.Mailbox ?? request.Login ?? \"user@example.com\";\n            profile!.Settings[MailProfileSettingsKeys.Mailbox] = mailbox;\n            profile.DefaultMailbox = mailbox;\n            await _profileStore.SaveAsync(profile, cancellationToken);\n            await _secretStore.SetSecretAsync(request.ProfileId, MailSecretNames.AccessToken, \"graph-access-token\", cancellationToken);\n            return new MailProfileAuthenticationResult {\n                Succeeded = true,\n                Message = \"Graph login completed.\",\n                ProfileId = request.ProfileId,\n                ProfileKind = MailProfileKind.Graph,\n                UserName = mailbox\n            };\n        }\n\n        public async Task<MailProfileAuthenticationResult> RefreshAsync(string profileId, CancellationToken cancellationToken = default) {\n            RefreshCalls++;\n            LastRefreshProfileId = profileId;\n            var profile = await _profileStore.GetByIdAsync(profileId, cancellationToken);\n            Assert.NotNull(profile);\n            return profile!.Kind switch {\n                MailProfileKind.Gmail => await LoginGmailAsync(new GmailProfileLoginRequest { ProfileId = profileId }, cancellationToken),\n                MailProfileKind.Graph => await LoginGraphAsync(new GraphProfileLoginRequest { ProfileId = profileId }, cancellationToken),\n                _ => new MailProfileAuthenticationResult {\n                    Succeeded = false,\n                    Code = \"refresh_not_supported\",\n                    Message = \"Refresh not supported.\",\n                    ProfileId = profileId,\n                    ProfileKind = profile.Kind\n                }\n            };\n        }\n\n        private static (string ProfileId, string SecretName) ParseSecretReference(string secretReference, string defaultProfileId) {\n            var normalized = secretReference.Trim();\n            var colonIndex = normalized.IndexOf(':');\n            var slashIndex = normalized.IndexOf('/');\n            var separatorIndex = colonIndex >= 0 && slashIndex >= 0\n                ? Math.Min(colonIndex, slashIndex)\n                : Math.Max(colonIndex, slashIndex);\n\n            if (separatorIndex < 0) {\n                return (defaultProfileId, normalized);\n            }\n\n            return (normalized[..separatorIndex], normalized[(separatorIndex + 1)..]);\n        }\n    }\n\n    private sealed class FakeProfileConnectionService : IMailProfileConnectionService {\n        public string? LastProfileId { get; private set; }\n\n        public MailProfileConnectionTestScope LastScope { get; private set; }\n\n        public Task<MailProfileConnectionTestResult> TestAsync(\n            string profileId,\n            MailProfileConnectionTestScope scope = MailProfileConnectionTestScope.Auto,\n            CancellationToken cancellationToken = default) {\n            LastProfileId = profileId;\n            LastScope = scope;\n            return Task.FromResult(new MailProfileConnectionTestResult {\n                Succeeded = true,\n                Message = \"Profile connection succeeded.\",\n                ProfileId = profileId,\n                ProfileKind = MailProfileKind.Gmail,\n                Probe = \"getProfile\",\n                Target = \"gmail-work\",\n                RequestedScope = scope,\n                ExecutedScope = scope == MailProfileConnectionTestScope.Auto ? MailProfileConnectionTestScope.Mailbox : scope\n            });\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MailboxSearcherBodyTests.cs",
    "content": "using MailKit;\nusing MailKit.Net.Pop3;\nusing MimeKit;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class MailboxSearcherBodyTests {\n    [Fact]\n    public async Task SearchPop3Async_BodyContains_FiltersMessages() {\n        var msg1 = new MimeMessage();\n        msg1.Body = new TextPart(\"plain\") { Text = \"hello world\" };\n        var msg2 = new MimeMessage();\n        msg2.Body = new TextPart(\"plain\") { Text = \"other\" };\n        var client = new FakePop3Client(new[] { msg1, msg2 });\n        var result = await MailboxSearcher.SearchPop3Async(client, bodyContains: \"hello\");\n        Assert.Single(result);\n    }\n\n    private sealed class FakePop3Client : Pop3Client {\n        private readonly List<MimeMessage> _messages;\n        public FakePop3Client(IEnumerable<MimeMessage> messages) => _messages = new List<MimeMessage>(messages);\n        public override bool IsConnected => true;\n        public override bool IsAuthenticated => true;\n        public override int Count => _messages.Count;\n        public override Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null)\n            => Task.FromResult(_messages[index]);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MailgunClientTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.IO;\nusing System.Linq;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class MailgunClientTests\n{\n    private class DummyCredentials : ICredentials\n    {\n        public NetworkCredential GetCredential(Uri uri, string authType) => new NetworkCredential();\n    }\n\n    private sealed class TrackingHandler : HttpMessageHandler\n    {\n        private readonly HttpStatusCode _statusCode;\n        private readonly string _content;\n        public bool ResponseDisposed { get; private set; }\n\n        public TrackingHandler(HttpStatusCode statusCode, string content = \"\")\n        {\n            _statusCode = statusCode;\n            _content = content;\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            var response = new HttpResponseMessage(_statusCode)\n            {\n                Content = new TrackingContent(_content, () => ResponseDisposed = true)\n            };\n            return Task.FromResult(response);\n        }\n\n        private sealed class TrackingContent : StringContent\n        {\n            private readonly Action _onDispose;\n\n            public TrackingContent(string content, Action onDispose) : base(content)\n            {\n                _onDispose = onDispose;\n            }\n\n            protected override void Dispose(bool disposing)\n            {\n                base.Dispose(disposing);\n                _onDispose();\n            }\n        }\n    }\n\n    private sealed class DisposingHandler : HttpMessageHandler\n    {\n        public bool Disposed { get; private set; }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));\n\n        protected override void Dispose(bool disposing)\n        {\n            base.Dispose(disposing);\n            Disposed = true;\n        }\n    }\n\n    private sealed class DerivedMailgunClient : MailgunClient\n    {\n        public bool Disposed { get; private set; }\n\n        protected override void Dispose(bool disposing)\n        {\n            if (disposing)\n            {\n                Disposed = true;\n            }\n            base.Dispose(disposing);\n        }\n    }\n\n    [Fact]\n    public void EmailDomain_InvalidAddress_ThrowsArgumentException()\n    {\n        using var client = new MailgunClient { From = \"invalid\" };\n        PropertyInfo? prop = typeof(MailgunClient).GetProperty(\"EmailDomain\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var ex = Assert.Throws<TargetInvocationException>(() => prop!.GetValue(client));\n        Assert.IsType<ArgumentException>(ex.InnerException);\n        Assert.Contains(\"invalid\", ex.InnerException!.Message, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task CreateContentAsync_WithHeaders_IncludesHeaders()\n    {\n        using var client = new MailgunClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Headers = new Dictionary<string, string> { [\"X-Test\"] = \"123\" }\n        };\n        MethodInfo? method = typeof(MailgunClient).GetMethod(\"CreateContentAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n        Assert.NotNull(method);\n        var task = (Task<MultipartFormDataContent>)method!.Invoke(client, new object[] { default(System.Threading.CancellationToken) })!;\n        using var content = await task;\n        string body = await content.ReadAsStringAsync();\n        Assert.Contains(\"h:X-Test\", body, StringComparison.OrdinalIgnoreCase);\n        Assert.Contains(\"123\", body, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public async Task CreateContentAsync_DuplicateAttachmentPaths_SkipsDuplicates()\n    {\n        var file = Path.GetTempFileName();\n        try\n        {\n            using var client = new MailgunClient\n            {\n                From = \"sender@example.com\",\n                To = new List<object> { \"to@example.com\" },\n                Attachment = new[] { file, file }\n            };\n            MethodInfo? method = typeof(MailgunClient).GetMethod(\"CreateContentAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n            var task = (Task<MultipartFormDataContent>)method!.Invoke(client, new object[] { default(CancellationToken) })!;\n            using var content = await task;\n            var parts = content.Count(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"attachment\");\n            Assert.Equal(1, parts);\n        }\n        finally\n        {\n            File.Delete(file);\n        }\n    }\n\n    [Fact]\n    public async Task CreateContentAsync_DuplicateInlineAttachmentPaths_SkipsDuplicates()\n    {\n        var file = Path.GetTempFileName();\n        try\n        {\n            using var client = new MailgunClient\n            {\n                From = \"sender@example.com\",\n                To = new List<object> { \"to@example.com\" },\n                InlineAttachment = new[] { file, file }\n            };\n            MethodInfo? method = typeof(MailgunClient).GetMethod(\"CreateContentAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n            var task = (Task<MultipartFormDataContent>)method!.Invoke(client, new object[] { default(CancellationToken) })!;\n            using var content = await task;\n            var parts = content.Count(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"inline\");\n            Assert.Equal(1, parts);\n        }\n        finally\n        {\n            File.Delete(file);\n        }\n    }\n\n    [Fact]\n    public async Task CreateContentAsync_AttachmentAndInlineDuplicatePaths_SkipsDuplicates()\n    {\n        var file1 = Path.GetTempFileName();\n        var file2 = Path.GetTempFileName();\n        try\n        {\n            using var client = new MailgunClient\n            {\n                From = \"sender@example.com\",\n                To = new List<object> { \"to@example.com\" },\n                Attachment = new[] { file1 },\n                InlineAttachment = new[] { file1, file2, file2 }\n            };\n            MethodInfo? method = typeof(MailgunClient).GetMethod(\"CreateContentAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n            var task = (Task<MultipartFormDataContent>)method!.Invoke(client, new object[] { default(CancellationToken) })!;\n            using var content = await task;\n            var attachments = content.Count(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"attachment\");\n            var inlines = content.Count(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"inline\");\n            Assert.Equal(1, attachments);\n            Assert.Equal(1, inlines);\n        }\n        finally\n        {\n            File.Delete(file1);\n            File.Delete(file2);\n        }\n    }\n\n    [Fact]\n    public async Task CreateContentAsync_UsesStreamContentForFiles()\n    {\n        var attachment = Path.GetTempFileName();\n        var inline = Path.GetTempFileName();\n        try\n        {\n            File.WriteAllBytes(attachment, new byte[1024]);\n            File.WriteAllBytes(inline, new byte[1024]);\n\n            using var client = new MailgunClient\n            {\n                From = \"sender@example.com\",\n                To = new List<object> { \"to@example.com\" },\n                Attachment = new[] { attachment },\n                InlineAttachment = new[] { inline }\n            };\n\n            MethodInfo? method = typeof(MailgunClient).GetMethod(\"CreateContentAsync\", BindingFlags.NonPublic | BindingFlags.Instance);\n            var task = (Task<MultipartFormDataContent>)method!.Invoke(client, new object[] { default(CancellationToken) })!;\n            using var content = await task;\n\n            var attachmentContent = content.Single(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"attachment\");\n            var inlineContent = content.Single(c => c.Headers.ContentDisposition?.Name?.Trim('\"') == \"inline\");\n\n            Assert.IsType<StreamContent>(attachmentContent);\n            Assert.IsType<StreamContent>(inlineContent);\n\n            var streamField = typeof(StreamContent)\n                .GetFields(BindingFlags.NonPublic | BindingFlags.Instance)\n                .FirstOrDefault(f => typeof(Stream).IsAssignableFrom(f.FieldType));\n            Assert.NotNull(streamField);\n\n            var attachmentStream = (Stream)streamField!.GetValue(attachmentContent)!;\n            var inlineStream = (Stream)streamField.GetValue(inlineContent)!;\n\n            Assert.IsType<FileStream>(attachmentStream);\n            Assert.IsType<FileStream>(inlineStream);\n            Assert.Equal(new FileInfo(attachment).Length, attachmentContent.Headers.ContentLength);\n            Assert.Equal(new FileInfo(inline).Length, inlineContent.Headers.ContentLength);\n        }\n        finally\n        {\n            File.Delete(attachment);\n            File.Delete(inline);\n        }\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_InvalidCredentials_ThrowsInvalidOperationException()\n    {\n        using var client = new MailgunClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Credentials = new DummyCredentials()\n        };\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendEmailAsync());\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_DisposesResponse_OnSuccess()\n    {\n        var handler = new TrackingHandler(HttpStatusCode.OK);\n        using var client = CreateClient(handler);\n        var result = await client.SendEmailAsync();\n        Assert.True(result.Status);\n        Assert.True(handler.ResponseDisposed);\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_DisposesResponse_OnFailure()\n    {\n        var handler = new TrackingHandler(HttpStatusCode.BadRequest, \"bad\");\n        using var client = CreateClient(handler);\n        var result = await client.SendEmailAsync();\n        Assert.False(result.Status);\n        Assert.True(handler.ResponseDisposed);\n    }\n\n    [Fact]\n    public void Dispose_DerivedType_DisposesHttpClient()\n    {\n        var handler = new DisposingHandler();\n        var httpClient = new HttpClient(handler);\n        var client = new DerivedMailgunClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Credentials = new NetworkCredential(string.Empty, \"key\")\n        };\n        var field = typeof(MailgunClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance);\n        field!.SetValue(client, httpClient);\n\n        client.Dispose();\n\n        Assert.True(handler.Disposed);\n        Assert.True(client.Disposed);\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_AfterDispose_ThrowsObjectDisposedException()\n    {\n        var client = new MailgunClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Credentials = new NetworkCredential(string.Empty, \"key\")\n        };\n\n        client.Dispose();\n\n        await Assert.ThrowsAsync<ObjectDisposedException>(() => client.SendEmailAsync());\n    }\n\n    private static MailgunClient CreateClient(HttpMessageHandler handler)\n    {\n        var httpClient = new HttpClient(handler);\n        var client = new MailgunClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Credentials = new NetworkCredential(string.Empty, \"key\")\n        };\n        var field = typeof(MailgunClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance);\n        field!.SetValue(client, httpClient);\n        return client;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Mailozaurr.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFrameworks>net472;net8.0</TargetFrameworks>\n        <IsPackable>false</IsPackable>\n        <IsTestProject>true</IsTestProject>\n        <Nullable>enable</Nullable>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <LangVersion>latest</LangVersion>\n        <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>\n        <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\" />\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n        <PackageReference Include=\"Moq\" Version=\"4.20.72\" />\n        <PackageReference Include=\"PowerShellStandard.Library\" Version=\"5.1.1\" PrivateAssets=\"all\" />\n        <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n        <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <Using Include=\"Xunit\" />\n    </ItemGroup>\n\n    <ItemGroup Condition=\" '$(TargetFramework)' == 'net472' \">\n        <Reference Include=\"System.IO.Compression\" />\n    </ItemGroup>\n\n  <ItemGroup>\n        <ProjectReference Include=\"..\\Mailozaurr.Cli\\Mailozaurr.Cli.csproj\" Condition=\" '$(TargetFramework)' == 'net8.0' \" />\n        <ProjectReference Include=\"..\\Mailozaurr.Application\\Mailozaurr.Application.csproj\" />\n        <ProjectReference Include=\"..\\Mailozaurr\\Mailozaurr.csproj\" />\n        <ProjectReference Include=\"..\\Mailozaurr.Msg\\Mailozaurr.Msg.csproj\" />\n        <ProjectReference Include=\"..\\Mailozaurr.PowerShell\\Mailozaurr.PowerShell.csproj\" />\n        <ProjectReference Include=\"..\\Mailozaurr.Examples\\Mailozaurr.Examples.csproj\" Condition=\" '$(TargetFramework)' == 'net8.0' \" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MessageFlaggerTests.cs",
    "content": "using System.Threading.Tasks;\nusing MailKit.Net.Pop3;\nusing MailKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class MessageFlaggerTests {\n    private class FakeFolder : MessageFlagSetter.IImapFolder {\n        public readonly HashSet<uint> Read = new();\n        public Task AddFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            if ((flags & MessageFlags.Seen) != 0)\n                Read.Add(uid.Id);\n            return Task.CompletedTask;\n        }\n        public Task RemoveFlagsAsync(UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default) {\n            if ((flags & MessageFlags.Seen) != 0)\n                Read.Remove(uid.Id);\n            return Task.CompletedTask;\n        }\n    }\n\n    [Fact]\n    public async Task SetImapFlags_MarksRead() {\n        var folder = new FakeFolder();\n        await MessageFlagSetter.SetFlagsAsync(folder, new UniqueId(1), MessageFlags.Seen, true);\n        Assert.Contains((uint)1, folder.Read);\n        await MessageFlagSetter.SetFlagsAsync(folder, new UniqueId(1), MessageFlags.Seen, false);\n        Assert.DoesNotContain((uint)1, folder.Read);\n    }\n\n    [Fact]\n    public async Task SetImapFlags_DryRun_DoesNotChangeState() {\n        var folder = new FakeFolder();\n        await MessageFlagSetter.SetFlagsAsync(folder, new UniqueId(1), MessageFlags.Seen, true, dryRun: true);\n        Assert.DoesNotContain((uint)1, folder.Read);\n    }\n\n    [Fact]\n    public async Task SetPop3Flags_StoresState() {\n        using var client = new Pop3Client();\n        await MessageFlagSetter.SetReadAsync(client, 2, true);\n        Assert.True(MessageFlagSetter.TryGetPop3Read(client, 2, out var read) && read);\n        await MessageFlagSetter.SetReadAsync(client, 2, false);\n        Assert.True(MessageFlagSetter.TryGetPop3Read(client, 2, out read) && !read);\n    }\n\n    [Fact]\n    public async Task SetPop3Flags_DryRun_DoesNotStoreState() {\n        using var client = new Pop3Client();\n        await MessageFlagSetter.SetReadAsync(client, 2, true, dryRun: true);\n        Assert.False(MessageFlagSetter.TryGetPop3Read(client, 2, out _));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MessageInfoTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing MimeKit;\nusing MailKit;\nusing Mailozaurr;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Tests for message information helper classes.\n/// </summary>\npublic class MessageInfoTests {\n    [Fact]\n    public void ImapInfo_ExposesProperties() {\n        var msg = new MimeMessage();\n        msg.From.Add(new MailboxAddress(\"From\", \"from@example.com\"));\n        msg.To.Add(new MailboxAddress(\"To\", \"to@example.com\"));\n        msg.Subject = \"subj\";\n        msg.Date = DateTimeOffset.UtcNow;\n        msg.Body = new TextPart(\"plain\") { Text = \"content\" };\n        var raw = new ImapEmailMessage(new UniqueId(1), msg);\n        var info = new ImapMessageInfo(raw);\n        Assert.Equal(1u, info.Uid.Id);\n        Assert.Contains(\"from@example.com\", info.From);\n        Assert.Equal(\"subj\", info.Subject);\n        Assert.Same(raw, info.Raw);\n        Assert.Equal(\"content\", info.TextBody);\n    }\n\n    [Fact]\n    public void Pop3Info_ExposesProperties() {\n        var msg = new MimeMessage();\n        msg.From.Add(new MailboxAddress(\"F\", \"f@example.com\"));\n        msg.To.Add(new MailboxAddress(\"T\", \"t@example.com\"));\n        msg.Subject = \"hello\";\n        msg.Date = DateTimeOffset.UtcNow;\n        msg.Body = new TextPart(\"plain\") { Text = \"pcontent\" };\n        var raw = new Pop3EmailMessage(5, msg);\n        var info = new Pop3MessageInfo(raw);\n        Assert.Equal(5, info.Index);\n        Assert.Contains(\"f@example.com\", info.From);\n        Assert.Equal(\"hello\", info.Subject);\n        Assert.Same(raw, info.Raw);\n        Assert.Equal(\"pcontent\", info.TextBody);\n    }\n\n    [Fact]\n    public void GraphInfo_ExposesProperties() {\n        var raw = new Dictionary<string, object> {\n            [\"id\"] = \"1\",\n            [\"subject\"] = \"sub\",\n            [\"from\"] = new Dictionary<string, object> {\n                [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"a@example.com\" }\n            },\n            [\"toRecipients\"] = new object[] {\n                new Dictionary<string, object> {\n                    [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"b@example.com\" }\n                }\n            },\n            [\"sentDateTime\"] = DateTime.UtcNow.ToString(\"o\"),\n            [\"bodyPreview\"] = \"preview\",\n            [\"body\"] = new Dictionary<string, object> {\n                [\"content\"] = \"gcontent\"\n            },\n            [\"isRead\"] = true,\n            [\"importance\"] = \"high\"\n        };\n        var info = new GraphMessageInfo(raw, \"user@example.com\");\n        Assert.Equal(\"1\", info.Id);\n        Assert.Equal(\"user@example.com\", info.UserPrincipalName);\n        Assert.Contains(\"a@example.com\", info.From!);\n        Assert.Contains(\"b@example.com\", info.To!);\n        Assert.Equal(\"sub\", info.Subject);\n        Assert.Same(raw, info.Raw);\n        Assert.Equal(\"preview\", info.BodyPreview);\n        Assert.Equal(\"gcontent\", info.Content);\n        Assert.True(info.IsRead);\n        Assert.Equal(GraphImportance.High, info.Importance);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MicrosoftGraphUtilsCertificateTokenCachingTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class MicrosoftGraphUtilsCertificateTokenCachingTests : IDisposable {\n    public MicrosoftGraphUtilsCertificateTokenCachingTests() {\n        ClearCaches();\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_WithCertificateBytes_UsesCachedToken() {\n        ClearCaches();\n        int callCount = 0;\n        var authorization = new GraphAuthorization {\n            AccessToken = \"bytes-token\",\n            TokenType = \"Bearer\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n        MicrosoftGraphUtils.AcquireGraphCertificateBytesTokenAsyncFunc = (clientId, tenant, bytes, password, scopes) => {\n            callCount++;\n            return Task.FromResult(authorization);\n        };\n        var credential = new GraphCredential {\n            ClientId = \"client-id\",\n            DirectoryId = \"tenant-id\",\n            CertificateBytes = new byte[] { 1, 2, 3 },\n            CertificatePassword = \"pwd\"\n        };\n\n        try {\n            var token1 = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\");\n            var token2 = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\");\n\n            Assert.Equal(\"Bearer bytes-token\", token1);\n            Assert.Equal(token1, token2);\n            Assert.Equal(1, callCount);\n        } finally {\n            MicrosoftGraphUtils.ResetOAuthHelperOverrides();\n        }\n    }\n\n    [Fact]\n    public async Task ConnectO365GraphAsync_WithCertificatePem_UsesCachedToken() {\n        ClearCaches();\n        int callCount = 0;\n        var authorization = new GraphAuthorization {\n            AccessToken = \"pem-token\",\n            TokenType = \"Bearer\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n        MicrosoftGraphUtils.AcquireGraphCertificatePemTokenAsyncFunc = (clientId, tenant, path, scopes) => {\n            callCount++;\n            return Task.FromResult(authorization);\n        };\n        var credential = new GraphCredential {\n            ClientId = \"client-id\",\n            DirectoryId = \"tenant-id\",\n            CertificatePemPath = \"certificate.pem\"\n        };\n\n        try {\n            var token1 = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\");\n            var token2 = await MicrosoftGraphUtils.ConnectO365GraphAsync(credential, credential.DirectoryId, \"https://graph.microsoft.com\");\n\n            Assert.Equal(\"Bearer pem-token\", token1);\n            Assert.Equal(token1, token2);\n            Assert.Equal(1, callCount);\n        } finally {\n            MicrosoftGraphUtils.ResetOAuthHelperOverrides();\n        }\n    }\n\n    private static void ClearCaches() {\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static);\n        if (cacheField?.GetValue(null) is ConcurrentDictionary<string, GraphAuthorization> cache) {\n            cache.Clear();\n        }\n\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n    }\n\n    public void Dispose() {\n        MicrosoftGraphUtils.ResetOAuthHelperOverrides();\n        ClearCaches();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MicrosoftGraphUtilsJunkMailTests.cs",
    "content": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic sealed class MicrosoftGraphUtilsJunkMailTests {\n    [Fact]\n    public void FilterJunkMessages_SingleUseSkipEnumerables_AppliesFiltersAcrossMultipleMessages() {\n        var skipFrom = new SingleUseEnumerable<string>(\"skip-from@example.com\");\n        var skipTo = new SingleUseEnumerable<string>(\"skip-to@example.com\");\n        var skipSubjectContains = new SingleUseEnumerable<string>(\"urgent\");\n\n        var first = new Dictionary<string, object> {\n            [\"id\"] = \"1\",\n            [\"from\"] = new Dictionary<string, object> {\n                [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"keep@example.com\" }\n            },\n            [\"toRecipients\"] = new object[] {\n                new Dictionary<string, object> {\n                    [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"keep-recipient@example.com\" }\n                }\n            },\n            [\"subject\"] = \"normal\"\n        };\n        var second = new Dictionary<string, object> {\n            [\"id\"] = \"2\",\n            [\"from\"] = new Dictionary<string, object> {\n                [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"skip-from@example.com\" }\n            },\n            [\"toRecipients\"] = new object[] {\n                new Dictionary<string, object> {\n                    [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"skip-to@example.com\" }\n                }\n            },\n            [\"subject\"] = \"urgent message\"\n        };\n\n        var filtered = MicrosoftGraphUtils.FilterJunkMessages(\n            new[] { first, second },\n            skipFrom: skipFrom,\n            skipTo: skipTo,\n            skipSubjectContains: skipSubjectContains);\n\n        var remaining = Assert.Single(filtered);\n        Assert.Same(first, remaining);\n        Assert.Equal(1, skipFrom.EnumerationCount);\n        Assert.Equal(1, skipTo.EnumerationCount);\n        Assert.Equal(1, skipSubjectContains.EnumerationCount);\n    }\n\n    [Fact]\n    public async Task GetJunkMailMessagesAsync_SingleUseSkipAttachmentExtensions_AppliesFilter() {\n        var handler = new JunkMailHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var cacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var cache = (System.Collections.Concurrent.ConcurrentDictionary<string, GraphAuthorization>)cacheField.GetValue(null)!;\n        cache.Clear();\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n        try {\n            var credential = new GraphCredential { ClientId = \"id\", ClientSecret = \"secret\", DirectoryId = \"tenant\" };\n            var skipExtensions = new SingleUseEnumerable<string>(\".pdf\");\n\n            var results = await MicrosoftGraphUtils.GetJunkMailMessagesAsync(\n                credential,\n                \"user@example.com\",\n                skipAttachmentExtension: skipExtensions);\n\n            var remaining = Assert.Single(results);\n            Assert.Equal(\"message-keep\", remaining[\"id\"]);\n            Assert.Equal(1, skipExtensions.EnumerationCount);\n            Assert.Equal(1, handler.AttachmentRequestCount);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    private static FieldInfo GetHandlerField()\n        => typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    private sealed class JunkMailHandler : HttpMessageHandler {\n        public int AttachmentRequestCount { get; private set; }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.IndexOf(\"oauth2\", StringComparison.Ordinal) >= 0) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/mailFolders/junkemail/messages\", StringComparison.Ordinal)) {\n                const string json = \"{\\\"value\\\":[{\\\"id\\\":\\\"message-drop\\\",\\\"hasAttachments\\\":true},{\\\"id\\\":\\\"message-keep\\\",\\\"hasAttachments\\\":false}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages/message-drop/attachments\", StringComparison.Ordinal)) {\n                AttachmentRequestCount++;\n                const string json = \"{\\\"value\\\":[{\\\"name\\\":\\\"invoice.pdf\\\"}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages/message-keep/attachments\", StringComparison.Ordinal)) {\n                AttachmentRequestCount++;\n                const string json = \"{\\\"value\\\":[{\\\"name\\\":\\\"note.txt\\\"}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(json)\n                });\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private sealed class SingleUseEnumerable<T> : IEnumerable<T> {\n        private readonly IReadOnlyList<T> items;\n\n        public SingleUseEnumerable(params T[] items) {\n            this.items = items;\n        }\n\n        public int EnumerationCount { get; private set; }\n\n        public IEnumerator<T> GetEnumerator() {\n            EnumerationCount++;\n            if (EnumerationCount > 1) {\n                throw new InvalidOperationException(\"Sequence was enumerated more than once.\");\n            }\n\n            return items.GetEnumerator();\n        }\n\n        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MicrosoftGraphUtilsNullResponseTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class MicrosoftGraphUtilsNullResponseTests {\n    private sealed class NullContentHandler : HttpMessageHandler {\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            if (request.RequestUri!.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"null\") });\n        }\n    }\n\n    private static FieldInfo GetHandlerField() =>\n        typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    [Fact]\n    public async Task NewRuleAsync_NullResponse_Throws() {\n        var handler = new NullContentHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            var rule = new GraphInboxRule();\n            await Assert.ThrowsAsync<System.IO.InvalidDataException>(() =>\n                MicrosoftGraphUtils.NewRuleAsync(cred, \"user@example.com\", rule));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task NewEventAsync_NullResponse_Throws() {\n        var handler = new NullContentHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            var ev = new GraphEvent();\n            await Assert.ThrowsAsync<System.IO.InvalidDataException>(() =>\n                MicrosoftGraphUtils.NewEventAsync(cred, \"user@example.com\", ev));\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MicrosoftGraphUtilsPagingTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Mailozaurr;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class MicrosoftGraphUtilsPagingTests {\n    private static FieldInfo GetHandlerField() =>\n        typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    [Fact]\n    public async Task GetMailMessagesAsync_FollowsNextLinkAndRespectsLimit() {\n        var page1 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"id\\\":\\\"1\\\"},{\\\"id\\\":\\\"2\\\"}],\\\"@odata.nextLink\\\":\\\"https://graph.microsoft.com/v1.0/users/u/messages?$skip=2\\\"}\")\n        };\n        var page2 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"id\\\":\\\"3\\\"},{\\\"id\\\":\\\"4\\\"}]}\")\n        };\n        var handler = new RecordingHandler(page1, page2);\n        var httpClientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)httpClientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var tokenCacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var tokenCache = (ConcurrentDictionary<string, GraphAuthorization>)tokenCacheField.GetValue(null)!;\n        var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n        var key = \"id|tenant||secret|https://graph.microsoft.com\";\n        tokenCache[key] = new GraphAuthorization { AccessToken = \"token\", TokenType = \"Bearer\", ExpiresOn = DateTimeOffset.UtcNow.AddHours(1) };\n        try {\n            var messages = await MicrosoftGraphUtils.GetMailMessagesAsync(cred, \"u\", limit: 3);\n            Assert.Equal(3, messages.Count);\n            Assert.Equal(\"1\", messages[0][\"id\"]);\n            Assert.Equal(\"2\", messages[1][\"id\"]);\n            Assert.Equal(\"3\", messages[2][\"id\"]);\n            Assert.Equal(\"https://graph.microsoft.com/v1.0/users/u/messages?$skip=2\", handler.Requests[1].RequestUri!.AbsoluteUri);\n            Assert.Equal(2, handler.Requests.Count);\n        } finally {\n            handlerField.SetValue(client, original);\n            tokenCache.TryRemove(key, out _);\n        }\n    }\n\n    [Fact]\n    public async Task GetMailMessagesAsync_CanBeCancelledDuringPaging() {\n        const string firstPage = \"{\\\"value\\\":[{\\\"id\\\":\\\"1\\\"}],\\\"@odata.nextLink\\\":\\\"https://graph.microsoft.com/v1.0/users/u/messages?$skip=1\\\"}\";\n        var handler = new BlockingHandler(firstPage);\n        var httpClientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)httpClientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var tokenCacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var tokenCache = (ConcurrentDictionary<string, GraphAuthorization>)tokenCacheField.GetValue(null)!;\n        var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n        var key = \"id|tenant||secret|https://graph.microsoft.com\";\n        tokenCache[key] = new GraphAuthorization { AccessToken = \"token\", TokenType = \"Bearer\", ExpiresOn = DateTimeOffset.UtcNow.AddHours(1) };\n        using var cts = new CancellationTokenSource();\n        try {\n            var task = MicrosoftGraphUtils.GetMailMessagesAsync(cred, \"u\", cancellationToken: cts.Token);\n            await handler.FirstRequestProcessed.Task;\n            var secondRequestObserved = await Task.WhenAny(handler.SecondRequestStarted.Task, Task.Delay(TimeSpan.FromSeconds(5)));\n            Assert.Same(handler.SecondRequestStarted.Task, secondRequestObserved);\n            cts.Cancel();\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);\n            Assert.Equal(2, handler.Requests.Count);\n        } finally {\n            handlerField.SetValue(client, original);\n            tokenCache.TryRemove(key, out _);\n        }\n    }\n\n    [Fact]\n    public async Task GetMailMessageAttachmentsAsync_FollowsNextLink() {\n        var page1 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"name\\\":\\\"a1\\\",\\\"contentBytes\\\":\\\"QQ==\\\"}],\\\"@odata.nextLink\\\":\\\"https://graph.microsoft.com/v1.0/users/u/messages/m/attachments?$skip=1\\\"}\")\n        };\n        var page2 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"name\\\":\\\"a2\\\",\\\"contentBytes\\\":\\\"Qg==\\\"}]}\")\n        };\n        var handler = new RecordingHandler(page1, page2);\n        var httpClientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)httpClientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var tokenCacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var tokenCache = (ConcurrentDictionary<string, GraphAuthorization>)tokenCacheField.GetValue(null)!;\n        var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n        var key = \"id|tenant||secret|https://graph.microsoft.com\";\n        tokenCache[key] = new GraphAuthorization { AccessToken = \"token\", TokenType = \"Bearer\", ExpiresOn = DateTimeOffset.UtcNow.AddHours(1) };\n        try {\n            var attachments = await MicrosoftGraphUtils.GetMailMessageAttachmentsAsync(cred, \"u\", \"m\");\n\n            Assert.Equal(2, attachments.Count);\n            Assert.Equal(\"a1\", attachments[0].Name);\n            Assert.Equal(\"a2\", attachments[1].Name);\n            Assert.Equal(\"https://graph.microsoft.com/v1.0/users/u/messages/m/attachments?$skip=1\", handler.Requests[1].RequestUri!.AbsoluteUri);\n            Assert.Equal(2, handler.Requests.Count);\n        } finally {\n            handlerField.SetValue(client, original);\n            tokenCache.TryRemove(key, out _);\n        }\n    }\n\n    [Fact]\n    public async Task GetMailFoldersAsync_FollowsNextLink() {\n        var page1 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"id\\\":\\\"f1\\\"}],\\\"@odata.nextLink\\\":\\\"https://graph.microsoft.com/v1.0/users/u/mailFolders?$skip=1\\\"}\")\n        };\n        var page2 = new HttpResponseMessage(HttpStatusCode.OK) {\n            Content = new StringContent(\"{\\\"value\\\":[{\\\"id\\\":\\\"f2\\\"}]}\")\n        };\n        var handler = new RecordingHandler(page1, page2);\n        var httpClientField = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)httpClientField.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        var tokenCacheField = typeof(MicrosoftGraphUtils).GetField(\"TokenCache\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var tokenCache = (ConcurrentDictionary<string, GraphAuthorization>)tokenCacheField.GetValue(null)!;\n        var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n        var key = \"id|tenant||secret|https://graph.microsoft.com\";\n        tokenCache[key] = new GraphAuthorization { AccessToken = \"token\", TokenType = \"Bearer\", ExpiresOn = DateTimeOffset.UtcNow.AddHours(1) };\n        try {\n            var folders = await MicrosoftGraphUtils.GetMailFoldersAsync(cred, \"u\");\n\n            Assert.Equal(2, folders.Count);\n            Assert.Equal(\"f1\", folders[0].GetProperty(\"id\").GetString());\n            Assert.Equal(\"f2\", folders[1].GetProperty(\"id\").GetString());\n            Assert.Equal(\"https://graph.microsoft.com/v1.0/users/u/mailFolders?$skip=1\", handler.Requests[1].RequestUri!.AbsoluteUri);\n            Assert.Equal(2, handler.Requests.Count);\n        } finally {\n            handlerField.SetValue(client, original);\n            tokenCache.TryRemove(key, out _);\n        }\n    }\n\n    private sealed class BlockingHandler : HttpMessageHandler {\n        private readonly string _firstPageJson;\n        public List<HttpRequestMessage> Requests { get; } = new();\n        public TaskCompletionSource<bool> FirstRequestProcessed { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        public TaskCompletionSource<bool> SecondRequestStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        public BlockingHandler(string firstPageJson) {\n            _firstPageJson = firstPageJson;\n        }\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var copy = new HttpRequestMessage(request.Method, request.RequestUri);\n            foreach (var header in request.Headers) {\n                copy.Headers.TryAddWithoutValidation(header.Key, header.Value);\n            }\n            if (request.Content != null) {\n                var bytes = await request.Content.ReadAsByteArrayAsync();\n                copy.Content = new ByteArrayContent(bytes);\n                foreach (var header in request.Content.Headers) {\n                    copy.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);\n                }\n            }\n            Requests.Add(copy);\n            if (Requests.Count == 1) {\n                FirstRequestProcessed.TrySetResult(true);\n                return new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(_firstPageJson)\n                };\n            }\n\n            SecondRequestStarted.TrySetResult(true);\n            await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);\n            return new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"value\\\":[]}\")\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MicrosoftGraphUtilsTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Xunit;\nusing Mailozaurr;\n\nnamespace Mailozaurr.Tests;\n\npublic class MicrosoftGraphUtilsTests\n{\n    [Fact]\n    public void BuildGraphUri_JoinsBaseAndPath()\n    {\n        string uri = MicrosoftGraphUtils.BuildGraphUri(GraphEndpoint.V1, \"/users/me\");\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/users/me\", uri);\n    }\n\n    [Fact]\n    public void BuildGraphUri_EncodesQueryParameters()\n    {\n        var uri = MicrosoftGraphUtils.BuildGraphUri(\"https://api.example.com\", \"search\", new Dictionary<string, string> { { \"q\", \"a b\" } });\n        Assert.Equal(\"https://api.example.com/search?q=a%20b\", uri);\n    }\n\n    [Fact]\n    public void JoinUriQuery_JoinsUriCorrectly()\n    {\n        string uri = MicrosoftGraphUtils.JoinUriQuery(GraphEndpoint.V1, \"users/me\");\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/users/me\", uri);\n    }\n\n    [Fact]\n    public void JoinUriQuery_EncodesQueryParameters()\n    {\n        var uri = MicrosoftGraphUtils.JoinUriQuery(\"https://example.com\", \"path\", new Dictionary<string, object> { { \"filter\", \"a&b\" } });\n        Assert.Equal(\"https://example.com/path?filter=a%26b\", uri);\n    }\n\n    [Fact]\n    public void RemoveEmptyValues_RemovesNestedEmptyEntries()\n    {\n        var dict = new Dictionary<string, object>\n        {\n            [\"keep\"] = \"value\",\n            [\"empty\"] = \"\",\n            [\"nested\"] = new Dictionary<string, object> { [\"sub\"] = \"\" }\n        };\n\n        MicrosoftGraphUtils.RemoveEmptyValues(dict);\n\n        Assert.True(dict.ContainsKey(\"keep\"));\n        Assert.False(dict.ContainsKey(\"empty\"));\n        Assert.False(dict.ContainsKey(\"nested\"));\n    }\n\n    [Fact]\n    public void RemoveEmptyValues_RespectsExcludeParameter()\n    {\n        var dict = new Dictionary<string, object>\n        {\n            [\"remove\"] = \"\",\n            [\"keep\"] = \"\"\n        };\n        var exclude = new HashSet<string> { \"keep\" };\n\n        MicrosoftGraphUtils.RemoveEmptyValues(dict, exclude);\n\n        Assert.False(dict.ContainsKey(\"remove\"));\n        Assert.True(dict.ContainsKey(\"keep\"));\n    }\n\n    [Fact]\n    public void FilterJunkMessages_SkipId()\n    {\n        var msg1 = new Dictionary<string, object> { [\"id\"] = \"1\" };\n        var msg2 = new Dictionary<string, object> { [\"id\"] = \"2\" };\n        var filtered = MicrosoftGraphUtils.FilterJunkMessages(new[] { msg1, msg2 }, skipIds: new[] { \"1\" });\n        Assert.DoesNotContain(msg1, filtered);\n        Assert.Contains(msg2, filtered);\n    }\n\n    [Fact]\n    public void FilterJunkMessages_SkipFromToSubjectAndAttachment()\n    {\n        var msg1 = new Dictionary<string, object>\n        {\n            [\"id\"] = \"1\",\n            [\"from\"] = new Dictionary<string, object>\n            {\n                [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"sender@example.com\" }\n            },\n            [\"toRecipients\"] = new object[]\n            {\n                new Dictionary<string, object>\n                {\n                    [\"emailAddress\"] = new Dictionary<string, object> { [\"address\"] = \"rcpt@example.com\" }\n                }\n            },\n            [\"subject\"] = \"Urgent meeting\",\n            [\"hasAttachments\"] = true\n        };\n\n        var filtered = MicrosoftGraphUtils.FilterJunkMessages(\n            new[] { msg1 },\n            skipFrom: new[] { \"sender@example.com\" },\n            skipTo: new[] { \"rcpt@example.com\" },\n            skipSubjectContains: new[] { \"urgent\" },\n            skipHasAttachment: true);\n\n        Assert.Empty(filtered);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/MimeKitNonDeliveryReportTests.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing MimeKit;\nusing System.IO;\nusing System.Text;\nusing System;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class MimeKitNonDeliveryReportTests {\n    [Fact]\n    public void GetNonDeliveryReports_ParsesSingleMessage() {\n        const string raw = \"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\n\\n--XXX\\nContent-Type: text/plain; charset=utf-8\\n\\ntext\\n\\n--XXX\\nContent-Type: message/delivery-status\\n\\nOriginal-Recipient: rfc822; user@example.com\\nFinal-Recipient: rfc822; user@example.com\\nReporting-MTA: dns; mx.example.com\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\nStatus: 5.1.1\\nArrival-Date: Wed, 24 Jul 2024 10:00:00 +0000\\n\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        Assert.Single(reports);\n        Assert.Equal(NonDeliveryReportType.UnknownRecipient, reports[0].Type);\n    }\n\n    [Fact]\n    public void GetNonDeliveryReports_ParsesMultipleReports() {\n        const string raw = \"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\n\\n--XXX\\nContent-Type: text/plain; charset=utf-8\\n\\ntext\\n\\n--XXX\\nContent-Type: message/delivery-status\\n\\nOriginal-Recipient: rfc822; user1@example.com\\nFinal-Recipient: rfc822; user1@example.com\\nReporting-MTA: dns; mx.example.com\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\nStatus: 5.1.1\\nArrival-Date: Wed, 24 Jul 2024 10:00:00 +0000\\n\\nOriginal-Recipient: rfc822; user2@example.com\\nFinal-Recipient: rfc822; user2@example.com\\nReporting-MTA: dns; mx.example.com\\nDiagnostic-Code: smtp; 550 5.2.2 Mailbox full\\nStatus: 5.2.2\\nArrival-Date: Wed, 24 Jul 2024 10:00:00 +0000\\n\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        Assert.Equal(2, reports.Count);\n        Assert.EndsWith(\"user1@example.com\", reports[0].FinalRecipient);\n        Assert.EndsWith(\"user2@example.com\", reports[1].FinalRecipient);\n    }\n\n    [Fact]\n    public void GetNonDeliveryReports_DetectsReportViaSubject() {\n        const string raw = \"Subject: Mail Delivery Subsystem\\n\\ntext\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        Assert.Single(reports);\n    }\n\n    [Fact]\n    public void GetNonDeliveryReports_SubjectReportUsesMessageDate() {\n        var date = new DateTimeOffset(2024, 7, 24, 10, 0, 0, TimeSpan.Zero);\n        string raw = $\"Date: {date:R}\\r\\nSubject: Mail Delivery Subsystem\\r\\n\\r\\ntext\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        Assert.Single(reports);\n        Assert.Equal(date, reports[0].Timestamp);\n    }\n\n    [Fact]\n    public void GetNonDeliveryReports_IgnoresDuplicateHeaders() {\n        const string raw = \"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\n\\n--XXX\\nContent-Type: text/plain; charset=utf-8\\n\\ntext\\n\\n--XXX\\nContent-Type: message/delivery-status\\n\\nFinal-Recipient: rfc822; user@example.com\\nFinal-Recipient: rfc822; other@example.com\\nStatus: 5.1.1\\n\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var reports = MimeKitUtils.GetNonDeliveryReports(message);\n        Assert.Single(reports);\n        Assert.EndsWith(\"user@example.com\", reports[0].FinalRecipient);\n        Assert.DoesNotContain(\"other@example.com\", reports[0].FinalRecipient, StringComparison.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NativeMailboxBrowserSessionsTests.cs",
    "content": "using System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class NativeMailboxBrowserSessionsTests {\n    [Fact]\n    public async Task GraphMailboxBrowserSession_UsesProvidedHttpClientAndCredential() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"value\\\":[]}\") });\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") };\n        var credential = new OAuthCredential { UserName = \"me\", AccessToken = \"graph-token\", ExpiresOn = DateTimeOffset.MaxValue };\n\n        using var session = new GraphMailboxBrowserSession(client, credential);\n        var folders = await session.Browser.ListFoldersAsync();\n\n        Assert.Empty(folders);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://graph.microsoft.com/v1.0/me/mailFolders\", handler.Requests[0].RequestUri!.GetLeftPart(UriPartial.Path));\n        Assert.Contains(\"$top=200\", handler.Requests[0].RequestUri!.Query, StringComparison.Ordinal);\n        Assert.Contains(\"displayName\", handler.Requests[0].RequestUri!.Query, StringComparison.Ordinal);\n        Assert.Equal(\"Bearer\", handler.Requests[0].Headers.Authorization?.Scheme);\n        Assert.Equal(\"graph-token\", handler.Requests[0].Headers.Authorization?.Parameter);\n    }\n\n    [Fact]\n    public void GraphMailboxBrowserSession_CreateWithAccessToken_RejectsEmptyToken() {\n        var ex = Assert.Throws<ArgumentException>(() => GraphMailboxBrowserSession.CreateWithAccessToken(\" \"));\n        Assert.Equal(\"accessToken\", ex.ParamName);\n    }\n\n    [Fact]\n    public async Task GmailMailboxBrowserSession_UsesProvidedUserIdAndCredential() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"labels\\\":[]}\") });\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        var credential = new OAuthCredential { UserName = \"mailbox-user\", AccessToken = \"gmail-token\", ExpiresOn = DateTimeOffset.MaxValue };\n\n        using var session = new GmailMailboxBrowserSession(client, credential, userId: \"custom-user\");\n        var labels = await session.Browser.ListFoldersAsync();\n\n        Assert.Empty(labels);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/custom-user/labels?fields=labels(id,name,type)\", handler.Requests[0].RequestUri!.ToString());\n        Assert.Equal(\"Bearer\", handler.Requests[0].Headers.Authorization?.Scheme);\n        Assert.Equal(\"gmail-token\", handler.Requests[0].Headers.Authorization?.Parameter);\n    }\n\n    [Fact]\n    public async Task GmailMailboxBrowserSession_CreateWithAccessToken_DefaultsToMeUserId() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"labels\\\":[]}\") });\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n\n        using var session = GmailMailboxBrowserSession.CreateWithAccessToken(\"token-1\", client: client);\n        var labels = await session.Browser.ListFoldersAsync();\n\n        Assert.Empty(labels);\n        Assert.Single(handler.Requests);\n        Assert.Equal(\"https://gmail.googleapis.com/gmail/v1/users/me/labels?fields=labels(id,name,type)\", handler.Requests[0].RequestUri!.ToString());\n    }\n\n    [Fact]\n    public async Task GraphMailboxBrowserSession_Dispose_IsIdempotent_AndPreventsFurtherUse() {\n        var handler = new RecordingHandler();\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") };\n        var credential = new OAuthCredential { UserName = \"me\", AccessToken = \"graph-token\", ExpiresOn = DateTimeOffset.MaxValue };\n        var session = new GraphMailboxBrowserSession(client, credential);\n\n        session.Dispose();\n        session.Dispose();\n\n        await Assert.ThrowsAsync<ObjectDisposedException>(() => session.Browser.ListFoldersAsync());\n    }\n\n    [Fact]\n    public async Task GraphMailboxBrowserSession_Dispose_DoesNotDisposeProvidedHttpClient() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"value\\\":[]}\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{}\") });\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\") };\n        var credential = new OAuthCredential { UserName = \"me\", AccessToken = \"graph-token\", ExpiresOn = DateTimeOffset.MaxValue };\n        var session = new GraphMailboxBrowserSession(client, credential);\n\n        await session.Browser.ListFoldersAsync();\n        session.Dispose();\n\n        using var response = await client.GetAsync(\"me\");\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    [Fact]\n    public async Task GmailMailboxBrowserSession_Dispose_IsIdempotent_AndPreventsFurtherUse() {\n        var handler = new RecordingHandler();\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        var credential = new OAuthCredential { UserName = \"me\", AccessToken = \"gmail-token\", ExpiresOn = DateTimeOffset.MaxValue };\n        var session = new GmailMailboxBrowserSession(client, credential);\n\n        session.Dispose();\n        session.Dispose();\n\n        await Assert.ThrowsAsync<ObjectDisposedException>(() => session.Browser.ListFoldersAsync());\n    }\n\n    [Fact]\n    public async Task GmailMailboxBrowserSession_Dispose_DoesNotDisposeProvidedHttpClient() {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"labels\\\":[]}\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{}\") });\n        var client = new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        var credential = new OAuthCredential { UserName = \"me\", AccessToken = \"gmail-token\", ExpiresOn = DateTimeOffset.MaxValue };\n        var session = new GmailMailboxBrowserSession(client, credential);\n\n        await session.Browser.ListFoldersAsync();\n        session.Dispose();\n\n        using var response = await client.GetAsync(\"users/me/profile\");\n        Assert.True(response.IsSuccessStatusCode);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NativeMailboxThreadingMetadataOperationsTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class NativeMailboxThreadingMetadataOperationsTests {\n    [Fact]\n    public void Normalize_FromRawValues_NormalizesAndDeduplicates() {\n        var metadata = NativeMailboxThreadingMetadataOperations.Normalize(\n            messageId: \" <child@example.test> \",\n            replyTo: \" reply@example.test \",\n            cc: \" cc@example.test \",\n            inReplyTo: \" <parent@example.test> \",\n            references: new[] {\n                \"<root@example.test>\",\n                \"parent@example.test\",\n                \" <ROOT@example.test> \",\n                \"  \"\n            });\n\n        Assert.Equal(\"child@example.test\", metadata.MessageId);\n        Assert.Equal(\"reply@example.test\", metadata.ReplyTo);\n        Assert.Equal(\"cc@example.test\", metadata.Cc);\n        Assert.Equal(\"parent@example.test\", metadata.InReplyTo);\n        Assert.NotNull(metadata.References);\n        Assert.Equal(2, metadata.References!.Count);\n        Assert.Equal(\"root@example.test\", metadata.References[0]);\n        Assert.Equal(\"parent@example.test\", metadata.References[1]);\n    }\n\n    [Fact]\n    public void Normalize_FromGraphResult_UsesSharedNormalization() {\n        var graph = new GraphMailboxBrowser.GraphMailboxThreadingMetadataResult {\n            MessageId = \" <graph@example.test> \",\n            ReplyTo = \" graph-reply@example.test \",\n            Cc = \" graph-cc@example.test \",\n            InReplyTo = \" <graph-parent@example.test> \",\n            References = new List<string> {\n                \"<graph-root@example.test>\",\n                \"graph-parent@example.test\",\n                \"<GRAPH-ROOT@example.test>\"\n            }\n        };\n\n        var metadata = NativeMailboxThreadingMetadataOperations.Normalize(graph);\n\n        Assert.Equal(\"graph@example.test\", metadata.MessageId);\n        Assert.Equal(\"graph-reply@example.test\", metadata.ReplyTo);\n        Assert.Equal(\"graph-cc@example.test\", metadata.Cc);\n        Assert.Equal(\"graph-parent@example.test\", metadata.InReplyTo);\n        Assert.NotNull(metadata.References);\n        Assert.Equal(2, metadata.References!.Count);\n    }\n\n    [Fact]\n    public void Normalize_FromGmailResult_UsesSharedNormalization() {\n        var gmail = new GmailMailboxBrowser.GmailMailboxThreadingMetadataResult {\n            MessageId = \" <gmail@example.test> \",\n            ReplyTo = \" gmail-reply@example.test \",\n            Cc = \" gmail-cc@example.test \",\n            InReplyTo = \" <gmail-parent@example.test> \",\n            References = new List<string> {\n                \"<gmail-root@example.test>\",\n                \"gmail-parent@example.test\",\n                \"<GMAIL-ROOT@example.test>\"\n            }\n        };\n\n        var metadata = NativeMailboxThreadingMetadataOperations.Normalize(gmail);\n\n        Assert.Equal(\"gmail@example.test\", metadata.MessageId);\n        Assert.Equal(\"gmail-reply@example.test\", metadata.ReplyTo);\n        Assert.Equal(\"gmail-cc@example.test\", metadata.Cc);\n        Assert.Equal(\"gmail-parent@example.test\", metadata.InReplyTo);\n        Assert.NotNull(metadata.References);\n        Assert.Equal(2, metadata.References!.Count);\n    }\n\n    [Fact]\n    public void Normalize_EmptyReferences_ReturnsNullReferences() {\n        var metadata = NativeMailboxThreadingMetadataOperations.Normalize(\n            messageId: null,\n            replyTo: null,\n            cc: null,\n            inReplyTo: null,\n            references: new[] { \" \", \"\\t\" });\n\n        Assert.Null(metadata.MessageId);\n        Assert.Null(metadata.ReplyTo);\n        Assert.Null(metadata.Cc);\n        Assert.Null(metadata.InReplyTo);\n        Assert.Null(metadata.References);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NativeSentMailboxOperationsTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class NativeSentMailboxOperationsTests {\n    [Fact]\n    public void ResolveGraphSentFolderName_PrefersRequestedFolder() {\n        var folder = NativeSentMailboxOperations.ResolveGraphSentFolderName(\n            requestedSentFolder: \" Team/Sent \",\n            configuredSentFolder: \"ConfigSent\");\n\n        Assert.Equal(\"Team/Sent\", folder);\n    }\n\n    [Fact]\n    public void ResolveGraphSentFolderName_UsesConfiguredWhenRequestedMissing() {\n        var folder = NativeSentMailboxOperations.ResolveGraphSentFolderName(\n            requestedSentFolder: null,\n            configuredSentFolder: \" ConfigSent \");\n\n        Assert.Equal(\"ConfigSent\", folder);\n    }\n\n    [Fact]\n    public void ResolveGraphSentFolderName_UsesFallbackWhenOverridesMissing() {\n        var folder = NativeSentMailboxOperations.ResolveGraphSentFolderName(\n            requestedSentFolder: null,\n            configuredSentFolder: null);\n\n        Assert.Equal(NativeSentMailboxOperations.DefaultGraphSentFolder, folder);\n    }\n\n    [Fact]\n    public void ResolveGraphSentFolderSelector_UsesGraphFolderSelectorRules() {\n        var selector = NativeSentMailboxOperations.ResolveGraphSentFolderSelector(\n            requestedSentFolder: null,\n            configuredSentFolder: \"Sent Items\");\n\n        Assert.Equal(GraphMailboxBrowser.ResolveFolderSelector(\"Sent Items\"), selector);\n    }\n\n    [Fact]\n    public void ResolveGmailSentLabelId_IsAlwaysSystemSentLabel() {\n        var label = NativeSentMailboxOperations.ResolveGmailSentLabelId(\n            requestedSentFolder: \"CustomSent\",\n            configuredSentFolder: \"ConfiguredSent\");\n\n        Assert.Equal(NativeSentMailboxOperations.DefaultGmailSentLabelId, label);\n    }\n\n    [Fact]\n    public void ResolveGmailSentFolderName_PrefersRequestedThenConfiguredThenFallback() {\n        var requested = NativeSentMailboxOperations.ResolveGmailSentFolderName(\n            requestedSentFolder: \" Requested \",\n            configuredSentFolder: \"Configured\");\n        var configured = NativeSentMailboxOperations.ResolveGmailSentFolderName(\n            requestedSentFolder: null,\n            configuredSentFolder: \" Configured \");\n        var fallback = NativeSentMailboxOperations.ResolveGmailSentFolderName(\n            requestedSentFolder: null,\n            configuredSentFolder: null);\n\n        Assert.Equal(\"Requested\", requested);\n        Assert.Equal(\"Configured\", configured);\n        Assert.Equal(NativeSentMailboxOperations.DefaultGmailSentLabelId, fallback);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NonDeliveryReportDetectionTests.cs",
    "content": "using MailKit;\nusing Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing MimeKit;\nusing System.IO;\nusing System.Text;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class NonDeliveryReportDetectionTests {\n    private static MimeMessage CreateNdrMessage() {\n        const string raw = \"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\n\\n--XXX\\nContent-Type: text/plain; charset=utf-8\\n\\ntext\\n\\n--XXX\\nContent-Type: message/delivery-status\\n\\nOriginal-Recipient: rfc822; orig@example.com\\nFinal-Recipient: rfc822; final@example.com\\nReporting-MTA: dns; mx.example.com\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\nStatus: 5.1.1\\nArrival-Date: Wed, 24 Jul 2024 10:00:00 +0000\\n\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        return MimeMessage.Load(stream);\n    }\n\n    [Fact]\n    public void ImapEmailMessageDetectsNdr() {\n        var msg = CreateNdrMessage();\n        var imap = new ImapEmailMessage(new UniqueId(1), msg);\n        Assert.Single(imap.NonDeliveryReports);\n        Assert.Equal(NonDeliveryReportType.UnknownRecipient, imap.NonDeliveryReports[0].Type);\n    }\n\n    [Fact]\n    public void Pop3EmailMessageDetectsNdr() {\n        var msg = CreateNdrMessage();\n        var pop3 = new Pop3EmailMessage(0, msg);\n        Assert.Single(pop3.NonDeliveryReports);\n        Assert.Equal(NonDeliveryReportType.UnknownRecipient, pop3.NonDeliveryReports[0].Type);\n    }\n\n    [Fact]\n    public void GraphEmailMessageDetectsNdr() {\n        var msg = CreateNdrMessage();\n        var graph = new GraphEmailMessage(\"id\", msg);\n        Assert.Single(graph.NonDeliveryReports);\n        Assert.Equal(NonDeliveryReportType.UnknownRecipient, graph.NonDeliveryReports[0].Type);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_IgnoresRecipientPrefixes() {\n        var msg = CreateNdrMessage();\n        var reports = MailboxSearcher.FilterNonDeliveryReports(new[] { msg }, null, null, \"final@example.com\", null);\n        Assert.Single(reports);\n        reports = MailboxSearcher.FilterNonDeliveryReports(new[] { msg }, null, null, \"orig@example.com\", null);\n        Assert.Single(reports);\n        reports = MailboxSearcher.FilterNonDeliveryReports(new[] { msg }, null, null, \"rfc822\", null);\n        Assert.Empty(reports);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NonDeliveryReportServiceTests.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class NonDeliveryReportServiceTests {\n    private sealed class InMemorySentMessageRepository : ISentMessageRepository {\n        private readonly Dictionary<string, SentMessageRecord> store = new();\n\n        public Task SaveAsync(SentMessageRecord record, CancellationToken cancellationToken = default) {\n            store[record.MessageId] = record;\n            return Task.CompletedTask;\n        }\n\n        public Task<SentMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            if (messageId is null) {\n                return Task.FromResult<SentMessageRecord?>(null);\n            }\n            store.TryGetValue(messageId, out SentMessageRecord? record);\n            return Task.FromResult<SentMessageRecord?>(record);\n        }\n    }\n\n    private sealed class TestService : NonDeliveryReportServiceBase {\n        private readonly IList<NonDeliveryReport> reports;\n\n        public TestService(IList<NonDeliveryReport> reports, SendLogResolver resolver) : base(resolver) => this.reports = reports;\n\n        protected override Task<IList<NonDeliveryReport>> SearchInternalAsync(\n            DateTime? since,\n            DateTime? before,\n            string? recipientContains,\n            string? messageId,\n            int maxResults,\n            CancellationToken cancellationToken) => Task.FromResult(reports);\n    }\n\n    private sealed class ConcurrencyTestService : NonDeliveryReportServiceBase {\n        private int current;\n        private int maxConcurrent;\n\n        public ConcurrencyTestService(SendLogResolver resolver) : base(resolver) { }\n\n        public int MaxConcurrency => maxConcurrent;\n\n        protected override async Task<IList<NonDeliveryReport>> SearchInternalAsync(\n            DateTime? since,\n            DateTime? before,\n            string? recipientContains,\n            string? messageId,\n            int maxResults,\n            CancellationToken cancellationToken) {\n            int concurrency = Interlocked.Increment(ref current);\n            int initial;\n            do {\n                initial = maxConcurrent;\n                if (concurrency <= initial) {\n                    break;\n                }\n            }\n            while (Interlocked.CompareExchange(ref maxConcurrent, concurrency, initial) != initial);\n            await Task.Delay(50, cancellationToken);\n            Interlocked.Decrement(ref current);\n            return Array.Empty<NonDeliveryReport>();\n        }\n    }\n\n    [Fact]\n    public async Task SearchAsync_ResolvesSentMessage() {\n        var repo = new InMemorySentMessageRepository();\n        var record = new SentMessageRecord { MessageId = \"id1\", Recipients = \"user@example.com\", Subject = \"s\", Timestamp = DateTimeOffset.UtcNow };\n        await repo.SaveAsync(record);\n        var resolver = new SendLogResolver(repo);\n        var report = new NonDeliveryReport { OriginalMessageId = \"id1\", FinalRecipient = \"user@example.com\", Timestamp = DateTimeOffset.UtcNow };\n        var service = new TestService(new List<NonDeliveryReport> { report }, resolver);\n\n        IList<NonDeliveryReportResult> results = await service.SearchAsync();\n        Assert.Single(results);\n        Assert.Equal(report, results[0].Report);\n        Assert.Equal(record, results[0].SentMessage);\n    }\n\n    [Fact]\n    public async Task SearchAsync_SerializesInternalCalls() {\n        var repo = new InMemorySentMessageRepository();\n        var resolver = new SendLogResolver(repo);\n        var service = new ConcurrencyTestService(resolver);\n\n        Task[] tasks = Enumerable.Range(0, 5).Select(_ => service.SearchAsync()).ToArray();\n        await Task.WhenAll(tasks);\n\n        Assert.Equal(1, service.MaxConcurrency);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/NonDeliveryReportTests.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Collections.Generic;\nusing MimeKit.Utils;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class NonDeliveryReportTests {\n    [Theory]\n    [InlineData(\"5.1.1\", DsnStatusClass.PermanentFailure, NonDeliveryReportType.UnknownRecipient)]\n    [InlineData(\"5.2.2\", DsnStatusClass.PermanentFailure, NonDeliveryReportType.MailboxFull)]\n    [InlineData(\"4.2.2\", DsnStatusClass.PersistentTransientFailure, NonDeliveryReportType.MailboxFull)]\n    [InlineData(\"4.1.0\", DsnStatusClass.PersistentTransientFailure, NonDeliveryReportType.SoftBounce)]\n    [InlineData(\"5.0.0\", DsnStatusClass.PermanentFailure, NonDeliveryReportType.HardBounce)]\n    [InlineData(\"5.7.1\", DsnStatusClass.PermanentFailure, NonDeliveryReportType.PolicyBlock)]\n    [InlineData(\"5.6.1\", DsnStatusClass.PermanentFailure, NonDeliveryReportType.ContentRejected)]\n    [InlineData(\"4.4.1\", DsnStatusClass.PersistentTransientFailure, NonDeliveryReportType.DnsFailure)]\n    public void ParseStatusAndMapType(string code, DsnStatusClass expectedClass, NonDeliveryReportType expectedType) {\n        var status = DsnStatus.Parse(code);\n        Assert.Equal(expectedClass, status.Class);\n        var type = NonDeliveryReport.GetReportType(status);\n        Assert.Equal(expectedType, type);\n    }\n\n    [Fact]\n    public void ParseHeadersCreatesReport() {\n        var lastAttempt = DateTimeOffset.UtcNow;\n        var lastAttemptHeader = lastAttempt.ToString(\"R\");\n        var headers = new Dictionary<string, string> {\n            [\"Original-Recipient\"] = \"rfc822; orig@example.com\",\n            [\"Final-Recipient\"] = \"rfc822; final@example.com\",\n            [\"Reporting-MTA\"] = \"dns; mx.example.com\",\n            [\"Action\"] = \"failed\",\n            [\"Remote-MTA\"] = \"dns; remote.example.com\",\n            [\"Last-Attempt-Date\"] = lastAttemptHeader,\n            [\"Final-Log-ID\"] = \"ABC123\",\n            [\"Diagnostic-Code\"] = \"smtp; 550 5.1.1 User unknown\",\n            [\"Status\"] = \"5.1.1\",\n            [\"Arrival-Date\"] = DateTimeOffset.UtcNow.ToString(\"R\")\n        };\n        var report = NonDeliveryReport.FromHeaders(headers);\n        Assert.Equal(\"rfc822; orig@example.com\", report.OriginalRecipient);\n        Assert.Equal(\"rfc822; final@example.com\", report.FinalRecipient);\n        Assert.Equal(\"orig@example.com\", report.OriginalRecipientAddress);\n        Assert.Equal(\"final@example.com\", report.FinalRecipientAddress);\n        Assert.Equal(\"dns; mx.example.com\", report.ReportingMta);\n        Assert.Equal(\"failed\", report.Action);\n        Assert.Equal(\"dns; remote.example.com\", report.RemoteMta);\n        Assert.Equal(DateTimeOffset.Parse(lastAttemptHeader), report.LastAttemptDate);\n        Assert.Equal(\"ABC123\", report.FinalLogId);\n        Assert.NotNull(report.DiagnosticCode);\n        Assert.NotNull(report.Status);\n        Assert.Equal(NonDeliveryReportType.UnknownRecipient, report.Type);\n    }\n\n    [Fact]\n    public void ParsesOriginalMessageId() {\n        var headers = new Dictionary<string, string> {\n            [\"Original-Message-ID\"] = \"<msg1@local>\"\n        };\n        var report = NonDeliveryReport.FromHeaders(headers);\n        Assert.Equal(\"msg1@local\", report.OriginalMessageId);\n    }\n\n    [Theory]\n    [InlineData(\"Fri, 21 Jun 2024 10:12:34 +0000\")]\n    [InlineData(\"Fri, 21 Jun 2024 10:12:34 +0000 (UTC)\")]\n    [InlineData(\"21 Jun 2024 10:12:34 -0700\")]\n    public void ParseTimestampHandlesVariousFormats(string dateHeader) {\n        var headers = new Dictionary<string, string> {\n            [\"Arrival-Date\"] = dateHeader,\n            [\"Last-Attempt-Date\"] = dateHeader\n        };\n        var report = NonDeliveryReport.FromHeaders(headers);\n        Assert.True(DateUtils.TryParse(dateHeader, out var expected));\n        Assert.Equal(expected, report.Timestamp);\n        Assert.Equal(expected, report.LastAttemptDate);\n\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"not a date\")]\n    public void LastAttemptDateMissingOrInvalid_ReturnsNull(string? header) {\n        var headers = new Dictionary<string, string>();\n        if (header != null) headers[\"Last-Attempt-Date\"] = header;\n        var report = NonDeliveryReport.FromHeaders(headers);\n        Assert.Null(report.LastAttemptDate);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/OAuthCacheTestHelper.cs",
    "content": "using System;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading;\n\nnamespace Mailozaurr.Tests;\n\ninternal static class OAuthCacheTestHelper {\n    internal static void ResetOAuthTokenCache() {\n        var field = typeof(OAuthTokenCache).GetField(\"_cache\", BindingFlags.Static | BindingFlags.NonPublic);\n        field?.SetValue(null, null);\n    }\n\n    internal static string GetOAuthCacheFilePath() {\n        var pathField = typeof(OAuthTokenCache).GetField(\"CacheFilePath\", BindingFlags.Static | BindingFlags.NonPublic);\n        return (string)pathField!.GetValue(null)!;\n    }\n\n    internal static void DeleteOAuthCacheFile() {\n        var path = GetOAuthCacheFilePath();\n        for (var attempt = 0; ; attempt++) {\n            try {\n                if (File.Exists(path)) {\n                    File.Delete(path);\n                }\n\n                return;\n            } catch (IOException) when (attempt < 4) {\n                Thread.Sleep(25 * (attempt + 1));\n            } catch (UnauthorizedAccessException) when (attempt < 4) {\n                Thread.Sleep(25 * (attempt + 1));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/OAuthHelpersCachedTokenTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class OAuthHelpersCachedTokenTests {\n    public OAuthHelpersCachedTokenTests() {\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n    }\n\n    [Fact]\n    public async Task AcquireO365TokenCachedAsync_ReturnsCachedToken() {\n        var cacheKey = BuildO365CacheKey(\n            \"test@example.com\",\n            \"client-id\",\n            \"tenant-id\",\n            \"https://login.microsoftonline.com/common/oauth2/nativeclient\",\n            Array.Empty<string>());\n        var credential = new OAuthCredential {\n            UserName = \"test@example.com\",\n            AccessToken = \"token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = \"client-id\"\n        };\n        await OAuthTokenCache.SetAsync(cacheKey, credential);\n\n        var result = await OAuthHelpers.AcquireO365TokenCachedAsync(\n            \"test@example.com\",\n            \"client-id\",\n            \"tenant-id\",\n            \"https://login.microsoftonline.com/common/oauth2/nativeclient\",\n            Array.Empty<string>());\n\n        Assert.Equal(credential.AccessToken, result.AccessToken);\n        Assert.Equal(credential.UserName, result.UserName);\n    }\n\n    [Fact]\n    public async Task AcquireO365TokenCachedAsync_SeparatesEntriesPerClientAndScope() {\n        var login = \"test@example.com\";\n        var redirectUri = \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n        var cacheKeyA = BuildO365CacheKey(login, \"client-a\", \"tenant-id\", redirectUri, new[] { \"Mail.Read\" });\n        var cacheKeyB = BuildO365CacheKey(login, \"client-b\", \"tenant-id\", redirectUri, new[] { \"Mail.Read\", \"offline_access\" });\n\n        await OAuthTokenCache.SetAsync(cacheKeyA, new OAuthCredential {\n            UserName = login,\n            AccessToken = \"token-a\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = \"client-a\"\n        });\n        await OAuthTokenCache.SetAsync(cacheKeyB, new OAuthCredential {\n            UserName = login,\n            AccessToken = \"token-b\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = \"client-b\"\n        });\n\n        var result = await OAuthHelpers.AcquireO365TokenCachedAsync(\n            login,\n            \"client-b\",\n            \"tenant-id\",\n            redirectUri,\n            new[] { \"offline_access\", \"Mail.Read\" });\n\n        Assert.Equal(\"token-b\", result.AccessToken);\n        Assert.Equal(\"client-b\", result.ClientId);\n    }\n\n    [Fact]\n    public async Task PersistO365CredentialAsync_WritesLegacyAndCompositeEntries() {\n        var login = \"persist@example.com\";\n        var redirectUri = \"https://login.microsoftonline.com/common/oauth2/nativeclient\";\n        var credential = new OAuthCredential {\n            UserName = login,\n            AccessToken = \"persist-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n\n        await PersistO365CredentialAsync(\n            credential,\n            \"client-id\",\n            \"tenant-id\",\n            redirectUri,\n            new[] { \"offline_access\", \"Mail.Read\" });\n\n        var composite = await OAuthTokenCache.GetAsync(BuildO365CacheKey(login, \"client-id\", \"tenant-id\", redirectUri, new[] { \"Mail.Read\", \"offline_access\" }));\n        var legacy = await OAuthTokenCache.GetAsync(\"o365:persist@example.com\");\n\n        Assert.NotNull(composite);\n        Assert.NotNull(legacy);\n        Assert.Equal(\"persist-token\", composite!.AccessToken);\n        Assert.Equal(\"persist-token\", legacy!.AccessToken);\n        Assert.Equal(\"client-id\", legacy.ClientId);\n    }\n\n    [Fact]\n    public async Task GetAsync_ThrowsWhenCancellationRequested() {\n        var cacheKey = \"o365:cancel@example.com\";\n        var credential = new OAuthCredential {\n            UserName = \"cancel@example.com\",\n            AccessToken = \"token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n        await OAuthTokenCache.SetAsync(cacheKey, credential);\n\n        var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAsync<OperationCanceledException>(() => OAuthTokenCache.GetAsync(cacheKey, cts.Token));\n    }\n\n    [Fact]\n    public async Task SetAsync_ThrowsWhenCancellationRequested() {\n        var cacheKey = \"o365:cancel-set@example.com\";\n        var credential = new OAuthCredential {\n            UserName = \"cancel-set@example.com\",\n            AccessToken = \"token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n\n        var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAsync<OperationCanceledException>(() => OAuthTokenCache.SetAsync(cacheKey, credential, cts.Token));\n    }\n\n    private static string BuildO365CacheKey(\n        string login,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        string[] scopes) {\n        var method = typeof(OAuthHelpers).GetMethod(\"BuildO365CacheKey\", BindingFlags.Static | BindingFlags.NonPublic);\n        return (string)method!.Invoke(null, new object[] { login, clientId, tenantId, redirectUri, scopes })!;\n    }\n\n    private static async Task PersistO365CredentialAsync(\n        OAuthCredential credential,\n        string clientId,\n        string tenantId,\n        string redirectUri,\n        string[] scopes) {\n        var method = typeof(OAuthHelpers).GetMethod(\"PersistO365CredentialAsync\", BindingFlags.Static | BindingFlags.NonPublic);\n        var task = (Task)method!.Invoke(null, new object[] { credential, clientId, tenantId, redirectUri, scopes })!;\n        await task.ConfigureAwait(false);\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/OAuthHelpersGoogleCachedTokenTests.cs",
    "content": "using System;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic class OAuthHelpersGoogleCachedTokenTests {\n    public OAuthHelpersGoogleCachedTokenTests() {\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n    }\n\n    [Fact]\n    public async Task AcquireGoogleTokenCachedAsync_PrefersCompositeKey_WhenBothExist() {\n        var account = \"user@example.com\";\n        var clientId = \"client-abc\";\n        var compositeKey = $\"google:{clientId}:{account}\";\n        var legacyKey = $\"google:{account}\";\n\n        var composite = new OAuthCredential {\n            UserName = account,\n            AccessToken = \"composite-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = clientId,\n            ClientSecret = \"secret\"\n        };\n        var legacy = new OAuthCredential {\n            UserName = account,\n            AccessToken = \"legacy-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n\n        await OAuthTokenCache.SetAsync(compositeKey, composite);\n        await OAuthTokenCache.SetAsync(legacyKey, legacy);\n\n        var result = await OAuthHelpers.AcquireGoogleTokenCachedAsync(account, clientId, \"secret\", Array.Empty<string>());\n\n        Assert.Equal(\"composite-token\", result.AccessToken);\n        Assert.Equal(clientId, result.ClientId);\n    }\n\n    [Fact]\n    public async Task AcquireGoogleTokenCachedAsync_FillsMissingClientData_AndMigratesLegacy() {\n        var account = \"user2@example.com\";\n        var clientId = \"client-xyz\";\n        var legacyKey = $\"google:{account}\";\n        var compositeKey = $\"google:{clientId}:{account}\";\n\n        var legacy = new OAuthCredential {\n            UserName = account,\n            AccessToken = \"legacy-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n        await OAuthTokenCache.SetAsync(legacyKey, legacy);\n\n        var result = await OAuthHelpers.AcquireGoogleTokenCachedAsync(account, clientId, \"top-secret\", Array.Empty<string>());\n\n        Assert.Equal(\"legacy-token\", result.AccessToken);\n        Assert.Equal(clientId, result.ClientId);\n        Assert.Equal(\"top-secret\", result.ClientSecret);\n\n        // Legacy entry should be migrated to composite key for future lookups\n        var migrated = await OAuthTokenCache.GetAsync(compositeKey);\n        Assert.NotNull(migrated);\n        Assert.Equal(\"legacy-token\", migrated!.AccessToken);\n    }\n\n    [Fact]\n    public async Task PersistGoogleCredentialAsync_WritesLegacyAndCompositeEntries() {\n        var account = \"user3@example.com\";\n        var clientId = \"client-123\";\n        var compositeKey = $\"google:{clientId}:{account}\";\n        var legacyKey = $\"google:{account}\";\n        var credential = new OAuthCredential {\n            UserName = account,\n            AccessToken = \"persisted-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n        };\n\n        await PersistGoogleCredentialAsync(credential, account, clientId);\n\n        var composite = await OAuthTokenCache.GetAsync(compositeKey);\n        var legacy = await OAuthTokenCache.GetAsync(legacyKey);\n\n        Assert.NotNull(composite);\n        Assert.NotNull(legacy);\n        Assert.Equal(\"persisted-token\", composite!.AccessToken);\n        Assert.Equal(\"persisted-token\", legacy!.AccessToken);\n        Assert.Equal(clientId, legacy.ClientId);\n    }\n\n    private static async Task PersistGoogleCredentialAsync(\n        OAuthCredential credential,\n        string gmailAccount,\n        string clientId) {\n        var method = typeof(OAuthHelpers).GetMethod(\"PersistGoogleCredentialAsync\", BindingFlags.Static | BindingFlags.NonPublic);\n        var task = (Task)method!.Invoke(null, new object[] { credential, gmailAccount, clientId })!;\n        await task.ConfigureAwait(false);\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/OAuthTokenCacheProtectionTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\n[Collection(\"GraphCollection\")]\npublic sealed class OAuthTokenCacheProtectionTests {\n    public OAuthTokenCacheProtectionTests() {\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        OAuthCacheTestHelper.DeleteOAuthCacheFile();\n    }\n\n    [Fact]\n    public async Task SetAsync_WritesProtectedSecretsToDisk() {\n        var cacheKey = \"oauth:protected@example.com\";\n        var credential = new OAuthCredential {\n            UserName = \"protected@example.com\",\n            AccessToken = \"access-token-value\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            RefreshToken = \"refresh-token-value\",\n            ClientId = \"client-id\",\n            ClientSecret = \"client-secret-value\",\n            ServiceAccountJson = \"{\\\"type\\\":\\\"service_account\\\"}\",\n            ServiceAccountSubject = \"subject@example.com\"\n        };\n\n        await OAuthTokenCache.SetAsync(cacheKey, credential);\n\n        var path = OAuthCacheTestHelper.GetOAuthCacheFilePath();\n        Assert.True(File.Exists(path));\n\n        var json = File.ReadAllText(path);\n        Assert.DoesNotContain(\"access-token-value\", json, StringComparison.Ordinal);\n        Assert.DoesNotContain(\"refresh-token-value\", json, StringComparison.Ordinal);\n        Assert.DoesNotContain(\"client-secret-value\", json, StringComparison.Ordinal);\n        Assert.DoesNotContain(\"\\\"type\\\":\\\"service_account\\\"\", json, StringComparison.Ordinal);\n\n        using var document = JsonDocument.Parse(json);\n        var entry = document.RootElement.GetProperty(cacheKey);\n        Assert.True(entry.TryGetProperty(\"AccessTokenProtected\", out _));\n        Assert.True(entry.TryGetProperty(\"RefreshTokenProtected\", out _));\n        Assert.True(entry.TryGetProperty(\"ClientSecretProtected\", out _));\n        Assert.True(entry.TryGetProperty(\"ServiceAccountJsonProtected\", out _));\n\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        var reloaded = await OAuthTokenCache.GetAsync(cacheKey);\n\n        Assert.NotNull(reloaded);\n        Assert.Equal(credential.AccessToken, reloaded!.AccessToken);\n        Assert.Equal(credential.RefreshToken, reloaded.RefreshToken);\n        Assert.Equal(credential.ClientSecret, reloaded.ClientSecret);\n        Assert.Equal(credential.ServiceAccountJson, reloaded.ServiceAccountJson);\n        Assert.Equal(credential.ServiceAccountSubject, reloaded.ServiceAccountSubject);\n    }\n\n    [Fact]\n    public async Task GetAsync_LoadsLegacyPlaintextCacheFile() {\n        var cacheKey = \"oauth:legacy@example.com\";\n        var credential = new OAuthCredential {\n            UserName = \"legacy@example.com\",\n            AccessToken = \"legacy-access-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(30),\n            RefreshToken = \"legacy-refresh-token\",\n            ClientId = \"legacy-client\",\n            ClientSecret = \"legacy-secret\",\n            ServiceAccountJson = \"{\\\"legacy\\\":true}\",\n            ServiceAccountSubject = \"legacy-subject@example.com\"\n        };\n\n        var path = OAuthCacheTestHelper.GetOAuthCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        var legacyCache = new Dictionary<string, OAuthCredential>(StringComparer.Ordinal) {\n            [cacheKey] = credential\n        };\n        var json = JsonSerializer.Serialize(legacyCache, MailozaurrJsonContext.Default.DictionaryStringOAuthCredential);\n        File.WriteAllText(path, json);\n\n        var loaded = await OAuthTokenCache.GetAsync(cacheKey);\n\n        Assert.NotNull(loaded);\n        Assert.Equal(credential.UserName, loaded!.UserName);\n        Assert.Equal(credential.AccessToken, loaded.AccessToken);\n        Assert.Equal(credential.RefreshToken, loaded.RefreshToken);\n        Assert.Equal(credential.ClientId, loaded.ClientId);\n        Assert.Equal(credential.ClientSecret, loaded.ClientSecret);\n        Assert.Equal(credential.ServiceAccountJson, loaded.ServiceAccountJson);\n        Assert.Equal(credential.ServiceAccountSubject, loaded.ServiceAccountSubject);\n    }\n\n    [Fact]\n    public async Task GetAsync_MalformedCacheFileReturnsNull() {\n        var cacheKey = \"oauth:malformed@example.com\";\n        var path = OAuthCacheTestHelper.GetOAuthCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        File.WriteAllText(path, \"{broken\");\n\n        var loaded = await OAuthTokenCache.GetAsync(cacheKey);\n\n        Assert.Null(loaded);\n    }\n\n    [Fact]\n    public async Task SetAsync_MalformedCacheFileIsRecovered() {\n        var cacheKey = \"oauth:recover@example.com\";\n        var path = OAuthCacheTestHelper.GetOAuthCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        File.WriteAllText(path, \"{broken\");\n\n        var credential = new OAuthCredential {\n            UserName = \"recover@example.com\",\n            AccessToken = \"recovered-access-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(30)\n        };\n\n        await OAuthTokenCache.SetAsync(cacheKey, credential);\n\n        OAuthCacheTestHelper.ResetOAuthTokenCache();\n        var loaded = await OAuthTokenCache.GetAsync(cacheKey);\n\n        Assert.NotNull(loaded);\n        Assert.Equal(credential.UserName, loaded!.UserName);\n        Assert.Equal(credential.AccessToken, loaded.AccessToken);\n\n        var json = File.ReadAllText(path);\n        Assert.DoesNotContain(\"{broken\", json, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task GetAsync_RetriesTransientFileLockAndLoadsCredential() {\n        var cacheKey = \"oauth:locked@example.com\";\n        var credential = new OAuthCredential {\n            UserName = \"locked@example.com\",\n            AccessToken = \"locked-access-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(30)\n        };\n\n        var path = OAuthCacheTestHelper.GetOAuthCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        var cacheEntries = new Dictionary<string, OAuthCredentialCacheEntry>(StringComparer.Ordinal) {\n            [cacheKey] = OAuthCredentialCacheEntry.FromCredential(credential, CredentialProtection.Default)\n        };\n        var json = JsonSerializer.Serialize(cacheEntries, MailozaurrJsonContext.Default.DictionaryStringOAuthCredentialCacheEntry);\n        File.WriteAllText(path, json);\n        var lockStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);\n        try {\n            var releaseTask = Task.Run(async () => {\n                await Task.Delay(75);\n                lockStream.Dispose();\n            });\n\n            var loaded = await OAuthTokenCache.GetAsync(cacheKey);\n            await releaseTask;\n\n            Assert.NotNull(loaded);\n            Assert.Equal(credential.AccessToken, loaded!.AccessToken);\n        } finally {\n            lockStream.Dispose();\n        }\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Pop3AttachmentPayloadBuilderTests.cs",
    "content": "using MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class Pop3AttachmentPayloadBuilderTests {\n    [Fact]\n    public void Build_ReturnsNotFound_WhenIndexOutOfRange() {\n        var message = new MimeMessage {\n            Body = new TextPart(\"plain\") { Text = \"body\" }\n        };\n\n        var result = Pop3AttachmentPayloadBuilder.Build(message, attachmentIndex: 0, maxBytes: 1024);\n\n        Assert.Equal(Pop3AttachmentBuildStatus.NotFound, result.Status);\n        Assert.False(result.Success);\n        Assert.Null(result.Payload);\n    }\n\n    [Fact]\n    public void Build_ReturnsSizeLimitExceeded_WhenPayloadTooLarge() {\n        var message = new MimeMessage();\n        var body = new BodyBuilder { TextBody = \"body\" };\n        body.Attachments.Add(\"a.txt\", new byte[64]);\n        message.Body = body.ToMessageBody();\n\n        var result = Pop3AttachmentPayloadBuilder.Build(message, attachmentIndex: 0, maxBytes: 8);\n\n        Assert.Equal(Pop3AttachmentBuildStatus.SizeLimitExceeded, result.Status);\n        Assert.False(result.Success);\n        Assert.Null(result.Payload);\n    }\n\n    [Fact]\n    public void Build_ReturnsPayload_ForMimePart() {\n        var message = new MimeMessage();\n        var body = new BodyBuilder { TextBody = \"body\" };\n        body.Attachments.Add(\"a.txt\", new byte[] { 1, 2, 3, 4 });\n        message.Body = body.ToMessageBody();\n\n        var result = Pop3AttachmentPayloadBuilder.Build(message, attachmentIndex: 0, maxBytes: 1024);\n\n        Assert.Equal(Pop3AttachmentBuildStatus.Success, result.Status);\n        Assert.True(result.Success);\n        Assert.NotNull(result.Payload);\n        Assert.Equal(\"a.txt\", result.Payload!.FileName);\n        Assert.Equal(4, result.Payload.Bytes.Length);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Pop3ConnectionTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests {\n    public class Pop3ConnectionTests {\n        private class FakePop3Client {\n            public bool ValidCredentials { get; set; } = true;\n            public bool SimulateTimeout { get; set; }\n            public bool Connected { get; private set; }\n\n            public void Connect() {\n                if (SimulateTimeout) {\n                    throw new TimeoutException(\"Timeout\");\n                }\n\n                if (!ValidCredentials) {\n                    throw new InvalidOperationException(\"Invalid credentials\");\n                }\n\n                Connected = true;\n            }\n\n            public IReadOnlyList<string> FetchMessageList() {\n                if (!Connected) {\n                    throw new InvalidOperationException(\"Not connected\");\n                }\n\n                return new List<string> { \"msg1\", \"msg2\" };\n            }\n        }\n        [Fact]\n        public void Pop3_Connect_WithValidCredentials_Succeeds() {\n            // Arrange\n            var client = new FakePop3Client();\n\n            // Act\n            client.Connect();\n\n            // Assert\n            Assert.True(client.Connected);\n        }\n\n        [Fact]\n        public void Pop3_Connect_WithInvalidCredentials_Fails() {\n            // Arrange\n            var client = new FakePop3Client { ValidCredentials = false };\n\n            // Act & Assert\n            Assert.Throws<InvalidOperationException>(() => client.Connect());\n        }\n\n        [Fact]\n        public void Pop3_Connect_WithTimeout_Fails() {\n            // Arrange\n            var client = new FakePop3Client { SimulateTimeout = true };\n\n            // Act & Assert\n            Assert.Throws<TimeoutException>(() => client.Connect());\n        }\n\n        [Fact]\n        public void Pop3_FetchMessageList_Succeeds() {\n            // Arrange\n            var client = new FakePop3Client();\n            client.Connect();\n\n            // Act\n            var list = client.FetchMessageList();\n\n            // Assert\n            Assert.Equal(2, list.Count);\n        }\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Pop3MailboxBrowserTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class Pop3MailboxBrowserTests {\n    [Fact]\n    public async Task ListMessageHeadersCoreAsync_ListsNewestFirst_WithOffsetAndLimit() {\n        var fake = new FakePop3Client(count: 5);\n        // indices: 0..4, newest is 4\n        fake.SetHeaders(4, BuildHeaders(messageId: \"<m4>\", from: \"f4\", to: \"t4\", subject: \"s4\", date: \"Mon, 01 Jan 2024 10:00:00 +0000\"));\n        fake.SetHeaders(3, BuildHeaders(messageId: \"<m3>\", from: \"f3\", to: \"t3\", subject: \"s3\", date: \"Mon, 01 Jan 2024 09:00:00 +0000\"));\n        fake.SetHeaders(2, BuildHeaders(messageId: \"<m2>\", from: \"f2\", to: \"t2\", subject: \"s2\", date: \"Mon, 01 Jan 2024 08:00:00 +0000\"));\n        fake.SetHeaders(1, BuildHeaders(messageId: \"<m1>\", from: \"f1\", to: \"t1\", subject: \"s1\", date: \"Mon, 01 Jan 2024 07:00:00 +0000\"));\n        fake.SetHeaders(0, BuildHeaders(messageId: \"<m0>\", from: \"f0\", to: \"t0\", subject: \"s0\", date: \"Mon, 01 Jan 2024 06:00:00 +0000\"));\n\n        fake.SetUid(4, \"u4\");\n        fake.SetUid(3, \"u3\");\n        fake.SetUid(2, \"u2\");\n        fake.SetUid(1, \"u1\");\n        fake.SetUid(0, \"u0\");\n\n        fake.SetSize(4, 400);\n        fake.SetSize(3, 300);\n        fake.SetSize(2, 200);\n        fake.SetSize(1, 100);\n        fake.SetSize(0, 0);\n\n        var list = await Pop3MailboxBrowser.ListMessageHeadersCoreAsync(fake, limit: 2, offset: 1, CancellationToken.None);\n        Assert.Equal(5, list.TotalCount);\n        Assert.Equal(2, list.Messages.Count);\n\n        // offset=1 skips newest (4), so first is 3 then 2.\n        Assert.Equal(3, list.Messages[0].Index);\n        Assert.Equal(\"u3\", list.Messages[0].Uid);\n        Assert.Equal(300, list.Messages[0].MessageSize);\n        Assert.Equal(\"m3\", list.Messages[0].MessageId);\n        Assert.Equal(\"f3\", list.Messages[0].From);\n        Assert.Equal(\"t3\", list.Messages[0].To);\n        Assert.Equal(\"s3\", list.Messages[0].Subject);\n\n        Assert.Equal(2, list.Messages[1].Index);\n        Assert.Equal(\"m2\", list.Messages[1].MessageId);\n    }\n\n    [Fact]\n    public async Task ResolveMessageCoreAsync_MissingIdentifier_ReturnsMissingIdentifier() {\n        var fake = new FakePop3Client(count: 1);\n        var result = await Pop3MailboxBrowser.ResolveMessageCoreAsync(fake, requestedIndex: null, requestedUid: null, CancellationToken.None);\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.MissingIdentifier, result.Status);\n        Assert.Null(result.Snapshot);\n    }\n\n    [Fact]\n    public async Task ResolveMessageCoreAsync_InvalidIndex_ReturnsInvalidIndex() {\n        var fake = new FakePop3Client(count: 1);\n        var result = await Pop3MailboxBrowser.ResolveMessageCoreAsync(fake, requestedIndex: -1, requestedUid: null, CancellationToken.None);\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.InvalidIndex, result.Status);\n    }\n\n    [Fact]\n    public async Task ResolveMessageCoreAsync_UidLookupUnsupported_ReturnsUidLookupUnsupported() {\n        var fake = new FakePop3Client(count: 1) { ThrowUidListNotSupported = true };\n        var result = await Pop3MailboxBrowser.ResolveMessageCoreAsync(fake, requestedIndex: null, requestedUid: \"u1\", CancellationToken.None);\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.UidLookupUnsupported, result.Status);\n    }\n\n    [Fact]\n    public async Task ResolveMessageCoreAsync_ResolvesByUid() {\n        var fake = new FakePop3Client(count: 3);\n        fake.Uids = new List<string> { \"u0\", \"u1\", \"u2\" };\n        fake.SetSize(1, 123);\n        fake.SetMessage(1, new MimeMessage { Subject = \"x\" });\n\n        var result = await Pop3MailboxBrowser.ResolveMessageCoreAsync(fake, requestedIndex: null, requestedUid: \"u1\", CancellationToken.None);\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.Success, result.Status);\n        Assert.NotNull(result.Snapshot);\n        Assert.Equal(1, result.Snapshot!.Index);\n        Assert.Equal(\"u1\", result.Snapshot.Uid);\n        Assert.Equal(123, result.Snapshot.MessageSize);\n        Assert.Equal(\"x\", result.Snapshot.Message.Subject);\n    }\n\n    [Fact]\n    public async Task DeleteMessageCoreAsync_ResolvesByUid_AndDeletesResolvedIndex() {\n        var fake = new FakePop3Client(count: 3);\n        fake.Uids = new List<string> { \"u0\", \"u1\", \"u2\" };\n        fake.SetMessage(1, new MimeMessage { Subject = \"x\" });\n        fake.SetUid(1, \"u1\");\n\n        var result = await Pop3MailboxBrowser.DeleteMessageCoreAsync(fake, requestedIndex: null, requestedUid: \"u1\", CancellationToken.None);\n\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.Success, result.Status);\n        Assert.Equal(1, result.DeletedIndex);\n        Assert.Equal(\"u1\", result.DeletedUid);\n        Assert.Equal(new[] { 1 }, fake.DeletedIndices);\n    }\n\n    [Fact]\n    public async Task DeleteMessageCoreAsync_MissingIdentifier_ReturnsMissingIdentifier() {\n        var fake = new FakePop3Client(count: 1);\n        var result = await Pop3MailboxBrowser.DeleteMessageCoreAsync(fake, requestedIndex: null, requestedUid: null, CancellationToken.None);\n\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.MissingIdentifier, result.Status);\n        Assert.Empty(fake.DeletedIndices);\n    }\n\n    [Fact]\n    public async Task DeleteMessageCoreAsync_UidLookupUnsupported_ReturnsUidLookupUnsupported() {\n        var fake = new FakePop3Client(count: 1) { ThrowUidListNotSupported = true };\n        var result = await Pop3MailboxBrowser.DeleteMessageCoreAsync(fake, requestedIndex: null, requestedUid: \"u1\", CancellationToken.None);\n\n        Assert.Equal(Pop3MailboxBrowser.Pop3MessageResolveStatus.UidLookupUnsupported, result.Status);\n        Assert.Empty(fake.DeletedIndices);\n    }\n\n    [Fact]\n    public void NormalizeMessageIdValue_StripsAngleBrackets() {\n        Assert.Equal(\"x@y\", Pop3MailboxBrowser.NormalizeMessageIdValue(\"<x@y>\"));\n        Assert.Equal(\"x@y\", Pop3MailboxBrowser.NormalizeMessageIdValue(\"x@y\"));\n        Assert.Null(Pop3MailboxBrowser.NormalizeMessageIdValue(\"   \"));\n    }\n\n    private static HeaderList BuildHeaders(string messageId, string from, string to, string subject, string date) {\n        var headers = new HeaderList();\n        headers.Add(HeaderId.MessageId, messageId);\n        headers.Add(HeaderId.From, from);\n        headers.Add(HeaderId.To, to);\n        headers.Add(HeaderId.Subject, subject);\n        headers.Add(HeaderId.Date, date);\n        return headers;\n    }\n\n    private sealed class FakePop3Client : Pop3MailboxBrowser.IPop3MailboxClient {\n        private readonly Dictionary<int, HeaderList> _headers = new();\n        private readonly Dictionary<int, string> _uids = new();\n        private readonly Dictionary<int, long> _sizes = new();\n        private readonly Dictionary<int, MimeMessage> _messages = new();\n\n        internal FakePop3Client(int count) {\n            Count = count;\n        }\n\n        public int Count { get; }\n        public bool ThrowUidListNotSupported { get; set; }\n        public IList<string> Uids { get; set; } = new List<string>();\n        public List<int> DeletedIndices { get; } = new();\n\n        internal void SetHeaders(int index, HeaderList headers) => _headers[index] = headers;\n        internal void SetUid(int index, string uid) => _uids[index] = uid;\n        internal void SetSize(int index, long size) => _sizes[index] = size;\n        internal void SetMessage(int index, MimeMessage msg) => _messages[index] = msg;\n\n        public Task<HeaderList> GetMessageHeadersAsync(int index, CancellationToken cancellationToken) =>\n            Task.FromResult(_headers.TryGetValue(index, out var h) ? h : new HeaderList());\n\n        public Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken) =>\n            Task.FromResult(_messages.TryGetValue(index, out var m) ? m : new MimeMessage());\n\n        public Task<string> GetMessageUidAsync(int index, CancellationToken cancellationToken) =>\n            Task.FromResult(_uids.TryGetValue(index, out var u) ? u : $\"u{index}\");\n\n        public Task<IList<string>> GetMessageUidsAsync(CancellationToken cancellationToken) {\n            if (ThrowUidListNotSupported) {\n                throw new NotSupportedException(\"uid list not supported\");\n            }\n            return Task.FromResult(Uids);\n        }\n\n        public Task DeleteMessageAsync(int index, CancellationToken cancellationToken) {\n            DeletedIndices.Add(index);\n            return Task.CompletedTask;\n        }\n\n        public long GetMessageSize(int index, CancellationToken cancellationToken) =>\n            _sizes.TryGetValue(index, out var s) ? s : 0;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/Pop3PollListenerTests.cs",
    "content": "using MailKit.Net.Pop3;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class Pop3PollListenerTests {\n    [Fact]\n    public void Dispose_DisposesCancellationTokenSourceAndNullsField() {\n        var listener = new Pop3PollListener(new Pop3Client());\n        var field = typeof(Pop3PollListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var pollingTaskField = typeof(Pop3PollListener).GetField(\"_pollingTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var cts = new CancellationTokenSource();\n        field.SetValue(listener, cts);\n        pollingTaskField.SetValue(listener, Task.CompletedTask);\n\n        listener.Dispose();\n\n        Assert.Null(field.GetValue(listener));\n        Assert.Null(pollingTaskField.GetValue(listener));\n        Assert.Throws<ObjectDisposedException>(() => _ = cts.Token.WaitHandle);\n    }\n\n    [Fact]\n    public async Task DisposeAsync_DisposesCancellationTokenSourceAndNullsField() {\n        var listener = new Pop3PollListener(new Pop3Client());\n        var field = typeof(Pop3PollListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var pollingTaskField = typeof(Pop3PollListener).GetField(\"_pollingTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var cts = new CancellationTokenSource();\n        field.SetValue(listener, cts);\n        pollingTaskField.SetValue(listener, Task.CompletedTask);\n\n        await listener.DisposeAsync();\n\n        Assert.Null(field.GetValue(listener));\n        Assert.Null(pollingTaskField.GetValue(listener));\n        Assert.Throws<ObjectDisposedException>(() => _ = cts.Token.WaitHandle);\n    }\n\n    [Fact]\n    public async Task StartAsync_FailureDuringInitialSnapshot_CleansUpState() {\n        var listener = new Pop3PollListener(new Pop3Client());\n        var cancelField = typeof(Pop3PollListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var pollingTaskField = typeof(Pop3PollListener).GetField(\"_pollingTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n\n        await Assert.ThrowsAnyAsync<Exception>(() => listener.StartAsync());\n\n        Assert.Null(cancelField.GetValue(listener));\n        Assert.Null(pollingTaskField.GetValue(listener));\n\n        await Assert.ThrowsAnyAsync<Exception>(() => listener.StartAsync());\n    }\n\n    [Fact]\n    public async Task StartAsync_RaisesEventsForNewUidsOnly() {\n        var listener = new TestPop3PollListener();\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")));\n        var receivedSubjects = new List<string>();\n        listener.MessageArrived += (_, message) => receivedSubjects.Add(message.Message.Subject ?? string.Empty);\n\n        await listener.StartAsync();\n\n        await listener.WaitForDelayAsync();\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")),\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"New\")));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n\n        await listener.StopAsync();\n\n        Assert.Single(receivedSubjects);\n        Assert.Equal(\"New\", receivedSubjects[0]);\n    }\n\n    [Fact]\n    public async Task PollLoop_HandlesDeletionsAndReorderedIndexes() {\n        var listener = new TestPop3PollListener();\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"First\")),\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"Second\")));\n        var receivedSubjects = new List<string>();\n        listener.MessageArrived += (_, message) => receivedSubjects.Add(message.Message.Subject ?? string.Empty);\n\n        await listener.StartAsync();\n\n        await listener.WaitForDelayAsync();\n        listener.SetMessages(new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"Second\")));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"Second\")),\n            new TestPop3PollListener.TestMessage(\"uid3\", CreateMessage(\"Third\")));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n\n        await listener.StopAsync();\n\n        Assert.Single(receivedSubjects);\n        Assert.Equal(\"Third\", receivedSubjects[0]);\n    }\n\n    [Fact]\n    public async Task StopAsync_WaitsForPollingLoopToComplete() {\n        var listener = new TestPop3PollListener { DelayIgnoresCancellation = true };\n\n        await listener.StartAsync();\n        await listener.WaitForDelayAsync();\n\n        var stopTask = listener.StopAsync();\n\n        var completed = await Task.WhenAny(stopTask, Task.Delay(100));\n        Assert.NotSame(stopTask, completed);\n\n        listener.ReleaseNextDelay();\n\n        await stopTask;\n\n        var cancelField = typeof(Pop3PollListener).GetField(\"_cancel\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var pollingTaskField = typeof(Pop3PollListener).GetField(\"_pollingTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n\n        Assert.Null(cancelField.GetValue(listener));\n        Assert.Null(pollingTaskField.GetValue(listener));\n    }\n\n    [Fact]\n    public async Task PollLoop_RaisesPollErrorAndRecoversAfterException() {\n        var listener = new TestPop3PollListener();\n        listener.SetMessages(new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")));\n        var receivedSubjects = new List<string>();\n        var errors = new List<Exception>();\n        listener.MessageArrived += (_, message) => receivedSubjects.Add(message.Message.Subject ?? string.Empty);\n        listener.PollError += (_, exception) => errors.Add(exception);\n\n        await listener.StartAsync();\n        await listener.WaitForDelayAsync();\n\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")),\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"New\")));\n        listener.ThrowOnNextFetch(new InvalidOperationException(\"Boom\"));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")),\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"New\")));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n\n        await listener.StopAsync();\n\n        Assert.Single(errors);\n        Assert.IsType<InvalidOperationException>(errors[0]);\n        Assert.Single(receivedSubjects);\n        Assert.Equal(\"New\", receivedSubjects[0]);\n    }\n\n    [Fact]\n    public async Task StopAsync_DuringErrorBackoff_CompletesPollingTaskSuccessfully() {\n        var listener = new TestPop3PollListener();\n        listener.SetMessages(new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")));\n\n        await listener.StartAsync();\n        await listener.WaitForDelayAsync();\n\n        listener.SetMessages(\n            new TestPop3PollListener.TestMessage(\"uid1\", CreateMessage(\"Initial\")),\n            new TestPop3PollListener.TestMessage(\"uid2\", CreateMessage(\"New\")));\n        listener.ThrowOnNextFetch(new InvalidOperationException(\"Boom\"));\n        listener.ReleaseNextDelay();\n\n        await listener.WaitForDelayAsync();\n\n        var pollingTaskField = typeof(Pop3PollListener).GetField(\"_pollingTask\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var pollingTask = (Task)pollingTaskField.GetValue(listener)!;\n\n        await listener.StopAsync();\n\n        Assert.Equal(TaskStatus.RanToCompletion, pollingTask.Status);\n        Assert.Null(pollingTaskField.GetValue(listener));\n    }\n\n    private static MimeMessage CreateMessage(string subject) {\n        var message = new MimeMessage();\n        message.Subject = subject;\n        return message;\n    }\n\n    private sealed class TestPop3PollListener : Pop3PollListener {\n        private readonly object _syncRoot = new object();\n        private readonly List<TestMessage> _messages = new List<TestMessage>();\n        private readonly Queue<TaskCompletionSource<bool>> _pendingDelays = new Queue<TaskCompletionSource<bool>>();\n        private readonly SemaphoreSlim _delayScheduled = new SemaphoreSlim(0);\n        private Exception? _nextFetchException;\n\n        public TestPop3PollListener()\n            : base(new Pop3Client(), TimeSpan.Zero) {\n        }\n\n        public bool DelayIgnoresCancellation { get; set; }\n\n        public void SetMessages(params TestMessage[] messages) {\n            lock (_syncRoot) {\n                _messages.Clear();\n                if (messages != null && messages.Length > 0) {\n                    _messages.AddRange(messages);\n                }\n            }\n        }\n\n        public Task WaitForDelayAsync() => _delayScheduled.WaitAsync();\n\n        public void ReleaseNextDelay() {\n            TaskCompletionSource<bool>? pending = null;\n            lock (_syncRoot) {\n                if (_pendingDelays.Count > 0) {\n                    pending = _pendingDelays.Dequeue();\n                }\n            }\n\n            pending?.TrySetResult(true);\n        }\n\n        public void ThrowOnNextFetch(Exception exception) {\n            if (exception == null) {\n                throw new ArgumentNullException(nameof(exception));\n            }\n\n            lock (_syncRoot) {\n                _nextFetchException = exception;\n            }\n        }\n\n        protected override int GetMessageCount() {\n            lock (_syncRoot) {\n                return _messages.Count;\n            }\n        }\n\n        protected override Task<string> GetMessageUidAsync(int index, CancellationToken cancellationToken) {\n            lock (_syncRoot) {\n                return Task.FromResult(_messages[index].Uid);\n            }\n        }\n\n        protected override Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken) {\n            lock (_syncRoot) {\n                if (_nextFetchException != null) {\n                    var exception = _nextFetchException;\n                    _nextFetchException = null;\n                    throw exception;\n                }\n\n                return Task.FromResult(_messages[index].Message);\n            }\n        }\n\n        protected override Task NoOpAsync(CancellationToken cancellationToken) => Task.CompletedTask;\n\n        protected override Task DelayAsync(TimeSpan interval, CancellationToken cancellationToken) {\n            var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);\n            if (!DelayIgnoresCancellation && cancellationToken.CanBeCanceled) {\n                cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));\n            }\n\n            lock (_syncRoot) {\n                _pendingDelays.Enqueue(tcs);\n            }\n\n            _delayScheduled.Release();\n\n            return tcs.Task;\n        }\n\n        public readonly struct TestMessage {\n            public TestMessage(string uid, MimeMessage message) {\n                Uid = uid;\n                Message = message;\n            }\n\n            public string Uid { get; }\n\n            public MimeMessage Message { get; }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ProtocolAuthTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class ProtocolAuthTests {\n    [Theory]\n    [InlineData(null, ProtocolAuthMode.Basic)]\n    [InlineData(\"\", ProtocolAuthMode.Basic)]\n    [InlineData(\" \", ProtocolAuthMode.Basic)]\n    [InlineData(\"basic\", ProtocolAuthMode.Basic)]\n    [InlineData(\"BASIC\", ProtocolAuthMode.Basic)]\n    [InlineData(\"oauth2\", ProtocolAuthMode.OAuth2)]\n    [InlineData(\"oauth\", ProtocolAuthMode.OAuth2)]\n    [InlineData(\"xoauth2\", ProtocolAuthMode.OAuth2)]\n    [InlineData(\"unexpected\", ProtocolAuthMode.Basic)]\n    public void ParseMode_ParsesExpectedValues(string? raw, ProtocolAuthMode expected) {\n        var mode = ProtocolAuth.ParseMode(raw);\n        Assert.Equal(expected, mode);\n    }\n\n    [Fact]\n    public void ParseMode_UsesProvidedFallbackForUnknownValue() {\n        var mode = ProtocolAuth.ParseMode(\"unexpected\", fallback: ProtocolAuthMode.OAuth2);\n        Assert.Equal(ProtocolAuthMode.OAuth2, mode);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/QueryLanguageParserTests.cs",
    "content": "using System;\nusing MailKit.Search;\nusing Mailozaurr;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class QueryLanguageParserTests {\n    [Fact]\n    public void ParseQuery_ParsesBasicFields() {\n        var result = MailboxSearcher.ParseQuery(\"from:boss subject:\\\"report\\\"\");\n        Assert.Equal(\"boss\", result.FromContains);\n        Assert.Equal(\"report\", result.Subject);\n    }\n\n    [Fact]\n    public void ParseQuery_ParsesDatesAndFlags() {\n        var result = MailboxSearcher.ParseQuery(\"since:2024-01-01 before:2024-02-01 has:attachment priority:high\");\n        Assert.Equal(new DateTime(2024, 1, 1), result.Since);\n        Assert.Equal(new DateTime(2024, 2, 1), result.Before);\n        Assert.True(result.HasAttachment);\n        Assert.Equal(MessagePriority.High, result.Priority);\n    }\n\n    [Fact]\n    public void ParseQuery_IsCaseInsensitive() {\n        var result = MailboxSearcher.ParseQuery(\"FROM:Boss SUBJECT:\\\"Report\\\" HAS:ATTACHMENT\");\n        Assert.Equal(\"Boss\", result.FromContains);\n        Assert.Equal(\"Report\", result.Subject);\n        Assert.True(result.HasAttachment);\n    }\n\n    [Fact]\n    public void ParseQuery_ParsesBodyContains() {\n        var result = MailboxSearcher.ParseQuery(\"body:invoice\");\n        Assert.Equal(\"invoice\", result.BodyContains);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/RecordingHandler.cs",
    "content": "using System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\npublic class RecordingHandler : HttpMessageHandler\n{\n    public List<HttpRequestMessage> Requests { get; } = new();\n    private readonly Queue<HttpResponseMessage> _responses;\n\n    public RecordingHandler(params HttpResponseMessage[] responses)\n    {\n        _responses = new Queue<HttpResponseMessage>(responses);\n    }\n\n    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        HttpRequestMessage copy = new(request.Method, request.RequestUri);\n        foreach (var header in request.Headers)\n        {\n            copy.Headers.TryAddWithoutValidation(header.Key, header.Value);\n        }\n        if (request.Content != null)\n        {\n            byte[] bytes = await request.Content.ReadAsByteArrayAsync();\n            copy.Content = new ByteArrayContent(bytes);\n            foreach (var header in request.Content.Headers)\n            {\n                copy.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);\n            }\n        }\n        Requests.Add(copy);\n        HttpResponseMessage response = _responses.Count > 0 ? _responses.Dequeue() : new HttpResponseMessage(HttpStatusCode.OK);\n        return response;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/RetryAlwaysTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests {\n    public class RetryAlwaysTests {\n        [Fact]\n        public void Smtp_RetryAlways_DefaultsFalse() {\n            var smtp = new Smtp();\n            Assert.False(smtp.RetryAlways);\n        }\n\n        [Fact]\n        public void Smtp_RetryAlways_Settable() {\n            var smtp = new Smtp { RetryAlways = true };\n            Assert.True(smtp.RetryAlways);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SearchDmarcReportsTests.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.DmarcReports;\nusing MailKit;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Net.Pop3;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SearchDmarcReportsTests {\n    private static MimeMessage CreateDmarc(string domain, DateTimeOffset date) {\n        var message = new MimeMessage();\n        message.Subject = $\"Report domain: {domain}\";\n        message.Date = date;\n        message.From.Add(new MailboxAddress(\"reporter\", \"reporter@example.com\"));\n        var builder = new BodyBuilder();\n        var ms = new MemoryStream(Encoding.UTF8.GetBytes(\"dummy\"));\n        var part = new MimePart(\"application\", \"zip\") {\n            Content = new MimeContent(ms),\n            FileName = $\"{domain}.zip\"\n        };\n        builder.Attachments.Add(part);\n        message.Body = builder.ToMessageBody();\n        return message;\n    }\n\n    private static MimeMessage CreateXmlDmarc(string domain, DateTimeOffset date, string mediaType, string mediaSubtype, string fileName) {\n        var message = new MimeMessage();\n        message.Subject = $\"Report domain: {domain}\";\n        message.Date = date;\n        message.From.Add(new MailboxAddress(\"reporter\", \"reporter@example.com\"));\n        var builder = new BodyBuilder();\n        var xml = $\"<feedback><policy_published><domain>{domain}</domain></policy_published></feedback>\";\n        var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml));\n        var part = new MimePart(mediaType, mediaSubtype) {\n            Content = new MimeContent(ms),\n            FileName = fileName\n        };\n        builder.Attachments.Add(part);\n        message.Body = builder.ToMessageBody();\n        return message;\n    }\n\n    private static MimeMessage CreateZippedXmlDmarc(string domain, DateTimeOffset date) {\n        var message = new MimeMessage();\n        message.Subject = \"Report\";\n        message.Date = date;\n        message.From.Add(new MailboxAddress(\"reporter\", \"reporter@example.com\"));\n        var builder = new BodyBuilder();\n        var ms = new MemoryStream();\n        using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, true)) {\n            var entry = zip.CreateEntry(\"report.xml\");\n            using var entryStream = entry.Open();\n            var bytes = Encoding.UTF8.GetBytes($\"<feedback><policy_published><domain>{domain}</domain></policy_published></feedback>\");\n            entryStream.Write(bytes, 0, bytes.Length);\n        }\n        ms.Position = 0;\n        var part = new MimePart(\"application\", \"zip\") {\n            Content = new MimeContent(ms),\n            FileName = \"report.zip\"\n        };\n        builder.Attachments.Add(part);\n        message.Body = builder.ToMessageBody();\n        return message;\n    }\n\n    private class TrackingPop3Client : Pop3Client {\n        private readonly List<MimeMessage> _messages;\n        public int FetchCount { get; private set; }\n        public TrackingPop3Client(IEnumerable<MimeMessage> messages) {\n            _messages = new List<MimeMessage>(messages);\n        }\n        public override bool IsConnected => true;\n        public override bool IsAuthenticated => true;\n        public override int Count => _messages.Count;\n        public override async Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            await Task.Yield();\n            FetchCount++;\n            return _messages[index];\n        }\n    }\n\n    [Fact]\n    public void FilterDmarcReports_ExtractsAttachments() {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateDmarc(\"example.com\", now);\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n        var report = reports[0];\n        Assert.Equal(\"reporter@example.com\", report.From);\n        Assert.Single(report.Attachments);\n        Assert.EndsWith(\".zip\", report.Attachments[0].Name);\n        using var ms = new MemoryStream();\n        report.Attachments[0].Content.CopyTo(ms);\n        Assert.True(ms.Length > 0);\n    }\n\n    [Fact]\n    public void FilterDmarcReports_MatchesDomainInAttachmentName() {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateDmarc(\"example.com\", now);\n        msg.Subject = \"Report\";\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n        Assert.Single(reports[0].Attachments);\n    }\n\n    [Fact]\n    public void FilterDmarcReports_MatchesDomainInXmlContent() {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateZippedXmlDmarc(\"example.com\", now);\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n    }\n\n    [Fact]\n    public void FilterDmarcReports_FiltersOutMismatchedDomain() {\n        var now = DateTimeOffset.UtcNow;\n        var good = CreateDmarc(\"example.com\", now);\n        good.Subject = \"Report\";\n        var bad = CreateDmarc(\"other.com\", now);\n        bad.Subject = \"Report\";\n        var list = new List<MimeMessage> { good, bad };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n        Assert.All(reports[0].Attachments, a => Assert.Contains(\"example.com\", a.Name, StringComparison.OrdinalIgnoreCase));\n    }\n\n    [Fact]\n    public void BuildDmarcReportSearchQuery_ContainsSubject() {\n        var query = MailboxSearcher.BuildDmarcReportSearchQuery(null, null, \"example.com\");\n        bool Contains(MailKit.Search.SearchQuery q) {\n            var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;\n            var term = q.GetType().GetProperty(\"Term\", flags)?.GetValue(q)?.ToString();\n            if (term == \"SubjectContains\") {\n                var text = q.GetType().GetProperty(\"Text\", flags)?.GetValue(q)?.ToString();\n                if (text?.IndexOf(\"example.com\", StringComparison.OrdinalIgnoreCase) >= 0 == true) return true;\n            }\n            var left = q.GetType().GetProperty(\"Left\", flags)?.GetValue(q) as MailKit.Search.SearchQuery;\n            var right = q.GetType().GetProperty(\"Right\", flags)?.GetValue(q) as MailKit.Search.SearchQuery;\n            if (left != null && Contains(left)) return true;\n            if (right != null && Contains(right)) return true;\n            return false;\n        }\n        Assert.True(Contains(query));\n    }\n\n    [Fact]\n    public void BuildDmarcReportSearchQuery_RequiresAttachments() {\n        var query = MailboxSearcher.BuildDmarcReportSearchQuery(null, null, null);\n        bool ContainsAttachment(MailKit.Search.SearchQuery q) {\n            var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;\n            var term = q.GetType().GetProperty(\"Term\", flags)?.GetValue(q)?.ToString();\n            if (term == \"HasAttachment\") return true;\n            if (term == \"HeaderContains\") {\n                var field = q.GetType().GetProperty(\"Field\", flags)?.GetValue(q)?.ToString();\n                var value = q.GetType().GetProperty(\"Value\", flags)?.GetValue(q)?.ToString();\n                if (field?.Equals(\"Content-Disposition\", StringComparison.OrdinalIgnoreCase) == true &&\n                    value?.IndexOf(\"attachment\", StringComparison.OrdinalIgnoreCase) >= 0) return true;\n            }\n            var left = q.GetType().GetProperty(\"Left\", flags)?.GetValue(q) as MailKit.Search.SearchQuery;\n            var right = q.GetType().GetProperty(\"Right\", flags)?.GetValue(q) as MailKit.Search.SearchQuery;\n            if (left != null && ContainsAttachment(left)) return true;\n            if (right != null && ContainsAttachment(right)) return true;\n            return false;\n        }\n        Assert.True(ContainsAttachment(query));\n    }\n\n    [Fact]\n    public void BuildGmailDmarcReportQuery_IncludesDomainAndDates() {\n        var since = new DateTime(2024, 1, 1);\n        var before = new DateTime(2024, 2, 1);\n        var q = MailboxSearcher.BuildGmailDmarcReportQuery(since, before, \"example.com\");\n        Assert.Contains(\"example.com\", q);\n        Assert.Contains(\"after:2024/01/01\", q);\n        Assert.Contains(\"before:2024/02/01\", q);\n        Assert.Contains(\"has:attachment\", q);\n    }\n\n    [Fact]\n    public void BuildGmailDmarcReportQuery_DomainWithDots_ReturnsQuery() {\n        var domain = \"sub.example.co.uk\";\n        var q = MailboxSearcher.BuildGmailDmarcReportQuery(null, null, domain);\n        Assert.Contains(domain, q);\n    }\n\n    [Fact]\n    public void BuildGmailDmarcReportQuery_DomainWithQuotes_EscapesQuotes() {\n        var domain = \"exa\\\"mple.com\";\n        var q = MailboxSearcher.BuildGmailDmarcReportQuery(null, null, domain);\n        Assert.Contains(\"exa\\\\\\\"mple.com\", q);\n    }\n\n    [Fact]\n    public void FilterDmarcReports_FiltersAcrossTimeZones() {\n        var msg1 = CreateDmarc(\"example.com\", new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.FromHours(2)));\n        var msg2 = CreateDmarc(\"example.com\", new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.FromHours(-5)));\n        var list = new List<MimeMessage> { msg1, msg2 };\n        var since = new DateTime(2023, 12, 31, 21, 0, 0, DateTimeKind.Utc);\n        var before = new DateTime(2024, 1, 1, 1, 0, 0, DateTimeKind.Utc);\n        var reports = MailboxSearcher.FilterDmarcReports(list, since, before, domain: null);\n        Assert.Single(reports);\n        Assert.Equal(msg1.Subject, reports[0].Subject);\n    }\n\n    [Fact]\n    public void FilterDmarcReports_ExtractsXmlAttachmentByExtension() {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateXmlDmarc(\"example.com\", now, \"application\", \"octet-stream\", \"example.xml\");\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n        Assert.Single(reports[0].Attachments);\n        Assert.EndsWith(\".xml\", reports[0].Attachments[0].Name);\n    }\n\n    [Theory]\n    [InlineData(\"application\")]\n    [InlineData(\"text\")]\n    public void FilterDmarcReports_ExtractsXmlAttachmentByContentType(string mediaType) {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateXmlDmarc(\"example.com\", now, mediaType, \"xml\", \"example\");\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterDmarcReports(list, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\");\n        Assert.Single(reports);\n        Assert.Single(reports[0].Attachments);\n        Assert.Equal(\"example\", reports[0].Attachments[0].Name);\n    }\n\n    [Fact]\n    public async Task SearchDmarcReportsAsync_Pop3_StopsAfterMaxResults() {\n        var now = DateTimeOffset.UtcNow;\n        var msgs = new List<MimeMessage>();\n        msgs.Add(new MimeMessage());\n        msgs.Add(CreateDmarc(\"example.com\", now));\n        msgs.Add(CreateDmarc(\"example.com\", now));\n        msgs.Add(CreateDmarc(\"example.com\", now));\n        var client = new TrackingPop3Client(msgs);\n        var reports = await MailboxSearcher.SearchDmarcReportsAsync(\n            client,\n            maxResults: 2,\n            parallelDownloadLimit: 2,\n            cancellationToken: CancellationToken.None);\n        Assert.Equal(2, reports.Count);\n        Assert.True(client.FetchCount <= 4, $\"fetched {client.FetchCount}\");\n    }\n\n    [Fact]\n    public async Task SearchDmarcReportsAsync_Graph_PropagatesCancellationToMimeDownload() {\n        var handler = new CancelDuringGraphMimeHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            using var cts = new CancellationTokenSource();\n            var searchTask = MailboxSearcher.SearchDmarcReportsAsync(\n                cred,\n                \"user@example.com\",\n                cancellationToken: cts.Token);\n\n            var startedTask = handler.MimeStarted.Task;\n            var completed = await Task.WhenAny(startedTask, Task.Delay(TimeSpan.FromSeconds(1)));\n            Assert.Same(startedTask, completed);\n\n            cts.Cancel();\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await searchTask);\n\n            Assert.True(handler.MimeRequestCanceled);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public void FilterDmarcReports_IgnoresMalformedArchive() {\n        var now = DateTimeOffset.UtcNow;\n        var message = new MimeMessage();\n        message.Subject = \"Report\";\n        message.Date = now;\n        message.From.Add(new MailboxAddress(\"reporter\", \"reporter@example.com\"));\n        var builder = new BodyBuilder();\n        var ms = new MemoryStream(new byte[] { 1, 2, 3, 4 });\n        var part = new MimePart(\"application\", \"zip\") {\n            Content = new MimeContent(ms),\n            FileName = \"report.zip\"\n        };\n        builder.Attachments.Add(part);\n        message.Body = builder.ToMessageBody();\n        bool logged = false;\n        void Handler(object? s, LogEventArgs e) => logged = true;\n        LoggingMessages.Logger.OnErrorMessage += Handler;\n        try {\n            var reports = MailboxSearcher.FilterDmarcReports(new[] { message }, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\", maxUncompressedSize: 1024);\n            Assert.Empty(reports);\n            Assert.True(logged);\n        } finally {\n            LoggingMessages.Logger.OnErrorMessage -= Handler;\n        }\n    }\n\n    [Fact]\n    public void FilterDmarcReports_SkipsOversizedAttachment() {\n        var now = DateTimeOffset.UtcNow;\n        var message = new MimeMessage();\n        message.Subject = \"Report\";\n        message.Date = now;\n        message.From.Add(new MailboxAddress(\"reporter\", \"reporter@example.com\"));\n        var builder = new BodyBuilder();\n        var ms = new MemoryStream();\n        using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, true)) {\n            var entry = zip.CreateEntry(\"report.xml\");\n            using var entryStream = entry.Open();\n            var large = new string('a', 2048);\n            var bytes = Encoding.UTF8.GetBytes($\"<feedback><policy_published><domain>example.com</domain></policy_published><data>{large}</data></feedback>\");\n            entryStream.Write(bytes, 0, bytes.Length);\n        }\n        ms.Position = 0;\n        var part = new MimePart(\"application\", \"zip\") {\n            Content = new MimeContent(ms),\n            FileName = \"report.zip\"\n        };\n        builder.Attachments.Add(part);\n        message.Body = builder.ToMessageBody();\n        bool logged = false;\n        void Handler(object? s, LogEventArgs e) => logged = true;\n        LoggingMessages.Logger.OnErrorMessage += Handler;\n        try {\n            var reports = MailboxSearcher.FilterDmarcReports(new[] { message }, since: now.AddMinutes(-1).DateTime, before: now.AddMinutes(1).DateTime, domain: \"example.com\", maxUncompressedSize: 512);\n            Assert.Empty(reports);\n            Assert.True(logged);\n        } finally {\n            LoggingMessages.Logger.OnErrorMessage -= Handler;\n        }\n    }\n\n    private static FieldInfo GetHandlerField() =>\n        typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    private sealed class CancelDuringGraphMimeHandler : HttpMessageHandler {\n        public TaskCompletionSource<object?> MimeStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        public bool MimeRequestCanceled { get; private set; }\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) };\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages\", StringComparison.Ordinal)) {\n                var json = \"{\\\"value\\\":[{\\\"id\\\":\\\"1\\\"}]}\";\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) };\n            }\n\n            if (uri.AbsolutePath.IndexOf(\"/messages/\", StringComparison.Ordinal) >= 0 && uri.AbsolutePath.EndsWith(\"/$value\", StringComparison.Ordinal)) {\n                MimeStarted.TrySetResult(null);\n                try {\n                    await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken).ConfigureAwait(false);\n                } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                    MimeRequestCanceled = true;\n                    throw;\n                }\n\n                const string raw = \"Date: Mon, 1 Jan 2024 00:00:00 +0000\\r\\nSubject: report domain: example.com\\r\\n\\r\\nbody\";\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(raw) };\n            }\n\n            return new HttpResponseMessage(HttpStatusCode.NotFound);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SearchNonDeliveryReportsTests.cs",
    "content": "using Mailozaurr;\nusing Mailozaurr.NonDeliveryReports;\nusing MimeKit;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing System.Globalization;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing MailKit.Search;\nusing MailKit.Net.Pop3;\nusing MailKit;\nusing System.Diagnostics;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SearchNonDeliveryReportsTests {\n    private static MimeMessage CreateNdr(string recipient, string messageId, DateTimeOffset date, DateTimeOffset? lastAttempt = null, bool includeArrival = true) {\n        string arrival = date.ToString(\"ddd, dd MMM yyyy HH:mm:ss K\", CultureInfo.InvariantCulture);\n        string? lastAttemptStr = lastAttempt?.ToString(\"ddd, dd MMM yyyy HH:mm:ss K\", CultureInfo.InvariantCulture);\n        string raw = $\"Date: {arrival}\\r\\nContent-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\r\\n\\r\\n--XXX\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\ntext\\r\\n\\r\\n--XXX\\r\\nContent-Type: message/delivery-status\\r\\n\\r\\nOriginal-Recipient: rfc822; {recipient}\\r\\nFinal-Recipient: rfc822; {recipient}\\r\\nOriginal-Message-ID: {messageId}\\r\\nReporting-MTA: dns; mx.example.com\\r\\nDiagnostic-Code: smtp; 550 5.1.1 User unknown\\r\\nStatus: 5.1.1\\r\\n\";\n        if (includeArrival) raw += $\"Arrival-Date: {arrival}\\r\\n\";\n        if (lastAttemptStr != null) raw += $\"Last-Attempt-Date: {lastAttemptStr}\\r\\n\";\n        raw += \"\\r\\n--XXX--\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        return MimeMessage.Load(stream);\n    }\n\n    private static MimeMessage CreateMultiRecipientNdr(string messageId, DateTimeOffset date, params string[] recipients) {\n        if (recipients == null || recipients.Length == 0) {\n            throw new ArgumentException(\"At least one recipient is required.\", nameof(recipients));\n        }\n\n        string arrival = date.ToString(\"ddd, dd MMM yyyy HH:mm:ss K\", CultureInfo.InvariantCulture);\n        var sb = new StringBuilder();\n        sb.Append($\"Date: {arrival}\\r\\n\");\n        sb.Append(\"Content-Type: multipart/report; report-type=delivery-status; boundary=\\\"XXX\\\"\\r\\n\\r\\n\");\n        sb.Append(\"--XXX\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\ntext\\r\\n\\r\\n\");\n        sb.Append(\"--XXX\\r\\nContent-Type: message/delivery-status\\r\\n\\r\\n\");\n        foreach (var recipient in recipients) {\n            sb.Append($\"Original-Recipient: rfc822; {recipient}\\r\\n\");\n            sb.Append($\"Final-Recipient: rfc822; {recipient}\\r\\n\");\n            sb.Append($\"Original-Message-ID: {messageId}\\r\\n\");\n            sb.Append(\"Reporting-MTA: dns; mx.example.com\\r\\n\");\n            sb.Append(\"Diagnostic-Code: smtp; 550 5.1.1 User unknown\\r\\n\");\n            sb.Append(\"Status: 5.1.1\\r\\n\");\n            sb.Append($\"Arrival-Date: {arrival}\\r\\n\\r\\n\");\n        }\n        sb.Append(\"--XXX--\");\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString()));\n        return MimeMessage.Load(stream);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_FiltersByRecipientAndMessageId() {\n        var now = DateTimeOffset.UtcNow;\n        var msg1 = CreateNdr(\"user@example.com\", \"<id1>\", now);\n        var msg2 = CreateNdr(\"other@example.com\", \"<id2>\", now);\n        var list = new List<MimeMessage> { msg1, msg2 };\n        var reports = MailboxSearcher.FilterNonDeliveryReports(\n            list,\n            since: now.AddMinutes(-5).DateTime,\n            before: now.AddMinutes(5).DateTime,\n            recipientContains: \"user@example.com\",\n            messageId: \"id1\");\n        Assert.Single(reports);\n        Assert.Equal(\"id1\", reports[0].OriginalMessageId);\n    }\n\n    [Theory]\n    [InlineData(\"<id1>\", \"<id1>\")]\n    [InlineData(\"<id1>\", \"id1\")]\n    [InlineData(\"id1\", \"<id1>\")]\n    [InlineData(\"id1\", \"id1\")]\n    public void FilterNonDeliveryReports_MatchesMessageIdWithOrWithoutBrackets(string headerId, string filter) {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateNdr(\"user@example.com\", headerId, now);\n        var reports = MailboxSearcher.FilterNonDeliveryReports(\n            new List<MimeMessage> { msg },\n            since: now.AddMinutes(-5).DateTime,\n            before: now.AddMinutes(5).DateTime,\n            recipientContains: null,\n            messageId: filter);\n        Assert.Single(reports);\n        Assert.Equal(\"id1\", reports[0].OriginalMessageId);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_FiltersByDate() {\n        var now = DateTimeOffset.UtcNow;\n        var msg1 = CreateNdr(\"user@example.com\", \"<id1>\", now.AddMinutes(-10));\n        var msg2 = CreateNdr(\"user@example.com\", \"<id2>\", now);\n        var list = new List<MimeMessage> { msg1, msg2 };\n        var reports = MailboxSearcher.FilterNonDeliveryReports(list, since: now.AddMinutes(-5).DateTime, before: null, recipientContains: null, messageId: null);\n        Assert.Single(reports);\n        Assert.Equal(\"id2\", reports[0].OriginalMessageId);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_UsesLastAttemptDateWhenPresent() {\n        var now = DateTimeOffset.UtcNow;\n        var msg = CreateNdr(\"user@example.com\", \"<id1>\", now.AddDays(-2), lastAttempt: now);\n        var list = new List<MimeMessage> { msg };\n        var reports = MailboxSearcher.FilterNonDeliveryReports(list, since: now.AddHours(-1).DateTime, before: null, recipientContains: null, messageId: null);\n        Assert.Single(reports);\n        Assert.Equal(\"id1\", reports[0].OriginalMessageId);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_FiltersAcrossTimeZones() {\n        var msg1 = CreateNdr(\"user@example.com\", \"<id1>\", new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.FromHours(2)));\n        var msg2 = CreateNdr(\"user@example.com\", \"<id2>\", new DateTimeOffset(2024, 1, 1, 5, 0, 0, TimeSpan.FromHours(-5)));\n        var list = new List<MimeMessage> { msg1, msg2 };\n        var since = new DateTime(2023, 12, 31, 21, 0, 0, DateTimeKind.Utc);\n        var before = new DateTime(2024, 1, 1, 1, 0, 0, DateTimeKind.Utc);\n        var reports = MailboxSearcher.FilterNonDeliveryReports(list, since, before, recipientContains: null, messageId: null);\n        Assert.Single(reports);\n        Assert.Equal(\"id1\", reports[0].OriginalMessageId);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_SubjectDetectedReportSurvivesDateFilter() {\n        var now = DateTimeOffset.UtcNow;\n        var date = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Offset);\n        string raw = $\"Date: {date:R}\\r\\nSubject: Mail Delivery Subsystem\\r\\n\\r\\ntext\";\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(raw));\n        var message = MimeMessage.Load(stream);\n        var list = new List<MimeMessage> { message };\n        var since = date.AddMinutes(-5).UtcDateTime;\n        var before = date.AddMinutes(5).UtcDateTime;\n        var reports = MailboxSearcher.FilterNonDeliveryReports(list, since, before, recipientContains: null, messageId: null);\n        Assert.Single(reports);\n        Assert.Equal(date, reports[0].Timestamp);\n    }\n\n    [Fact]\n    public void FilterNonDeliveryReports_ReportWithoutTimestampUsesMessageDate() {\n        var now = DateTimeOffset.UtcNow;\n        now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Offset);\n        var msg = CreateNdr(\"user@example.com\", \"<id1>\", now, includeArrival: false);\n        var list = new List<MimeMessage> { msg };\n        var since = now.AddMinutes(-5).UtcDateTime;\n        var before = now.AddMinutes(5).UtcDateTime;\n        var reports = MailboxSearcher.FilterNonDeliveryReports(list, since, before, recipientContains: null, messageId: null);\n        Assert.Single(reports);\n        Assert.Equal(now, reports[0].Timestamp);\n    }\n\n    private static bool Contains(SearchQuery query, Func<SearchQuery, bool> predicate) {\n        if (predicate(query)) return true;\n        var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;\n        var leftProp = query.GetType().GetProperty(\"Left\", flags);\n        var rightProp = query.GetType().GetProperty(\"Right\", flags);\n        var left = leftProp?.GetValue(query) as SearchQuery;\n        var right = rightProp?.GetValue(query) as SearchQuery;\n        if (left != null && Contains(left, predicate)) return true;\n        if (right != null && Contains(right, predicate)) return true;\n        return false;\n    }\n\n    [Fact]\n    public void BuildNonDeliveryReportSearchQuery_ContainsFilters() {\n        var query = MailboxSearcher.BuildNonDeliveryReportSearchQuery(null, null);\n        var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;\n        bool hasHeader = Contains(query, q => {\n            var term = q.GetType().GetProperty(\"Term\", flags)?.GetValue(q)?.ToString();\n            if (term == \"HeaderContains\") {\n                var field = q.GetType().GetProperty(\"Field\", flags)?.GetValue(q)?.ToString();\n                var value = q.GetType().GetProperty(\"Value\", flags)?.GetValue(q)?.ToString();\n                return string.Equals(field, \"Content-Type\", StringComparison.OrdinalIgnoreCase) &&\n                    value?.IndexOf(\"delivery-status\", StringComparison.OrdinalIgnoreCase) >= 0;\n            }\n            return false;\n        });\n        Assert.True(hasHeader);\n        bool hasSubject = Contains(query, q => {\n            var term = q.GetType().GetProperty(\"Term\", flags)?.GetValue(q)?.ToString();\n            if (term == \"SubjectContains\") {\n                var text = q.GetType().GetProperty(\"Text\", flags)?.GetValue(q)?.ToString();\n                return text?.IndexOf(\"Mail delivery failed\", StringComparison.OrdinalIgnoreCase) >= 0;\n            }\n            return false;\n        });\n        Assert.True(hasSubject);\n    }\n\n    private class FakePop3Client : Pop3Client {\n        private readonly List<MimeMessage> _messages;\n        private readonly int _delay;\n        public FakePop3Client(IEnumerable<MimeMessage> messages, int delay) {\n            _messages = new List<MimeMessage>(messages);\n            _delay = delay;\n        }\n        public override bool IsConnected => true;\n        public override bool IsAuthenticated => true;\n        public override int Count => _messages.Count;\n        public override async Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            await Task.Delay(_delay, cancellationToken).ConfigureAwait(false);\n            return _messages[index];\n        }\n    }\n\n    private class CountingPop3Client : Pop3Client {\n        private readonly List<MimeMessage> _messages;\n        private readonly int _delay;\n        private readonly object _lock = new();\n        private int _current;\n        public int MaxConcurrency { get; private set; }\n        public CountingPop3Client(IEnumerable<MimeMessage> messages, int delay) {\n            _messages = new List<MimeMessage>(messages);\n            _delay = delay;\n        }\n        public override bool IsConnected => true;\n        public override bool IsAuthenticated => true;\n        public override int Count => _messages.Count;\n        public override async Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            lock (_lock) {\n                _current++;\n                if (_current > MaxConcurrency) MaxConcurrency = _current;\n            }\n            try {\n                await Task.Delay(_delay, cancellationToken).ConfigureAwait(false);\n            } finally {\n                lock (_lock) { _current--; }\n            }\n            return _messages[index];\n        }\n    }\n\n    private class TrackingPop3Client : Pop3Client {\n        private readonly List<MimeMessage> _messages;\n        public int FetchCount { get; private set; }\n        public TrackingPop3Client(IEnumerable<MimeMessage> messages) {\n            _messages = new List<MimeMessage>(messages);\n        }\n        public override bool IsConnected => true;\n        public override bool IsAuthenticated => true;\n        public override int Count => _messages.Count;\n        public override async Task<MimeMessage> GetMessageAsync(int index, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            await Task.Yield();\n            FetchCount++;\n            return _messages[index];\n        }\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_Pop3_DownloadsInParallel() {\n        var now = DateTimeOffset.UtcNow;\n        var msgs = new List<MimeMessage>();\n        for (int i = 0; i < 4; i++) msgs.Add(CreateNdr($\"u{i}@example.com\", $\"<id{i}>\", now));\n        var client = new FakePop3Client(msgs, 500);\n        var seq = Stopwatch.StartNew();\n        _ = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            parallelDownloadLimit: 0,\n            cancellationToken: CancellationToken.None);\n        seq.Stop();\n        var sw = Stopwatch.StartNew();\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            parallelDownloadLimit: 4,\n            cancellationToken: CancellationToken.None);\n        sw.Stop();\n        Assert.Equal(4, reports.Count);\n        Assert.True(sw.Elapsed < seq.Elapsed, $\"sequential: {seq.ElapsedMilliseconds}, parallel: {sw.ElapsedMilliseconds}\");\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_Pop3_RespectsParallelLimit() {\n        var now = DateTimeOffset.UtcNow;\n        var msgs = new List<MimeMessage>();\n        for (int i = 0; i < 20; i++) msgs.Add(CreateNdr($\"u{i}@example.com\", $\"<id{i}>\", now));\n        var client = new CountingPop3Client(msgs, 100);\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            parallelDownloadLimit: 4,\n            cancellationToken: CancellationToken.None);\n        Assert.Equal(msgs.Count, reports.Count);\n        Assert.Equal(4, client.MaxConcurrency);\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_Pop3_StopsAfterMaxResults() {\n        var now = DateTimeOffset.UtcNow;\n        var msgs = new List<MimeMessage>();\n        msgs.Add(new MimeMessage());\n        msgs.Add(CreateNdr(\"u1@example.com\", \"<id1>\", now));\n        msgs.Add(CreateNdr(\"u2@example.com\", \"<id2>\", now));\n        msgs.Add(CreateNdr(\"u3@example.com\", \"<id3>\", now));\n        var client = new TrackingPop3Client(msgs);\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            maxResults: 2,\n            parallelDownloadLimit: 2,\n            cancellationToken: CancellationToken.None);\n        Assert.Equal(2, reports.Count);\n        Assert.True(client.FetchCount <= 4, $\"fetched {client.FetchCount}\");\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_Graph_FiltersBySubject() {\n        var handler = new NdrHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                cred,\n                \"user@example.com\",\n                cancellationToken: CancellationToken.None);\n            Assert.Single(reports);\n            Assert.Equal(1, handler.MimeFetches);\n            Assert.NotNull(handler.Filter);\n            foreach (var pattern in NonDeliveryReportSubjectPatterns.Values) {\n                var expectedPattern = pattern.Replace(\"'\", \"''\");\n                Assert.Contains(expectedPattern, handler.Filter!, StringComparison.Ordinal);\n            }\n            Assert.Contains(\"contains(subject,'Undeliverable:')\", handler.Filter!, StringComparison.Ordinal);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(2)]\n    public async Task SearchNonDeliveryReportsAsync_Graph_RespectsMaxResultsWithinSingleMime(int parallelDownloadLimit) {\n        var handler = new MultiRecipientGraphHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n                cred,\n                \"user@example.com\",\n                maxResults: 1,\n                parallelDownloadLimit: parallelDownloadLimit,\n                cancellationToken: CancellationToken.None);\n            Assert.Single(reports);\n            Assert.Equal(\"multi\", reports[0].OriginalMessageId);\n            Assert.Equal(1, handler.MimeFetches);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Fact]\n    public async Task SearchNonDeliveryReportsAsync_Graph_PropagatesCancellationToMessageListing() {\n        var handler = new CancelDuringGraphListHandler();\n        var field = typeof(MicrosoftGraphUtils).GetField(\"HttpClient\", BindingFlags.NonPublic | BindingFlags.Static)!;\n        var client = (HttpClient)field.GetValue(null)!;\n        var handlerField = GetHandlerField();\n        var original = (HttpMessageHandler)handlerField.GetValue(client)!;\n        handlerField.SetValue(client, handler);\n        try {\n            var cred = new GraphCredential { ClientId = \"id\", DirectoryId = \"tenant\", ClientSecret = \"secret\" };\n            using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>\n                MailboxSearcher.SearchNonDeliveryReportsAsync(\n                    cred,\n                    \"user@example.com\",\n                    cancellationToken: cts.Token));\n\n            Assert.True(handler.ListRequestCanceled);\n        } finally {\n            handlerField.SetValue(client, original);\n        }\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(2)]\n    public async Task SearchNonDeliveryReportsAsync_Gmail_RespectsMaxResultsWithinSingleMime(int parallelDownloadLimit) {\n        var handler = new MultiRecipientGmailHandler();\n        var httpClient = new HttpClient(handler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        using var client = new GmailApiClient(httpClient);\n        var reports = await MailboxSearcher.SearchNonDeliveryReportsAsync(\n            client,\n            \"me\",\n            maxResults: 1,\n            parallelDownloadLimit: parallelDownloadLimit,\n            cancellationToken: CancellationToken.None);\n        Assert.Single(reports);\n        Assert.Equal(\"multi\", reports[0].OriginalMessageId);\n        Assert.Equal(1, handler.MimeFetches);\n        Assert.Equal(1, handler.ListRequests);\n    }\n\n    private static FieldInfo GetHandlerField() =>\n        typeof(HttpMessageInvoker).GetField(\"_handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? typeof(HttpMessageInvoker).GetField(\"handler\", BindingFlags.NonPublic | BindingFlags.Instance)\n        ?? throw new InvalidOperationException(\"HttpClient handler field not found\");\n\n    private sealed class NdrHandler : HttpMessageHandler {\n        public string? Filter { get; private set; }\n        public int MimeFetches { get; private set; }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages\")) {\n                var query = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries);\n                foreach (var q in query) {\n                    var parts = q.Split(new[] { '=' }, 2);\n                    if (parts.Length == 2 && Uri.UnescapeDataString(parts[0]) == \"$filter\") Filter = Uri.UnescapeDataString(parts[1]);\n                }\n                var json = \"{\\\"value\\\":[{\\\"id\\\":\\\"1\\\"}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            if (uri.AbsolutePath.Contains(\"/messages/\") && uri.AbsolutePath.EndsWith(\"/$value\")) {\n                MimeFetches++;\n                const string raw = \"Date: Mon, 1 Jan 2024 00:00:00 +0000\\r\\nSubject: Undeliverable: Delivery has failed\\r\\n\\r\\nbody\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(raw) });\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private sealed class MultiRecipientGraphHandler : HttpMessageHandler {\n        private readonly string _raw;\n        public int MimeFetches { get; private set; }\n\n        public MultiRecipientGraphHandler() {\n            var message = CreateMultiRecipientNdr(\"<multi>\", new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), \"user1@example.com\", \"user2@example.com\");\n            using var stream = new MemoryStream();\n            message.WriteTo(stream);\n            _raw = Encoding.UTF8.GetString(stream.ToArray());\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages\")) {\n                var json = \"{\\\"value\\\":[{\\\"id\\\":\\\"1\\\"}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            if (uri.AbsolutePath.Contains(\"/messages/\") && uri.AbsolutePath.EndsWith(\"/$value\")) {\n                MimeFetches++;\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(_raw) });\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private sealed class MultiRecipientGmailHandler : HttpMessageHandler {\n        private readonly string _raw;\n        public int MimeFetches { get; private set; }\n        public int ListRequests { get; private set; }\n\n        public MultiRecipientGmailHandler() {\n            var message = CreateMultiRecipientNdr(\"<multi>\", new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), \"user1@example.com\", \"user2@example.com\");\n            using var stream = new MemoryStream();\n            message.WriteTo(stream);\n            var bytes = stream.ToArray();\n            var base64 = Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');\n            _raw = base64;\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsolutePath.EndsWith(\"/messages\")) {\n                ListRequests++;\n                var json = \"{\\\"messages\\\":[{\\\"id\\\":\\\"1\\\"}]}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            if (uri.AbsolutePath.Contains(\"/messages/\") && uri.Query.Contains(\"format=raw\")) {\n                MimeFetches++;\n                var json = $\"{{\\\"id\\\":\\\"1\\\",\\\"raw\\\":\\\"{_raw}\\\"}}\";\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });\n            }\n\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));\n        }\n    }\n\n    private sealed class CancelDuringGraphListHandler : HttpMessageHandler {\n        public bool ListRequestCanceled { get; private set; }\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            var uri = request.RequestUri!;\n            if (uri.AbsoluteUri.Contains(\"oauth2\")) {\n                var json = \"{\\\"access_token\\\":\\\"token\\\",\\\"token_type\\\":\\\"Bearer\\\"}\";\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) };\n            }\n\n            if (uri.AbsolutePath.EndsWith(\"/messages\", StringComparison.Ordinal)) {\n                try {\n                    await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken).ConfigureAwait(false);\n                } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                    ListRequestCanceled = true;\n                    throw;\n                }\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"{\\\"value\\\":[]}\") };\n            }\n\n            return new HttpResponseMessage(HttpStatusCode.NotFound);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SecureStringHelperAsyncTests.cs",
    "content": "using System;\nusing System.Security;\nusing System.Runtime.InteropServices;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SecureStringHelperAsyncTests\n{\n    private static string ToPlainString(SecureString s)\n    {\n        IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(s);\n        try\n        {\n            return Marshal.PtrToStringUni(ptr)!;\n        }\n        finally\n        {\n            Marshal.ZeroFreeCoTaskMemUnicode(ptr);\n        }\n    }\n\n    [Fact]\n    public async Task EncryptDecryptAsync_RoundTrip()\n    {\n        using SecureString input = SecureStringHelper.FromPlainTextString(\"secret\");\n        byte[] key = new byte[32];\n        new Random().NextBytes(key);\n        var result = await SecureStringHelper.EncryptAsync(input, key);\n        using SecureString decrypted = await SecureStringHelper.DecryptAsync(result.EncryptedData, key, Convert.FromBase64String(result.IV));\n        Assert.Equal(\"secret\", ToPlainString(decrypted));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendEmailBasicTests.cs",
    "content": "using Xunit;\nusing Mailozaurr.Definitions;\nusing Mailozaurr;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.IO;\nusing System.Management.Automation;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests {\n    public class SendEmailBasicTests {\n        private class FakeSmtpClient : ClientSmtp\n        {\n            public bool SendCalled;\n            public MimeMessage? LastMessage;\n\n            public override Task<string> SendAsync(MimeMessage message, System.Threading.CancellationToken cancellationToken = default, MailKit.ITransferProgress? progress = null)\n            {\n                SendCalled = true;\n                LastMessage = message;\n                return Task.FromResult(string.Empty);\n            }\n        }\n\n        [Fact]\n        public void SendEmail_Smtp_WithValidInput_Succeeds() {\n            var smtp = new Smtp();\n            var fake = new FakeSmtpClient();\n            var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;\n            field.SetValue(smtp, fake);\n\n            smtp.From = \"sender@example.com\";\n            smtp.To = new[] { \"recipient@example.com\" };\n            smtp.Subject = \"Test Email (SMTP)\";\n            smtp.HtmlBody = \"<b>Hello from Mailozaurr SMTP!</b>\";\n            smtp.CreateMessage();\n\n            var result = smtp.Send();\n\n            Assert.True(result.Status, $\"SMTP send failed: {result.Error}\");\n            Assert.True(fake.SendCalled);\n        }\n\n        [Fact]\n        public void SendEmail_Smtp_WithAutoCreateMessage_BuildsMessage() {\n            var smtp = new Smtp { AutoCreateMessage = true };\n            var fake = new FakeSmtpClient();\n            var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;\n            field.SetValue(smtp, fake);\n\n            smtp.From = \"sender@example.com\";\n            smtp.To = new[] { \"recipient@example.com\" };\n            smtp.Subject = \"Test Email (SMTP)\";\n            smtp.HtmlBody = \"<b>Hello from Mailozaurr SMTP!</b>\";\n\n            var result = smtp.Send();\n\n            Assert.True(result.Status, $\"SMTP send failed: {result.Error}\");\n            Assert.True(fake.SendCalled);\n            Assert.NotNull(fake.LastMessage);\n            Assert.NotEmpty(fake.LastMessage!.From);\n        }\n\n        [Fact]\n        public void SendEmail_Smtp_WithAutoCreateMessage_PreservesCustomHeaders() {\n            var smtp = new Smtp { AutoCreateMessage = true };\n            var fake = new FakeSmtpClient();\n            var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;\n            field.SetValue(smtp, fake);\n\n            smtp.Message.Headers.Add(\"X-Test\", \"1\");\n            smtp.From = \"sender@example.com\";\n            smtp.To = new[] { \"recipient@example.com\" };\n            smtp.Subject = \"Test Email (SMTP)\";\n            smtp.HtmlBody = \"<b>Hello from Mailozaurr SMTP!</b>\";\n\n            var result = smtp.Send();\n\n            Assert.True(result.Status, $\"SMTP send failed: {result.Error}\");\n            Assert.NotNull(fake.LastMessage);\n            Assert.Equal(\"1\", fake.LastMessage!.Headers[\"X-Test\"]);\n        }\n\n        [Fact]\n        public void SendEmail_Smtp_WithoutCreateMessage_ReturnsHelpfulError() {\n            var smtp = new Smtp();\n            var fake = new FakeSmtpClient();\n            var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;\n            field.SetValue(smtp, fake);\n\n            smtp.From = \"sender@example.com\";\n            smtp.To = new[] { \"recipient@example.com\" };\n            smtp.Subject = \"Test Email (SMTP)\";\n            smtp.HtmlBody = \"<b>Hello from Mailozaurr SMTP!</b>\";\n\n            var result = smtp.Send();\n\n            Assert.False(result.Status);\n            Assert.Contains(\"CreateMessage\", result.Error);\n            Assert.False(fake.SendCalled);\n        }\n\n        [Fact]\n        public void SendEmail_Smtp_WithErrorActionStopAndMissingSender_Throws() {\n            var smtp = new Smtp {\n                AutoCreateMessage = true,\n                ErrorAction = ActionPreference.Stop\n            };\n            var fake = new FakeSmtpClient();\n            var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;\n            field.SetValue(smtp, fake);\n\n            smtp.To = new[] { \"recipient@example.com\" };\n            smtp.Subject = \"Missing sender\";\n            smtp.HtmlBody = \"<b>Hello</b>\";\n\n            var ex = Assert.Throws<InvalidOperationException>(() => smtp.Send());\n            Assert.Contains(\"no sender\", ex.Message, System.StringComparison.OrdinalIgnoreCase);\n            Assert.False(fake.SendCalled);\n        }\n\n        [Fact]\n        public async Task SendEmail_SendGrid_WithValidInput_Succeeds() {\n            var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n            var client = new SendGridClient();\n            var field = typeof(SendGridClient).GetField(\"_client\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;\n            field.SetValue(client, new HttpClient(handler));\n\n            client.From = \"sender@example.com\";\n            client.To = new System.Collections.Generic.List<object> { \"recipient@example.com\" };\n            client.Subject = \"Test Email (SendGrid)\";\n            client.Html = \"<b>Hello from Mailozaurr SendGrid!</b>\";\n            client.Text = \"Hello from Mailozaurr SendGrid!\";\n            client.Credentials = new NetworkCredential(\"apikey\", \"SENDGRID_API_KEY\");\n            client.CreateMessage();\n\n            var result = await client.SendEmailAsync();\n\n            Assert.True(result.Status, $\"SendGrid send failed: {result.Error}\");\n            Assert.Single(handler.Requests);\n        }\n\n        [Fact]\n        public async Task SendEmail_SendGrid_WithToken_Succeeds() {\n            var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n            var client = new SendGridClient();\n            var field = typeof(SendGridClient).GetField(\"_client\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;\n            field.SetValue(client, new HttpClient(handler));\n\n            client.From = \"sender@example.com\";\n            client.To = new System.Collections.Generic.List<object> { \"recipient@example.com\" };\n            client.Subject = \"Test Email (SendGrid)\";\n            client.Html = \"<b>Hello from Mailozaurr SendGrid!</b>\";\n            client.Text = \"Hello from Mailozaurr SendGrid!\";\n            client.Credentials = new NetworkCredential(\"apikey\", \"SENDGRID_API_KEY\");\n            client.CreateMessage();\n\n            using var cts = new CancellationTokenSource();\n            var result = await client.SendEmailAsync(cts.Token);\n\n            Assert.True(result.Status, $\"SendGrid send failed: {result.Error}\");\n            Assert.Single(handler.Requests);\n        }\n\n        [Fact]\n        public async Task SendEmail_SendGrid_InvalidCredentialType_Fails() {\n            var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n            var client = new SendGridClient();\n            var field = typeof(SendGridClient).GetField(\"_client\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;\n            field.SetValue(client, new HttpClient(handler));\n\n            client.From = \"sender@example.com\";\n            client.To = new System.Collections.Generic.List<object> { \"recipient@example.com\" };\n            client.Subject = \"Test Email (SendGrid)\";\n            client.Html = \"<b>Hello from Mailozaurr SendGrid!</b>\";\n            client.Text = \"Hello from Mailozaurr SendGrid!\";\n            client.Credentials = new CredentialCache();\n            client.CreateMessage();\n\n            var result = await client.SendEmailAsync();\n\n            Assert.False(result.Status);\n            Assert.Empty(handler.Requests);\n        }\n\n        [Fact]\n        public async Task SendEmail_SendGrid_ServerError_Fails() {\n            var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(\"err\") });\n            var client = new SendGridClient();\n            var field = typeof(SendGridClient).GetField(\"_client\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;\n            field.SetValue(client, new HttpClient(handler));\n\n            client.From = \"sender@example.com\";\n            client.To = new System.Collections.Generic.List<object> { \"recipient@example.com\" };\n            client.Subject = \"Test Email (SendGrid Failure)\";\n            client.Html = \"<b>Body</b>\";\n            client.Text = \"Body\";\n            client.Credentials = new NetworkCredential(\"apikey\", \"SENDGRID_API_KEY\");\n            client.CreateMessage();\n\n            var result = await client.SendEmailAsync();\n\n            Assert.False(result.Status);\n            Assert.Single(handler.Requests);\n        }\n\n        [Fact]\n        public async Task SendEmail_Graph_WithValidInput_Succeeds() {\n            var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n            using var graph = new Graph();\n            var field = typeof(Graph).GetField(\"_client\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;\n            field.SetValue(graph, new HttpClient(handler));\n\n            graph.From = \"sender@example.com\";\n            graph.To = new object[] { \"recipient@example.com\" };\n            graph.Subject = \"Test Email (Graph)\";\n            graph.HTML = \"<b>Hello from Mailozaurr Graph!</b>\";\n            graph.ContentType = \"HTML\";\n            graph.AccessToken = \"token\";\n            graph.TokenType = \"Bearer\";\n\n            var result = await graph.SendMessageAsync();\n\n            Assert.True(result.Status, $\"Graph send failed: {result.Error}\");\n            Assert.Single(handler.Requests);\n        }\n\n        [Fact]\n        public async Task ConnectO365GraphAsync_GraphCanceled_ThrowsOperationCanceledException() {\n            using var graph = new Graph();\n            using var cts = new CancellationTokenSource();\n            cts.Cancel();\n            graph.Authenticate(new NetworkCredential(\"client@tenant\", \"secret\"));\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => graph.ConnectO365GraphAsync(cts.Token));\n        }\n\n        [Fact]\n        public async Task SendEmail_GraphCanceled_ThrowsOperationCanceledException() {\n            using var graph = new Graph {\n                From = \"sender@example.com\",\n                To = new object[] { \"recipient@example.com\" },\n                Subject = \"Test Email (Graph Cancel)\",\n                HTML = \"<b>Hello from Mailozaurr Graph!</b>\",\n                ContentType = \"HTML\",\n                AccessToken = \"token\",\n                TokenType = \"Bearer\"\n            };\n            using var cts = new CancellationTokenSource();\n            cts.Cancel();\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => graph.SendMessageAsync(cts.Token));\n        }\n\n        [Fact]\n        public async Task SendDraftEmail_GraphCanceled_ThrowsOperationCanceledException() {\n            using var graph = new Graph {\n                From = \"sender@example.com\",\n                To = new object[] { \"recipient@example.com\" },\n                Subject = \"Test Draft Email (Graph Cancel)\",\n                HTML = \"<b>Hello from Mailozaurr Graph Draft!</b>\",\n                ContentType = \"HTML\",\n                AccessToken = \"token\",\n                TokenType = \"Bearer\"\n            };\n            using var cts = new CancellationTokenSource();\n            cts.Cancel();\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => graph.SendMessageDraftAsync(cts.Token));\n        }\n\n        [Fact]\n        public void SendEmail_WithInvalidEmailAddress_Fails() {\n            // Arrange\n            var result = Validator.ValidateEmail(\"invalid\");\n\n            // Act & Assert\n            Assert.False(result.IsValid);\n        }\n\n        [Fact]\n        public void SendEmail_WithMissingSubjectOrBody_Fails() {\n            // Arrange\n            var smtp = new Smtp();\n            smtp.From = \"a@b.com\";\n            smtp.To = new object[] { \"c@d.com\" };\n            smtp.Subject = string.Empty;\n            smtp.TextBody = string.Empty;\n            smtp.CreateMessage();\n\n            // Act\n            var subjectEmpty = string.IsNullOrWhiteSpace(smtp.Message.Subject);\n            var bodyEmpty = smtp.Message.Body is TextPart part && string.IsNullOrWhiteSpace(part.Text);\n\n            // Assert\n            Assert.True(subjectEmpty || bodyEmpty);\n        }\n\n        [Fact]\n        public void SendEmail_WithAttachment_Succeeds() {\n            // Arrange\n            var tmp = Path.GetTempFileName();\n            File.WriteAllText(tmp, \"data\");\n            var smtp = new Smtp();\n            smtp.From = \"a@b.com\";\n            smtp.To = new object[] { \"c@d.com\" };\n            smtp.Subject = \"test\";\n            smtp.Attachments = new System.Collections.Generic.List<AttachmentDescriptor> { new FileAttachmentDescriptor(tmp) };\n            smtp.CreateMessage();\n\n            // Act\n            var attachCount = smtp.Message.BodyParts\n                .OfType<MimePart>()\n                .Count(p => p.IsAttachment);\n            File.Delete(tmp);\n\n            // Assert\n            Assert.Equal(1, attachCount);\n        }\n\n        [Fact]\n        public void SendEmail_WithDuplicateAttachments_AddsOnce() {\n            var tmp = Path.GetTempFileName();\n            File.WriteAllText(tmp, \"data\");\n            var smtp = new Smtp();\n            smtp.From = \"a@b.com\";\n            smtp.To = new object[] { \"c@d.com\" };\n            smtp.Subject = \"test\";\n            smtp.Attachments = new System.Collections.Generic.List<AttachmentDescriptor>\n            {\n                new FileAttachmentDescriptor(tmp),\n                new FileAttachmentDescriptor(tmp)\n            };\n            smtp.CreateMessage();\n\n            var attachCount = smtp.Message.BodyParts\n                .OfType<MimePart>()\n                .Count(p => p.IsAttachment);\n            File.Delete(tmp);\n\n            Assert.Equal(1, attachCount);\n        }\n    }}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendGridAttachmentTests.cs",
    "content": "using System;\nusing System.IO;\nusing System.Text;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SendGridAttachmentTests\n{\n    [Fact]\n    public void Constructor_FromFilePath_ReadsFileAndSetsMetadata()\n    {\n        var path = Path.GetTempFileName();\n        try\n        {\n            File.WriteAllText(path, \"hello\");\n            var attachment = new SendGridAttachment(path);\n\n            Assert.Equal(Path.GetFileName(path), attachment.Filename);\n            Assert.Equal(Convert.ToBase64String(Encoding.UTF8.GetBytes(\"hello\")), attachment.Content);\n            Assert.Equal(MimeTypes.GetMimeType(path), attachment.Type);\n            Assert.Equal(\"attachment\", attachment.Disposition);\n            Assert.Null(attachment.ContentId);\n        }\n        finally\n        {\n            File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public void Constructor_FromBytes_UsesProvidedMetadata()\n    {\n        var data = Encoding.UTF8.GetBytes(\"inline\");\n        var attachment = new SendGridAttachment(\"inline.txt\", data, \"text/plain\", \"inline\", \"cid123\");\n\n        Assert.Equal(\"inline.txt\", attachment.Filename);\n        Assert.Equal(Convert.ToBase64String(data), attachment.Content);\n        Assert.Equal(\"text/plain\", attachment.Type);\n        Assert.Equal(\"inline\", attachment.Disposition);\n        Assert.Equal(\"cid123\", attachment.ContentId);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendGridClientDisposeTests.cs",
    "content": "namespace Mailozaurr.Tests;\n\npublic class SendGridClientDisposeTests {\n    [Fact]\n    public void Dispose_CanBeCalledMultipleTimes() {\n        using var client = new SendGridClient();\n        client.Dispose();\n        client.Dispose();\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_AfterDispose_ThrowsObjectDisposedException() {\n        var client = new SendGridClient();\n        client.Dispose();\n\n        await Assert.ThrowsAsync<ObjectDisposedException>(() => client.SendEmailAsync());\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendGridConvertToEmailObjectTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Reflection;\n\nnamespace Mailozaurr.Tests;\n\npublic class SendGridConvertToEmailObjectTests\n{\n    [Fact]\n    public void ConvertToEmailObject_String_ReturnsEmail()\n    {\n        var client = new SendGridClient();\n        MethodInfo? method = typeof(SendGridClient).GetMethod(\"ConvertToEmailObject\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var result = method?.Invoke(client, new object?[] { \"user@example.com\" }) as SendGridEmailAddress;\n        Assert.NotNull(result);\n        Assert.Equal(\"user@example.com\", result!.Email);\n        Assert.Null(result.Name);\n    }\n\n    [Fact]\n    public void ConvertToEmailObject_Dictionary_ReturnsEmail()\n    {\n        var client = new SendGridClient();\n        MethodInfo? method = typeof(SendGridClient).GetMethod(\"ConvertToEmailObject\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var input = new Dictionary<string, object>\n        {\n            [\"Email\"] = \"dict@example.com\",\n            [\"Name\"] = \"Dict\"\n        };\n        var result = method?.Invoke(client, new object?[] { input }) as SendGridEmailAddress;\n        Assert.NotNull(result);\n        Assert.Equal(\"dict@example.com\", result!.Email);\n        Assert.Equal(\"Dict\", result.Name);\n    }\n\n    [Fact]\n    public void ConvertToEmailObject_SendGridEmailAddress_ReturnsSameInstance()\n    {\n        var client = new SendGridClient();\n        MethodInfo? method = typeof(SendGridClient).GetMethod(\"ConvertToEmailObject\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var address = new SendGridEmailAddress { Email = \"sg@example.com\", Name = \"SG\" };\n        var result = method?.Invoke(client, new object?[] { address }) as SendGridEmailAddress;\n        Assert.Same(address, result);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendGridCreateMessageTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Reflection;\nusing Xunit;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr.Tests;\n\npublic class SendGridCreateMessageTests\n{\n    [Fact]\n    public void CreateMessage_WithValidData_BuildsJson()\n    {\n        var client = new SendGridClient\n        {\n            From = \"from@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = \"text\",\n            Html = \"<b>body</b>\",\n            Credentials = new NetworkCredential(\"apikey\", \"test\")\n        };\n        client.CreateMessage();\n        PropertyInfo? prop = typeof(SendGridClient).GetProperty(\"MessageJson\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var json = prop?.GetValue(client) as string;\n        Assert.NotNull(json);\n        Assert.Contains(\"subject\", json!, StringComparison.OrdinalIgnoreCase);\n        Assert.Contains(\"to@example.com\", json!, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void CreateMessage_InvalidAddress_ThrowsArgumentException()\n    {\n        var client = new SendGridClient\n        {\n            From = \"from@example.com\",\n            To = new List<object> { new Dictionary<string, object> { { \"Name\", \"Test\" } } },\n            Subject = \"subject\",\n            Text = \"text\",\n            Html = \"<b>body</b>\",\n            Credentials = new NetworkCredential(\"apikey\", \"test\")\n        };\n        Assert.Throws<ArgumentException>(() => client.CreateMessage());\n    }\n\n    [Fact]\n    public void CreateMessage_WithHeaders_IncludesHeaders()\n    {\n        var client = new SendGridClient\n        {\n            From = \"from@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = \"text\",\n            Credentials = new NetworkCredential(\"apikey\", \"test\"),\n            Headers = new Dictionary<string, string> { [\"X-Test\"] = \"123\" }\n        };\n        client.CreateMessage();\n        PropertyInfo? prop = typeof(SendGridClient).GetProperty(\"MessageJson\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var json = prop?.GetValue(client) as string;\n        Assert.NotNull(json);\n        Assert.Contains(\"X-Test\", json!, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void CreateMessage_DuplicateAttachments_IncludedOnce()\n    {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        var client = new SendGridClient\n        {\n            From = \"from@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = \"text\",\n            Credentials = new NetworkCredential(\"apikey\", \"test\"),\n            Attachments = new List<AttachmentDescriptor>\n            {\n                new FileAttachmentDescriptor(tmp),\n                new FileAttachmentDescriptor(tmp)\n            }\n        };\n        client.CreateMessage();\n        PropertyInfo? prop = typeof(SendGridClient).GetProperty(\"MessageJson\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var json = prop?.GetValue(client) as string;\n        File.Delete(tmp);\n        Assert.NotNull(json);\n        using var doc = System.Text.Json.JsonDocument.Parse(json!);\n        var count = doc.RootElement.GetProperty(\"Attachments\").GetArrayLength();\n        Assert.Equal(1, count);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"<b>body</b>\", \"text/html\")]\n    [InlineData(\"text\", \"\", \"text/plain\")]\n    public void CreateMessage_WithoutBody_OmitsCorrespondingContent(string text, string html, string expectedType)\n    {\n        var client = new SendGridClient\n        {\n            From = \"from@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = text,\n            Html = html,\n            Credentials = new NetworkCredential(\"apikey\", \"test\")\n        };\n        client.CreateMessage();\n        PropertyInfo? prop = typeof(SendGridClient).GetProperty(\"MessageJson\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var json = prop?.GetValue(client) as string;\n        Assert.NotNull(json);\n        using var doc = System.Text.Json.JsonDocument.Parse(json!);\n        var content = doc.RootElement.GetProperty(\"Content\");\n        Assert.Equal(1, content.GetArrayLength());\n        Assert.Equal(expectedType, content[0].GetProperty(\"Type\").GetString());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SendLogResolverTests.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SendLogResolverTests {\n    private sealed class InMemoryRepository : ISentMessageRepository {\n        private readonly SentMessageRecord record;\n        public InMemoryRepository(SentMessageRecord record) => this.record = record;\n        public Task SaveAsync(SentMessageRecord record, CancellationToken cancellationToken = default) => Task.CompletedTask;\n        public Task<SentMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default)\n            => Task.FromResult(record.MessageId == messageId ? record : null);\n    }\n\n    [Fact]\n    public async Task ResolveAsync_ReturnsMatchingRecord() {\n        var record = new SentMessageRecord { MessageId = \"id1\", Recipients = \"user@example.com\", Subject = \"s\", Timestamp = DateTimeOffset.UtcNow };\n        var repo = new InMemoryRepository(record);\n        var resolver = new SendLogResolver(repo);\n        var report = new NonDeliveryReport { OriginalMessageId = \"id1\" };\n        var result = await resolver.ResolveAsync(report);\n        Assert.Equal(record, result);\n    }\n\n    [Fact]\n    public async Task ResolveAsync_ReturnsNullWhenNotFound() {\n        var record = new SentMessageRecord { MessageId = \"id1\", Recipients = \"user@example.com\" };\n        var repo = new InMemoryRepository(record);\n        var resolver = new SendLogResolver(repo);\n        var report = new NonDeliveryReport { OriginalMessageId = \"other\" };\n        var result = await resolver.ResolveAsync(report);\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public async Task ResolveAsync_MatchesRecipientWithPrefix() {\n        var record = new SentMessageRecord { MessageId = \"id1\", Recipients = \"user@example.com\" };\n        var repo = new InMemoryRepository(record);\n        var resolver = new SendLogResolver(repo);\n        var headers = new Dictionary<string, string> {\n            [\"Original-Message-ID\"] = \"<id1>\",\n            [\"Final-Recipient\"] = \"rfc822; user@example.com\"\n        };\n        var report = NonDeliveryReport.FromHeaders(headers);\n        var result = await resolver.ResolveAsync(report);\n        Assert.Equal(record, result);\n    }\n\n    [Fact]\n    public async Task ResolveAsync_MatchesLegacyFormattedRecipientsWithDisplayNameComma() {\n        var record = new SentMessageRecord {\n            MessageId = \"id1\",\n            Recipients = \"\\\"Doe, Jane\\\" <jane@example.com>, John Smith <john@example.com>\"\n        };\n        var repo = new InMemoryRepository(record);\n        var resolver = new SendLogResolver(repo);\n        var report = new NonDeliveryReport {\n            OriginalMessageId = \"id1\",\n            FinalRecipientAddress = \"jane@example.com\"\n        };\n\n        var result = await resolver.ResolveAsync(report);\n\n        Assert.Equal(record, result);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessageRepositoryTests.cs",
    "content": "using Mailozaurr.NonDeliveryReports;\nusing System.Diagnostics;\nusing System.Text;\nusing System.Text.Json;\n\nnamespace Mailozaurr.Tests;\n\npublic class SentMessageRepositoryTests {\n    [Fact]\n    public async Task ResolverMatchesSavedRecord() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            var repo = new FileSentMessageRepository(path);\n            var record = new SentMessageRecord {\n                MessageId = \"id1\",\n                Recipients = \"c@d.com\",\n                Subject = \"subject\",\n                Timestamp = DateTimeOffset.UtcNow\n            };\n            await repo.SaveAsync(record);\n            var resolver = new SendLogResolver(repo);\n            var ndr = new NonDeliveryReport { OriginalMessageId = \"id1\", FinalRecipient = \"c@d.com\" };\n            var resolved = await resolver.ResolveAsync(ndr);\n            Assert.NotNull(resolved);\n            Assert.Equal(\"subject\", resolved!.Subject);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public async Task SaveAsync_AppendsMultipleRecords() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            var repo = new FileSentMessageRepository(path);\n            await repo.SaveAsync(new SentMessageRecord { MessageId = \"1\", Recipients = \"a@b.com\", Subject = \"s1\", Timestamp = DateTimeOffset.UtcNow });\n            await repo.SaveAsync(new SentMessageRecord { MessageId = \"2\", Recipients = \"c@d.com\", Subject = \"s2\", Timestamp = DateTimeOffset.UtcNow });\n            var record = await repo.GetByMessageIdAsync(\"2\");\n            Assert.NotNull(record);\n            Assert.Equal(\"s2\", record!.Subject);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public async Task GetByMessageIdAsync_FastLookupInLargeLog() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            const int count = 10000;\n            var newline = Encoding.UTF8.GetBytes(Environment.NewLine);\n            using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) {\n                for (int i = 0; i < count; i++) {\n                    var record = new SentMessageRecord {\n                        MessageId = i.ToString(),\n                        Recipients = \"a@b.com\",\n                        Subject = \"s\",\n                        Timestamp = DateTimeOffset.UtcNow\n                    };\n                    await JsonSerializer.SerializeAsync(stream, record);\n                    await stream.WriteAsync(newline, 0, newline.Length);\n                }\n            }\n            // Reinitialize repository to rebuild the index\n            var repo = new FileSentMessageRepository(path);\n            var targetId = (count - 1).ToString();\n            var seqWatch = Stopwatch.StartNew();\n            _ = await SequentialSearchAsync(path, targetId);\n            seqWatch.Stop();\n            var idxWatch = Stopwatch.StartNew();\n            var recordFound = await repo.GetByMessageIdAsync(targetId);\n            idxWatch.Stop();\n            Assert.NotNull(recordFound);\n            Assert.True(idxWatch.Elapsed < seqWatch.Elapsed);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public async Task Constructor_IgnoresMalformedLinesAndIndexesValidRecords() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            var newline = Encoding.UTF8.GetBytes(Environment.NewLine);\n            using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) {\n                await JsonSerializer.SerializeAsync(stream, new SentMessageRecord {\n                    MessageId = \"1\",\n                    Recipients = \"a@b.com\",\n                    Subject = \"ok-1\",\n                    Timestamp = DateTimeOffset.UtcNow\n                });\n                await stream.WriteAsync(newline, 0, newline.Length);\n                await stream.WriteAsync(Encoding.UTF8.GetBytes(\"{not-json\"), 0, Encoding.UTF8.GetByteCount(\"{not-json\"));\n                await stream.WriteAsync(newline, 0, newline.Length);\n                await JsonSerializer.SerializeAsync(stream, new SentMessageRecord {\n                    MessageId = \"2\",\n                    Recipients = \"c@d.com\",\n                    Subject = \"ok-2\",\n                    Timestamp = DateTimeOffset.UtcNow\n                });\n                await stream.WriteAsync(newline, 0, newline.Length);\n            }\n\n            var repo = new FileSentMessageRepository(path);\n            var record = await repo.GetByMessageIdAsync(\"2\");\n\n            Assert.NotNull(record);\n            Assert.Equal(\"ok-2\", record!.Subject);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public async Task GetByMessageIdAsync_ReturnsNullWhenIndexedLineBecomesMalformed() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            var repo = new FileSentMessageRepository(path);\n            await repo.SaveAsync(new SentMessageRecord {\n                MessageId = \"1\",\n                Recipients = \"a@b.com\",\n                Subject = \"subject\",\n                Timestamp = DateTimeOffset.UtcNow\n            });\n\n            File.WriteAllText(path, \"{broken\");\n\n            var record = await repo.GetByMessageIdAsync(\"1\");\n\n            Assert.Null(record);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    [Fact]\n    public async Task Constructor_RebuildsLfIndexedLogWithoutOffsetDrift() {\n        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + \".json\");\n        try {\n            var payload = string.Join(\"\\n\", new[] {\n                JsonSerializer.Serialize(new SentMessageRecord {\n                    MessageId = \"1\",\n                    Recipients = \"first@example.com\",\n                    Subject = \"first\",\n                    Timestamp = DateTimeOffset.UtcNow\n                }, MailozaurrJsonContext.Default.SentMessageRecord),\n                JsonSerializer.Serialize(new SentMessageRecord {\n                    MessageId = \"2\",\n                    Recipients = \"second@example.com\",\n                    Subject = \"second\",\n                    Timestamp = DateTimeOffset.UtcNow.AddMinutes(1)\n                }, MailozaurrJsonContext.Default.SentMessageRecord)\n            }) + \"\\n\";\n            File.WriteAllText(path, payload, Encoding.UTF8);\n\n            var repo = new FileSentMessageRepository(path);\n            var record = await repo.GetByMessageIdAsync(\"2\");\n\n            Assert.NotNull(record);\n            Assert.Equal(\"second\", record!.Subject);\n            Assert.Equal(\"second@example.com\", record.Recipients);\n        } finally {\n            if (File.Exists(path)) File.Delete(path);\n        }\n    }\n\n    private static async Task<SentMessageRecord?> SequentialSearchAsync(string path, string messageId) {\n        using var read = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        using var reader = new StreamReader(read);\n        while (!reader.EndOfStream) {\n            string? line = await reader.ReadLineAsync();\n            if (string.IsNullOrWhiteSpace(line)) {\n                continue;\n            }\n            var record = JsonSerializer.Deserialize<SentMessageRecord>(line);\n            if (record != null && string.Equals(record.MessageId, messageId, StringComparison.OrdinalIgnoreCase)) {\n                return record;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/GmailPendingMessageSenderTests.cs",
    "content": "using System.Globalization;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Runtime.CompilerServices;\nusing System.IO;\nusing MimeKit;\nusing Org.BouncyCastle.Crypto;\nusing Org.BouncyCastle.Crypto.Generators;\nusing Org.BouncyCastle.Pkcs;\nusing Org.BouncyCastle.Security;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class GmailPendingMessageSenderTests {\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler;\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            this.handler = handler ?? throw new ArgumentNullException(nameof(handler));\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            handler(request, cancellationToken);\n    }\n\n    [Fact]\n    public async Task SendAsync_UsesAccessTokenAndUserId() {\n        Uri? requestUri = null;\n        string? authorization = null;\n        var handler = new TestHandler((request, _) => {\n            requestUri = request.RequestUri;\n            authorization = request.Headers.Authorization?.ToString();\n            var response = new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"id\\\":\\\"msg\\\",\\\"threadId\\\":\\\"msg\\\"}\", Encoding.UTF8, \"application/json\")\n            };\n            return Task.FromResult(response);\n        });\n        using var httpClient = new HttpClient(handler) {\n            BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n        };\n\n        var sender = new GmailPendingMessageSender((credential, refresher) => new GmailApiClient(httpClient, refresher, credential));\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n\n        var record = new PendingMessageRecord {\n            MimeMessage = Convert.ToBase64String(ms.ToArray())\n        };\n        record.ProviderData[GmailPendingMessageSender.UserIdKey] = \"me\";\n        record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey] = CredentialProtection.Default.Protect(\"token\");\n        record.ProviderData[GmailPendingMessageSender.UserNameKey] = \"me@example.com\";\n        record.ProviderData[GmailPendingMessageSender.ExpiresOnKey] = DateTimeOffset.UtcNow.AddHours(1).ToString(\"o\", CultureInfo.InvariantCulture);\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(\"Bearer token\", authorization);\n        Assert.Equal(new Uri(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/send\"), requestUri);\n    }\n\n    [Fact]\n    public async Task SendAsync_RefreshesExpiredTokenAndSendsMessage() {\n        var refreshCallCount = 0;\n        var refreshHandler = new TestHandler(async (request, cancellationToken) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Equal(new Uri(\"https://oauth2.googleapis.com/token\"), request.RequestUri);\n#if NET5_0_OR_GREATER\n            var payload = await request.Content!.ReadAsStringAsync(cancellationToken);\n#else\n            var payload = await request.Content!.ReadAsStringAsync();\n#endif\n            Assert.Contains(\"client_id=client-123\", payload);\n            Assert.Contains(\"client_secret=client-secret\", payload);\n            Assert.Contains(\"refresh_token=refresh-token\", payload);\n            Interlocked.Increment(ref refreshCallCount);\n            return new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\n                    \"{\\\"access_token\\\":\\\"new-access-token\\\",\\\"expires_in\\\":3600,\\\"refresh_token\\\":\\\"updated-refresh\\\"}\",\n                    Encoding.UTF8,\n                    \"application/json\")\n            };\n        });\n\n        var refreshClient = new HttpClient(refreshHandler);\n        Helpers.SharedHttpClient = refreshClient;\n\n        try {\n            string? authorization = null;\n            var sendCallCount = 0;\n            var gmailHandler = new TestHandler((request, _) => {\n                authorization = request.Headers.Authorization?.ToString();\n                Interlocked.Increment(ref sendCallCount);\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                    Content = new StringContent(\"{\\\"id\\\":\\\"msg\\\",\\\"threadId\\\":\\\"msg\\\"}\", Encoding.UTF8, \"application/json\")\n                });\n            });\n            using var gmailClient = new HttpClient(gmailHandler) {\n                BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n            };\n            var sender = new GmailPendingMessageSender((credential, refresher) => new GmailApiClient(gmailClient, refresher, credential));\n\n            var message = new MimeMessage();\n            message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n            message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n            message.Subject = \"queued\";\n            message.Body = new TextPart(\"plain\") { Text = \"body\" };\n            using var ms = new MemoryStream();\n            await message.WriteToAsync(ms);\n\n            var record = new PendingMessageRecord {\n                MimeMessage = Convert.ToBase64String(ms.ToArray())\n            };\n            record.ProviderData[GmailPendingMessageSender.UserIdKey] = \"me\";\n            record.ProviderData[GmailPendingMessageSender.UserNameKey] = \"user@example.com\";\n            record.ProviderData[GmailPendingMessageSender.ExpiresOnKey] = DateTimeOffset.UtcNow.AddMinutes(-5).ToString(\"o\", CultureInfo.InvariantCulture);\n            record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey] = CredentialProtection.Default.Protect(\"expired-token\");\n            record.ProviderData[GmailPendingMessageSender.RefreshTokenProtectedKey] = CredentialProtection.Default.Protect(\"refresh-token\");\n            record.ProviderData[GmailPendingMessageSender.ClientIdKey] = \"client-123\";\n            record.ProviderData[GmailPendingMessageSender.ClientSecretProtectedKey] = CredentialProtection.Default.Protect(\"client-secret\");\n\n            await sender.SendAsync(record, CancellationToken.None);\n\n            Assert.Equal(1, refreshCallCount);\n            Assert.Equal(1, sendCallCount);\n            Assert.Equal(\"Bearer new-access-token\", authorization);\n\n            var storedAccess = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey]);\n            Assert.Equal(\"new-access-token\", storedAccess);\n            var storedRefresh = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.RefreshTokenProtectedKey]);\n            Assert.Equal(\"updated-refresh\", storedRefresh);\n            var storedExpiry = DateTimeOffset.Parse(record.ProviderData[GmailPendingMessageSender.ExpiresOnKey], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);\n            Assert.True(storedExpiry > DateTimeOffset.UtcNow);\n        } finally {\n            Helpers.SharedHttpClient = new HttpClient();\n        }\n    }\n\n    [Fact]\n    public async Task ProcessAsync_MintsServiceAccountTokenAndSendsMessage() {\n        var mintedToken = \"service-access-token\";\n        var tokenRequestCount = 0;\n        var tokenHandler = new TestHandler(async (request, cancellationToken) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Equal(new Uri(\"https://oauth2.googleapis.com/token\"), request.RequestUri);\n#if NET5_0_OR_GREATER\n            var payload = await request.Content!.ReadAsStringAsync(cancellationToken);\n#else\n            var payload = await request.Content!.ReadAsStringAsync();\n#endif\n            Assert.Contains(\"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer\", payload);\n            Assert.Contains(\"assertion=\", payload);\n            Interlocked.Increment(ref tokenRequestCount);\n            var response = new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"access_token\\\":\\\"service-access-token\\\",\\\"expires_in\\\":3600}\", Encoding.UTF8, \"application/json\")\n            };\n            return response;\n        });\n\n        string? authorization = null;\n        var gmailCallCount = 0;\n        var gmailHandler = new TestHandler((request, _) => {\n            authorization = request.Headers.Authorization?.ToString();\n            Interlocked.Increment(ref gmailCallCount);\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"id\\\":\\\"msg\\\",\\\"threadId\\\":\\\"msg\\\"}\", Encoding.UTF8, \"application/json\")\n            });\n        });\n\n        using var gmailClient = new HttpClient(gmailHandler) { BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\") };\n        var sender = new GmailPendingMessageSender((credential, refresher) => new GmailApiClient(gmailClient, refresher, credential));\n\n        var serviceAccountJson = CreateServiceAccountJson();\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n\n        var now = DateTimeOffset.UtcNow;\n        var record = new PendingMessageRecord {\n            MessageId = Guid.NewGuid().ToString(\"N\"),\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            Provider = EmailProvider.Gmail,\n            Timestamp = now.AddMinutes(-10),\n            NextAttemptAt = now.AddMinutes(-1)\n        };\n        var subject = \"impersonated@example.com\";\n        record.ProviderData[GmailPendingMessageSender.UserIdKey] = subject;\n        record.ProviderData[GmailPendingMessageSender.UserNameKey] = subject;\n        record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey] = CredentialProtection.Default.Protect(\"expired-token\");\n        record.ProviderData[GmailPendingMessageSender.ExpiresOnKey] = now.AddMinutes(-5).ToString(\"o\", CultureInfo.InvariantCulture);\n        record.ProviderData[GmailPendingMessageSender.ServiceAccountJsonProtectedKey] = CredentialProtection.Default.Protect(serviceAccountJson);\n        record.ProviderData[GmailPendingMessageSender.ServiceAccountSubjectKey] = subject;\n\n        var repository = new SingleRecordRepository(record);\n        var observer = new ProcessorObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Gmail, sender }\n        });\n\n        Helpers.SharedHttpClient = new HttpClient(tokenHandler);\n\n        try {\n            var processor = new PendingMessageProcessor(\n                repository,\n                factory,\n                retryDelaySelector: _ => TimeSpan.FromMinutes(1),\n                clock: () => now,\n                observer: observer,\n                processingLeaseDuration: TimeSpan.Zero);\n\n            await processor.ProcessAsync();\n        } finally {\n            Helpers.SharedHttpClient = new HttpClient();\n        }\n\n        Assert.Equal(1, tokenRequestCount);\n        Assert.Equal(1, gmailCallCount);\n        Assert.Equal(\"Bearer service-access-token\", authorization);\n        Assert.True(repository.IsEmpty);\n        Assert.Equal(1, observer.SentCount);\n        Assert.Equal(0, observer.FailedCount);\n        Assert.Equal(0, observer.DroppedCount);\n\n        var storedAccess = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey]);\n        Assert.Equal(mintedToken, storedAccess);\n        var storedExpiry = DateTimeOffset.Parse(record.ProviderData[GmailPendingMessageSender.ExpiresOnKey], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);\n        Assert.True(storedExpiry > now);\n    }\n\n    private static string CreateServiceAccountJson() {\n        var generator = new RsaKeyPairGenerator();\n        generator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));\n        AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair();\n        var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private);\n        var builder = new StringBuilder();\n        builder.AppendLine(\"-----BEGIN PRIVATE KEY-----\");\n        builder.AppendLine(Convert.ToBase64String(privateKeyInfo.GetEncoded(), Base64FormattingOptions.InsertLineBreaks));\n        builder.AppendLine(\"-----END PRIVATE KEY-----\");\n\n        var payload = new Dictionary<string, object> {\n            { \"type\", \"service_account\" },\n            { \"project_id\", \"test-project\" },\n            { \"private_key_id\", \"test-key\" },\n            { \"private_key\", builder.ToString() },\n            { \"client_email\", \"mailer@test-project.iam.gserviceaccount.com\" },\n            { \"client_id\", \"1234567890\" },\n            { \"token_uri\", \"https://oauth2.googleapis.com/token\" }\n        };\n\n        return JsonSerializer.Serialize(payload);\n    }\n\n    private sealed class SingleRecordRepository : IPendingMessageRepository {\n        private PendingMessageRecord? record;\n\n        public SingleRecordRepository(PendingMessageRecord record) {\n            this.record = record ?? throw new ArgumentNullException(nameof(record));\n        }\n\n        public bool IsEmpty => record == null;\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            this.record = record;\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var current = record;\n            if (current == null || !string.Equals(current.MessageId, messageId, StringComparison.Ordinal) || current.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            current.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(current);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult(record);\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            var current = record;\n            if (current != null) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return current;\n            }\n            await Task.CompletedTask;\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            var current = record;\n            if (current != null && string.Equals(current.MessageId, messageId, StringComparison.Ordinal)) {\n                record = null;\n            }\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class ProcessorObserver : IPendingMessageProcessorObserver {\n        public int SentCount { get; private set; }\n        public int FailedCount { get; private set; }\n        public int DroppedCount { get; private set; }\n\n        public void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason) {\n        }\n\n        public void MessageAttemptStarted(PendingMessageRecord record, int attempt) {\n        }\n\n        public void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration) {\n            SentCount++;\n        }\n\n        public void MessageFailed(\n            PendingMessageRecord record,\n            int attempt,\n            Exception exception,\n            TimeSpan duration,\n            bool willRetry,\n            TimeSpan? retryDelay) {\n            FailedCount++;\n        }\n\n        public void MessageDropped(PendingMessageRecord record, int attempt, PendingMessageDropReason reason, Exception? exception) {\n            DroppedCount++;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/GraphPendingMessageSenderTests.cs",
    "content": "using System.Globalization;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class GraphPendingMessageSenderTests {\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handler;\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            _handler = handler ?? throw new ArgumentNullException(nameof(handler));\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            _handler(request, cancellationToken);\n    }\n\n    [Fact]\n    public async Task SendAsync_UsesStoredAccessTokenAndUserId() {\n        var requests = new List<HttpRequestMessage>();\n        using var client = new HttpClient(new TestHandler((request, cancellationToken) => {\n            requests.Add(request);\n\n            if (request.RequestUri!.AbsoluteUri.EndsWith(\"/messages\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created) {\n                    Content = new StringContent(\"{\\\"id\\\":\\\"draft-1\\\"}\", Encoding.UTF8, \"application/json\")\n                });\n            }\n\n            if (request.RequestUri.AbsoluteUri.EndsWith(\"/send\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted) {\n                    Content = new StringContent(string.Empty, Encoding.UTF8, \"text/plain\")\n                });\n            }\n\n            throw new InvalidOperationException(\"Unexpected Graph request: \" + request.RequestUri);\n        })) {\n            BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\")\n        };\n\n        var sender = new GraphPendingMessageSender(credential => new GraphApiClient(client, credential: credential));\n        var record = await CreateRecordAsync();\n        record.ProviderData[GraphPendingMessageSender.UserIdKey] = \"shared@example.com\";\n        record.ProviderData[GraphPendingMessageSender.UserNameKey] = \"shared@example.com\";\n        record.ProviderData[GraphPendingMessageSender.AccessTokenProtectedKey] = CredentialProtection.Default.Protect(\"graph-token\");\n        record.ProviderData[GraphPendingMessageSender.ExpiresOnKey] = DateTimeOffset.UtcNow.AddHours(1).ToString(\"o\", CultureInfo.InvariantCulture);\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(2, requests.Count);\n        Assert.Equal(\"Bearer graph-token\", requests[0].Headers.Authorization?.ToString());\n        Assert.Contains(\"/users/shared%40example.com/messages\", requests[0].RequestUri!.AbsoluteUri, StringComparison.Ordinal);\n        Assert.Contains(\"/users/shared%40example.com/messages/draft-1/send\", requests[1].RequestUri!.AbsoluteUri, StringComparison.Ordinal);\n    }\n\n    [Fact]\n    public async Task SendAsync_ReacquiresTokenWhenCredentialMetadataIsPresent() {\n        var requests = new List<HttpRequestMessage>();\n        using var client = new HttpClient(new TestHandler((request, cancellationToken) => {\n            requests.Add(request);\n\n            if (request.RequestUri!.AbsoluteUri.EndsWith(\"/messages\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created) {\n                    Content = new StringContent(\"{\\\"id\\\":\\\"draft-2\\\"}\", Encoding.UTF8, \"application/json\")\n                });\n            }\n\n            if (request.RequestUri.AbsoluteUri.EndsWith(\"/send\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted) {\n                    Content = new StringContent(string.Empty, Encoding.UTF8, \"text/plain\")\n                });\n            }\n\n            throw new InvalidOperationException(\"Unexpected Graph request: \" + request.RequestUri);\n        })) {\n            BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\")\n        };\n\n        GraphCredential? acquired = null;\n        var sender = new GraphPendingMessageSender(\n            credential => new GraphApiClient(client, credential: credential),\n            (graphCredential, cancellationToken) => {\n                acquired = graphCredential;\n                return Task.FromResult(\"fresh-token\");\n            });\n\n        var record = await CreateRecordAsync();\n        record.ProviderData[GraphPendingMessageSender.UserIdKey] = \"shared@example.com\";\n        record.ProviderData[GraphPendingMessageSender.UserNameKey] = \"shared@example.com\";\n        record.ProviderData[GraphPendingMessageSender.AccessTokenProtectedKey] = CredentialProtection.Default.Protect(\"expired-token\");\n        record.ProviderData[GraphPendingMessageSender.ExpiresOnKey] = DateTimeOffset.UtcNow.AddMinutes(-5).ToString(\"o\", CultureInfo.InvariantCulture);\n        record.ProviderData[GraphPendingMessageSender.ClientIdKey] = \"client-id\";\n        record.ProviderData[GraphPendingMessageSender.TenantIdKey] = \"tenant-id\";\n        record.ProviderData[GraphPendingMessageSender.ClientSecretProtectedKey] = CredentialProtection.Default.Protect(\"client-secret\");\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.NotNull(acquired);\n        Assert.Equal(\"client-id\", acquired!.ClientId);\n        Assert.Equal(\"tenant-id\", acquired.DirectoryId);\n        Assert.Equal(\"client-secret\", acquired.ClientSecret);\n        Assert.Equal(\"Bearer fresh-token\", requests[0].Headers.Authorization?.ToString());\n        var storedToken = CredentialProtection.UnprotectWithFallback(record.ProviderData[GraphPendingMessageSender.AccessTokenProtectedKey]);\n        Assert.Equal(\"fresh-token\", storedToken);\n    }\n\n    private static async Task<PendingMessageRecord> CreateRecordAsync() {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued-graph\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n\n        using var stream = new MemoryStream();\n        await message.WriteToAsync(stream);\n\n        return new PendingMessageRecord {\n            Provider = EmailProvider.Graph,\n            MessageId = Guid.NewGuid().ToString(\"N\"),\n            Timestamp = DateTimeOffset.UtcNow.AddMinutes(-10),\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(-1),\n            MimeMessage = Convert.ToBase64String(stream.ToArray())\n        };\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/MailgunPendingMessageSenderTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class MailgunPendingMessageSenderTests {\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler;\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            if (handler == null) {\n                throw new ArgumentNullException(nameof(handler));\n            }\n            this.handler = handler;\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            handler(request, cancellationToken);\n    }\n\n    [Fact]\n    public async Task SendAsync_PostsMimeMessageWithBasicAuth() {\n        Uri? requestUri = null;\n        string? authorization = null;\n        string? payload = null;\n        var handler = new TestHandler(async (request, _) => {\n            requestUri = request.RequestUri;\n            authorization = request.Headers.Authorization?.ToString();\n            payload = await request.Content!.ReadAsStringAsync();\n            return new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(string.Empty)\n            };\n        });\n        using var httpClient = new HttpClient(handler);\n        var sender = new MailgunPendingMessageSender(httpClient);\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"mailgun-message\";\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n\n        var record = new PendingMessageRecord {\n            MessageId = message.MessageId ?? string.Empty,\n            MimeMessage = Convert.ToBase64String(ms.ToArray())\n        };\n        record.ProviderData[MailgunPendingMessageSender.DomainKey] = \"example.com\";\n        record.ProviderData[MailgunPendingMessageSender.ApiKeyKey] = \"key\";\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(new Uri(\"https://api.mailgun.net/v3/example.com/messages.mime\"), requestUri);\n        var expectedAuth = \"Basic \" + Convert.ToBase64String(Encoding.ASCII.GetBytes(\"api:key\"));\n        Assert.Equal(expectedAuth, authorization);\n        Assert.Contains(\"mailgun-message\", payload);\n        Assert.Contains(\"body\", payload);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/PendingMessageProcessorFileRepositoryTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class PendingMessageProcessorFileRepositoryTests {\n    [Fact]\n    public async Task ProcessAsync_CompletesAgainstFileRepository() {\n        var directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions {\n            DirectoryPath = directory,\n            FileNamingScheme = () => \"pending.log\"\n        };\n        Directory.CreateDirectory(directory);\n        var repository = new FilePendingMessageRepository(options);\n\n        try {\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = DateTimeOffset.UtcNow,\n                NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(-5),\n                Provider = EmailProvider.None,\n                MimeMessage = Convert.ToBase64String(Encoding.UTF8.GetBytes(\"payload\"))\n            };\n            await repository.SaveAsync(record);\n\n            var sender = new RecordingPendingMessageSender();\n            var pair = new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.None, sender);\n            var factory = new PendingMessageSenderFactory(new[] { pair });\n            var processor = new PendingMessageProcessor(repository, factory, clock: () => DateTimeOffset.UtcNow);\n\n            await processor.ProcessAsync();\n\n            Assert.Single(sender.SentRecords);\n            Assert.Equal(record.MessageId, sender.SentRecords[0].MessageId);\n\n            var remaining = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.Null(remaining);\n        } finally {\n            if (File.Exists(Path.Combine(directory, \"pending.log\"))) {\n                File.Delete(Path.Combine(directory, \"pending.log\"));\n            }\n            if (Directory.Exists(directory)) {\n                Directory.Delete(directory, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task ProcessAsync_RetriesRespectDelaysAndRetryLimit() {\n        var directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var options = new PendingMessageRepositoryOptions {\n            DirectoryPath = directory,\n            FileNamingScheme = () => \"pending.log\"\n        };\n        Directory.CreateDirectory(directory);\n        var repository = new FilePendingMessageRepository(options);\n        var filePath = Path.Combine(directory, \"pending.log\");\n\n        try {\n            var baseTime = DateTimeOffset.Parse(\"2024-06-01T10:00:00Z\");\n            var currentTime = baseTime;\n            var retryDelay = TimeSpan.FromMinutes(10);\n            var leaseDuration = TimeSpan.FromMinutes(1);\n            const int maxAttempts = 3;\n\n            var record = new PendingMessageRecord {\n                MessageId = Guid.NewGuid().ToString(\"N\"),\n                Timestamp = baseTime,\n                NextAttemptAt = baseTime,\n                Provider = EmailProvider.None\n            };\n            await repository.SaveAsync(record);\n\n            var sender = new FailingPendingMessageSender();\n            var pair = new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.None, sender);\n            var factory = new PendingMessageSenderFactory(new[] { pair });\n            var processor = new PendingMessageProcessor(\n                repository,\n                factory,\n                retryDelaySelector: _ => retryDelay,\n                clock: () => currentTime,\n                maxRetryAttempts: maxAttempts,\n                processingLeaseDuration: leaseDuration);\n\n            await processor.ProcessAsync();\n\n            Assert.Equal(1, sender.Attempts);\n            var pending = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(pending);\n            Assert.Equal(1, pending!.AttemptCount);\n            Assert.Equal(baseTime + retryDelay, pending.NextAttemptAt);\n            var entriesAfterFirstAttempt = ReadLogEntries(filePath);\n            Assert.NotEmpty(entriesAfterFirstAttempt);\n            var lastUpsertAfterFirstAttempt = entriesAfterFirstAttempt.Last(e => string.Equals(e.EntryType, \"upsert\", StringComparison.OrdinalIgnoreCase));\n            Assert.NotNull(lastUpsertAfterFirstAttempt.Record);\n            Assert.Equal(1, lastUpsertAfterFirstAttempt.Record!.AttemptCount);\n            Assert.Equal(baseTime + retryDelay, lastUpsertAfterFirstAttempt.Record.NextAttemptAt);\n            var entryCountAfterFirstAttempt = entriesAfterFirstAttempt.Count;\n\n            currentTime = baseTime.AddMinutes(5);\n            await processor.ProcessAsync();\n            pending = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(pending);\n            Assert.Equal(1, pending!.AttemptCount);\n            Assert.Equal(baseTime + retryDelay, pending.NextAttemptAt);\n            Assert.Equal(1, sender.Attempts);\n            Assert.Equal(entryCountAfterFirstAttempt, ReadLogEntries(filePath).Count);\n\n            currentTime = baseTime + retryDelay + TimeSpan.FromMinutes(1);\n            await processor.ProcessAsync();\n            pending = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(pending);\n            Assert.Equal(2, pending!.AttemptCount);\n            var expectedNextAttempt = currentTime + retryDelay;\n            Assert.Equal(expectedNextAttempt, pending.NextAttemptAt);\n            Assert.Equal(2, sender.Attempts);\n            var entriesAfterSecondAttempt = ReadLogEntries(filePath);\n            var lastUpsertAfterSecondAttempt = entriesAfterSecondAttempt.Last(e => string.Equals(e.EntryType, \"upsert\", StringComparison.OrdinalIgnoreCase));\n            Assert.NotNull(lastUpsertAfterSecondAttempt.Record);\n            Assert.Equal(2, lastUpsertAfterSecondAttempt.Record!.AttemptCount);\n            Assert.Equal(expectedNextAttempt, lastUpsertAfterSecondAttempt.Record.NextAttemptAt);\n            var entryCountAfterSecondAttempt = entriesAfterSecondAttempt.Count;\n\n            currentTime = expectedNextAttempt.AddMinutes(-2);\n            await processor.ProcessAsync();\n            pending = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.NotNull(pending);\n            Assert.Equal(2, pending!.AttemptCount);\n            Assert.Equal(expectedNextAttempt, pending.NextAttemptAt);\n            Assert.Equal(2, sender.Attempts);\n            Assert.Equal(entryCountAfterSecondAttempt, ReadLogEntries(filePath).Count);\n\n            currentTime = expectedNextAttempt.AddMinutes(1);\n            await processor.ProcessAsync();\n            pending = await repository.GetByMessageIdAsync(record.MessageId);\n            Assert.Null(pending);\n            Assert.Equal(maxAttempts, sender.Attempts);\n            var finalEntries = ReadLogEntries(filePath);\n            Assert.NotEmpty(finalEntries);\n            var lastEntry = finalEntries[finalEntries.Count - 1];\n            Assert.Equal(\"tombstone\", lastEntry.EntryType, StringComparer.OrdinalIgnoreCase);\n            Assert.Equal(record.MessageId, lastEntry.MessageId);\n        } finally {\n            if (File.Exists(filePath)) {\n                File.Delete(filePath);\n            }\n            if (Directory.Exists(directory)) {\n                Directory.Delete(directory, true);\n            }\n        }\n    }\n\n    private static List<PendingLogEntry> ReadLogEntries(string path) {\n        var entries = new List<PendingLogEntry>();\n\n        if (!File.Exists(path)) {\n            return entries;\n        }\n\n        var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };\n\n        foreach (var line in File.ReadAllLines(path)) {\n            if (string.IsNullOrWhiteSpace(line)) {\n                continue;\n            }\n\n            var entry = JsonSerializer.Deserialize<PendingLogEntry>(line, options);\n            if (entry != null) {\n                entries.Add(entry);\n            }\n        }\n\n        return entries;\n    }\n\n    private sealed class FailingPendingMessageSender : IPendingMessageSender {\n        public int Attempts { get; private set; }\n\n        public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n            Attempts++;\n            throw new HttpRequestException(\"Simulated failure\");\n        }\n    }\n\n    private sealed class RecordingPendingMessageSender : IPendingMessageSender {\n        public List<PendingMessageRecord> SentRecords { get; } = new();\n\n        public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n            SentRecords.Add(record);\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class PendingLogEntry {\n        public string? EntryType { get; set; }\n\n        public string? MessageId { get; set; }\n\n        public PendingMessageRecord? Record { get; set; }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/PendingMessageProcessorTests.cs",
    "content": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class PendingMessageProcessorTests {\n    private sealed class InMemoryPendingMessageRepository : IPendingMessageRepository {\n        private readonly ConcurrentDictionary<string, PendingMessageRecord> records = new(StringComparer.OrdinalIgnoreCase);\n        private readonly object syncRoot = new();\n\n        public void Add(PendingMessageRecord record) {\n            records[record.MessageId] = record;\n        }\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            records[record.MessageId] = record;\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            lock (syncRoot) {\n                if (!records.TryGetValue(messageId, out var record) || record.NextAttemptAt > dueBeforeOrAt) {\n                    return Task.FromResult<PendingMessageRecord?>(null);\n                }\n\n                record.NextAttemptAt = leaseUntil;\n                return Task.FromResult<PendingMessageRecord?>(record);\n            }\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            records.TryGetValue(messageId, out var record);\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync(\n            [EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            foreach (var record in records.Values) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return record;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            records.TryRemove(messageId, out _);\n            return Task.CompletedTask;\n        }\n\n        public bool Contains(string messageId) => records.ContainsKey(messageId);\n    }\n\n    private sealed class RecordingPendingMessageSender : IPendingMessageSender {\n        public List<PendingMessageRecord> SentRecords { get; } = new();\n        public bool ShouldThrow { get; set; }\n        public Exception ExceptionToThrow { get; set; } = new InvalidOperationException(\"Send failure\");\n\n        public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n            SentRecords.Add(record);\n            if (ShouldThrow) {\n                throw ExceptionToThrow;\n            }\n\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class CancellingPendingMessageSender : IPendingMessageSender {\n        private readonly CancellationTokenSource cts;\n\n        public CancellingPendingMessageSender(CancellationTokenSource cts) {\n            this.cts = cts;\n        }\n\n        public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n            cts.Cancel();\n            ct.ThrowIfCancellationRequested();\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class RecordingObserver : IPendingMessageProcessorObserver {\n        public List<(PendingMessageRecord Record, PendingMessageSkipReason Reason)> Skipped { get; } = new();\n        public List<(PendingMessageRecord Record, int Attempt)> Started { get; } = new();\n        public List<(PendingMessageRecord Record, int Attempt, TimeSpan Duration)> Sent { get; } = new();\n        public List<MessageFailureEvent> Failed { get; } = new();\n        public List<MessageDropEvent> Dropped { get; } = new();\n\n        public void MessageSkipped(PendingMessageRecord record, PendingMessageSkipReason reason) {\n            Skipped.Add((record, reason));\n        }\n\n        public void MessageAttemptStarted(PendingMessageRecord record, int attempt) {\n            Started.Add((record, attempt));\n        }\n\n        public void MessageSent(PendingMessageRecord record, int attempt, TimeSpan duration) {\n            Sent.Add((record, attempt, duration));\n        }\n\n        public void MessageFailed(\n            PendingMessageRecord record,\n            int attempt,\n            Exception exception,\n            TimeSpan duration,\n            bool willRetry,\n            TimeSpan? retryDelay) {\n            Failed.Add(new MessageFailureEvent(record, attempt, exception, duration, willRetry, retryDelay));\n        }\n\n        public void MessageDropped(\n            PendingMessageRecord record,\n            int attempt,\n            PendingMessageDropReason reason,\n            Exception? exception) {\n            Dropped.Add(new MessageDropEvent(record, attempt, reason, exception));\n        }\n\n        public sealed class MessageFailureEvent {\n            public MessageFailureEvent(\n                PendingMessageRecord record,\n                int attempt,\n                Exception exception,\n                TimeSpan duration,\n                bool willRetry,\n                TimeSpan? retryDelay) {\n                Record = record;\n                Attempt = attempt;\n                Exception = exception;\n                Duration = duration;\n                WillRetry = willRetry;\n                RetryDelay = retryDelay;\n            }\n\n            public PendingMessageRecord Record { get; }\n            public int Attempt { get; }\n            public Exception Exception { get; }\n            public TimeSpan Duration { get; }\n            public bool WillRetry { get; }\n            public TimeSpan? RetryDelay { get; }\n        }\n\n        public sealed class MessageDropEvent {\n            public MessageDropEvent(\n                PendingMessageRecord record,\n                int attempt,\n                PendingMessageDropReason reason,\n                Exception? exception) {\n                Record = record;\n                Attempt = attempt;\n                Reason = reason;\n                Exception = exception;\n            }\n\n            public PendingMessageRecord Record { get; }\n            public int Attempt { get; }\n            public PendingMessageDropReason Reason { get; }\n            public Exception? Exception { get; }\n        }\n    }\n\n    private static readonly EmailProvider[] ProvidersUnderTest = new[] {\n        EmailProvider.None,\n        EmailProvider.SendGrid,\n        EmailProvider.Mailgun,\n        EmailProvider.SES,\n        EmailProvider.Gmail,\n        EmailProvider.Graph\n    };\n\n    private static PendingMessageRecord CreateRecord(DateTimeOffset nextAttempt, string? messageId = null) => new() {\n        MessageId = messageId ?? Guid.NewGuid().ToString(\"N\"),\n        Timestamp = nextAttempt,\n        NextAttemptAt = nextAttempt,\n        Provider = EmailProvider.None\n    };\n\n    [Fact]\n    public async Task ProcessAsync_SendsDueMessagesAndRemovesThem() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-01T12:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-5));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender();\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var lease = TimeSpan.FromMinutes(2);\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: _ => TimeSpan.FromMinutes(1),\n            clock: () => currentTime,\n            observer: observer,\n            processingLeaseDuration: lease);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Single(sender.SentRecords);\n        Assert.Equal(1, sender.SentRecords[0].AttemptCount);\n        Assert.Equal(currentTime + lease, sender.SentRecords[0].NextAttemptAt);\n        Assert.Single(observer.Started);\n        Assert.Single(observer.Sent);\n        Assert.Empty(observer.Failed);\n        Assert.Equal(1, observer.Started[0].Attempt);\n        Assert.True(observer.Sent[0].Duration >= TimeSpan.Zero);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_OnFailureSchedulesNextAttemptAndPersistsRecord() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-02T08:30:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime);\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new Exception(\"Send failure\")\n        };\n        var delay = TimeSpan.FromMinutes(15);\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: _ => delay,\n            clock: () => currentTime,\n            observer: observer);\n\n        await processor.ProcessAsync();\n\n        var stored = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(stored);\n        Assert.True(repository.Contains(record.MessageId));\n        Assert.Equal(1, stored!.AttemptCount);\n        Assert.Equal(currentTime + delay, stored.NextAttemptAt);\n        var failure = Assert.Single(observer.Failed);\n        Assert.True(failure.WillRetry);\n        Assert.True(failure.RetryDelay.HasValue);\n        Assert.Equal(delay, failure.RetryDelay.Value);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_ReleasesLeaseWhenCancelled() {\n        var currentTime = DateTimeOffset.Parse(\"2024-07-01T09:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(record);\n        using var cts = new CancellationTokenSource();\n        var sender = new CancellingPendingMessageSender(cts);\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory, clock: () => currentTime);\n\n        await Assert.ThrowsAsync<OperationCanceledException>(() => processor.ProcessAsync(cts.Token));\n\n        var stored = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(stored);\n        Assert.Equal(0, stored!.AttemptCount);\n        Assert.Equal(currentTime, stored.NextAttemptAt);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_SkipsRecordsNotYetDue() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-03T09:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(10));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender();\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: _ => TimeSpan.FromMinutes(5),\n            clock: () => currentTime,\n            observer: observer);\n\n        await processor.ProcessAsync();\n\n        var stored = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(stored);\n        Assert.True(repository.Contains(record.MessageId));\n        Assert.Equal(0, stored!.AttemptCount);\n        Assert.Empty(sender.SentRecords);\n        Assert.Equal(record.NextAttemptAt, stored.NextAttemptAt);\n        Assert.Single(observer.Skipped);\n        Assert.Equal(PendingMessageSkipReason.NotDue, observer.Skipped[0].Reason);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_SkipsRecordsWithMissingMessageId() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-04T10:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var invalid = CreateRecord(currentTime.AddMinutes(-2), string.Empty);\n        var valid = CreateRecord(currentTime.AddMinutes(-2));\n        repository.Add(invalid);\n        repository.Add(valid);\n        var sender = new RecordingPendingMessageSender();\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory, clock: () => currentTime, observer: observer);\n\n        await processor.ProcessAsync();\n\n        Assert.True(repository.Contains(string.Empty));\n        Assert.False(repository.Contains(valid.MessageId));\n        Assert.Single(sender.SentRecords);\n        Assert.Equal(valid.MessageId, sender.SentRecords[0].MessageId);\n        Assert.Single(observer.Skipped);\n        Assert.Equal(PendingMessageSkipReason.MissingMessageId, observer.Skipped[0].Reason);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_RemovesRecordWhenPermanentFailureOccurs() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-05T09:30:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new InvalidOperationException(\"Permanent failure\")\n        };\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory, clock: () => currentTime, observer: observer);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Single(sender.SentRecords);\n        var failure = Assert.Single(observer.Failed);\n        Assert.False(failure.WillRetry);\n        Assert.Null(failure.RetryDelay);\n        var drop = Assert.Single(observer.Dropped);\n        Assert.Equal(PendingMessageDropReason.PermanentFailure, drop.Reason);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_StopsAfterCancellation() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-06T07:15:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var first = CreateRecord(currentTime.AddMinutes(-1));\n        var second = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(first);\n        repository.Add(second);\n        var sender = new RecordingPendingMessageSender();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory, clock: () => currentTime);\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAsync<OperationCanceledException>(() => processor.ProcessAsync(cts.Token));\n\n        Assert.Empty(sender.SentRecords);\n        Assert.True(repository.Contains(first.MessageId));\n        Assert.True(repository.Contains(second.MessageId));\n        Assert.Equal(0, first.AttemptCount);\n        Assert.Equal(0, second.AttemptCount);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_RemovesRecordsThatExceededRetryLimit() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-07T11:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        record.AttemptCount = 5;\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender();\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory, clock: () => currentTime, maxRetryAttempts: 5, observer: observer);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Empty(sender.SentRecords);\n        Assert.Equal(5, record.AttemptCount);\n        Assert.Single(observer.Dropped);\n        Assert.Equal(PendingMessageDropReason.RetryLimitReached, observer.Dropped[0].Reason);\n        Assert.Equal(5, observer.Dropped[0].Attempt);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_RemovesRecordAfterFinalFailedAttempt() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-08T14:45:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        record.AttemptCount = 4;\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new Exception(\"Transient failure\")\n        };\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: _ => TimeSpan.FromMinutes(5),\n            clock: () => currentTime,\n            maxRetryAttempts: 5,\n            observer: observer);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Single(sender.SentRecords);\n        Assert.Equal(5, record.AttemptCount);\n        var failure = Assert.Single(observer.Failed);\n        Assert.False(failure.WillRetry);\n        Assert.Null(failure.RetryDelay);\n        var drop = Assert.Single(observer.Dropped);\n        Assert.Equal(PendingMessageDropReason.RetryLimitReached, drop.Reason);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_UsesCustomPermanentFailureDetector() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-09T12:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new HttpRequestException(\"Unrecoverable remote error\")\n        };\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            clock: () => currentTime,\n            observer: observer,\n            permanentFailureDetector: ex => ex is HttpRequestException);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        var failure = Assert.Single(observer.Failed);\n        Assert.False(failure.WillRetry);\n        Assert.Null(failure.RetryDelay);\n        var drop = Assert.Single(observer.Dropped);\n        Assert.Equal(PendingMessageDropReason.PermanentFailure, drop.Reason);\n        Assert.IsType<HttpRequestException>(drop.Exception);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_NormalizesNegativeRetryDelayToZero() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-10T10:15:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new Exception(\"Transient\")\n        };\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: _ => TimeSpan.FromMinutes(-5),\n            clock: () => currentTime,\n            observer: observer,\n            maxRetryAttempts: 3);\n\n        await processor.ProcessAsync();\n\n        var stored = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(stored);\n        Assert.Equal(currentTime, stored!.NextAttemptAt);\n        var failure = Assert.Single(observer.Failed);\n        Assert.True(failure.RetryDelay.HasValue);\n        Assert.Equal(TimeSpan.Zero, failure.RetryDelay.Value);\n    }\n\n    public static IEnumerable<object[]> ProviderDispatchData() {\n        foreach (var provider in ProvidersUnderTest) {\n            yield return new object[] { provider };\n        }\n    }\n\n    [Theory]\n    [MemberData(nameof(ProviderDispatchData))]\n    public async Task ProcessAsync_DispatchesToRegisteredProviderSender(EmailProvider provider) {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-11T08:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        record.Provider = provider;\n        repository.Add(record);\n        var senders = new Dictionary<EmailProvider, RecordingPendingMessageSender>();\n        var entries = new List<KeyValuePair<EmailProvider, IPendingMessageSender>>();\n        foreach (var providerUnderTest in ProvidersUnderTest) {\n            var stub = new RecordingPendingMessageSender();\n            senders[providerUnderTest] = stub;\n            entries.Add(new KeyValuePair<EmailProvider, IPendingMessageSender>(providerUnderTest, stub));\n        }\n\n        var factory = new PendingMessageSenderFactory(entries);\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            clock: () => currentTime,\n            processingLeaseDuration: TimeSpan.Zero);\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Single(senders[provider].SentRecords);\n        Assert.Equal(provider, senders[provider].SentRecords[0].Provider);\n        foreach (var pair in senders) {\n            if (pair.Key == provider) {\n                continue;\n            }\n\n            Assert.Empty(pair.Value.SentRecords);\n        }\n    }\n\n    [Fact]\n    public async Task ProcessAsync_UsesSafeMinimumLeaseWhenConfiguredLeaseIsZero() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-11T08:00:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-1));\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            clock: () => currentTime,\n            processingLeaseDuration: TimeSpan.Zero);\n\n        await processor.ProcessAsync();\n\n        var sent = Assert.Single(sender.SentRecords);\n        Assert.Equal(currentTime + TimeSpan.FromSeconds(30), sent.NextAttemptAt);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_RetriesUsingDelaySelectorPerAttempt() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-12T07:30:00Z\");\n        var repository = new InMemoryPendingMessageRepository();\n        var record = CreateRecord(currentTime.AddMinutes(-2));\n        record.Provider = EmailProvider.Mailgun;\n        repository.Add(record);\n        var sender = new RecordingPendingMessageSender {\n            ShouldThrow = true,\n            ExceptionToThrow = new Exception(\"Transient provider failure\")\n        };\n        var attempts = new List<int>();\n        var delays = new Dictionary<int, TimeSpan> {\n            { 1, TimeSpan.FromMinutes(5) },\n            { 2, TimeSpan.FromMinutes(10) }\n        };\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Mailgun, sender }\n        });\n        var processor = new PendingMessageProcessor(\n            repository,\n            factory,\n            retryDelaySelector: attempt => {\n                attempts.Add(attempt);\n                if (delays.TryGetValue(attempt, out var delay)) {\n                    return delay;\n                }\n\n                return TimeSpan.FromMinutes(15);\n            },\n            clock: () => currentTime,\n            maxRetryAttempts: 4);\n\n        await processor.ProcessAsync();\n\n        var storedAfterFirstAttempt = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(storedAfterFirstAttempt);\n        Assert.Equal(1, storedAfterFirstAttempt!.AttemptCount);\n        Assert.Equal(currentTime + delays[1], storedAfterFirstAttempt.NextAttemptAt);\n        Assert.Equal(new[] { 1 }, attempts);\n\n        currentTime = storedAfterFirstAttempt.NextAttemptAt.AddMinutes(1);\n\n        await processor.ProcessAsync();\n\n        var storedAfterSecondAttempt = await repository.GetByMessageIdAsync(record.MessageId);\n        Assert.NotNull(storedAfterSecondAttempt);\n        Assert.Equal(2, storedAfterSecondAttempt!.AttemptCount);\n        Assert.Equal(currentTime + delays[2], storedAfterSecondAttempt.NextAttemptAt);\n        Assert.Equal(new[] { 1, 2 }, attempts);\n\n        currentTime = storedAfterSecondAttempt.NextAttemptAt.AddMinutes(1);\n        sender.ShouldThrow = false;\n\n        await processor.ProcessAsync();\n\n        Assert.False(repository.Contains(record.MessageId));\n        Assert.Equal(3, sender.SentRecords.Count);\n        var lastRecord = sender.SentRecords[sender.SentRecords.Count - 1];\n        Assert.Equal(3, lastRecord.AttemptCount);\n        Assert.Equal(EmailProvider.Mailgun, lastRecord.Provider);\n        Assert.Equal(new[] { 1, 2 }, attempts);\n    }\n\n    [Fact]\n    public async Task ProcessAsync_DoesNotDoubleSendWhenTwoProcessorsRace() {\n        var currentTime = DateTimeOffset.Parse(\"2024-06-13T09:00:00Z\");\n        var repository = new CoordinatedPendingMessageRepository(currentTime.AddMinutes(-1));\n        var sender = new BlockingPendingMessageSender();\n        var observer = new RecordingObserver();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.None, sender }\n        });\n        var first = new PendingMessageProcessor(repository, factory, clock: () => currentTime, observer: observer);\n        var second = new PendingMessageProcessor(repository, factory, clock: () => currentTime, observer: observer);\n\n        var firstTask = first.ProcessAsync();\n        var secondTask = second.ProcessAsync();\n\n        repository.ReleaseEnumerations();\n        await sender.WaitForFirstSendAsync();\n        sender.Release();\n        await Task.WhenAll(firstTask, secondTask);\n\n        Assert.Equal(1, sender.SendCount);\n        Assert.Contains(observer.Skipped, skipped => skipped.Reason == PendingMessageSkipReason.LeaseNotAcquired);\n    }\n\n    private sealed class CoordinatedPendingMessageRepository : IPendingMessageRepository {\n        private readonly object syncRoot = new();\n        private readonly PendingMessageRecord record;\n        private readonly TaskCompletionSource<bool> firstEnumeratorReached = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        private readonly TaskCompletionSource<bool> secondEnumeratorReached = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        private readonly TaskCompletionSource<bool> firstSnapshotCaptured = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        private readonly TaskCompletionSource<bool> secondSnapshotCaptured = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        private readonly TaskCompletionSource<bool> releaseEnumerators = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        internal CoordinatedPendingMessageRepository(DateTimeOffset nextAttemptAt) {\n            record = CreateRecord(nextAttemptAt, \"coordinated-message\");\n        }\n\n        public Task SaveAsync(PendingMessageRecord updatedRecord, CancellationToken cancellationToken = default) {\n            lock (syncRoot) {\n                record.AttemptCount = updatedRecord.AttemptCount;\n                record.NextAttemptAt = updatedRecord.NextAttemptAt;\n                record.Timestamp = updatedRecord.Timestamp;\n                record.MimeMessage = updatedRecord.MimeMessage;\n                record.Server = updatedRecord.Server;\n                record.Port = updatedRecord.Port;\n                record.UserName = updatedRecord.UserName;\n                record.Password = updatedRecord.Password;\n                record.Provider = updatedRecord.Provider;\n                record.ProviderData = new Dictionary<string, string>(updatedRecord.ProviderData);\n            }\n\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            lock (syncRoot) {\n                if (!string.Equals(record.MessageId, messageId, StringComparison.OrdinalIgnoreCase) || record.NextAttemptAt > dueBeforeOrAt) {\n                    return Task.FromResult<PendingMessageRecord?>(null);\n                }\n\n                record.NextAttemptAt = leaseUntil;\n                return Task.FromResult<PendingMessageRecord?>(record.Clone());\n            }\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            lock (syncRoot) {\n                return Task.FromResult<PendingMessageRecord?>(\n                    string.Equals(record.MessageId, messageId, StringComparison.OrdinalIgnoreCase) ? record.Clone() : null);\n            }\n        }\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            var isFirstEnumerator = !firstEnumeratorReached.Task.IsCompleted;\n            if (isFirstEnumerator) {\n                firstEnumeratorReached.TrySetResult(true);\n            } else {\n                secondEnumeratorReached.TrySetResult(true);\n            }\n\n            await Task.WhenAll(firstEnumeratorReached.Task, secondEnumeratorReached.Task, releaseEnumerators.Task).ConfigureAwait(false);\n            cancellationToken.ThrowIfCancellationRequested();\n            var snapshot = record.Clone();\n            if (isFirstEnumerator) {\n                firstSnapshotCaptured.TrySetResult(true);\n            } else {\n                secondSnapshotCaptured.TrySetResult(true);\n            }\n\n            await Task.WhenAll(firstSnapshotCaptured.Task, secondSnapshotCaptured.Task).ConfigureAwait(false);\n            yield return snapshot;\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) => Task.CompletedTask;\n\n        internal void ReleaseEnumerations() => releaseEnumerators.TrySetResult(true);\n    }\n\n    private sealed class BlockingPendingMessageSender : IPendingMessageSender {\n        private readonly TaskCompletionSource<bool> sendStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);\n        private readonly TaskCompletionSource<bool> release = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        public int SendCount { get; private set; }\n\n        public async Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n            SendCount++;\n            sendStarted.TrySetResult(true);\n            await release.Task.ConfigureAwait(false);\n            ct.ThrowIfCancellationRequested();\n        }\n\n        internal Task WaitForFirstSendAsync() => sendStarted.Task;\n\n        internal void Release() => release.TrySetResult(true);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/PendingMessageRecordSerializationTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Text;\nusing System.Text.Json;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class PendingMessageRecordSerializationTests {\n    public static IEnumerable<object[]> ProviderRecords() {\n        yield return new object[] {\n            EmailProvider.None,\n            new Dictionary<string, string> {\n                [\"Server\"] = \"smtp.example.com\",\n                [\"Port\"] = \"587\",\n                [\"Secure\"] = \"true\"\n            }\n        };\n        yield return new object[] {\n            EmailProvider.SendGrid,\n            new Dictionary<string, string> {\n                [\"ApiKeyId\"] = \"sendgrid-key\",\n                [\"TemplateId\"] = \"templ-001\"\n            }\n        };\n        yield return new object[] {\n            EmailProvider.Gmail,\n            new Dictionary<string, string> {\n                [\"RefreshToken\"] = \"refresh-token\",\n                [\"ClientId\"] = \"gmail-client\"\n            }\n        };\n        yield return new object[] {\n            EmailProvider.Graph,\n            new Dictionary<string, string> {\n                [\"TenantId\"] = \"tenant-id\",\n                [\"UserId\"] = \"shared@example.com\"\n            }\n        };\n        yield return new object[] {\n            EmailProvider.Mailgun,\n            new Dictionary<string, string> {\n                [\"Domain\"] = \"mg.example.com\",\n                [\"ApiBase\"] = \"https://api.mailgun.net\"\n            }\n        };\n        yield return new object[] {\n            EmailProvider.SES,\n            new Dictionary<string, string> {\n                [\"Region\"] = \"us-east-1\",\n                [\"ConfigurationSet\"] = \"config-set\"\n            }\n        };\n    }\n\n    [Theory]\n    [MemberData(nameof(ProviderRecords))]\n    public void SerializationRoundTripPreservesProviderData(EmailProvider provider, Dictionary<string, string> providerData) {\n        var record = new PendingMessageRecord {\n            MessageId = $\"message-{provider}\",\n            Timestamp = DateTimeOffset.Parse(\"2024-01-01T00:00:00Z\"),\n            NextAttemptAt = DateTimeOffset.Parse(\"2024-01-01T01:00:00Z\"),\n            AttemptCount = 3,\n            MimeMessage = Convert.ToBase64String(Encoding.UTF8.GetBytes($\"Message body for {provider}\")),\n            Provider = provider\n        };\n        foreach (var pair in providerData) {\n            record.ProviderData[pair.Key] = pair.Value;\n        }\n\n        var json = JsonSerializer.Serialize(record);\n        var restored = JsonSerializer.Deserialize<PendingMessageRecord>(json);\n\n        Assert.NotNull(restored);\n        Assert.Equal(provider, restored!.Provider);\n        Assert.Equal(record.ProviderData, restored.ProviderData);\n        Assert.Equal(record.MessageId, restored.MessageId);\n        Assert.Equal(record.AttemptCount, restored.AttemptCount);\n    }\n\n    [Fact]\n    public void DeserializeLegacyRecordWithoutProviderInformationInitializesDefaults() {\n        const string legacyJson = \"{\\\"MessageId\\\":\\\"legacy\\\",\\\"Timestamp\\\":\\\"2024-01-10T00:00:00+00:00\\\",\\\"NextAttemptAt\\\":\\\"2024-01-10T01:00:00+00:00\\\",\\\"MimeMessage\\\":\\\"bWVzc2FnZQ==\\\",\\\"Server\\\":\\\"smtp.old.example.com\\\",\\\"Port\\\":25,\\\"UserName\\\":\\\"legacy-user\\\",\\\"Password\\\":null}\";\n\n        var record = JsonSerializer.Deserialize<PendingMessageRecord>(legacyJson);\n\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.None, record!.Provider);\n        Assert.Empty(record.ProviderData);\n        Assert.Equal(0, record.AttemptCount);\n    }\n\n    [Fact]\n    public void DeserializeNullProviderDataInitializesDictionary() {\n        const string json = \"{\\\"MessageId\\\":\\\"null-provider-data\\\",\\\"Timestamp\\\":\\\"2024-02-01T00:00:00+00:00\\\",\\\"NextAttemptAt\\\":\\\"2024-02-01T00:10:00+00:00\\\",\\\"MimeMessage\\\":\\\"bWVzc2FnZQ==\\\",\\\"Provider\\\":3,\\\"ProviderData\\\":null}\";\n\n        var record = JsonSerializer.Deserialize<PendingMessageRecord>(json);\n\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.SES, record!.Provider);\n        Assert.Empty(record.ProviderData);\n        Assert.Equal(0, record.AttemptCount);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/PendingMessageSenderFactoryTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class PendingMessageSenderFactoryTests {\n    private sealed class TestSender : IPendingMessageSender {\n        public Task SendAsync(PendingMessageRecord record, CancellationToken ct) => Task.CompletedTask;\n    }\n\n    [Fact]\n    public void GetSender_ReturnsRegisteredSender() {\n        var sender = new TestSender();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.SendGrid, sender }\n        });\n\n        var record = new PendingMessageRecord { Provider = EmailProvider.SendGrid };\n\n        var result = factory.GetSender(record);\n\n        Assert.Same(sender, result);\n    }\n\n    [Fact]\n    public void GetSender_ReturnsNoopWhenProviderUnknown() {\n        var factory = new PendingMessageSenderFactory();\n        var record = new PendingMessageRecord { Provider = (EmailProvider)int.MaxValue };\n\n        var result = factory.GetSender(record);\n\n        Assert.Same(NoopPendingMessageSender.Instance, result);\n    }\n\n    [Fact]\n    public void GetSender_ThrowsWhenRecordIsNull() {\n        var factory = new PendingMessageSenderFactory();\n\n        Assert.Throws<ArgumentNullException>(() => factory.GetSender(null!));\n    }\n\n    [Fact]\n    public void Resolve_ReturnsRegisteredSender() {\n        var sender = new TestSender();\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Mailgun, sender }\n        });\n\n        var result = factory.Resolve(EmailProvider.Mailgun);\n\n        Assert.Same(sender, result);\n    }\n\n    [Theory]\n    [InlineData(EmailProvider.None, typeof(SmtpPendingMessageSender))]\n    [InlineData(EmailProvider.SendGrid, typeof(SendGridPendingMessageSender))]\n    [InlineData(EmailProvider.Mailgun, typeof(MailgunPendingMessageSender))]\n    [InlineData(EmailProvider.SES, typeof(SesPendingMessageSender))]\n    [InlineData(EmailProvider.Gmail, typeof(GmailPendingMessageSender))]\n    [InlineData(EmailProvider.Graph, typeof(GraphPendingMessageSender))]\n    public void Resolve_ReturnsDefaultSenderForKnownProviders(EmailProvider provider, Type expectedType) {\n        var factory = new PendingMessageSenderFactory();\n\n        var result = factory.Resolve(provider);\n\n        Assert.IsType(expectedType, result);\n    }\n\n    [Fact]\n    public void Resolve_ReturnsFallbackForUnknownProvider() {\n        var factory = new PendingMessageSenderFactory();\n\n        var result = factory.Resolve((EmailProvider)int.MaxValue);\n\n        Assert.Same(NoopPendingMessageSender.Instance, result);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/ProviderPendingMessageTests.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Text;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class ProviderPendingMessageTests {\n    private sealed class InMemoryPendingMessageRepository : IPendingMessageRepository {\n        private readonly Dictionary<string, PendingMessageRecord> records = new(StringComparer.OrdinalIgnoreCase);\n\n        public PendingMessageRecord? LastSaved { get; private set; }\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            if (record == null) {\n                throw new ArgumentNullException(nameof(record));\n            }\n\n            LastSaved = record;\n            var key = record.MessageId ?? throw new InvalidOperationException(\"Pending message requires an identifier.\");\n            records[key] = record;\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            if (messageId == null) {\n                throw new ArgumentNullException(nameof(messageId));\n            }\n\n            if (!records.TryGetValue(messageId, out var record) || record.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            record.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            if (messageId == null) {\n                throw new ArgumentNullException(nameof(messageId));\n            }\n\n            records.TryGetValue(messageId, out var record);\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            foreach (var record in records.Values.ToArray()) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return record;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            if (messageId == null) {\n                throw new ArgumentNullException(nameof(messageId));\n            }\n\n            records.Remove(messageId);\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler;\n\n        public int CallCount { get; private set; }\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            this.handler = handler ?? throw new ArgumentNullException(nameof(handler));\n        }\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            CallCount++;\n            return await handler(request, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    private sealed class DelayedCancellationHandler : HttpMessageHandler {\n        private readonly TaskCompletionSource<bool> started = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        public Task WaitForStartAsync() => started.Task;\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {\n            started.TrySetResult(true);\n            await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);\n            throw new InvalidOperationException(\"Unreachable\");\n        }\n    }\n\n    private sealed class CancellationOnSavePendingMessageRepository : IPendingMessageRepository {\n        private readonly TaskCompletionSource<bool> saveStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        public Task WaitForSaveStartAsync() => saveStarted.Task;\n\n        public async Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            saveStarted.TrySetResult(true);\n            await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) =>\n            Task.FromResult<PendingMessageRecord?>(null);\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<PendingMessageRecord?>(null);\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            await Task.CompletedTask;\n            yield break;\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) => Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task SendGridClientQueuesAndProcessesPendingMessage() {\n        using var client = new SendGridClient {\n            Credentials = new NetworkCredential(\"apikey\", \"SG.API\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"queued\",\n            Text = \"hello\"\n        };\n        client.CreateMessage();\n\n        var repository = new InMemoryPendingMessageRepository();\n        client.PendingMessageRepository = repository;\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) {\n            Content = new StringContent(\"failure\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(SendGridClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        var result = await client.SendEmailAsync(CancellationToken.None);\n        Assert.False(result.Status);\n\n        var record = repository.LastSaved;\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.SendGrid, record.Provider);\n        Assert.False(string.IsNullOrWhiteSpace(record.MessageId));\n        Assert.True(record.ProviderData.TryGetValue(SendGridPendingMessageSender.MessageJsonKey, out var json));\n        Assert.False(string.IsNullOrWhiteSpace(json));\n        Assert.True(record.ProviderData.TryGetValue(SendGridPendingMessageSender.ApiKeyProtectedKey, out var apiKeyProtected));\n        Assert.False(string.IsNullOrWhiteSpace(apiKeyProtected));\n        Assert.Equal(\"SG.API\", CredentialProtection.UnprotectWithFallback(apiKeyProtected));\n        Assert.DoesNotContain(SendGridPendingMessageSender.ApiKeyBase64Key, record.ProviderData.Keys);\n\n        var successHandler = new TestHandler((request, _) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Equal(\"Bearer SG.API\", request.Headers.Authorization?.ToString());\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted));\n        });\n        var sender = new SendGridPendingMessageSender(new HttpClient(successHandler));\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.SendGrid, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory);\n\n        await processor.ProcessAsync(CancellationToken.None);\n\n        Assert.Equal(1, successHandler.CallCount);\n        Assert.Null(await repository.GetByMessageIdAsync(record!.MessageId, CancellationToken.None));\n    }\n\n    [Fact]\n    public async Task SendGridClient_CallerCancellation_DoesNotQueuePendingMessage() {\n        using var client = new SendGridClient {\n            Credentials = new NetworkCredential(\"apikey\", \"SG.API\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"canceled\",\n            Text = \"hello\"\n        };\n        client.CreateMessage();\n\n        var repository = new InMemoryPendingMessageRepository();\n        client.PendingMessageRepository = repository;\n\n        var delayedHandler = new DelayedCancellationHandler();\n        var httpClientField = typeof(SendGridClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(delayedHandler));\n\n        using var cts = new CancellationTokenSource();\n        var sendTask = client.SendEmailAsync(cts.Token);\n        await delayedHandler.WaitForStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sendTask);\n        Assert.Null(repository.LastSaved);\n    }\n\n    [Fact]\n    public async Task SendGridClient_CancellationDuringPendingSave_PropagatesCancellation() {\n        using var client = new SendGridClient {\n            Credentials = new NetworkCredential(\"apikey\", \"SG.API\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"pending-save-cancel\",\n            Text = \"hello\"\n        };\n        client.CreateMessage();\n\n        var repository = new CancellationOnSavePendingMessageRepository();\n        client.PendingMessageRepository = repository;\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) {\n            Content = new StringContent(\"failure\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(SendGridClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        using var cts = new CancellationTokenSource();\n        var sendTask = client.SendEmailAsync(cts.Token);\n        await repository.WaitForSaveStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sendTask);\n    }\n\n    [Fact]\n    public async Task MailgunClientQueuesAndProcessesPendingMessage() {\n        var repository = new InMemoryPendingMessageRepository();\n        using var client = new MailgunClient {\n            PendingMessageRepository = repository,\n            Credentials = new NetworkCredential(\"user\", \"mailgun-api-key\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"mailgun\",\n            Text = \"body\"\n        };\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway) {\n            Content = new StringContent(\"error\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(MailgunClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        var result = await client.SendEmailAsync(CancellationToken.None);\n        Assert.False(result.Status);\n\n        var record = repository.LastSaved;\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.Mailgun, record.Provider);\n        Assert.Equal(\"example.com\", record.ProviderData[MailgunPendingMessageSender.DomainKey]);\n        Assert.True(record.ProviderData.TryGetValue(MailgunPendingMessageSender.ApiKeyProtectedKey, out var mailgunProtected));\n        Assert.False(string.IsNullOrWhiteSpace(mailgunProtected));\n        Assert.Equal(\"mailgun-api-key\", CredentialProtection.UnprotectWithFallback(mailgunProtected));\n        Assert.DoesNotContain(MailgunPendingMessageSender.ApiKeyBase64Key, record.ProviderData.Keys);\n        var mime = await MimeMessage.LoadAsync(new MemoryStream(Convert.FromBase64String(record.MimeMessage)));\n        Assert.Equal(\"mailgun\", mime.Subject);\n\n        var successHandler = new TestHandler((request, _) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Contains(\"messages.mime\", request.RequestUri!.AbsoluteUri);\n            Assert.Equal(\"Basic \" + Convert.ToBase64String(Encoding.ASCII.GetBytes(\"api:mailgun-api-key\")), request.Headers.Authorization?.ToString());\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));\n        });\n        var sender = new MailgunPendingMessageSender(new HttpClient(successHandler));\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Mailgun, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory);\n\n        await processor.ProcessAsync(CancellationToken.None);\n\n        Assert.Equal(1, successHandler.CallCount);\n        Assert.Null(await repository.GetByMessageIdAsync(record!.MessageId, CancellationToken.None));\n    }\n\n    [Fact]\n    public async Task MailgunClient_CancellationDuringPendingSave_PropagatesCancellation() {\n        var repository = new CancellationOnSavePendingMessageRepository();\n        using var client = new MailgunClient {\n            PendingMessageRepository = repository,\n            Credentials = new NetworkCredential(\"user\", \"mailgun-api-key\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"mailgun-cancel\",\n            Text = \"body\"\n        };\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway) {\n            Content = new StringContent(\"error\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(MailgunClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        using var cts = new CancellationTokenSource();\n        var sendTask = client.SendEmailAsync(cts.Token);\n        await repository.WaitForSaveStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sendTask);\n    }\n\n    [Fact]\n    public async Task GmailClientQueuesAndProcessesPendingMessage() {\n        var credential = new OAuthCredential {\n            UserName = \"user@example.com\",\n            AccessToken = \"access-token\",\n            RefreshToken = \"refresh-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = \"client-123\",\n            ClientSecret = \"client-secret\"\n        };\n        var repository = new InMemoryPendingMessageRepository();\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) {\n            Content = new StringContent(\"{\\\"error\\\":\\\"failed\\\"}\", Encoding.UTF8, \"application/json\")\n        }));\n        using var failureClient = new HttpClient(failureHandler) {\n            BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n        };\n        using var client = new GmailApiClient(failureClient, credential: credential) {\n            PendingMessageRepository = repository\n        };\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"gmail\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n\n        await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(\"me\", message, CancellationToken.None));\n\n        var record = repository.LastSaved;\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.Gmail, record.Provider);\n        Assert.Equal(\"me\", record.ProviderData[GmailPendingMessageSender.UserIdKey]);\n        Assert.Equal(\"user@example.com\", record.ProviderData[GmailPendingMessageSender.UserNameKey]);\n        var storedAccess = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.AccessTokenProtectedKey]);\n        Assert.Equal(\"access-token\", storedAccess);\n        var storedRefresh = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.RefreshTokenProtectedKey]);\n        Assert.Equal(\"refresh-token\", storedRefresh);\n        Assert.Equal(\"client-123\", record.ProviderData[GmailPendingMessageSender.ClientIdKey]);\n        var storedSecret = CredentialProtection.UnprotectWithFallback(record.ProviderData[GmailPendingMessageSender.ClientSecretProtectedKey]);\n        Assert.Equal(\"client-secret\", storedSecret);\n        var queuedMessage = await MimeMessage.LoadAsync(new MemoryStream(Convert.FromBase64String(record.MimeMessage)));\n        Assert.Equal(\"gmail\", queuedMessage.Subject);\n\n        var successHandler = new TestHandler((request, _) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Equal(\"Bearer access-token\", request.Headers.Authorization?.ToString());\n            Assert.Equal(new Uri(\"https://gmail.googleapis.com/gmail/v1/users/me/messages/send\"), request.RequestUri);\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"{\\\"id\\\":\\\"msg\\\",\\\"threadId\\\":\\\"msg\\\"}\", Encoding.UTF8, \"application/json\")\n            });\n        });\n        var successClient = new HttpClient(successHandler) {\n            BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n        };\n        var sender = new GmailPendingMessageSender((c, refresher) => new GmailApiClient(successClient, refresher, credential: c));\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Gmail, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory);\n\n        await processor.ProcessAsync(CancellationToken.None);\n\n        Assert.Equal(1, successHandler.CallCount);\n        Assert.Null(await repository.GetByMessageIdAsync(record!.MessageId, CancellationToken.None));\n    }\n\n    [Fact]\n    public async Task GmailClient_CancellationDuringPendingSave_PropagatesCancellation() {\n        var credential = new OAuthCredential {\n            UserName = \"user@example.com\",\n            AccessToken = \"access-token\",\n            RefreshToken = \"refresh-token\",\n            ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),\n            ClientId = \"client-123\",\n            ClientSecret = \"client-secret\"\n        };\n        var repository = new CancellationOnSavePendingMessageRepository();\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError) {\n            Content = new StringContent(\"{\\\"error\\\":\\\"failed\\\"}\", Encoding.UTF8, \"application/json\")\n        }));\n        using var failureClient = new HttpClient(failureHandler) {\n            BaseAddress = new Uri(\"https://gmail.googleapis.com/gmail/v1/\")\n        };\n        using var client = new GmailApiClient(failureClient, credential: credential) {\n            PendingMessageRepository = repository\n        };\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"gmail-cancel\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n\n        using var cts = new CancellationTokenSource();\n        var sendTask = client.SendAsync(\"me\", message, cts.Token);\n        await repository.WaitForSaveStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sendTask);\n    }\n\n    [Fact]\n    public async Task GraphApplicationHandlerQueuesAndProcessesPendingMessage() {\n        var repository = new InMemoryPendingMessageRepository();\n        var handler = new Application.GraphMailSendHandler(\n            new FakeGraphSessionFactory(),\n            pendingMessageRepository: repository,\n            sendAsync: (session, profile, request, message, cancellationToken) =>\n                Task.FromResult(new GraphMessage { Id = \"sent-now\" }));\n\n        var queued = await handler.SendAsync(\n            new Application.MailProfile {\n                Id = \"graph-profile\",\n                DisplayName = \"Graph Profile\",\n                Kind = Application.MailProfileKind.Graph,\n                DefaultMailbox = \"shared@example.com\",\n                DefaultSender = \"sender@example.com\",\n                Settings = new Dictionary<string, string> {\n                    [Application.MailProfileSettingsKeys.TenantId] = \"tenant-id\"\n                }\n            },\n            new Application.SendMessageRequest {\n                ProfileId = \"graph-profile\",\n                Message = new Application.DraftMessage {\n                    Subject = \"graph-queued\",\n                    TextBody = \"body\",\n                    To = {\n                        new Application.MessageRecipient { Address = \"recipient@example.com\" }\n                    }\n                }\n            },\n            CancellationToken.None);\n\n        Assert.True(queued.Queued);\n        var record = repository.LastSaved;\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.Graph, record!.Provider);\n        Assert.Equal(\"shared@example.com\", record.ProviderData[GraphPendingMessageSender.UserIdKey]);\n\n        var successHandler = new TestHandler((request, _) => {\n            if (request.RequestUri!.AbsoluteUri.EndsWith(\"/messages\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created) {\n                    Content = new StringContent(\"{\\\"id\\\":\\\"draft-graph\\\"}\", Encoding.UTF8, \"application/json\")\n                });\n            }\n\n            if (request.RequestUri.AbsoluteUri.EndsWith(\"/send\", StringComparison.OrdinalIgnoreCase)) {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted) {\n                    Content = new StringContent(string.Empty, Encoding.UTF8, \"text/plain\")\n                });\n            }\n\n            throw new InvalidOperationException(\"Unexpected Graph request: \" + request.RequestUri);\n        });\n        using var graphHttpClient = new HttpClient(successHandler) {\n            BaseAddress = new Uri(\"https://graph.microsoft.com/v1.0/\")\n        };\n        var sender = new GraphPendingMessageSender(credential => new GraphApiClient(graphHttpClient, credential: credential));\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.Graph, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory);\n\n        await processor.ProcessAsync(CancellationToken.None);\n\n        Assert.Equal(2, successHandler.CallCount);\n        Assert.Null(await repository.GetByMessageIdAsync(record.MessageId, CancellationToken.None));\n    }\n\n    [Fact]\n    public async Task SesClientQueuesAndProcessesPendingMessage() {\n        var repository = new InMemoryPendingMessageRepository();\n        using var client = new SesClient {\n            PendingMessageRepository = repository,\n            Credentials = new NetworkCredential(\"AKIA123\", \"secret-key\"),\n            Region = \"us-east-1\",\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"ses\",\n            Text = \"body\"\n        };\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable) {\n            Content = new StringContent(\"failure\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(SesClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        var result = await client.SendEmailAsync(CancellationToken.None);\n        Assert.False(result.Status);\n\n        var record = repository.LastSaved;\n        Assert.NotNull(record);\n        Assert.Equal(EmailProvider.SES, record.Provider);\n        Assert.True(record.ProviderData.TryGetValue(SesPendingMessageSender.AccessKeyIdProtectedKey, out var sesAccessProtected));\n        Assert.True(record.ProviderData.TryGetValue(SesPendingMessageSender.SecretAccessKeyProtectedKey, out var sesSecretProtected));\n        Assert.Equal(\"AKIA123\", CredentialProtection.UnprotectWithFallback(sesAccessProtected));\n        Assert.Equal(\"secret-key\", CredentialProtection.UnprotectWithFallback(sesSecretProtected));\n        Assert.DoesNotContain(SesPendingMessageSender.AccessKeyIdBase64Key, record.ProviderData.Keys);\n        Assert.DoesNotContain(SesPendingMessageSender.SecretAccessKeyBase64Key, record.ProviderData.Keys);\n        Assert.Equal(\"us-east-1\", record.ProviderData[SesPendingMessageSender.RegionKey]);\n\n        var successHandler = new TestHandler((request, _) => {\n            Assert.Equal(HttpMethod.Post, request.Method);\n            Assert.Equal(new Uri(\"https://email.us-east-1.amazonaws.com/\"), request.RequestUri);\n            Assert.Equal(\"application/x-www-form-urlencoded\", request.Content!.Headers.ContentType!.MediaType);\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));\n        });\n        var sender = new SesPendingMessageSender(new HttpClient(successHandler), () => new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc));\n        var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n            { EmailProvider.SES, sender }\n        });\n        var processor = new PendingMessageProcessor(repository, factory);\n\n        await processor.ProcessAsync(CancellationToken.None);\n\n        Assert.Equal(1, successHandler.CallCount);\n        Assert.Null(await repository.GetByMessageIdAsync(record!.MessageId, CancellationToken.None));\n    }\n\n    [Fact]\n    public async Task SesClient_CancellationDuringPendingSave_PropagatesCancellation() {\n        var repository = new CancellationOnSavePendingMessageRepository();\n        using var client = new SesClient {\n            PendingMessageRepository = repository,\n            Credentials = new NetworkCredential(\"AKIA123\", \"secret-key\"),\n            Region = \"us-east-1\",\n            From = \"sender@example.com\",\n            To = new List<object> { \"recipient@example.com\" },\n            Subject = \"ses-cancel\",\n            Text = \"body\"\n        };\n\n        var failureHandler = new TestHandler((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable) {\n            Content = new StringContent(\"failure\", Encoding.UTF8, \"text/plain\")\n        }));\n        var httpClientField = typeof(SesClient).GetField(\"_client\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        httpClientField.SetValue(client, new HttpClient(failureHandler));\n\n        using var cts = new CancellationTokenSource();\n        var sendTask = client.SendEmailAsync(cts.Token);\n        await repository.WaitForSaveStartAsync();\n        cts.Cancel();\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sendTask);\n    }\n\n    private sealed class FakeGraphSessionFactory : Application.IGraphSessionFactory {\n        public Task<Application.GraphSession> ConnectAsync(Application.MailProfile profile, CancellationToken cancellationToken = default) =>\n            Task.FromResult(new Application.GraphSession(\n                new GraphApiClient(new OAuthCredential {\n                    UserName = profile.DefaultMailbox ?? \"me\",\n                    AccessToken = \"graph-token\",\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                }),\n                profile.DefaultMailbox ?? \"me\",\n                new OAuthCredential {\n                    UserName = profile.DefaultMailbox ?? \"me\",\n                    AccessToken = \"graph-token\",\n                    ExpiresOn = DateTimeOffset.UtcNow.AddHours(1)\n                },\n                new GraphCredential {\n                    ClientId = \"client-id\",\n                    DirectoryId = \"tenant-id\",\n                    ClientSecret = \"client-secret\"\n                }));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/SendGridPendingMessageSenderTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Text;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class SendGridPendingMessageSenderTests {\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler;\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            this.handler = handler;\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            handler(request, cancellationToken);\n    }\n\n    [Fact]\n    public async Task SendAsync_PostsJsonPayloadWithBearerToken() {\n        Uri? requestUri = null;\n        string? authorization = null;\n        string? payload = null;\n        var handler = new TestHandler(async (request, _) => {\n            requestUri = request.RequestUri;\n            authorization = request.Headers.Authorization?.ToString();\n            payload = await request.Content!.ReadAsStringAsync();\n            return new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent(string.Empty) };\n        });\n        using var httpClient = new HttpClient(handler);\n        var sender = new SendGridPendingMessageSender(httpClient);\n\n        var json = \"{\\\"personalizations\\\":[]}\";\n        var record = new PendingMessageRecord();\n        record.ProviderData[SendGridPendingMessageSender.MessageJsonKey] = json;\n        record.ProviderData[SendGridPendingMessageSender.ApiKeyBase64Key] = Convert.ToBase64String(Encoding.UTF8.GetBytes(\"SG.API\"));\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(new Uri(\"https://api.sendgrid.com/v3/mail/send\"), requestUri);\n        Assert.Equal(\"Bearer SG.API\", authorization);\n        Assert.Equal(json, payload);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/SesPendingMessageSenderTests.cs",
    "content": "using System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class SesPendingMessageSenderTests {\n    private sealed class TestHandler : HttpMessageHandler {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler;\n\n        public TestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) {\n            this.handler = handler ?? throw new ArgumentNullException(nameof(handler));\n        }\n\n        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>\n            handler(request, cancellationToken);\n    }\n\n    [Fact]\n    public async Task SendAsync_ComputesAwsSignature() {\n        string? actualAuth = null;\n        string? actualDate = null;\n        Uri? requestUri = null;\n        var handler = new TestHandler((request, _) => {\n            requestUri = request.RequestUri;\n            actualAuth = request.Headers.TryGetValues(\"Authorization\", out var authValues)\n                ? authValues.Single()\n                : null;\n            actualDate = request.Headers.TryGetValues(\"x-amz-date\", out var dateValues)\n                ? dateValues.Single()\n                : null;\n            var response = new HttpResponseMessage(HttpStatusCode.OK) {\n                Content = new StringContent(\"<SendRawEmailResponse/>\")\n            };\n            return Task.FromResult(response);\n        });\n        using var httpClient = new HttpClient(handler);\n        var fixedTime = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc);\n        var sender = new SesPendingMessageSender(httpClient, () => fixedTime);\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n\n        var record = new PendingMessageRecord {\n            MimeMessage = Convert.ToBase64String(ms.ToArray())\n        };\n        const string accessKey = \"AKIDEXAMPLE\";\n        const string secretKey = \"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY\";\n        record.ProviderData[SesPendingMessageSender.AccessKeyIdKey] = accessKey;\n        record.ProviderData[SesPendingMessageSender.SecretAccessKeyKey] = secretKey;\n        record.ProviderData[SesPendingMessageSender.RegionKey] = \"us-east-1\";\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        using var expected = CreateExpectedRequest(accessKey, secretKey, \"us-east-1\", record.MimeMessage, fixedTime);\n        var expectedAuth = expected.Headers.GetValues(\"Authorization\").Single();\n        Assert.Equal(expectedAuth, actualAuth);\n        var expectedDate = expected.Headers.GetValues(\"x-amz-date\").Single();\n        Assert.Equal(expectedDate, actualDate);\n        Assert.Equal(new Uri(\"https://email.us-east-1.amazonaws.com/\"), requestUri);\n    }\n\n    private static HttpRequestMessage CreateExpectedRequest(string accessKey, string secretKey, string region, string base64Mime, DateTime now) {\n        var type = typeof(SesPendingMessageSender);\n        var buildBody = type.GetMethod(\"BuildRequestBody\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;\n        var body = (string)buildBody.Invoke(null, new object[] { base64Mime })!;\n        var createRequest = type.GetMethod(\"CreateRequest\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;\n        var request = (HttpRequestMessage)createRequest.Invoke(null, new object[] { accessKey, secretKey, region, body, now })!;\n        request.Content = new StringContent(body, Encoding.UTF8, \"application/x-www-form-urlencoded\");\n        return request;\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/SmtpPendingMessageEncryptionTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class SmtpPendingMessageEncryptionTests {\n    [Fact]\n    public async Task QueuedRecordsRoundTripCredentialsOnNonWindowsAsync() {\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {\n            return;\n        }\n\n        var repository = new RecordingPendingMessageRepository();\n        var originalFactory = Smtp.ClientFactory;\n        var failingClient = new ThrowingClientSmtp();\n        Smtp.ClientFactory = _ => failingClient;\n\n        Smtp? smtp = null;\n        try {\n            smtp = new Smtp();\n            smtp.PendingMessageRepository = repository;\n\n            var credentialSetter = typeof(Smtp).GetProperty(nameof(Smtp.Credential))?.GetSetMethod(true);\n            credentialSetter?.Invoke(smtp, new object[] { new NetworkCredential(\"queued-user\", \"SuperSecret!42\") });\n\n            var serverSetter = typeof(Smtp).GetProperty(nameof(Smtp.Server))?.GetSetMethod(true);\n            serverSetter?.Invoke(smtp, new object[] { \"smtp.example.com\" });\n\n            var portSetter = typeof(Smtp).GetProperty(nameof(Smtp.Port))?.GetSetMethod(true);\n            portSetter?.Invoke(smtp, new object[] { 2525 });\n\n            var message = new MimeMessage();\n            message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n            message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n            message.Subject = \"queued\";\n            message.Body = new TextPart(\"plain\") { Text = \"body\" };\n            message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId();\n            smtp.Message = message;\n\n            var result = await smtp.SendAsync(CancellationToken.None);\n            Assert.False(result.Status);\n\n            var record = Assert.Single(repository.Records);\n            Assert.Equal(\"queued-user\", record.UserName);\n            Assert.NotNull(record.Password);\n\n            var plainBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(\"SuperSecret!42\"));\n            Assert.NotEqual(plainBase64, record.Password);\n\n            var decrypted = CredentialProtection.Default.Unprotect(record.Password!);\n            Assert.Equal(\"SuperSecret!42\", decrypted);\n\n            var recordingProtector = new RecordingProtector(CredentialProtection.Default);\n            var sender = new SmtpPendingMessageSender(() => new RecordingClient(), credentialProtector: recordingProtector);\n            var decoded = sender.DecodePassword(record.Password);\n\n            Assert.Equal(\"SuperSecret!42\", decoded);\n            Assert.Equal(\"SuperSecret!42\", recordingProtector.LastUnprotected);\n        } finally {\n            smtp?.Dispose();\n            Smtp.ClientFactory = originalFactory;\n        }\n    }\n\n    private sealed class ThrowingClientSmtp : ClientSmtp {\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) =>\n            throw new InvalidOperationException(\"Simulated failure\");\n    }\n\n    private sealed class RecordingClient : ClientSmtp { }\n\n    private sealed class RecordingProtector : ICredentialProtector {\n        private readonly ICredentialProtector inner;\n\n        public RecordingProtector(ICredentialProtector inner) {\n            this.inner = inner ?? throw new ArgumentNullException(nameof(inner));\n        }\n\n        public string? LastUnprotected { get; private set; }\n\n        public string Protect(string plainText) => inner.Protect(plainText);\n\n        public string Unprotect(string protectedData) {\n            var value = inner.Unprotect(protectedData);\n            LastUnprotected = value;\n            return value;\n        }\n    }\n\n    private sealed class RecordingPendingMessageRepository : IPendingMessageRepository {\n        private readonly List<PendingMessageRecord> records = new();\n\n        public IReadOnlyList<PendingMessageRecord> Records => records;\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            records.RemoveAll(r => string.Equals(r.MessageId, record.MessageId, StringComparison.Ordinal));\n            records.Add(record);\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var record = records.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            if (record == null || record.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            record.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            var record = records.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            foreach (var record in records.ToList()) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return record;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            records.RemoveAll(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/SmtpPendingMessageProcessorIntegrationTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Security;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class SmtpPendingMessageProcessorIntegrationTests {\n    [Fact]\n    public async Task QueuedRecordReplaysStoredSecuritySettingsAsync() {\n        var repository = new RecordingPendingMessageRepository();\n        var originalFactory = Smtp.ClientFactory;\n        var failingClient = new FailingClient();\n        Smtp.ClientFactory = _ => failingClient;\n\n        Smtp? smtp = null;\n        try {\n            smtp = new Smtp();\n            smtp.PendingMessageRepository = repository;\n            smtp.SkipCertificateValidation = true;\n            smtp.CheckCertificateRevocation = false;\n            smtp.Timeout = 12345;\n\n            await smtp.ConnectAsync(\"smtp.integration.test\", 587, SecureSocketOptions.Auto, useSsl: true);\n\n            var message = new MimeMessage();\n            message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n            message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n            message.Subject = \"queued\";\n            message.Body = new TextPart(\"plain\") { Text = \"body\" };\n            message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId();\n            smtp.Message = message;\n\n            var sendResult = await smtp.SendAsync(CancellationToken.None);\n            Assert.False(sendResult.Status);\n\n            var record = Assert.Single(repository.Records);\n            Assert.Equal(\"smtp.integration.test\", record.Server);\n            Assert.Equal(587, record.Port);\n            Assert.Equal(SecureSocketOptions.StartTls.ToString(), record.ProviderData[\"SecureSocketOptions\"]);\n            Assert.Equal(bool.TrueString, record.ProviderData[\"UseSsl\"]);\n            Assert.Equal(bool.TrueString, record.ProviderData[\"SkipCertificateValidation\"]);\n            Assert.Equal(bool.FalseString, record.ProviderData[\"CheckCertificateRevocation\"]);\n            Assert.Equal(\"12345\", record.ProviderData[\"TimeoutMilliseconds\"]);\n\n            var captureClient = new CapturingClient();\n            var sender = new SmtpPendingMessageSender(\n                () => captureClient,\n                secureSocketOptions: SecureSocketOptions.SslOnConnect,\n                useSsl: false,\n                skipCertificateValidation: false,\n                checkCertificateRevocation: true,\n                timeout: 9876);\n\n            var factory = new PendingMessageSenderFactory(new Dictionary<EmailProvider, IPendingMessageSender> {\n                { EmailProvider.None, sender }\n            });\n\n            var processor = new PendingMessageProcessor(\n                repository,\n                factory,\n                clock: () => DateTimeOffset.UtcNow.AddMinutes(1),\n                processingLeaseDuration: TimeSpan.Zero);\n\n            await processor.ProcessAsync();\n\n            Assert.Equal(\"smtp.integration.test\", captureClient.Host);\n            Assert.Equal(587, captureClient.Port);\n            Assert.Equal(SecureSocketOptions.StartTls, captureClient.Options);\n            Assert.Equal(12345, captureClient.Timeout);\n            Assert.False(captureClient.CheckCertificateRevocation);\n\n            var callback = captureClient.ServerCertificateValidationCallback;\n            Assert.NotNull(callback);\n            Assert.True(callback!(new object(), null!, null!, SslPolicyErrors.None));\n\n            Assert.Empty(repository.Records);\n        } finally {\n            smtp?.Dispose();\n            Smtp.ClientFactory = originalFactory;\n        }\n    }\n\n    private sealed class FailingClient : ClientSmtp {\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) =>\n            Task.CompletedTask;\n\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) =>\n            throw new InvalidOperationException(\"Simulated failure\");\n    }\n\n    private sealed class CapturingClient : ClientSmtp {\n        public string? Host { get; private set; }\n        public int Port { get; private set; }\n        public SecureSocketOptions Options { get; private set; }\n\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n            Host = host;\n            Port = port;\n            Options = options;\n            return Task.CompletedTask;\n        }\n\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) =>\n            Task.FromResult(message.MessageId ?? string.Empty);\n    }\n\n    private sealed class RecordingPendingMessageRepository : IPendingMessageRepository {\n        private readonly List<PendingMessageRecord> records = new();\n\n        public IReadOnlyList<PendingMessageRecord> Records => records;\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            records.RemoveAll(r => string.Equals(r.MessageId, record.MessageId, StringComparison.Ordinal));\n            records.Add(record);\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var record = records.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            if (record == null || record.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            record.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) {\n            var record = records.FirstOrDefault(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            foreach (var record in records.ToList()) {\n                cancellationToken.ThrowIfCancellationRequested();\n                yield return record;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            records.RemoveAll(r => string.Equals(r.MessageId, messageId, StringComparison.Ordinal));\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SentMessages/SmtpPendingMessageSenderTests.cs",
    "content": "using System.Net.Security;\nusing System.Text;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests.SentMessages;\n\npublic sealed class SmtpPendingMessageSenderTests {\n    private sealed class RecordingClient : ClientSmtp {\n        public string? Host { get; private set; }\n        public int Port { get; private set; }\n        public SecureSocketOptions Options { get; private set; }\n        public MimeMessage? SentMessage { get; private set; }\n        public bool WasDisconnected { get; private set; }\n        public bool WasConnected { get; private set; }\n\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n            Host = host;\n            Port = port;\n            Options = options;\n            WasConnected = true;\n            return Task.CompletedTask;\n        }\n\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            SentMessage = message;\n            return Task.FromResult(message.MessageId ?? string.Empty);\n        }\n\n        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default) {\n            WasDisconnected = true;\n            return Task.CompletedTask;\n        }\n    }\n\n    [Fact]\n    public async Task SendAsync_UsesClientFactoryToSendMessage() {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"queued-message\";\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n        var record = new PendingMessageRecord {\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            MessageId = message.MessageId ?? string.Empty,\n            Server = \"smtp.example.com\",\n            Port = 2525\n        };\n\n        var client = new RecordingClient();\n        var sender = new SmtpPendingMessageSender(() => client);\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(\"smtp.example.com\", client.Host);\n        Assert.Equal(2525, client.Port);\n        Assert.Equal(SecureSocketOptions.Auto, client.Options);\n        Assert.NotNull(client.SentMessage);\n        Assert.Equal(\"queued\", client.SentMessage!.Subject);\n    }\n\n    [Fact]\n    public async Task SendAsync_ReplaysStoredSecuritySettings() {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(MailboxAddress.Parse(\"recipient@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"queued-message-security\";\n        using var ms = new MemoryStream();\n        await message.WriteToAsync(ms);\n\n        var record = new PendingMessageRecord {\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            MessageId = message.MessageId ?? string.Empty,\n            Server = \"smtp.secure.example.com\",\n            Port = 465\n        };\n\n        record.ProviderData[\"SecureSocketOptions\"] = SecureSocketOptions.Auto.ToString();\n        record.ProviderData[\"UseSsl\"] = bool.TrueString;\n        record.ProviderData[\"SkipCertificateValidation\"] = bool.TrueString;\n        record.ProviderData[\"CheckCertificateRevocation\"] = bool.FalseString;\n        record.ProviderData[\"TimeoutMilliseconds\"] = \"12345\";\n\n        var client = new RecordingClient();\n        var sender = new SmtpPendingMessageSender(\n            () => client,\n            secureSocketOptions: SecureSocketOptions.SslOnConnect,\n            useSsl: false,\n            skipCertificateValidation: false,\n            checkCertificateRevocation: true,\n            timeout: 54321);\n\n        await sender.SendAsync(record, CancellationToken.None);\n\n        Assert.Equal(\"smtp.secure.example.com\", client.Host);\n        Assert.Equal(465, client.Port);\n        Assert.Equal(SecureSocketOptions.StartTls, client.Options);\n        Assert.Equal(12345, client.Timeout);\n        Assert.False(client.CheckCertificateRevocation);\n\n        var callback = client.ServerCertificateValidationCallback;\n        Assert.NotNull(callback);\n        Assert.True(callback!(new object(), null!, null!, SslPolicyErrors.None));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SerializationContextTests.cs",
    "content": "using System.Text.Json;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SerializationContextTests {\n    [Fact]\n    public void ShouldProvideTypeInfoForCommonDictionaries() {\n        Assert.NotNull(MailozaurrJsonContext.Default.DictionaryStringString);\n        Assert.NotNull(MailozaurrJsonContext.Default.DictionaryStringBool);\n        Assert.NotNull(MailozaurrJsonContext.Default.DictionaryStringInt);\n    }\n\n    [Fact]\n    public void ShouldSerializeAndDeserializeGmailMessage() {\n        var message = new GmailMessage { Id = \"id\", ThreadId = \"tid\", Snippet = \"hi\" };\n        var json = JsonSerializer.Serialize(message, MailozaurrJsonContext.Default.GmailMessage);\n        var roundtrip = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GmailMessage);\n        Assert.NotNull(roundtrip);\n        Assert.Equal(message.Id, roundtrip!.Id);\n        Assert.Equal(message.ThreadId, roundtrip.ThreadId);\n        Assert.Equal(message.Snippet, roundtrip.Snippet);\n    }\n\n    [Fact]\n    public void ShouldSerializeAndDeserializePendingMessageRecord() {\n        var record = new PendingMessageRecord {\n            MessageId = \"mid\",\n            MimeMessage = \"body\",\n            Timestamp = DateTimeOffset.UtcNow,\n            NextAttemptAt = DateTimeOffset.UtcNow.AddMinutes(1),\n            AttemptCount = 2,\n            Provider = EmailProvider.Gmail,\n            ProviderData = new() { [\"k\"] = \"v\" }\n        };\n        var json = JsonSerializer.Serialize(record, MailozaurrJsonContext.Default.PendingMessageRecord);\n        var roundtrip = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.PendingMessageRecord);\n        Assert.NotNull(roundtrip);\n        Assert.Equal(record.MessageId, roundtrip!.MessageId);\n        Assert.Equal(record.Provider, roundtrip.Provider);\n        Assert.Equal(record.ProviderData[\"k\"], roundtrip.ProviderData[\"k\"]);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromUnixSeconds() {\n        const long unixSeconds = 1700000000;\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":\\\"{unixSeconds}\\\"}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(unixSeconds), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromUnixSecondsNumber() {\n        const long unixSeconds = 1700000000;\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":{unixSeconds}}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(unixSeconds), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromUnixMilliseconds() {\n        const long unixMilliseconds = 1700000000000;\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":{unixMilliseconds}}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(unixMilliseconds), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromIsoString() {\n        const string iso = \"2023-11-14T22:13:20.000Z\";\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":\\\"{iso}\\\"}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.Parse(iso), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromUnixSecondsDouble() {\n        const long unixSeconds = 1700000000;\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":{unixSeconds}.5}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(unixSeconds), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldDeserializeGraphAuthorizationExpiresOnFromUnixSecondsDoubleString() {\n        const long unixSeconds = 1700000000;\n        var json = $\"{{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":\\\"{unixSeconds}.5\\\"}}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(unixSeconds), auth!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldHandleGraphAuthorizationExpiresOnNullOrEmpty() {\n        var jsonNull = \"{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":null}\";\n        var authNull = JsonSerializer.Deserialize(jsonNull, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(authNull);\n        Assert.Equal(DateTimeOffset.MinValue, authNull!.ExpiresOn);\n\n        var jsonEmpty = \"{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":\\\"\\\"}\";\n        var authEmpty = JsonSerializer.Deserialize(jsonEmpty, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(authEmpty);\n        Assert.Equal(DateTimeOffset.MinValue, authEmpty!.ExpiresOn);\n    }\n\n    [Fact]\n    public void ShouldHandleGraphAuthorizationExpiresOnInvalid() {\n        var json = \"{\\\"token_type\\\":\\\"Bearer\\\",\\\"access_token\\\":\\\"abc\\\",\\\"expires_on\\\":\\\"not-a-date\\\"}\";\n        var auth = JsonSerializer.Deserialize(json, MailozaurrJsonContext.Default.GraphAuthorization);\n        Assert.NotNull(auth);\n        Assert.Equal(DateTimeOffset.MinValue, auth!.ExpiresOn);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SesClientSendEmailAsyncTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Tests for the SES client asynchronous email sending logic.\n/// </summary>\npublic class SesClientSendEmailAsyncTests\n{\n    private static byte[] HmacSha256(byte[] key, string data)\n    {\n        using HMACSHA256 hmac = new(key);\n        return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));\n    }\n\n    private static string Sha256Hex(string data)\n    {\n        using SHA256 sha = SHA256.Create();\n        byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(data));\n        return BitConverter.ToString(hash).Replace(\"-\", string.Empty).ToLowerInvariant();\n    }\n\n    private static string ExpectedAuthorization(string accessKey, string secretKey, string region, string amzDate, string body)\n    {\n        string service = \"ses\";\n        string dateStamp = amzDate.Substring(0, 8);\n        string canonicalHeaders = $\"content-type:application/x-www-form-urlencoded\\nhost:email.{region}.amazonaws.com\\nx-amz-date:{amzDate}\\n\";\n        string signedHeaders = \"content-type;host;x-amz-date\";\n        string payloadHash = Sha256Hex(body);\n        string canonicalRequest = $\"POST\\n/\\n\\n{canonicalHeaders}\\n{signedHeaders}\\n{payloadHash}\";\n        string credentialScope = $\"{dateStamp}/{region}/{service}/aws4_request\";\n        string stringToSign = $\"AWS4-HMAC-SHA256\\n{amzDate}\\n{credentialScope}\\n{Sha256Hex(canonicalRequest)}\";\n        byte[] kDate = HmacSha256(Encoding.UTF8.GetBytes(\"AWS4\" + secretKey), dateStamp);\n        byte[] kRegion = HmacSha256(kDate, region);\n        byte[] kService = HmacSha256(kRegion, service);\n        byte[] kSigning = HmacSha256(kService, \"aws4_request\");\n        byte[] sigBytes = HmacSha256(kSigning, stringToSign);\n        string signature = BitConverter.ToString(sigBytes).Replace(\"-\", string.Empty).ToLowerInvariant();\n        return $\"AWS4-HMAC-SHA256 Credential={accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}\";\n    }\n\n    private static SesClient CreateClient(HttpMessageHandler handler)\n    {\n        return new SesClient(handler)\n        {\n            Credentials = new NetworkCredential(\"AKID\", \"SECRET\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = \"body\",\n            RetryDelayMilliseconds = 0,\n            Region = \"us-east-1\"\n        };\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_ComputesSignature()\n    {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = CreateClient(handler);\n        client.WebhookUrl = null;\n\n        var result = await client.SendEmailAsync();\n\n        Assert.True(result.Status);\n        var request = Assert.Single(handler.Requests);\n        string amzDate = request.Headers.GetValues(\"x-amz-date\").Single();\n        string auth = request.Headers.GetValues(\"Authorization\").Single();\n        string body = await request.Content!.ReadAsStringAsync();\n        string expected = ExpectedAuthorization(\"AKID\", \"SECRET\", \"us-east-1\", amzDate, body);\n        Assert.Equal(expected, auth);\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_WithToken_Succeeds()\n    {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = CreateClient(handler);\n        client.WebhookUrl = null;\n\n        using var cts = new CancellationTokenSource();\n        var result = await client.SendEmailAsync(cts.Token);\n\n        Assert.True(result.Status);\n        Assert.Single(handler.Requests);\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_RetriesFailedRequest()\n    {\n        var handler = new RecordingHandler(\n            new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(\"fail\") },\n            new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = CreateClient(handler);\n        client.RetryCount = 1;\n        client.WebhookUrl = null;\n\n        var result = await client.SendEmailAsync();\n\n        Assert.True(result.Status);\n        Assert.Equal(2, handler.Requests.Count);\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_PostsWebhook_OnSuccess()\n    {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = CreateClient(handler);\n        client.WebhookUrl = \"http://localhost\";\n\n        await client.SendEmailAsync();\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(handler.Requests, r => r.RequestUri!.ToString() == \"http://localhost/\");\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_PostsWebhook_OnFailure()\n    {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(\"bad\") });\n        using var client = CreateClient(handler);\n        client.WebhookUrl = \"http://localhost\";\n        client.RetryCount = 0;\n\n        await client.SendEmailAsync();\n\n        Assert.Equal(2, handler.Requests.Count);\n        Assert.Contains(handler.Requests, r => r.RequestUri!.ToString() == \"http://localhost/\");\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SesClientSendTemplatedEmailAsyncTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SesClientSendTemplatedEmailAsyncTests {\n    private static byte[] HmacSha256(byte[] key, string data) {\n        using HMACSHA256 hmac = new(key);\n        return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));\n    }\n\n    private static string Sha256Hex(string data) {\n        using SHA256 sha = SHA256.Create();\n        byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(data));\n        return BitConverter.ToString(hash).Replace(\"-\", string.Empty).ToLowerInvariant();\n    }\n\n    private static string ExpectedAuthorization(string accessKey, string secretKey, string region, string amzDate, string body) {\n        string service = \"ses\";\n        string dateStamp = amzDate.Substring(0, 8);\n        string canonicalHeaders = $\"content-type:application/x-www-form-urlencoded\\nhost:email.{region}.amazonaws.com\\nx-amz-date:{amzDate}\\n\";\n        string signedHeaders = \"content-type;host;x-amz-date\";\n        string payloadHash = Sha256Hex(body);\n        string canonicalRequest = $\"POST\\n/\\n\\n{canonicalHeaders}\\n{signedHeaders}\\n{payloadHash}\";\n        string credentialScope = $\"{dateStamp}/{region}/{service}/aws4_request\";\n        string stringToSign = $\"AWS4-HMAC-SHA256\\n{amzDate}\\n{credentialScope}\\n{Sha256Hex(canonicalRequest)}\";\n        byte[] kDate = HmacSha256(Encoding.UTF8.GetBytes(\"AWS4\" + secretKey), dateStamp);\n        byte[] kRegion = HmacSha256(kDate, region);\n        byte[] kService = HmacSha256(kRegion, service);\n        byte[] kSigning = HmacSha256(kService, \"aws4_request\");\n        byte[] sigBytes = HmacSha256(kSigning, stringToSign);\n        string signature = BitConverter.ToString(sigBytes).Replace(\"-\", string.Empty).ToLowerInvariant();\n        return $\"AWS4-HMAC-SHA256 Credential={accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}\";\n    }\n\n    private static SesClient CreateClient(HttpMessageHandler handler) {\n        return new SesClient(handler) {\n            Credentials = new NetworkCredential(\"AKID\", \"SECRET\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            TemplateName = \"MyTemplate\",\n            TemplateData = new Dictionary<string, string> { [\"Name\"] = \"John\" },\n            RetryDelayMilliseconds = 0,\n            Region = \"us-east-1\"\n        };\n    }\n\n    [Fact]\n    public async Task SendTemplatedEmailAsync_ComputesSignature() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = CreateClient(handler);\n        client.WebhookUrl = null;\n\n        var result = await client.SendTemplatedEmailAsync();\n\n        Assert.True(result.Status);\n        var request = Assert.Single(handler.Requests);\n        string amzDate = request.Headers.GetValues(\"x-amz-date\").Single();\n        string auth = request.Headers.GetValues(\"Authorization\").Single();\n        string body = await request.Content!.ReadAsStringAsync();\n        string expected = ExpectedAuthorization(\"AKID\", \"SECRET\", \"us-east-1\", amzDate, body);\n        Assert.Equal(expected, auth);\n        Assert.Contains(\"Template=MyTemplate\", body);\n        string expectedJson = JsonSerializer.Serialize(client.TemplateData);\n        Assert.Contains(\"TemplateData=\" + Uri.EscapeDataString(expectedJson), body);\n    }\n\n    [Fact]\n    public async Task SendTemplatedEmailAsync_EncodesMultipleParameters() {\n        var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(\"ok\") });\n        using var client = new SesClient(handler) {\n            Credentials = new NetworkCredential(\"AKID\", \"SECRET\"),\n            From = \"sender@example.com\",\n            To = new List<object> { \"to1@example.com\", \"to2@example.com\" },\n            TemplateName = \"MyTemplate\",\n            TemplateData = new Dictionary<string, string> { [\"Name\"] = \"John\", [\"Code\"] = \"123\" },\n            RetryDelayMilliseconds = 0,\n            Region = \"us-east-1\"\n        };\n        client.WebhookUrl = null;\n\n        await client.SendTemplatedEmailAsync();\n\n        var request = Assert.Single(handler.Requests);\n        string body = await request.Content!.ReadAsStringAsync();\n        Assert.Contains(\"Destination.ToAddresses.member.1=to1%40example.com\", body);\n        Assert.Contains(\"Destination.ToAddresses.member.2=to2%40example.com\", body);\n        string expectedJson = JsonSerializer.Serialize(client.TemplateData);\n        Assert.Contains(\"TemplateData=\" + Uri.EscapeDataString(expectedJson), body);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SesClientTests.cs",
    "content": "using System.Reflection;\nusing System.Collections.Generic;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SesClientTests\n{\n    [Fact]\n    public void BuildMessage_WithValidData_ReturnsMimeMessage()\n    {\n        using var client = new SesClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Text = \"text\"\n        };\n        MethodInfo? method = typeof(SesClient).GetMethod(\"BuildMessage\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var msg = method!.Invoke(client, null) as MimeMessage;\n        Assert.NotNull(msg);\n        Assert.Equal(\"subject\", msg!.Subject);\n        Assert.Contains(\"to@example.com\", msg.To.ToString());\n    }\n\n    [Fact]\n    public void BuildMessage_WithHeaders_AddsHeaders()\n    {\n        using var client = new SesClient\n        {\n            From = \"sender@example.com\",\n            To = new List<object> { \"to@example.com\" },\n            Subject = \"subject\",\n            Headers = new Dictionary<string, string> { [\"X-Test\"] = \"123\" }\n        };\n        MethodInfo? method = typeof(SesClient).GetMethod(\"BuildMessage\", BindingFlags.NonPublic | BindingFlags.Instance);\n        var message = method!.Invoke(client, null) as MimeMessage;\n        Assert.NotNull(message);\n        Assert.Equal(\"123\", message!.Headers[\"X-Test\"]);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpAsyncWrappersTests.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit.Security;\nusing Mailozaurr.Definitions;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpAsyncWrappersTests\n{\n    private class FakeConnectClient : ClientSmtp\n    {\n        public bool ConnectCalled;\n        public bool ThrowOnConnect;\n        public bool ThrowOnAuthenticate;\n        public bool BlockConnectUntilCanceled;\n        public bool ConnectCanceled;\n        public SecureSocketOptions? LastSecureSocketOptions;\n        public CancellationToken LastConnectCancellationToken;\n        public string? AuthMechanism;\n        public override async Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default)\n        {\n            LastConnectCancellationToken = cancellationToken;\n            if (ThrowOnConnect) {\n                throw new InvalidOperationException(\"connect failed\");\n            }\n            if (BlockConnectUntilCanceled) {\n                try {\n                    await Task.Delay(global::System.Threading.Timeout.InfiniteTimeSpan, cancellationToken);\n                } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {\n                    ConnectCanceled = true;\n                    throw;\n                }\n            }\n            ConnectCalled = true;\n            LastSecureSocketOptions = options;\n        }\n        public override Task AuthenticateAsync(SaslMechanism mechanism, CancellationToken cancellationToken = default) {\n            if (ThrowOnAuthenticate) {\n                throw new InvalidOperationException(\"auth failed\");\n            }\n            AuthMechanism = mechanism.GetType().Name;\n            return Task.CompletedTask;\n        }\n    }\n\n    private static void SetClient(Smtp smtp, ClientSmtp client)\n    {\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, client);\n    }\n\n    private static void SetCredential(Smtp smtp, NetworkCredential credential)\n    {\n        var field = typeof(Smtp).GetField(\"<Credential>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, credential);\n    }\n\n    private static string? GetPoolIdentity(Smtp smtp)\n    {\n        var field = typeof(Smtp).GetField(\"_poolIdentity\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        return field.GetValue(smtp) as string;\n    }\n\n    [Fact]\n    public async Task ConnectAsync_InvokesClientConnectAsync()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAsync(\"host\", 25);\n\n        Assert.True(fake.ConnectCalled);\n        Assert.True(result.Status);\n    }\n\n    [Fact]\n    public void ConnectAsync_PreservesLegacyFourArgumentOverload()\n    {\n        var method = typeof(Smtp).GetMethod(\n            nameof(Smtp.ConnectAsync),\n            new[] { typeof(string), typeof(int), typeof(SecureSocketOptions), typeof(bool) });\n\n        Assert.NotNull(method);\n        Assert.Equal(typeof(Task<SmtpResult>), method!.ReturnType);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_ReturnsSuccessForOAuthAuthentication()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"oauth-token\",\n            SecureSocketOptions.StartTls,\n            useSsl: false,\n            authMode: ProtocolAuthMode.OAuth2);\n\n        Assert.True(result.IsSuccess);\n        Assert.Equal(SecureSocketOptions.StartTls, result.SecureSocketOptions);\n        Assert.Equal(nameof(SaslMechanismOAuth2), fake.AuthMechanism);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_MapsConnectFailure()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient { ThrowOnConnect = true };\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2);\n\n        Assert.False(result.IsSuccess);\n        Assert.Equal(\"connect_failed\", result.ErrorCode);\n        Assert.True(result.IsTransient);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_MapsValidationFailureAsNonTransient()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            string.Empty,\n            0,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2);\n\n        Assert.False(result.IsSuccess);\n        Assert.Equal(\"connect_failed\", result.ErrorCode);\n        Assert.False(result.IsTransient);\n        Assert.False(fake.ConnectCalled);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_MapsAuthenticationFailure()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient { ThrowOnAuthenticate = true };\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2);\n\n        Assert.False(result.IsSuccess);\n        Assert.Equal(\"auth_failed\", result.ErrorCode);\n        Assert.False(result.IsTransient);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_ReThrowsAuthFailureWhenErrorActionStop()\n    {\n        var smtp = new Smtp {\n            ErrorAction = ActionPreference.Stop\n        };\n        var fake = new FakeConnectClient { ThrowOnAuthenticate = true };\n        SetClient(smtp, fake);\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2));\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_DryRunSkipsAuthentication()\n    {\n        var smtp = new Smtp {\n            DryRun = true\n        };\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2);\n\n        Assert.True(result.IsSuccess);\n        Assert.True(string.IsNullOrWhiteSpace(result.ErrorCode));\n        Assert.Null(fake.AuthMechanism);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_ThrowsWhenAlreadyCanceled()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        await Assert.ThrowsAsync<OperationCanceledException>(() => smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2,\n            cancellationToken: cts.Token));\n\n        Assert.False(fake.ConnectCalled);\n        Assert.Null(fake.AuthMechanism);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_PropagatesCancellationIntoConnect()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient {\n            BlockConnectUntilCanceled = true\n        };\n        SetClient(smtp, fake);\n\n        using var cts = new CancellationTokenSource();\n        var operation = smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2,\n            cancellationToken: cts.Token);\n\n        cts.CancelAfter(TimeSpan.FromMilliseconds(25));\n\n        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operation);\n\n        Assert.True(fake.ConnectCanceled);\n        Assert.True(fake.LastConnectCancellationToken.CanBeCanceled);\n        Assert.Null(fake.AuthMechanism);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_UsesRequestedUsernameForPoolIdentityWhenCredentialWasStale()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeConnectClient();\n        SetClient(smtp, fake);\n        SetCredential(smtp, new NetworkCredential(\"old.user@example.com\", \"old-secret\"));\n\n        var result = await smtp.ConnectAndAuthenticateAsync(\n            \"host\",\n            587,\n            \"new.user@example.com\",\n            \"secret\",\n            authMode: ProtocolAuthMode.OAuth2);\n\n        var poolIdentity = GetPoolIdentity(smtp);\n\n        Assert.True(result.IsSuccess);\n        Assert.NotNull(poolIdentity);\n        Assert.StartsWith(\"new.user@example.com|\", poolIdentity!, StringComparison.Ordinal);\n        Assert.Null(smtp.ConnectionPoolIdentity);\n    }\n\n    [Fact]\n    public async Task CreateMessageAsync_AutoEmbedImagesAddsInlineAttachment()\n    {\n        var tempFile = Path.GetTempFileName();\n        try\n        {\n            File.WriteAllText(tempFile, \"data\");\n\n            var smtp = new Smtp\n            {\n                AutoEmbedImages = true,\n                HtmlBody = $\"<img src=\\\"{tempFile}\\\">\",\n                From = \"sender@example.com\",\n                To = new object[] { \"recipient@example.com\" },\n                Subject = \"test\",\n                TextBody = \"body\"\n            };\n\n            await smtp.CreateMessageAsync();\n\n            var inlineAttachments = smtp.InlineAttachments ?? new List<AttachmentDescriptor>();\n            Assert.Contains(inlineAttachments.OfType<FileAttachmentDescriptor>(), a => string.Equals(a.FilePath, tempFile, StringComparison.OrdinalIgnoreCase));\n            Assert.Contains($\"cid:{Path.GetFileName(tempFile)}\", smtp.HtmlBody);\n            Assert.Contains($\"cid:{Path.GetFileName(tempFile)}\", smtp.Message.HtmlBody);\n        }\n        finally\n        {\n            if (File.Exists(tempFile))\n            {\n                File.Delete(tempFile);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task CreateMessageAsync_CancellationTokenPreventsClientInvocation()\n    {\n        var tempFile = Path.GetTempFileName();\n        try\n        {\n            File.WriteAllText(tempFile, \"data\");\n\n            var smtp = new Smtp\n            {\n                AutoEmbedImages = true,\n                HtmlBody = $\"<img src=\\\"{tempFile}\\\">\"\n            };\n\n            using var cts = new CancellationTokenSource();\n            cts.Cancel();\n\n            var originalHtml = smtp.HtmlBody;\n\n            await Assert.ThrowsAsync<OperationCanceledException>(async () => await smtp.CreateMessageAsync(cts.Token));\n\n            Assert.Equal(originalHtml, smtp.HtmlBody);\n            var inlineAttachments = smtp.InlineAttachments ?? new List<AttachmentDescriptor>();\n            Assert.DoesNotContain(inlineAttachments.OfType<FileAttachmentDescriptor>(), a => string.Equals(a.FilePath, tempFile, StringComparison.OrdinalIgnoreCase));\n        }\n        finally\n        {\n            if (File.Exists(tempFile))\n            {\n                File.Delete(tempFile);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpAttachmentTests.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Text;\nusing MimeKit;\nusing Xunit;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpAttachmentTests\n{\n    [Fact]\n    public void CreateMessage_MissingAttachment_SkipsAttachment()\n    {\n        var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        if (File.Exists(path)) File.Delete(path);\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            TextBody = \"body\",\n            Attachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(path) }\n        };\n\n        smtp.CreateMessage();\n\n        Assert.IsNotType<Multipart>(smtp.Message.Body);\n}\n\n    [Fact]\n    public void CreateMessage_StreamAttachmentDescriptor_AddsAttachment()\n    {\n        var data = Encoding.UTF8.GetBytes(\"hello world\");\n        using var source = new MemoryStream(data);\n        var descriptor = new StreamAttachmentDescriptor(source, \"greeting.txt\")\n        {\n            ContentType = \"text/plain\",\n            Headers = new Dictionary<string, string> { { \"X-Test\", \"Stream\" } },\n        };\n\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            TextBody = \"body\",\n            Attachments = new List<AttachmentDescriptor> { descriptor },\n        };\n\n        smtp.CreateMessage();\n\n        var multipart = Assert.IsType<Multipart>(smtp.Message.Body);\n        var part = Assert.Single(multipart.OfType<MimePart>(), p => p.IsAttachment);\n\n        Assert.Equal(\"greeting.txt\", part.FileName);\n        Assert.Equal(\"text/plain\", part.ContentType.MimeType);\n        Assert.Equal(\"Stream\", part.Headers[\"X-Test\"]);\n\n        using var extracted = new MemoryStream();\n        var content = Assert.IsAssignableFrom<IMimeContent>(part.Content);\n        content.DecodeTo(extracted);\n        Assert.Equal(data, extracted.ToArray());\n\n        Assert.True(source.CanRead);\n    }\n\n    [Fact]\n    public void CreateMessage_ByteArrayAttachmentDescriptor_AddsAttachment()\n    {\n        var data = new byte[] { 1, 2, 3, 4, 5 };\n        var descriptor = new ByteArrayAttachmentDescriptor(data, \"data.bin\")\n        {\n            ContentType = \"application/octet-stream\",\n        };\n\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            TextBody = \"body\",\n            Attachments = new List<AttachmentDescriptor> { descriptor },\n        };\n\n        smtp.CreateMessage();\n\n        var multipart = Assert.IsType<Multipart>(smtp.Message.Body);\n        var part = Assert.Single(multipart.OfType<MimePart>(), p => p.IsAttachment);\n\n        Assert.Equal(\"data.bin\", part.FileName);\n        Assert.Equal(\"application/octet-stream\", part.ContentType.MimeType);\n\n        using var extracted = new MemoryStream();\n        var content = Assert.IsAssignableFrom<IMimeContent>(part.Content);\n        content.DecodeTo(extracted);\n        Assert.Equal(data, extracted.ToArray());\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpAuthenticationMechanismTests.cs",
    "content": "using System.Reflection;\nusing MailKit.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpAuthenticationMechanismTests\n{\n    private class FakeClient : ClientSmtp\n    {\n        public SaslMechanism? Mechanism;\n        public override void Authenticate(SaslMechanism mechanism, System.Threading.CancellationToken cancellationToken = default)\n        {\n            Mechanism = mechanism;\n        }\n    }\n\n    [Fact]\n    public void Authenticate_UsesSelectedMechanism()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeClient();\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, fake);\n\n        smtp.Authenticate(\"user\", \"pass\", false, AuthenticationMechanism.CramMd5);\n\n        Assert.IsType<SaslMechanismCramMd5>(fake.Mechanism);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpConcurrencyTests.cs",
    "content": "using System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpConcurrencyTests\n{\n    private class CountingClient : ClientSmtp\n    {\n        private int _active;\n        public int Max;\n\n        public override async Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null)\n        {\n            var current = Interlocked.Increment(ref _active);\n            int initialMax;\n            do\n            {\n                initialMax = Max;\n                if (current <= initialMax)\n                {\n                    break;\n                }\n            }\n            while (Interlocked.CompareExchange(ref Max, current, initialMax) != initialMax);\n\n            await Task.Delay(100, cancellationToken);\n            Interlocked.Decrement(ref _active);\n            return string.Empty;\n        }\n    }\n\n    [Fact]\n    public async Task SendAsync_SerializesConcurrentCalls()\n    {\n        var smtp = new Smtp();\n        var fake = new CountingClient();\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, fake);\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.WebhookUrl = null;\n        smtp.CreateMessage();\n\n        var tasks = new[]\n        {\n            smtp.SendAsync(),\n            smtp.SendAsync(),\n            smtp.SendAsync()\n        };\n\n        await Task.WhenAll(tasks);\n\n        Assert.Equal(1, fake.Max);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpConnectionPoolConcurrencyTests.cs",
    "content": "using System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpConnectionPoolConcurrencyTests {\n    private class FakeClient : ClientSmtp {\n        private bool _connected = true;\n        public override bool IsConnected => _connected;\n        public void SetConnected(bool value) => _connected = value;\n    }\n\n    [Fact]\n    public void ReturnClient_ConcurrentCallsRespectMaxPoolSize() {\n        SmtpConnectionPool.Configure(true, 2);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var clients = Enumerable.Range(0, 20).Select(_ => new FakeClient()).ToArray();\n        Parallel.ForEach(clients, c => SmtpConnectionPool.ReturnClient(\"h\", 25, c));\n\n        var rented = 0;\n        ClientSmtp? client;\n        while ((client = SmtpConnectionPool.TryRentClient(\"h\", 25)) != null) {\n            rented++;\n            client.Dispose();\n        }\n\n        Assert.Equal(SmtpConnectionPool.MaxPoolSize, rented);\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Fact]\n    public void TryRentClient_ConcurrentCallsEmptyPool() {\n        SmtpConnectionPool.Configure(true, 5);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        foreach (var _ in Enumerable.Range(0, SmtpConnectionPool.MaxPoolSize)) {\n            SmtpConnectionPool.ReturnClient(\"h\", 25, new FakeClient());\n        }\n\n        var rented = 0;\n        Parallel.For(0, SmtpConnectionPool.MaxPoolSize, _ => {\n            var client = SmtpConnectionPool.TryRentClient(\"h\", 25);\n            if (client != null) {\n                Interlocked.Increment(ref rented);\n                client.Dispose();\n            }\n        });\n\n        Assert.Equal(SmtpConnectionPool.MaxPoolSize, rented);\n        Assert.Null(SmtpConnectionPool.TryRentClient(\"h\", 25));\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n}"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpConnectionPoolMetricsTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpConnectionPoolMetricsTests {\n    private class FakeClient : ClientSmtp {\n        private bool _connected = true;\n        public override bool IsConnected => _connected;\n        public void SetConnected(bool value) => _connected = value;\n    }\n\n    [Fact]\n    public void CurrentPoolSize_TracksClients() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        Assert.Equal(0, SmtpConnectionPool.CurrentPoolSize);\n\n        var client = new FakeClient();\n        SmtpConnectionPool.ReturnClient(\"h\", 25, client);\n        Assert.Equal(1, SmtpConnectionPool.CurrentPoolSize);\n\n        var rented = SmtpConnectionPool.TryRentClient(\"h\", 25);\n        Assert.NotNull(rented);\n        Assert.Equal(0, SmtpConnectionPool.CurrentPoolSize);\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Fact]\n    public void PoolSizeChanged_Raised() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var values = new List<int>();\n        void Handler(int size) => values.Add(size);\n        SmtpConnectionPool.PoolSizeChanged += Handler;\n\n        var client = new FakeClient();\n        SmtpConnectionPool.ReturnClient(\"h\", 25, client);\n        var rented = SmtpConnectionPool.TryRentClient(\"h\", 25);\n        rented?.Dispose();\n\n        // clearing the pool should also raise the event\n        SmtpConnectionPool.ClearConnectionPool();\n\n        SmtpConnectionPool.PoolSizeChanged -= Handler;\n        SmtpConnectionPool.SetPoolingEnabled(false);\n\n        Assert.Equal(new[] { 1, 0, 0 }, values);\n    }\n\n    [Fact]\n    public void GetSnapshot_ReturnsEntries() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var client = new FakeClient();\n        SmtpConnectionPool.ReturnClient(\"h\", 25, client);\n\n        var snapshot = SmtpConnectionPool.GetSnapshot();\n        Assert.Equal(1, snapshot.CurrentPoolSize);\n        Assert.Single(snapshot.Entries);\n        var entry = snapshot.Entries[0];\n        Assert.Equal(\"h\", entry.Server);\n        Assert.Equal(25, entry.Port);\n        Assert.Equal(1, entry.Count);\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Fact]\n    public void GetSnapshot_DecodesServerWithDelimiters() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var client = new FakeClient();\n        var server = \"smtp:host|name\";\n        var identity = \"user|domain:ssl\";\n        SmtpConnectionPool.ReturnClient(server, 25, client, identity);\n\n        var snapshot = SmtpConnectionPool.GetSnapshot();\n        Assert.Equal(1, snapshot.CurrentPoolSize);\n        Assert.Single(snapshot.Entries);\n        var entry = snapshot.Entries[0];\n        Assert.Equal(server, entry.Server);\n        Assert.Equal(25, entry.Port);\n        Assert.Equal(1, entry.Count);\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpConnectionPoolTests.cs",
    "content": "using System;\nusing MailKit.Security;\nusing System.Threading;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpConnectionPoolTests {\n    private class FakeClient : ClientSmtp {\n        public int ConnectCalls;\n        private bool _connected;\n        public override bool IsConnected => _connected;\n        public void SetConnected(bool value) => _connected = value;\n        public override void Connect(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n            ConnectCalls++;\n            _connected = true;\n        }\n\n        public override void Disconnect(bool quit, CancellationToken cancellationToken = default) {\n            _connected = false;\n        }\n    }\n\n    private sealed class TrackingClient : ClientSmtp {\n        private bool _connected = true;\n        private bool _disposed;\n\n        public bool Disposed => _disposed;\n\n        public override bool IsConnected => _connected;\n\n        public void SetConnected(bool value) => _connected = value;\n\n        protected override void Dispose(bool disposing) {\n            _disposed = true;\n            _connected = false;\n            base.Dispose(disposing);\n        }\n    }\n\n    [Fact]\n    public void Disconnect_ReturnsClientToPool() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n        var fake = new FakeClient();\n        Smtp.ClientFactory = _ => fake;\n\n        var smtp1 = new Smtp();\n        smtp1.Connect(\"h\", 25);\n        smtp1.Disconnect();\n\n        var smtp2 = new Smtp();\n        Smtp.ClientFactory = _ => new FakeClient();\n        smtp2.Connect(\"h\", 25);\n\n        Assert.Same(fake, smtp2.Client);\n        Assert.Equal(1, fake.ConnectCalls);\n\n        Smtp.ClientFactory = logger => new ClientSmtp();\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Fact]\n    public void InstancePoolOverrideFalse_DoesNotUseGlobalPool() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var fake = new FakeClient();\n        Smtp.ClientFactory = _ => fake;\n\n        var smtp1 = new Smtp { UseConnectionPool = false };\n        smtp1.Connect(\"h\", 25);\n        smtp1.Disconnect();\n\n        Smtp.ClientFactory = _ => new FakeClient();\n        var smtp2 = new Smtp { UseConnectionPool = false };\n        smtp2.Connect(\"h\", 25);\n\n        Assert.NotSame(fake, smtp2.Client);\n        Assert.Equal(0, SmtpConnectionPool.CurrentPoolSize);\n\n        smtp2.Dispose();\n        Smtp.ClientFactory = _ => new ClientSmtp();\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Fact]\n    public void InstancePoolOverrideTrue_UsesPoolWhenGlobalPoolingIsDisabled() {\n        SmtpConnectionPool.SetPoolingEnabled(false);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var fake = new FakeClient();\n        Smtp.ClientFactory = _ => fake;\n\n        var smtp1 = new Smtp { UseConnectionPool = true };\n        smtp1.Connect(\"h\", 25);\n        smtp1.Disconnect();\n\n        var smtp2 = new Smtp { UseConnectionPool = true };\n        Smtp.ClientFactory = _ => new FakeClient();\n        smtp2.Connect(\"h\", 25);\n\n        Assert.Same(fake, smtp2.Client);\n        Assert.Equal(1, fake.ConnectCalls);\n\n        smtp2.Dispose();\n        Smtp.ClientFactory = _ => new ClientSmtp();\n        SmtpConnectionPool.ClearConnectionPool();\n    }\n\n    [Fact]\n    public void ReturnClosedClient_IsDiscarded() {\n        SmtpConnectionPool.SetPoolingEnabled(true);\n        SmtpConnectionPool.ClearConnectionPool();\n\n        var fake = new FakeClient();\n        fake.SetConnected(false);\n        SmtpConnectionPool.ReturnClient(\"h\", 25, fake);\n\n        var pooled = SmtpConnectionPool.TryRentClient(\"h\", 25);\n        Assert.Null(pooled);\n\n        SmtpConnectionPool.ClearConnectionPool();\n        SmtpConnectionPool.SetPoolingEnabled(false);\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-1)]\n    public void SetMaxPoolSize_InvalidValue_Throws(int value) {\n        var original = SmtpConnectionPool.MaxPoolSize;\n\n        var ex = Assert.Throws<ArgumentOutOfRangeException>(() => SmtpConnectionPool.SetMaxPoolSize(value));\n        Assert.Equal(\"value\", ex.ParamName);\n        Assert.Equal(original, SmtpConnectionPool.MaxPoolSize);\n    }\n\n    [Fact]\n    public void SetMaxPoolSize_ValidValue_UpdatesProperty() {\n        var original = SmtpConnectionPool.MaxPoolSize;\n        var expected = original + 1;\n\n        try {\n            SmtpConnectionPool.SetMaxPoolSize(expected);\n            Assert.Equal(expected, SmtpConnectionPool.MaxPoolSize);\n        } finally {\n            SmtpConnectionPool.SetMaxPoolSize(original);\n        }\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-5)]\n    public void Configure_InvalidMaxPoolSize_Throws(int value) {\n        var originalEnabled = SmtpConnectionPool.PoolingEnabled;\n        var originalMax = SmtpConnectionPool.MaxPoolSize;\n\n        var ex = Assert.Throws<ArgumentOutOfRangeException>(() => SmtpConnectionPool.Configure(true, value));\n        Assert.Equal(\"maxPoolSize\", ex.ParamName);\n        Assert.Equal(originalEnabled, SmtpConnectionPool.PoolingEnabled);\n        Assert.Equal(originalMax, SmtpConnectionPool.MaxPoolSize);\n    }\n\n    [Fact]\n    public void Configure_ValidMaxPoolSize_UpdatesProperty() {\n        var originalEnabled = SmtpConnectionPool.PoolingEnabled;\n        var originalMax = SmtpConnectionPool.MaxPoolSize;\n        var expected = originalMax + 1;\n\n        try {\n            SmtpConnectionPool.Configure(true, expected);\n            Assert.True(SmtpConnectionPool.PoolingEnabled);\n            Assert.Equal(expected, SmtpConnectionPool.MaxPoolSize);\n        } finally {\n            SmtpConnectionPool.Configure(originalEnabled, originalMax);\n        }\n    }\n\n    [Fact]\n    public void Configure_DisablingPooling_ClearsCachedClients() {\n        var originalEnabled = SmtpConnectionPool.PoolingEnabled;\n        var originalMax = SmtpConnectionPool.MaxPoolSize;\n\n        var client = new TrackingClient();\n        client.SetConnected(true);\n\n        try {\n            SmtpConnectionPool.Configure(true, Math.Max(2, originalMax));\n            SmtpConnectionPool.ClearConnectionPool();\n\n            SmtpConnectionPool.ReturnClient(\"h\", 25, client);\n            Assert.Equal(1, SmtpConnectionPool.CurrentPoolSize);\n\n            SmtpConnectionPool.Configure(false, originalMax);\n\n            Assert.True(client.Disposed);\n            Assert.Equal(0, SmtpConnectionPool.CurrentPoolSize);\n            Assert.False(SmtpConnectionPool.PoolingEnabled);\n        } finally {\n            SmtpConnectionPool.ClearConnectionPool();\n            SmtpConnectionPool.Configure(originalEnabled, originalMax);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpHeadersTests.cs",
    "content": "using System.Collections.Generic;\nusing Xunit;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpHeadersTests\n{\n    [Fact]\n    public void CreateMessage_WithHeaders_AddsHeaders()\n    {\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.Headers = new Dictionary<string, string> { [\"X-Test\"] = \"123\" };\n        smtp.CreateMessage();\n        Assert.Equal(\"123\", smtp.Message.Headers[\"X-Test\"]);\n    }\n\n    [Fact]\n    public void CreateMessage_WithMultipleHeaders_AddsAllHeaders()\n    {\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            TextBody = \"body\",\n            Headers = new Dictionary<string, string>\n            {\n                [\"X-One\"] = \"1\",\n                [\"X-Two\"] = \"2\"\n            }\n        };\n\n        smtp.CreateMessage();\n\n        Assert.Equal(\"1\", smtp.Message.Headers[\"X-One\"]);\n        Assert.Equal(\"2\", smtp.Message.Headers[\"X-Two\"]);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpInlineAttachmentTests.cs",
    "content": "using System.IO;\nusing System.Linq;\nusing System.Collections.Generic;\nusing MimeKit;\nusing Xunit;\nusing Mailozaurr.Definitions;\n\nnamespace Mailozaurr.Tests;\n\n/// <summary>\n/// Verifies handling of inline attachments for SMTP messages.\n/// </summary>\npublic class SmtpInlineAttachmentTests\n{\n    [Fact]\n    public void CreateMessage_WithInlineAttachment_AddsLinkedResource()\n    {\n        var tmp = Path.GetTempFileName();\n        File.WriteAllText(tmp, \"data\");\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.HtmlBody = \"<img src=\\\"cid:test\\\">\";\n        smtp.InlineAttachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(tmp) };\n        smtp.CreateMessage();\n        var body = Assert.IsType<MultipartRelated>(smtp.Message.Body);\n        var inlineCount = body.OfType<MimePart>().Count(p => p.ContentDisposition?.Disposition == ContentDisposition.Inline);\n        File.Delete(tmp);\n        Assert.Equal(1, inlineCount);\n    }\n\n    [Fact]\n    public void CreateMessage_NullInlineAttachments_DoesNotThrow()\n    {\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            HtmlBody = \"<b>body</b>\",\n            InlineAttachments = null\n        };\n\n        var ex = Record.Exception(() => smtp.CreateMessage());\n\n        Assert.Null(ex);\n    }\n\n    [Fact]\n    public void CreateMessage_MissingInlineAttachment_SkipsResource()\n    {\n        var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        if (File.Exists(path)) File.Delete(path);\n        var smtp = new Smtp\n        {\n            From = \"a@b.com\",\n            To = new object[] { \"c@d.com\" },\n            Subject = \"test\",\n            HtmlBody = \"<img src=\\\"cid:test\\\">\",\n            InlineAttachments = new List<AttachmentDescriptor> { new FileAttachmentDescriptor(path) }\n        };\n\n        smtp.CreateMessage();\n\n        Assert.IsType<TextPart>(smtp.Message.Body);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpPendingMessageTests.cs",
    "content": "using System.IO;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\nusing MailKit;\nusing System.Runtime.CompilerServices;\nusing MimeKit;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class SmtpPendingMessageTests {\n    private sealed class FailClient : ClientSmtp {\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            throw new Exception(\"fail\");\n        }\n    }\n\n    private sealed class SuccessClient : ClientSmtp {\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null) {\n            return Task.FromResult(string.Empty);\n        }\n    }\n\n    private sealed class InMemoryPendingRepository : IPendingMessageRepository {\n        public List<PendingMessageRecord> Saved { get; } = new();\n        public List<string> Removed { get; } = new();\n\n        public Task SaveAsync(PendingMessageRecord record, CancellationToken cancellationToken = default) {\n            if (record.NextAttemptAt == default) {\n                record.NextAttemptAt = DateTimeOffset.UtcNow;\n            }\n            var existing = Saved.FindIndex(r => r.MessageId == record.MessageId);\n            if (existing >= 0) {\n                Saved[existing] = record;\n            } else {\n                Saved.Add(record);\n            }\n            return Task.CompletedTask;\n        }\n\n        public Task<PendingMessageRecord?> TryAcquireLeaseAsync(\n            string messageId,\n            DateTimeOffset dueBeforeOrAt,\n            DateTimeOffset leaseUntil,\n            CancellationToken cancellationToken = default) {\n            var record = Saved.FirstOrDefault(r => r.MessageId == messageId);\n            if (record == null || record.NextAttemptAt > dueBeforeOrAt) {\n                return Task.FromResult<PendingMessageRecord?>(null);\n            }\n\n            record.NextAttemptAt = leaseUntil;\n            return Task.FromResult<PendingMessageRecord?>(record);\n        }\n\n        public Task<PendingMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<PendingMessageRecord?>(Saved.FirstOrDefault(r => r.MessageId == messageId));\n\n        public async IAsyncEnumerable<PendingMessageRecord> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) {\n            var snapshot = Saved.ToList();\n            foreach (var r in snapshot) {\n                yield return r;\n                await Task.Yield();\n            }\n        }\n\n        public Task RemoveAsync(string messageId, CancellationToken cancellationToken = default) {\n            Removed.Add(messageId);\n            return Task.CompletedTask;\n        }\n    }\n\n    private sealed class InMemorySentRepository : ISentMessageRepository {\n        public List<SentMessageRecord> Saved { get; } = new();\n\n        public Task SaveAsync(SentMessageRecord record, CancellationToken cancellationToken = default) {\n            Saved.Add(record);\n            return Task.CompletedTask;\n        }\n\n        public Task<SentMessageRecord?> GetByMessageIdAsync(string messageId, CancellationToken cancellationToken = default) =>\n            Task.FromResult<SentMessageRecord?>(Saved.FirstOrDefault(r => r.MessageId == messageId));\n    }\n\n    private static void SetClient(Smtp smtp, ClientSmtp client) {\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, client);\n    }\n\n    private static PendingMessageRecord CreatePendingRecord(string messageId, EmailProvider provider) {\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"a@b.com\"));\n        message.To.Add(MailboxAddress.Parse(\"b@c.com\"));\n        message.Subject = $\"queued-{messageId}\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = messageId;\n\n        using var ms = new MemoryStream();\n        message.WriteTo(ms);\n\n        return new PendingMessageRecord {\n            MessageId = messageId,\n            MimeMessage = Convert.ToBase64String(ms.ToArray()),\n            Timestamp = DateTimeOffset.UtcNow,\n            NextAttemptAt = DateTimeOffset.UtcNow,\n            Provider = provider\n        };\n    }\n\n    [Fact]\n    public void PendingMessagesPathCreatesRepository() {\n        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var smtp = new Smtp { PendingMessagesPath = dir };\n        Assert.NotNull(smtp.PendingMessageRepository);\n        var repo = Assert.IsType<FilePendingMessageRepository>(smtp.PendingMessageRepository);\n        var field = typeof(FilePendingMessageRepository).GetField(\"filePath\", BindingFlags.NonPublic | BindingFlags.Instance)!;\n        var expected = Path.Combine(dir, \"pending.log\");\n        Assert.Equal(expected, (string)field.GetValue(repo)!);\n    }\n\n    [Fact]\n    public async Task SendFailureQueuesMessage() {\n        var repo = new InMemoryPendingRepository();\n        var smtp = new Smtp { PendingMessageRepository = repo, RetryCount = 0 };\n        SetClient(smtp, new FailClient());\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n\n        var result = await smtp.SendAsync();\n\n        Assert.False(result.Status);\n        Assert.NotNull(result.MessageId);\n        Assert.Single(repo.Saved);\n        Assert.Equal(result.MessageId, repo.Saved[0].MessageId);\n        Assert.False(string.IsNullOrWhiteSpace(repo.Saved[0].MimeMessage));\n    }\n\n    [Fact]\n    public async Task QueuedMessagePasswordIsEncrypted() {\n        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {\n            return;\n        }\n        var repo = new InMemoryPendingRepository();\n        var smtp = new Smtp { PendingMessageRepository = repo, RetryCount = 0 };\n        SetClient(smtp, new FailClient());\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n        smtp.Authenticate(\"user\", \"secret\", false);\n\n        await smtp.SendAsync();\n\n        var record = Assert.Single(repo.Saved);\n        Assert.NotEqual(\"secret\", record.Password);\n        var bytes = Convert.FromBase64String(record.Password!);\n        var decrypted = ProtectedData.Unprotect(bytes, null, DataProtectionScope.CurrentUser);\n        Assert.Equal(\"secret\", Encoding.UTF8.GetString(decrypted));\n    }\n\n    [Fact]\n    public async Task SuccessfulSendRemovesQueuedMessage() {\n        var repo = new InMemoryPendingRepository();\n        var smtp = new Smtp { PendingMessageRepository = repo };\n        SetClient(smtp, new SuccessClient());\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n        smtp.Message.MessageId = \"msg-1\";\n\n        var result = await smtp.SendAsync();\n\n        Assert.True(result.Status);\n        Assert.Single(repo.Removed);\n        Assert.Equal(\"msg-1\", repo.Removed[0]);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_SendsAndLogs() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new SuccessClient());\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"a@b.com\"));\n        message.To.Add(MailboxAddress.Parse(\"b@c.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"msg-1\";\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms);\n            var record = new PendingMessageRecord {\n                MessageId = \"msg-1\",\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow\n            };\n            await pending.SaveAsync(record);\n        }\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        Assert.Single(pending.Removed);\n        Assert.Equal(\"msg-1\", pending.Removed[0]);\n        Assert.Single(sent.Saved);\n        Assert.Equal(\"msg-1\", sent.Saved[0].MessageId);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_StoresNormalizedRecipientAddressesInSentLog() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new SuccessClient());\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"sender@example.com\"));\n        message.To.Add(new MailboxAddress(\"Doe, Jane\", \"jane@example.com\"));\n        message.To.Add(new MailboxAddress(\"John Smith\", \"john@example.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"msg-normalized\";\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms);\n            await pending.SaveAsync(new PendingMessageRecord {\n                MessageId = \"msg-normalized\",\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow\n            });\n        }\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        var record = Assert.Single(sent.Saved);\n        Assert.Equal(\"jane@example.com,john@example.com\", record.Recipients);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_FailedSendRetainsMessage() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new FailClient());\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"a@b.com\"));\n        message.To.Add(MailboxAddress.Parse(\"b@c.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"msg-2\";\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms);\n            var record = new PendingMessageRecord {\n                MessageId = \"msg-2\",\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow\n            };\n            await pending.SaveAsync(record);\n        }\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        Assert.Empty(pending.Removed);\n        Assert.Empty(sent.Saved);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_SkipsFutureNextAttempt() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new SuccessClient());\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"a@b.com\"));\n        message.To.Add(MailboxAddress.Parse(\"b@c.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"msg-3\";\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms);\n            var record = new PendingMessageRecord {\n                MessageId = \"msg-3\",\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow,\n                NextAttemptAt = DateTimeOffset.UtcNow.AddDays(1)\n            };\n            await pending.SaveAsync(record);\n        }\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        Assert.Empty(sent.Saved);\n        Assert.Empty(pending.Removed);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_SendsWhenNextAttemptReached() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new SuccessClient());\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"a@b.com\"));\n        message.To.Add(MailboxAddress.Parse(\"b@c.com\"));\n        message.Subject = \"queued\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = \"msg-4\";\n        using (var ms = new MemoryStream()) {\n            await message.WriteToAsync(ms);\n            var record = new PendingMessageRecord {\n                MessageId = \"msg-4\",\n                MimeMessage = Convert.ToBase64String(ms.ToArray()),\n                Timestamp = DateTimeOffset.UtcNow,\n                NextAttemptAt = DateTimeOffset.UtcNow.AddDays(1)\n            };\n            await pending.SaveAsync(record);\n        }\n\n        await smtp.ProcessPendingMessagesAsync();\n        Assert.Empty(sent.Saved);\n\n        var existing = await pending.GetByMessageIdAsync(\"msg-4\");\n        Assert.NotNull(existing);\n        existing!.NextAttemptAt = DateTimeOffset.UtcNow;\n        await pending.SaveAsync(existing);\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        Assert.Single(sent.Saved);\n        Assert.Equal(\"msg-4\", sent.Saved[0].MessageId);\n        Assert.Single(pending.Removed);\n        Assert.Equal(\"msg-4\", pending.Removed[0]);\n    }\n\n    [Fact]\n    public async Task ProcessPendingMessages_SkipsRecordsForOtherProviders() {\n        var pending = new InMemoryPendingRepository();\n        var sent = new InMemorySentRepository();\n        var smtp = new Smtp { PendingMessageRepository = pending, SentMessageRepository = sent };\n        SetClient(smtp, new SuccessClient());\n\n        var smtpRecord = CreatePendingRecord(\"msg-smtp\", EmailProvider.None);\n        var gmailRecord = CreatePendingRecord(\"msg-gmail\", EmailProvider.Gmail);\n\n        await pending.SaveAsync(smtpRecord);\n        await pending.SaveAsync(gmailRecord);\n\n        var expectedNextAttempt = gmailRecord.NextAttemptAt;\n\n        await smtp.ProcessPendingMessagesAsync();\n\n        Assert.Single(pending.Removed);\n        Assert.Equal(\"msg-smtp\", pending.Removed[0]);\n        Assert.DoesNotContain(\"msg-gmail\", pending.Removed);\n        Assert.Equal(expectedNextAttempt, gmailRecord.NextAttemptAt);\n        var sentRecord = Assert.Single(sent.Saved);\n        Assert.Equal(\"msg-smtp\", sentRecord.MessageId);\n    }\n\n    [Fact]\n    public async Task SaveMessageAsync_WritesMessageToFile() {\n        var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var path = Path.Combine(tempDir, \"message.eml\");\n        var smtp = new Smtp();\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"async@from.com\"));\n        message.To.Add(MailboxAddress.Parse(\"async@to.com\"));\n        message.Subject = \"async-save\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        message.MessageId = $\"async-{Guid.NewGuid():N}\";\n        smtp.Message = message;\n\n        try {\n            await smtp.SaveMessageAsync(path);\n\n            Assert.True(File.Exists(path));\n            using var stream = File.OpenRead(path);\n            var loaded = await MimeMessage.LoadAsync(stream);\n            Assert.Equal(message.MessageId, loaded.MessageId);\n            var body = Assert.IsType<TextPart>(loaded.Body);\n            Assert.Equal(\"body\", body.Text.TrimEnd('\\r', '\\n'));\n        } finally {\n            if (Directory.Exists(tempDir)) {\n                Directory.Delete(tempDir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task SaveMessageAsync_RespectsCancellation() {\n        var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        var path = Path.Combine(tempDir, \"message.eml\");\n        var smtp = new Smtp();\n\n        var message = new MimeMessage();\n        message.From.Add(MailboxAddress.Parse(\"async@from.com\"));\n        message.To.Add(MailboxAddress.Parse(\"async@to.com\"));\n        message.Subject = \"async-cancel\";\n        message.Body = new TextPart(\"plain\") { Text = \"body\" };\n        smtp.Message = message;\n\n        using var cts = new CancellationTokenSource();\n        cts.Cancel();\n\n        try {\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => smtp.SaveMessageAsync(path, cts.Token));\n\n            if (File.Exists(path)) {\n                var info = new FileInfo(path);\n                Assert.Equal(0, info.Length);\n            }\n        } finally {\n            if (Directory.Exists(tempDir)) {\n                Directory.Delete(tempDir, true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSendAsyncTests.cs",
    "content": "using System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MimeKit;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSendAsyncTests\n{\n    private class FakeClient : ClientSmtp\n    {\n        public bool Called;\n        public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken = default, ITransferProgress? progress = null)\n        {\n            Called = true;\n            return Task.FromResult(string.Empty);\n        }\n    }\n\n    [Fact]\n    public async Task SendAsync_InvokesClientSendAsync()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeClient();\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, fake);\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.WebhookUrl = null;\n        await smtp.CreateMessageAsync();\n\n        var result = await smtp.SendAsync();\n\n        Assert.True(fake.Called);\n        Assert.True(result.Status);\n    }\n\n    [Fact]\n    public async Task SendAsync_DryRun_SkipsClientSendAsync()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeClient();\n        var field = typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!;\n        field.SetValue(smtp, fake);\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"b@c.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.WebhookUrl = null;\n        smtp.DryRun = true;\n        await smtp.CreateMessageAsync();\n\n        var result = await smtp.SendAsync();\n\n        Assert.False(fake.Called);\n        Assert.False(result.Status);\n        Assert.Equal(\"Email not sent (WhatIf)\", result.Error);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSendPipelineTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Search;\nusing MimeKit;\nusing Moq;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSendPipelineTests {\n    [Fact]\n    public void BuildExecutionResult_MapsSuccessOutcome() {\n        var result = SmtpSendPipeline.BuildExecutionResult(\n            sendRequested: true,\n            sendSucceeded: true,\n            messageId: \"msg-1@example.test\",\n            sendError: null,\n            appendedToSent: true,\n            appendedSentFolder: \"Sent\",\n            appendError: null);\n\n        Assert.True(result.Ok);\n        Assert.True(result.Sent);\n        Assert.Equal(\"msg-1@example.test\", result.MessageId);\n        Assert.Null(result.Error);\n        Assert.True(result.AppendedToSent);\n        Assert.Equal(\"Sent\", result.AppendedSentFolder);\n        Assert.Null(result.AppendError);\n    }\n\n    [Fact]\n    public void BuildExecutionResult_MapsDryRunAndAppendFailureOutcome() {\n        var result = SmtpSendPipeline.BuildExecutionResult(\n            sendRequested: false,\n            sendSucceeded: true,\n            messageId: \"msg-2@example.test\",\n            sendError: null,\n            appendedToSent: false,\n            appendedSentFolder: null,\n            appendError: \"append failed\");\n\n        Assert.True(result.Ok);\n        Assert.False(result.Sent);\n        Assert.Equal(\"msg-2@example.test\", result.MessageId);\n        Assert.Null(result.Error);\n        Assert.False(result.AppendedToSent);\n        Assert.Null(result.AppendedSentFolder);\n        Assert.Equal(\"append failed\", result.AppendError);\n    }\n\n    [Fact]\n    public async Task BuildExecutionResultWithOptionalSentAppendAsync_InvokesAppend_WhenRealSendSucceeded() {\n        var appendCalled = 0;\n\n        var result = await SmtpSendPipeline.BuildExecutionResultWithOptionalSentAppendAsync(\n            sendRequested: true,\n            sendSucceeded: true,\n            messageId: \"msg-3@example.test\",\n            sendError: null,\n            appendToSentRequested: true,\n            appendAsync: _ => {\n                appendCalled++;\n                return Task.FromResult(new SmtpAppendExecutionResult {\n                    Appended = true,\n                    Folder = \"Sent Items\",\n                    Error = null\n                });\n            });\n\n        Assert.Equal(1, appendCalled);\n        Assert.True(result.Ok);\n        Assert.True(result.Sent);\n        Assert.True(result.AppendedToSent);\n        Assert.Equal(\"Sent Items\", result.AppendedSentFolder);\n        Assert.Null(result.AppendError);\n    }\n\n    [Fact]\n    public async Task BuildExecutionResultWithOptionalSentAppendAsync_DoesNotInvokeAppend_WhenDryRun() {\n        var appendCalled = 0;\n\n        var result = await SmtpSendPipeline.BuildExecutionResultWithOptionalSentAppendAsync(\n            sendRequested: false,\n            sendSucceeded: true,\n            messageId: \"msg-4@example.test\",\n            sendError: null,\n            appendToSentRequested: true,\n            appendAsync: _ => {\n                appendCalled++;\n                return Task.FromResult(SmtpAppendExecutionResult.None);\n            });\n\n        Assert.Equal(0, appendCalled);\n        Assert.True(result.Ok);\n        Assert.False(result.Sent);\n        Assert.False(result.AppendedToSent);\n        Assert.Null(result.AppendedSentFolder);\n        Assert.Null(result.AppendError);\n    }\n\n    [Fact]\n    public async Task BuildExecutionResultWithOptionalSentAppendAsync_CapturesAppendException() {\n        var result = await SmtpSendPipeline.BuildExecutionResultWithOptionalSentAppendAsync(\n            sendRequested: true,\n            sendSucceeded: true,\n            messageId: \"msg-5@example.test\",\n            sendError: null,\n            appendToSentRequested: true,\n            appendAsync: _ => throw new InvalidOperationException(\"append crashed\"));\n\n        Assert.True(result.Ok);\n        Assert.True(result.Sent);\n        Assert.False(result.AppendedToSent);\n        Assert.Equal(\"append crashed\", result.AppendError);\n    }\n\n    [Fact]\n    public async Task TryAppendToSentAsync_AppendsMessage_WhenFolderIsWritable() {\n        var folder = new Mock<IMailFolder>();\n        var appendCalls = 0;\n        folder.SetupGet(f => f.FullName).Returns(\"Sent\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadWrite);\n\n        var result = await SmtpAppendPipeline.TryAppendToSentAsync(\n            folder.Object,\n            new MimeMessage(),\n            MessageFlags.Seen,\n            appendAsync: (_, _, _, _) => {\n                appendCalls++;\n                return Task.CompletedTask;\n            });\n\n        Assert.Equal(1, appendCalls);\n        Assert.True(result.Appended);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Null(result.Error);\n    }\n\n    [Fact]\n    public async Task TryAppendToSentAsync_ReturnsFailure_WhenAppendThrows() {\n        var folder = new Mock<IMailFolder>();\n        folder.SetupGet(f => f.FullName).Returns(\"Sent\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadWrite);\n\n        var result = await SmtpAppendPipeline.TryAppendToSentAsync(\n            folder.Object,\n            new MimeMessage(),\n            MessageFlags.Seen,\n            appendAsync: (_, _, _, _) => throw new InvalidOperationException(\"append failed\"));\n\n        Assert.False(result.Appended);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Equal(\"append failed\", result.Error);\n    }\n\n    [Fact]\n    public async Task TryAppendToSentAsync_Throws_ForInvalidArguments() {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpAppendPipeline.TryAppendToSentAsync(\n                sentFolder: null!,\n                message: new MimeMessage()));\n\n        var folder = new Mock<IMailFolder>();\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpAppendPipeline.TryAppendToSentAsync(\n                folder.Object,\n                message: null!));\n    }\n\n    [Fact]\n    public void ApplyThreadingHeaders_SetsNormalizedMessageThreadAndIdempotencyHeaders() {\n        var message = new MimeMessage();\n        var references = new List<string?> { \"ref-1@example.test\", \" <ref-1@example.test> \", \"ref-2@example.test\" };\n\n        SmtpSendPipeline.ApplyThreadingHeaders(\n            message,\n            inReplyToCandidate: \"in-reply@example.test\",\n            referenceCandidates: references,\n            messageId: \"message-id@example.test\",\n            idempotencyHeaderName: \"X-Test-Idempotency\",\n            idempotencyKey: \"idem-1\");\n\n        Assert.Equal(\"idem-1\", message.Headers[\"X-Test-Idempotency\"]);\n        Assert.Equal(\"message-id@example.test\", TrimBrackets(message.MessageId));\n        Assert.Equal(\"in-reply@example.test\", TrimBrackets(message.InReplyTo));\n\n        var refs = Canonicalize(message.References);\n        Assert.Contains(\"ref-1@example.test\", refs);\n        Assert.Contains(\"ref-2@example.test\", refs);\n        Assert.Contains(\"in-reply@example.test\", refs);\n        Assert.Equal(3, refs.Count);\n    }\n\n    [Fact]\n    public void ApplyThreadingHeaders_Throws_WhenMessageIsNull() {\n        Assert.Throws<ArgumentNullException>(() =>\n            SmtpSendPipeline.ApplyThreadingHeaders(\n                message: null!,\n                inReplyToCandidate: null,\n                referenceCandidates: null));\n    }\n\n    [Fact]\n    public async Task TryFindExistingSentCopyAsync_ReturnsMatch_FromIdempotencyHeaderProbe() {\n        var folder = new Mock<IMailFolder>();\n        var matched = new MimeMessage { MessageId = \"matched-101@example.test\" };\n        folder.SetupGet(f => f.FullName).Returns(\"Sent\");\n        folder.Setup(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(new List<UniqueId> { new(101) });\n        folder.Setup(f => f.GetMessageAsync(It.IsAny<UniqueId>(), It.IsAny<CancellationToken>(), It.IsAny<ITransferProgress>()))\n            .ReturnsAsync(matched);\n\n        var result = await SmtpSendPipeline.TryFindExistingSentCopyAsync(\n            folder.Object,\n            idempotencyHeaderName: \"X-Test-Idempotency\",\n            idempotencyKey: \"idem-101\",\n            idempotentMessageId: \"fallback@example.test\");\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Equal(\"matched-101@example.test\", result.MessageId);\n        folder.Verify(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()), Times.Once);\n    }\n\n    [Fact]\n    public async Task TryFindExistingSentCopyAsync_FallsBackToMessageIdProbe_WhenHeaderProbeMisses() {\n        var folder = new Mock<IMailFolder>();\n        var matched = new MimeMessage { MessageId = \"matched-202@example.test\" };\n        folder.SetupGet(f => f.FullName).Returns(\"Sent Items\");\n        folder.SetupSequence(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(new List<UniqueId>())\n            .ReturnsAsync(new List<UniqueId> { new(202) });\n        folder.Setup(f => f.GetMessageAsync(It.IsAny<UniqueId>(), It.IsAny<CancellationToken>(), It.IsAny<ITransferProgress>()))\n            .ReturnsAsync(matched);\n\n        var result = await SmtpSendPipeline.TryFindExistingSentCopyAsync(\n            folder.Object,\n            idempotencyHeaderName: \"X-Test-Idempotency\",\n            idempotencyKey: \"idem-202\",\n            idempotentMessageId: \"<message-202@example.test>\");\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"Sent Items\", result.Folder);\n        Assert.Equal(\"matched-202@example.test\", result.MessageId);\n        folder.Verify(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()), Times.Exactly(2));\n    }\n\n    [Fact]\n    public async Task TryFindExistingSentCopyAsync_ReturnsNone_WhenProbeThrows() {\n        var folder = new Mock<IMailFolder>();\n        folder.Setup(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ThrowsAsync(new InvalidOperationException(\"search failed\"));\n\n        var result = await SmtpSendPipeline.TryFindExistingSentCopyAsync(\n            folder.Object,\n            idempotencyHeaderName: \"X-Test-Idempotency\",\n            idempotencyKey: \"idem-500\",\n            idempotentMessageId: \"fallback@example.test\");\n\n        Assert.False(result.IsMatch);\n        Assert.Null(result.Folder);\n        Assert.Null(result.MessageId);\n    }\n\n    [Fact]\n    public async Task TryFindExistingSentCopyAsync_Throws_ForInvalidArguments() {\n        var folder = new Mock<IMailFolder>();\n\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpSendPipeline.TryFindExistingSentCopyAsync(\n                sentFolder: null!,\n                idempotencyHeaderName: \"X-Test-Idempotency\",\n                idempotencyKey: \"idem\",\n                idempotentMessageId: null));\n\n        await Assert.ThrowsAsync<ArgumentException>(() =>\n            SmtpSendPipeline.TryFindExistingSentCopyAsync(\n                folder.Object,\n                idempotencyHeaderName: \"\",\n                idempotencyKey: \"idem\",\n                idempotentMessageId: null));\n\n        await Assert.ThrowsAsync<ArgumentException>(() =>\n            SmtpSendPipeline.TryFindExistingSentCopyAsync(\n                folder.Object,\n                idempotencyHeaderName: \"X-Test-Idempotency\",\n                idempotencyKey: \"\",\n                idempotentMessageId: null));\n    }\n\n    private static List<string> Canonicalize(IEnumerable<string> values) {\n        var output = new List<string>();\n        foreach (var value in values) {\n            output.Add(TrimBrackets(value));\n        }\n        return output;\n    }\n\n    private static string TrimBrackets(string? value) {\n        if (string.IsNullOrWhiteSpace(value)) {\n            return string.Empty;\n        }\n        return (value ?? string.Empty).Trim().Trim('<', '>');\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSentFolderSessionPipelineTests.cs",
    "content": "using System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing MailKit;\nusing MailKit.Net.Imap;\nusing MailKit.Search;\nusing MimeKit;\nusing Moq;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSentFolderSessionPipelineTests {\n    [Fact]\n    public async Task TryFindExistingSentCopyAsync_UsesConnectedSession_AndDisconnects() {\n        var connectCalls = 0;\n        var resolveCalls = 0;\n        var disconnectCalls = 0;\n\n        var folder = new Mock<IMailFolder>();\n        var matched = new MimeMessage { MessageId = \"matched@example.test\" };\n        folder.SetupGet(f => f.FullName).Returns(\"Sent\");\n        folder.Setup(f => f.SearchAsync(It.IsAny<SearchQuery>(), It.IsAny<CancellationToken>()))\n            .ReturnsAsync(new List<UniqueId> { new(42) });\n        folder.Setup(f => f.GetMessageAsync(It.IsAny<UniqueId>(), It.IsAny<CancellationToken>(), It.IsAny<ITransferProgress>()))\n            .ReturnsAsync(matched);\n\n        var result = await SmtpSentFolderSessionPipeline.TryFindExistingSentCopyAsync(\n            connectAsync: _ => {\n                connectCalls++;\n                return Task.FromResult(new ImapClient());\n            },\n            resolveSentFolderAsync: (_, _) => {\n                resolveCalls++;\n                return Task.FromResult(folder.Object);\n            },\n            idempotencyHeaderName: \"X-Test-Idempotency\",\n            idempotencyKey: \"idem-42\",\n            idempotentMessageId: \"fallback@example.test\",\n            disconnectAsync: (_, _) => {\n                disconnectCalls++;\n                return Task.CompletedTask;\n            });\n\n        Assert.True(result.IsMatch);\n        Assert.Equal(\"Sent\", result.Folder);\n        Assert.Equal(\"matched@example.test\", result.MessageId);\n        Assert.Equal(1, connectCalls);\n        Assert.Equal(1, resolveCalls);\n        Assert.Equal(1, disconnectCalls);\n    }\n\n    [Fact]\n    public async Task TryAppendToSentAsync_UsesConnectedSession_AndDisconnects() {\n        var connectCalls = 0;\n        var resolveCalls = 0;\n        var appendCalls = 0;\n        var disconnectCalls = 0;\n\n        var folder = new Mock<IMailFolder>();\n        folder.SetupGet(f => f.FullName).Returns(\"Sent Items\");\n        folder.SetupGet(f => f.IsOpen).Returns(true);\n        folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadWrite);\n\n        var result = await SmtpSentFolderSessionPipeline.TryAppendToSentAsync(\n            connectAsync: _ => {\n                connectCalls++;\n                return Task.FromResult(new ImapClient());\n            },\n            resolveSentFolderAsync: (_, _) => {\n                resolveCalls++;\n                return Task.FromResult(folder.Object);\n            },\n            message: new MimeMessage(),\n            flags: MessageFlags.Seen,\n            appendAsync: (_, _, _, _) => {\n                appendCalls++;\n                return Task.CompletedTask;\n            },\n            disconnectAsync: (_, _) => {\n                disconnectCalls++;\n                return Task.CompletedTask;\n            });\n\n        Assert.True(result.Appended);\n        Assert.Equal(\"Sent Items\", result.Folder);\n        Assert.Null(result.Error);\n        Assert.Equal(1, connectCalls);\n        Assert.Equal(1, resolveCalls);\n        Assert.Equal(1, appendCalls);\n        Assert.Equal(1, disconnectCalls);\n    }\n\n    [Fact]\n    public async Task TryAppendToSentAsync_Throws_ForInvalidArguments() {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpSentFolderSessionPipeline.TryAppendToSentAsync(\n                connectAsync: null!,\n                resolveSentFolderAsync: (_, _) => Task.FromResult(Mock.Of<IMailFolder>()),\n                message: new MimeMessage()));\n\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpSentFolderSessionPipeline.TryAppendToSentAsync(\n                connectAsync: _ => Task.FromResult(new ImapClient()),\n                resolveSentFolderAsync: null!,\n                message: new MimeMessage()));\n\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpSentFolderSessionPipeline.TryAppendToSentAsync(\n                connectAsync: _ => Task.FromResult(new ImapClient()),\n                resolveSentFolderAsync: (_, _) => Task.FromResult(Mock.Of<IMailFolder>()),\n                message: null!));\n    }\n\n    [Fact]\n    public async Task TryGetThreadingMetadataAsync_UsesConnectedSession_AndDisconnects() {\n        var connectCalls = 0;\n        var metadataCalls = 0;\n        var disconnectCalls = 0;\n        var expected = new ImapSentMessageOperations.ImapThreadingMetadataResult {\n            MessageId = \"child@example.test\",\n            InReplyTo = \"parent@example.test\"\n        };\n\n        var actual = await SmtpSentFolderSessionPipeline.TryGetThreadingMetadataAsync(\n            connectAsync: _ => {\n                connectCalls++;\n                return Task.FromResult(new ImapClient());\n            },\n            folder: \"INBOX\",\n            uid: 42,\n            getMetadataAsync: (_, folder, uid, _) => {\n                metadataCalls++;\n                Assert.Equal(\"INBOX\", folder);\n                Assert.Equal((uint)42, uid);\n                return Task.FromResult<ImapSentMessageOperations.ImapThreadingMetadataResult?>(expected);\n            },\n            disconnectAsync: (_, _) => {\n                disconnectCalls++;\n                return Task.CompletedTask;\n            });\n\n        Assert.Same(expected, actual);\n        Assert.Equal(1, connectCalls);\n        Assert.Equal(1, metadataCalls);\n        Assert.Equal(1, disconnectCalls);\n    }\n\n    [Fact]\n    public async Task TryGetThreadingMetadataAsync_Throws_ForInvalidArguments() {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            SmtpSentFolderSessionPipeline.TryGetThreadingMetadataAsync(\n                connectAsync: null!,\n                folder: \"INBOX\",\n                uid: 1));\n\n        await Assert.ThrowsAsync<ArgumentException>(() =>\n            SmtpSentFolderSessionPipeline.TryGetThreadingMetadataAsync(\n                connectAsync: _ => Task.FromResult(new ImapClient()),\n                folder: \" \",\n                uid: 1));\n\n        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>\n            SmtpSentFolderSessionPipeline.TryGetThreadingMetadataAsync(\n                connectAsync: _ => Task.FromResult(new ImapClient()),\n                folder: \"INBOX\",\n                uid: 0));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSessionServiceTests.cs",
    "content": "using System;\nusing System.Threading.Tasks;\nusing MailKit.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSessionServiceTests {\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_ReturnsSuccess() {\n        var request = new SmtpSessionRequest {\n            Server = \"smtp.test\",\n            Port = 587,\n            SecureSocketOptions = SecureSocketOptions.Auto,\n            UserName = \"user\",\n            Password = \"pass\",\n            ConnectAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Connect, string.Empty, string.Empty, \"smtp.test\", 587, TimeSpan.Zero)),\n            AuthenticateAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Authenticate, string.Empty, string.Empty, \"smtp.test\", 587, TimeSpan.Zero))\n        };\n\n        var smtp = new Smtp();\n        var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);\n\n        Assert.True(result.IsSuccess);\n        Assert.Equal(SecureSocketOptions.Auto, result.SecureSocketOptions);\n        Assert.Null(result.ErrorCode);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_FlagsConnectFailures() {\n        var request = new SmtpSessionRequest {\n            Server = \"smtp.test\",\n            Port = 587,\n            SecureSocketOptions = SecureSocketOptions.Auto,\n            UserName = \"user\",\n            Password = \"pass\",\n            ConnectAsync = _ => Task.FromResult(new SmtpResult(false, EmailAction.Connect, string.Empty, string.Empty, \"smtp.test\", 587, TimeSpan.Zero, error: \"nope\"))\n        };\n\n        var smtp = new Smtp();\n        var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);\n\n        Assert.False(result.IsSuccess);\n        Assert.Equal(\"connect_failed\", result.ErrorCode);\n        Assert.Equal(\"nope\", result.Error);\n        Assert.True(result.IsTransient);\n    }\n\n    [Fact]\n    public async Task ConnectAndAuthenticateAsync_FlagsAuthFailures() {\n        var request = new SmtpSessionRequest {\n            Server = \"smtp.test\",\n            Port = 587,\n            SecureSocketOptions = SecureSocketOptions.Auto,\n            UserName = \"user\",\n            Password = \"pass\",\n            ConnectAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Connect, string.Empty, string.Empty, \"smtp.test\", 587, TimeSpan.Zero)),\n            AuthenticateAsync = _ => Task.FromResult(new SmtpResult(false, EmailAction.Authenticate, string.Empty, string.Empty, \"smtp.test\", 587, TimeSpan.Zero, error: \"bad auth\"))\n        };\n\n        var smtp = new Smtp();\n        var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);\n\n        Assert.False(result.IsSuccess);\n        Assert.Equal(\"auth_failed\", result.ErrorCode);\n        Assert.Equal(\"bad auth\", result.Error);\n        Assert.False(result.IsTransient);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSignTests.cs",
    "content": "using System;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSignTests\n{\n    [Fact]\n    public void Sign_MissingCertificate_ReturnsFailedResult()\n    {\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n\n        var result = smtp.Sign(\"0000000000000000000000000000000000000000\");\n\n        Assert.False(result.Status);\n        Assert.Equal(\"Certificate not found in the store.\", result.Error);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpSslOptionTests.cs",
    "content": "using System.Reflection;\nusing System.Threading.Tasks;\nusing MailKit.Security;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpSslOptionTests\n{\n    private class FakeClient : ClientSmtp\n    {\n        public SecureSocketOptions? Options;\n        public override void Connect(string host, int port, SecureSocketOptions options, System.Threading.CancellationToken cancellationToken = default)\n            => Options = options;\n        public override Task ConnectAsync(string host, int port, SecureSocketOptions options, System.Threading.CancellationToken cancellationToken = default)\n        {\n            Options = options;\n            return Task.CompletedTask;\n        }\n    }\n\n    [Fact]\n    public void Connect_WithUseSslAndExplicitOption_DoesNotOverride()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeClient();\n        typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(smtp, fake);\n\n        smtp.Connect(\"h\", 25, SecureSocketOptions.SslOnConnect, true);\n\n        Assert.Equal(SecureSocketOptions.SslOnConnect, fake.Options);\n    }\n\n    [Fact]\n    public async Task ConnectAsync_WithUseSslAndExplicitOption_DoesNotOverride()\n    {\n        var smtp = new Smtp();\n        var fake = new FakeClient();\n        typeof(Smtp).GetField(\"<Client>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(smtp, fake);\n\n        _ = await smtp.ConnectAsync(\"h\", 25, SecureSocketOptions.SslOnConnect, true);\n\n        Assert.Equal(SecureSocketOptions.SslOnConnect, fake.Options);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/SmtpValidationTests.cs",
    "content": "using Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class SmtpValidationTests {\n    [Fact]\n    public void TryValidateServer_WithMissingServer_Fails() {\n        var ok = SmtpValidation.TryValidateServer(null, 25, out var error);\n        Assert.False(ok);\n        Assert.Contains(\"server\", error);\n    }\n\n    [Fact]\n    public void TryValidateServer_WithInvalidPort_Fails() {\n        var ok = SmtpValidation.TryValidateServer(\"smtp.example.com\", 0, out var error);\n        Assert.False(ok);\n        Assert.Contains(\"port\", error);\n    }\n\n    [Fact]\n    public void TryValidateServer_WithValidInputs_Succeeds() {\n        var ok = SmtpValidation.TryValidateServer(\"smtp.example.com\", 25, out var error);\n        Assert.True(ok);\n        Assert.Null(error);\n    }\n\n    [Fact]\n    public void TryValidateCredentials_WithMissingUsername_Fails() {\n        var ok = SmtpValidation.TryValidateCredentials(null, \"secret\", out var error);\n        Assert.False(ok);\n        Assert.Contains(\"username\", error);\n    }\n\n    [Fact]\n    public void TryValidateCredentials_WithMissingPassword_Fails() {\n        var ok = SmtpValidation.TryValidateCredentials(\"user\", null, out var error);\n        Assert.False(ok);\n        Assert.Contains(\"password\", error);\n    }\n\n    [Fact]\n    public void TryValidateCredentials_WithValidInputs_Succeeds() {\n        var ok = SmtpValidation.TryValidateCredentials(\"user\", \"secret\", out var error);\n        Assert.True(ok);\n        Assert.Null(error);\n    }\n}\n\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/TemporaryPgpKeyPairTests.cs",
    "content": "using Mailozaurr;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class TemporaryPgpKeyPairTests\n{\n    [Fact]\n    public void Create_ReturnsFiles()\n    {\n        using var keys = TemporaryPgpKeyPair.Create(\"a@b.com\");\n        Assert.True(File.Exists(keys.PublicKeyPath));\n        Assert.True(File.Exists(keys.PrivateKeyPath));\n    }\n\n    [Fact(Skip = \"PGP sign and encrypt requires additional configuration in CI\" )]\n    public void KeyPair_CanSignAndEncryptMessage()\n    {\n        using var keys = TemporaryPgpKeyPair.Create(\"a@b.com\");\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n        var result = smtp.PgpSignAndEncrypt(keys.PublicKeyPath, keys.PrivateKeyPath, keys.PassPhrase, false);\n        Assert.True(result.Status, result.Error);\n    }\n\n    [Fact]\n    public void KeyPair_CanDecryptEncryptedMessage()\n    {\n        using var keys = TemporaryPgpKeyPair.Create(\"a@b.com\");\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"a@b.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"secret\";\n        smtp.CreateMessage();\n        var encResult = smtp.PgpEncrypt(keys.PublicKeyPath);\n        Assert.True(encResult.Status, encResult.Error);\n        string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\n        smtp.Message.WriteTo(path);\n        string decrypted = keys.DecryptToString(path);\n        Assert.Contains(\"secret\", decrypted);\n        File.Delete(path);\n    }\n\n    [Fact]\n    public void Dispose_RemovesGeneratedFiles()\n    {\n        string pub;\n        string priv;\n        string dir;\n        using (var keys = TemporaryPgpKeyPair.Create(\"a@b.com\"))\n        {\n            pub = keys.PublicKeyPath;\n            priv = keys.PrivateKeyPath;\n            dir = Path.GetDirectoryName(pub)!;\n            Assert.True(File.Exists(pub));\n            Assert.True(File.Exists(priv));\n        }\n\n        Assert.False(File.Exists(pub));\n        Assert.False(File.Exists(priv));\n        Assert.False(Directory.Exists(dir));\n    }\n\n    [Fact]\n    public void Dispose_WhenDeletionFails_LogsWarnings()\n    {\n        var pair = TemporaryPgpKeyPair.Create(\"a@b.com\");\n        string originalDir = Path.GetDirectoryName(pair.PublicKeyPath)!;\n\n        string protectedFile;\n        string protectedDir;\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n        {\n            protectedFile = Path.Combine(Environment.SystemDirectory, \"kernel32.dll\");\n            protectedDir = Path.Combine(Environment.SystemDirectory, \"drivers\");\n        }\n        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            protectedFile = \"/System/Library/CoreServices/SystemVersion.plist\";\n            protectedDir = \"/System/Library\";\n        }\n        else\n        {\n            protectedFile = \"/proc/version\";\n            protectedDir = \"/proc/self/fd\";\n        }\n\n        var type = typeof(TemporaryPgpKeyPair);\n        type.GetField(\"<PublicKeyPath>k__BackingField\", BindingFlags.Instance | BindingFlags.NonPublic)!\n            .SetValue(pair, protectedFile);\n        type.GetField(\"_tempDirectory\", BindingFlags.Instance | BindingFlags.NonPublic)!\n            .SetValue(pair, protectedDir);\n\n        var messages = new List<string>();\n        void Handler(object? _, LogEventArgs e) => messages.Add(e.Message);\n        LoggingMessages.Logger.OnWarningMessage += Handler;\n\n        pair.Dispose();\n\n        LoggingMessages.Logger.OnWarningMessage -= Handler;\n\n        if (Directory.Exists(originalDir))\n            Directory.Delete(originalDir, true);\n\n        Assert.Contains(messages, static m => m.Contains(\"Failed to delete public key\"));\n        Assert.Contains(messages, static m => m.Contains(\"Failed to delete temporary directory\"));\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/TemporarySmimeCertificateTests.cs",
    "content": "using Mailozaurr;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography.X509Certificates;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class TemporarySmimeCertificateTests\n{\n    [Fact]\n    public void CreateSelfSigned_ReturnsUsableCertificate()\n    {\n        // Skip test on macOS due to certificate compatibility issues\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            return;\n        }\n\n        using X509Certificate2 cert = TemporarySmimeCertificate.CreateSelfSigned();\n        Assert.True(cert.HasPrivateKey);\n        Assert.NotNull(cert.Subject);\n    }\n\n    [Fact]\n    public void Certificate_CanSignAndEncryptMessage()\n    {\n        // Skip test on macOS due to certificate compatibility issues\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            return;\n        }\n\n        using X509Certificate2 cert = TemporarySmimeCertificate.CreateSelfSigned();\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n        var signResult = smtp.Sign(cert);\n        Assert.True(signResult.Status);\n        smtp.CreateMessage();\n        var encryptResult = smtp.Encrypt(cert);\n        Assert.True(encryptResult.Status);\n    }\n\n    [Fact]\n    public void Certificate_CanVerifySmimeSignature()\n    {\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            return;\n        }\n\n        using X509Certificate2 cert = TemporarySmimeCertificate.CreateSelfSigned();\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n\n        var signResult = smtp.Sign(cert);\n\n        Assert.True(signResult.Status);\n        Assert.True(MimeKitUtils.VerifySmimeSignature(smtp.Message, cert));\n    }\n\n    [Fact]\n    public void Certificate_CanDecryptSmimeMessage()\n    {\n        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n        {\n            return;\n        }\n\n        using X509Certificate2 cert = TemporarySmimeCertificate.CreateSelfSigned();\n        var smtp = new Smtp();\n        smtp.From = \"a@b.com\";\n        smtp.To = new object[] { \"c@d.com\" };\n        smtp.Subject = \"test\";\n        smtp.TextBody = \"body\";\n        smtp.CreateMessage();\n\n        var encryptResult = smtp.Encrypt(cert);\n        var decrypted = MimeKitUtils.DecryptSmime(smtp.Message, cert);\n\n        Assert.True(encryptResult.Status);\n        Assert.Equal(\"body\", decrypted.TextBody);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/TestParallelization.cs",
    "content": "using Xunit;\n\n[assembly: CollectionBehavior(DisableTestParallelization = true)]"
  },
  {
    "path": "Sources/Mailozaurr.Tests/TokenCacheHelperTests.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Reflection;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace Mailozaurr.Tests;\n\npublic sealed class TokenCacheHelperTests : IDisposable {\n    private readonly string _cachePath;\n\n    public TokenCacheHelperTests() {\n        var directory = Path.Combine(Path.GetTempPath(), \"Mailozaurr.Tests\", \"TokenCache\", Process.GetCurrentProcess().Id.ToString(), Guid.NewGuid().ToString(\"N\"));\n        Directory.CreateDirectory(directory);\n        _cachePath = Path.Combine(directory, \"msal_cache.bin\");\n        Environment.SetEnvironmentVariable(\"MAILOZAURR_MSAL_CACHE_PATH\", _cachePath);\n        DeleteCacheFile();\n    }\n\n    public void Dispose() {\n        DeleteCacheFile();\n        var directory = Path.GetDirectoryName(_cachePath);\n        if (!string.IsNullOrWhiteSpace(directory) && Directory.Exists(directory)) {\n            Directory.Delete(directory, recursive: true);\n        }\n        Environment.SetEnvironmentVariable(\"MAILOZAURR_MSAL_CACHE_PATH\", null);\n    }\n\n    [Fact]\n    public async Task ReadCacheData_RetriesTransientFileLockAndReturnsBytes() {\n        var path = GetCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        var expected = Encoding.UTF8.GetBytes(\"cached-token-data\");\n        File.WriteAllBytes(path, expected);\n\n        var method = typeof(TokenCacheHelper).GetMethod(\"ReadCacheData\", BindingFlags.Static | BindingFlags.NonPublic);\n        Assert.NotNull(method);\n\n        var lockStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);\n        try {\n            var releaseTask = Task.Run(async () => {\n                await Task.Delay(75);\n                lockStream.Dispose();\n            });\n\n            var actual = (byte[]?)method!.Invoke(null, null);\n            await releaseTask;\n\n            Assert.NotNull(actual);\n            Assert.Equal(expected, actual);\n        } finally {\n            lockStream.Dispose();\n        }\n    }\n\n    [Fact]\n    public async Task WriteCacheData_RetriesTransientFileLockAndPersistsBytes() {\n        var path = GetCacheFilePath();\n        var directory = Path.GetDirectoryName(path);\n        if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) {\n            Directory.CreateDirectory(directory);\n        }\n\n        File.WriteAllBytes(path, new byte[] { 0x00 });\n\n        var method = typeof(TokenCacheHelper).GetMethod(\"WriteCacheData\", BindingFlags.Static | BindingFlags.NonPublic);\n        Assert.NotNull(method);\n\n        var expected = Encoding.UTF8.GetBytes(\"updated-token-data\");\n        var lockStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);\n        try {\n            var releaseTask = Task.Run(async () => {\n                await Task.Delay(75);\n                lockStream.Dispose();\n            });\n\n            method!.Invoke(null, new object[] { expected });\n            await releaseTask;\n\n            Assert.Equal(expected, File.ReadAllBytes(path));\n        } finally {\n            lockStream.Dispose();\n        }\n    }\n\n    private static string GetCacheFilePath() {\n        return Environment.GetEnvironmentVariable(\"MAILOZAURR_MSAL_CACHE_PATH\")!;\n    }\n\n    private static void DeleteCacheFile() {\n        var path = GetCacheFilePath();\n        if (File.Exists(path)) {\n            File.Delete(path);\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.Tests/ValidatorTests.cs",
    "content": "using System;\nusing System.Reflection;\nusing Xunit;\n\nnamespace Mailozaurr.Tests;\n\npublic class ValidatorTests\n{\n    [Fact]\n    public void ValidateEmail_DisposableDomain_IsMarkedDisposable()\n    {\n        var result = Validator.ValidateEmail(\"user@example.com\");\n        Assert.True(result.IsValid);\n        Assert.True(result.IsDisposable);\n    }\n\n    [Fact]\n    public void ValidateEmail_AllowedDomain_NotDisposable()\n    {\n        var result = Validator.ValidateEmail(\"user@allowed.example.com\");\n        Assert.True(result.IsValid);\n        Assert.False(result.IsDisposable);\n    }\n\n    [Fact]\n    public void ValidateEmail_InvalidEmail_ReturnsFalse()\n    {\n        var result = Validator.ValidateEmail(\"invalid\");\n        Assert.False(result.IsValid);\n    }\n}\n"
  },
  {
    "path": "Sources/Mailozaurr.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.3.32804.467\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Mailozaurr.PowerShell\", \"Mailozaurr.PowerShell\\Mailozaurr.PowerShell.csproj\", \"{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}\"\n\tProjectSection(ProjectDependencies) = postProject\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC} = {74FF5540-DB31-4031-BBE1-1A471A17CCFC}\n\tEndProjectSection\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Mailozaurr.Examples\", \"Mailozaurr.Examples\\Mailozaurr.Examples.csproj\", \"{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}\"\n\tProjectSection(ProjectDependencies) = postProject\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC} = {74FF5540-DB31-4031-BBE1-1A471A17CCFC}\n\tEndProjectSection\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Mailozaurr\", \"Mailozaurr\\Mailozaurr.csproj\", \"{74FF5540-DB31-4031-BBE1-1A471A17CCFC}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Mailozaurr.Tests\", \"Mailozaurr.Tests\\Mailozaurr.Tests.csproj\", \"{707AD94F-2462-450D-92D0-6E239E141F68}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Mailozaurr.Msg\", \"Mailozaurr.Msg\\Mailozaurr.Msg.csproj\", \"{23C45348-807F-462A-866E-9F90E81DBDBB}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Mailozaurr.Application\", \"Mailozaurr.Application\\Mailozaurr.Application.csproj\", \"{6844B2F5-759E-461A-9B8B-F6B492B492F7}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Mailozaurr.Cli\", \"Mailozaurr.Cli\\Mailozaurr.Cli.csproj\", \"{77665733-679C-492B-8AA4-9793DF741FA5}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tDebug|x64 = Debug|x64\n\t\tDebug|x86 = Debug|x86\n\t\tRelease|Any CPU = Release|Any CPU\n\t\tRelease|x64 = Release|x64\n\t\tRelease|x86 = Release|x86\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|x64.Build.0 = Release|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{EEF3BFAD-C8A5-474C-BA52-5C20E66EF0F9}.Release|x86.Build.0 = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|x64.Build.0 = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{FC455BBE-52F1-4FD3-AC16-F76172C9AAD2}.Release|x86.Build.0 = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|x64.Build.0 = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{74FF5540-DB31-4031-BBE1-1A471A17CCFC}.Release|x86.Build.0 = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|x64.Build.0 = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{707AD94F-2462-450D-92D0-6E239E141F68}.Release|x86.Build.0 = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|x64.Build.0 = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{23C45348-807F-462A-866E-9F90E81DBDBB}.Release|x86.Build.0 = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|x64.Build.0 = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{6844B2F5-759E-461A-9B8B-F6B492B492F7}.Release|x86.Build.0 = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|x64.Build.0 = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{77665733-679C-492B-8AA4-9793DF741FA5}.Release|x86.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {F37BB785-3AB1-4498-8102-689C0E4C3924}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "Sources/Mailozaurr.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Mailozaurr/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=personalizations/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "Tests/Add-GraphMailboxPermission.Tests.ps1",
    "content": "Describe 'Add-GraphMailboxPermission' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Add-GraphMailboxPermission -UserPrincipalName 'u' -Permission @{ Role = 'read' } -WhatIf -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Add-GraphMailboxPermission - Connection not provided and no default session available.'\n    }\n\n    It 'Accepts multiple permission objects' {\n        $p1 = [Mailozaurr.GraphMailboxPermission]::new()\n        $p2 = [Mailozaurr.GraphMailboxPermission]::new()\n        Add-GraphMailboxPermission -UserPrincipalName 'u' -MailboxPermission @($p1,$p2) -WhatIf -WarningVariable warn2\n        $warn2 | Should -Not -BeNullOrEmpty\n        $warn2[0] | Should -Be 'Add-GraphMailboxPermission - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Clear-GraphJunk.Tests.ps1",
    "content": "Describe 'Clear-GraphJunk' {\n    It 'Warns when Graph connection missing with -WhatIf and SkipId' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Clear-GraphJunk -UserPrincipalName 'u' -WhatIf -SkipId '1' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Clear-GraphJunk - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when Graph connection missing with -Preview and SkipFrom' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Clear-GraphJunk -UserPrincipalName 'u' -Preview -SkipFrom 'a' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Clear-GraphJunk - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when Graph connection missing with -WhatIf and SkipHasAttachment' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Clear-GraphJunk -UserPrincipalName 'u' -WhatIf -SkipHasAttachment -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Clear-GraphJunk - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when Graph connection missing with -Preview and SkipAttachmentExtension' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Clear-GraphJunk -UserPrincipalName 'u' -Preview -SkipAttachmentExtension 'pdf' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Clear-GraphJunk - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Clear-IMAPJunk.Tests.ps1",
    "content": "Describe 'Clear-IMAPJunk' {\n    It 'Throws when IMAP connection missing with -WhatIf and SkipFrom' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Clear-IMAPJunk -Client $info -WhatIf -SkipFrom 'a' } | Should -Throw\n    }\n\n    It 'Throws when IMAP connection missing with -Preview and SkipUid' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Clear-IMAPJunk -Client $info -Preview -SkipUid 1 } | Should -Throw\n    }\n\n    It 'Throws when IMAP connection missing with -WhatIf and SkipHasAttachment' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Clear-IMAPJunk -Client $info -WhatIf -SkipHasAttachment } | Should -Throw\n    }\n\n    It 'Throws when IMAP connection missing with -Preview and SkipAttachmentExtension' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Clear-IMAPJunk -Client $info -Preview -SkipAttachmentExtension 'zip' } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Clear-SmtpConnectionPool.Tests.ps1",
    "content": "Describe 'Clear-SmtpConnectionPool' {\n    It 'Forces new connection after clearing pool' {\n        $refs = @(\n            [Mailozaurr.Smtp].Assembly.Location,\n            [Mailozaurr.ClientSmtp].Assembly.Location,\n            [MimeKit.MimeMessage].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n        if (-not ('FakeClientPsClearPool' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class FakeClientPs2 : ClientSmtp {\n    public static int ConnectCalls;\n    private bool _connected;\n    public override bool IsConnected => _connected;\n    public override void Connect(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        ConnectCalls++;\n        _connected = true;\n    }\n    public override void Disconnect(bool quit, CancellationToken cancellationToken = default) {\n        _connected = false;\n    }\n    public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken, ITransferProgress progress) {\n        message.MessageId = message.MessageId ?? \"fake-\" + ConnectCalls.ToString();\n        return Task.FromResult(message.MessageId);\n    }\n}\npublic static class FakeClientPsClearPool {\n    public static ClientSmtp Create(ProtocolLogger logger) => new FakeClientPs2();\n    public static void Install() => Smtp.ClientFactory = Create;\n    public static void Reset() => FakeClientPs2.ConnectCalls = 0;\n}\n\"@\n        }\n\n        try {\n            [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [FakeClientPsClearPool]::Reset()\n            [FakeClientPsClearPool]::Install()\n\n            $params = @{ From='a@b.com'; To='c@d.com'; Server='h'; Subject='t'; Text='b'; UseConnectionPool=$true }\n            Send-EmailMessage @params | Out-Null\n            Send-EmailMessage @params | Out-Null\n\n            Clear-SmtpConnectionPool\n\n            Send-EmailMessage @params | Out-Null\n\n            [FakeClientPs2]::ConnectCalls | Should -Be 2\n        } finally {\n            [Mailozaurr.Smtp]::ResetClientFactory()\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Connect-EmailGraph.Tests.ps1",
    "content": "Describe 'Connect-EmailGraph cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletConnectEmailGraph].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n\n    It 'exposes ClientSecretSecureString as a SecureString parameter' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('ClientSecretSecureString') | Should -BeTrue\n        $command.Parameters['ClientSecretSecureString'].ParameterType | Should -Be ([securestring])\n    }\n\n    It 'exposes CertificatePasswordSecureString as a SecureString parameter' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('CertificatePasswordSecureString') | Should -BeTrue\n        $command.Parameters['CertificatePasswordSecureString'].ParameterType | Should -Be ([securestring])\n    }\n\n    It 'exposes OnBehalfOfTokenSecureString as a SecureString parameter' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('OnBehalfOfTokenSecureString') | Should -BeTrue\n        $command.Parameters['OnBehalfOfTokenSecureString'].ParameterType | Should -Be ([securestring])\n    }\n\n    It 'exposes ClientSecretSecretName for vault-based client secret resolution' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('ClientSecretSecretName') | Should -BeTrue\n        $command.Parameters['ClientSecretSecretName'].ParameterType | Should -Be ([string])\n    }\n\n    It 'exposes CertificatePasswordSecretName for vault-based certificate password resolution' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('CertificatePasswordSecretName') | Should -BeTrue\n        $command.Parameters['CertificatePasswordSecretName'].ParameterType | Should -Be ([string])\n    }\n\n    It 'exposes OnBehalfOfTokenSecretName for vault-based token resolution' {\n        $command = Get-Command Connect-EmailGraph\n\n        $command.Parameters.ContainsKey('OnBehalfOfTokenSecretName') | Should -BeTrue\n        $command.Parameters['OnBehalfOfTokenSecretName'].ParameterType | Should -Be ([string])\n    }\n}\n"
  },
  {
    "path": "Tests/Connect-OAuthGoogle.Tests.ps1",
    "content": "Describe 'Connect-OAuthGoogle cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletConnectOAuthGoogle].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n\n    It 'exposes ClientSecretSecureString as a SecureString parameter' {\n        $command = Get-Command Connect-OAuthGoogle\n\n        $command.Parameters.ContainsKey('ClientSecretSecureString') | Should -BeTrue\n        $command.Parameters['ClientSecretSecureString'].ParameterType | Should -Be ([securestring])\n    }\n\n    It 'exposes ClientSecretSecretName for vault-based resolution' {\n        $command = Get-Command Connect-OAuthGoogle\n\n        $command.Parameters.ContainsKey('ClientSecretSecretName') | Should -BeTrue\n        $command.Parameters['ClientSecretSecretName'].ParameterType | Should -Be ([string])\n    }\n\n    It 'exposes ClientSecretVaultName for vault selection' {\n        $command = Get-Command Connect-OAuthGoogle\n\n        $command.Parameters.ContainsKey('ClientSecretVaultName') | Should -BeTrue\n        $command.Parameters['ClientSecretVaultName'].ParameterType | Should -Be ([string])\n    }\n}\n"
  },
  {
    "path": "Tests/Connect-OAuthO365.Tests.ps1",
    "content": "Describe 'Connect-OAuthO365 cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletConnectOAuthO365].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n}\n\n"
  },
  {
    "path": "Tests/Connect-POP3.Tests.ps1",
    "content": "Describe 'Connect-POP3 SSL Options' {\n    It 'Forwards provided enum to Pop3Connector' {\n        $refs = @(\n            [Mailozaurr.Pop3Connector].Assembly.Location,\n            [MailKit.Net.Pop3.Pop3Client].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n        if (-not ('FakePop3Client' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class FakePop3Client : Pop3Client {\n    public SecureSocketOptions Passed;\n    public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        Passed = options;\n        return Task.CompletedTask;\n    }\n    public override bool IsConnected => true;\n    public override bool IsAuthenticated => true;\n}\npublic static class FakePop3ClientFactory {\n    public static Pop3Client Client;\n    public static Pop3Client Create() => Client;\n    public static void Install() => Pop3Connector.ClientFactory = Create;\n}\n\"@\n        }\n\n        $fake = [FakePop3Client]::new()\n        [FakePop3ClientFactory]::Client = $fake\n        [FakePop3ClientFactory]::Install()\n\n        Connect-POP3 -Server 'h' -UserName 'u' -Password 'p' -Port 995 -Options SslOnConnect | Out-Null\n\n        $fake.Passed | Should -Be ([MailKit.Security.SecureSocketOptions]::SslOnConnect)\n\n        [Mailozaurr.Pop3Connector]::ResetClientFactory()\n    }\n\n    It 'Uses StartTls when EnableExplicit set and Auto option' {\n        $refs = @(\n            [Mailozaurr.Pop3Connector].Assembly.Location,\n            [MailKit.Net.Pop3.Pop3Client].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n        if (-not ('FakePop3Client' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class FakePop3Client : Pop3Client {\n    public SecureSocketOptions Passed;\n    public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        Passed = options;\n        return Task.CompletedTask;\n    }\n    public override bool IsConnected => true;\n    public override bool IsAuthenticated => true;\n}\npublic static class FakePop3ClientFactory {\n    public static Pop3Client Client;\n    public static Pop3Client Create() => Client;\n    public static void Install() => Pop3Connector.ClientFactory = Create;\n}\n\"@\n        }\n\n        $fake = [FakePop3Client]::new()\n        [FakePop3ClientFactory]::Client = $fake\n        [FakePop3ClientFactory]::Install()\n\n        Connect-POP3 -Server 'h' -UserName 'u' -Password 'p' -Port 995 -EnableExplicit | Out-Null\n\n        $fake.Passed | Should -Be ([MailKit.Security.SecureSocketOptions]::StartTls)\n\n        [Mailozaurr.Pop3Connector]::ResetClientFactory()\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertFromGraphCredential.Tests.ps1",
    "content": "Describe 'MicrosoftGraphUtils.ConvertFromGraphCredential' {\n    It 'Parses \"client@tenant\" credential' {\n        $cred = [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('client@tenant', 'secret')\n        $cred.ClientId | Should -Be 'client'\n        $cred.DirectoryId | Should -Be 'tenant'\n        $cred.ClientSecret | Should -Be 'secret'\n    }\n\n    It 'Parses credential with whitespace' {\n        $cred = [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('  client@tenant  ', 'secret')\n        $cred.ClientId | Should -Be 'client'\n        $cred.DirectoryId | Should -Be 'tenant'\n        $cred.ClientSecret | Should -Be 'secret'\n    }\n    It 'Throws when username is null' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential($null, 'secret') } | Should -Throw\n    }\n    It 'Throws when username is whitespace' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('   ', 'secret') } | Should -Throw\n    }\n    It 'Throws when password is null' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('client@tenant', $null) } | Should -Throw\n    }\n    It 'Throws when password is whitespace' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('client@tenant', '   ') } | Should -Throw\n    }\n    It 'Throws for invalid format' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('invalid', 'pwd') } | Should -Throw\n    }\n\n    It 'Throws for invalid format with multiple @' {\n        { [Mailozaurr.MicrosoftGraphUtils]::ConvertFromGraphCredential('client@tenant@other', 'pwd') } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertTo-GraphCertificateCredential.Tests.ps1",
    "content": "Describe 'ConvertTo-GraphCertificateCredential cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletConvertToGraphCertificateCredential].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n\n    It 'exposes CertificatePasswordSecureString as a SecureString parameter' {\n        $command = Get-Command ConvertTo-GraphCertificateCredential\n\n        $command.Parameters.ContainsKey('CertificatePasswordSecureString') | Should -BeTrue\n        $command.Parameters['CertificatePasswordSecureString'].ParameterType | Should -Be ([securestring])\n    }\n\n    It 'exposes SecretName for vault-based certificate password resolution' {\n        $command = Get-Command ConvertTo-GraphCertificateCredential\n\n        $command.Parameters.ContainsKey('SecretName') | Should -BeTrue\n        $command.Parameters['SecretName'].ParameterType | Should -Be ([string])\n    }\n\n    It 'exposes VaultName for vault selection' {\n        $command = Get-Command ConvertTo-GraphCertificateCredential\n\n        $command.Parameters.ContainsKey('VaultName') | Should -BeTrue\n        $command.Parameters['VaultName'].ParameterType | Should -Be ([string])\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertTo-GraphCredential.Tests.ps1",
    "content": "Describe 'ConvertTo-GraphCredential cmdlet' {\n    It 'accepts ClientSecretSecureString and returns a PSCredential' {\n        $secret = ConvertTo-SecureString 'graph-secret' -AsPlainText -Force\n\n        $credential = ConvertTo-GraphCredential -ClientId 'client' -ClientSecretSecureString $secret -DirectoryId 'tenant'\n\n        $credential | Should -BeOfType ([pscredential])\n        $credential.UserName | Should -Be 'client@tenant'\n        $credential.GetNetworkCredential().Password | Should -Be 'graph-secret'\n    }\n\n    It 'accepts SecretName and resolves the client secret via Get-Secret' {\n        function Get-Secret {\n            param(\n                [string] $Name,\n                [string] $Vault\n            )\n\n            $script:LastVaultName = $Vault\n            if ($Name -ne 'graph-client-secret') {\n                throw \"Unexpected secret name: $Name\"\n            }\n\n            ConvertTo-SecureString 'graph-secret-from-vault' -AsPlainText -Force\n        }\n\n        try {\n            $credential = ConvertTo-GraphCredential -ClientId 'client' -DirectoryId 'tenant' -SecretName 'graph-client-secret' -VaultName 'LocalVault'\n\n            $credential.GetNetworkCredential().Password | Should -Be 'graph-secret-from-vault'\n            $script:LastVaultName | Should -Be 'LocalVault'\n        } finally {\n            Remove-Item Function:\\Get-Secret -ErrorAction SilentlyContinue\n            Remove-Variable LastVaultName -Scope Script -ErrorAction SilentlyContinue\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertTo-MailgunCredential.Tests.ps1",
    "content": "Describe 'ConvertTo-MailgunCredential cmdlet' {\n    It 'accepts ApiKeySecureString and returns a PSCredential' {\n        $secret = ConvertTo-SecureString 'mailgun-secret' -AsPlainText -Force\n\n        $credential = ConvertTo-MailgunCredential -ApiKeySecureString $secret\n\n        $credential | Should -BeOfType ([pscredential])\n        $credential.UserName | Should -Be 'Mailgun'\n        $credential.GetNetworkCredential().Password | Should -Be 'mailgun-secret'\n    }\n\n    It 'accepts SecretName and resolves the API key via Get-Secret' {\n        function Get-Secret {\n            param(\n                [string] $Name,\n                [string] $Vault\n            )\n\n            $script:LastVaultName = $Vault\n            if ($Name -ne 'mailgun-key') {\n                throw \"Unexpected secret name: $Name\"\n            }\n\n            'mailgun-secret-from-vault'\n        }\n\n        try {\n            $credential = ConvertTo-MailgunCredential -SecretName 'mailgun-key' -VaultName 'LocalVault'\n\n            $credential.GetNetworkCredential().Password | Should -Be 'mailgun-secret-from-vault'\n            $script:LastVaultName | Should -Be 'LocalVault'\n        } finally {\n            Remove-Item Function:\\Get-Secret -ErrorAction SilentlyContinue\n            Remove-Variable LastVaultName -Scope Script -ErrorAction SilentlyContinue\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertTo-OAuth2Credential.Tests.ps1",
    "content": "Describe 'ConvertTo-OAuth2Credential cmdlet' {\n    It 'accepts TokenSecureString and returns a PSCredential' {\n        $token = ConvertTo-SecureString 'tok-secure' -AsPlainText -Force\n\n        $credential = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -TokenSecureString $token\n\n        $credential | Should -BeOfType ([pscredential])\n        $credential.UserName | Should -Be 'user@gmail.com'\n        $credential.GetNetworkCredential().Password | Should -Be 'tok-secure'\n    }\n\n    It 'accepts SecretName and resolves the token via Get-Secret' {\n        function Get-Secret {\n            param(\n                [string] $Name,\n                [string] $Vault\n            )\n\n            $script:LastVaultName = $Vault\n            if ($Name -ne 'oauth-token') {\n                throw \"Unexpected secret name: $Name\"\n            }\n\n            'tok-from-vault'\n        }\n\n        try {\n            $credential = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -SecretName 'oauth-token' -VaultName 'LocalVault'\n\n            $credential.GetNetworkCredential().Password | Should -Be 'tok-from-vault'\n            $script:LastVaultName | Should -Be 'LocalVault'\n        } finally {\n            Remove-Item Function:\\Get-Secret -ErrorAction SilentlyContinue\n            Remove-Variable LastVaultName -Scope Script -ErrorAction SilentlyContinue\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ConvertTo-SendGridCredential.Tests.ps1",
    "content": "Describe 'ConvertTo-SendGridCredential cmdlet' {\n    It 'accepts ApiKeySecureString and returns a PSCredential' {\n        $secret = ConvertTo-SecureString 'sendgrid-secret' -AsPlainText -Force\n\n        $credential = ConvertTo-SendGridCredential -ApiKeySecureString $secret\n\n        $credential | Should -BeOfType ([pscredential])\n        $credential.UserName | Should -Be 'SendGrid'\n        $credential.GetNetworkCredential().Password | Should -Be 'sendgrid-secret'\n    }\n\n    It 'accepts SecretName and resolves the API key via Get-Secret' {\n        function Get-Secret {\n            param(\n                [string] $Name,\n                [string] $Vault\n            )\n\n            $script:LastVaultName = $Vault\n            if ($Name -ne 'sendgrid-key') {\n                throw \"Unexpected secret name: $Name\"\n            }\n\n            'sendgrid-secret-from-vault'\n        }\n\n        try {\n            $credential = ConvertTo-SendGridCredential -SecretName 'sendgrid-key' -VaultName 'LocalVault'\n\n            $credential.GetNetworkCredential().Password | Should -Be 'sendgrid-secret-from-vault'\n            $script:LastVaultName | Should -Be 'LocalVault'\n        } finally {\n            Remove-Item Function:\\Get-Secret -ErrorAction SilentlyContinue\n            Remove-Variable LastVaultName -Scope Script -ErrorAction SilentlyContinue\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Get-DmarcReport.Tests.ps1",
    "content": "Describe 'Get-DmarcReport' {\n    It 'Exposes GmailApi parameter set' {\n        (Get-Command Get-DmarcReport).ParameterSets.Name | Should -Contain 'GmailApi'\n    }\n    It 'Includes Protocol parameter' {\n        (Get-Command Get-DmarcReport).Parameters.ContainsKey('Protocol') | Should -BeTrue\n    }\n    It 'Includes Domain parameter' {\n        (Get-Command Get-DmarcReport).Parameters.ContainsKey('Domain') | Should -BeTrue\n    }\n}\n"
  },
  {
    "path": "Tests/Get-EmailDeliveryMatch.Tests.ps1",
    "content": "Describe 'Get-EmailDeliveryMatch' {\n    It 'Throws when connection missing' {\n        $repo = [Mailozaurr.FileSentMessageRepository]::new([IO.Path]::GetTempFileName())\n        $resolver = [Mailozaurr.SendLogResolver]::new($repo)\n        { Get-EmailDeliveryMatch -Protocol Imap -Resolver $resolver } | Should -Throw\n    }\n    It 'Throws when POP3 connection missing' {\n        $repo = [Mailozaurr.FileSentMessageRepository]::new([IO.Path]::GetTempFileName())\n        $resolver = [Mailozaurr.SendLogResolver]::new($repo)\n        { Get-EmailDeliveryMatch -Protocol Pop3 -Resolver $resolver } | Should -Throw\n    }\n    It 'Throws when Graph connection missing' {\n        $repo = [Mailozaurr.FileSentMessageRepository]::new([IO.Path]::GetTempFileName())\n        $resolver = [Mailozaurr.SendLogResolver]::new($repo)\n        { Get-EmailDeliveryMatch -Protocol Graph -Resolver $resolver -UserPrincipalName 'user@example.com' } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Get-EmailDeliveryStatus.Tests.ps1",
    "content": "Describe 'Get-EmailDeliveryStatus' {\n    It 'Exposes GmailApi parameter set' {\n        (Get-Command Get-EmailDeliveryStatus).ParameterSets.Name | Should -Contain 'GmailApi'\n    }\n    It 'Includes Protocol parameter' {\n        (Get-Command Get-EmailDeliveryStatus).Parameters.ContainsKey('Protocol') | Should -BeTrue\n    }\n    It 'Includes ParallelDownloadLimit parameter' {\n        (Get-Command Get-EmailDeliveryStatus).Parameters.ContainsKey('ParallelDownloadLimit') | Should -BeTrue\n    }\n}\n\n"
  },
  {
    "path": "Tests/Get-EmailPendingMessage.Tests.ps1",
    "content": "Describe 'Get-EmailPendingMessage' {\n    It 'Lists messages from repository' {\n        $path = Join-Path $TestDrive 'pending'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $msg.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $msg.Subject = 'Pending'\n        $msg.Body = [MimeKit.TextPart]::new('plain')\n        $ms = [System.IO.MemoryStream]::new()\n        $msg.WriteTo($ms)\n        $record = [Mailozaurr.PendingMessageRecord]::new()\n        $record.MessageId = $msg.MessageId\n        $record.MimeMessage = [Convert]::ToBase64String($ms.ToArray())\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $record.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $repo.SaveAsync($record).GetAwaiter().GetResult()\n\n        $result = Get-EmailPendingMessage -PendingMessagesPath $path\n        $result.MessageId | Should -Be $msg.MessageId\n    }\n}\n"
  },
  {
    "path": "Tests/Get-GmailMessage.Tests.ps1",
    "content": "Describe 'Get-GmailMessage cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletGetGmailMessage].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n}\n"
  },
  {
    "path": "Tests/Get-GmailThread.Tests.ps1",
    "content": "Describe 'Get-GmailThread cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletGetGmailThread].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n}\n\n"
  },
  {
    "path": "Tests/Get-GraphEvent.Tests.ps1",
    "content": "Describe 'Get-GraphEvent' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Get-GraphEvent -UserPrincipalName 'u' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Get-GraphEvent - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Get-GraphInboxRule.Tests.ps1",
    "content": "Describe 'Get-GraphInboxRule' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Get-GraphInboxRule -UserPrincipalName 'u' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Get-GraphInboxRule - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Get-GraphMailboxPermission.Tests.ps1",
    "content": "Describe 'Get-GraphMailboxPermission' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Get-GraphMailboxPermission -UserPrincipalName 'u' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Get-GraphMailboxPermission - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Get-GraphMailboxStatistics.Tests.ps1",
    "content": "Describe 'Get-GraphMailboxStatistics' {\n    It 'Warns when Graph connection missing' {\n        Get-GraphMailboxStatistics -UserPrincipalName 'u' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Get-GraphMailboxStatistics - Connection not provided and no default session available.'\n    }\n\n    It 'Exposes folder statistics type' {\n        [Mailozaurr.GraphMailboxFolderStatistics] | Should -Not -BeNullOrEmpty\n    }\n}\n"
  },
  {
    "path": "Tests/Get-IMAPFolderRoot.Tests.ps1",
    "content": "Describe 'Get-IMAPFolder -Root' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Get-IMAPFolder -Client $info -Root } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Get-IMAPMessage.Tests.ps1",
    "content": "Describe 'Get-IMAPMessage' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Get-IMAPMessage -Client $info } | Should -Throw\n    }\n    It 'Throws when deleting without connection' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Get-IMAPMessage -Client $info -Delete } | Should -Throw\n    }\n    It 'Throws when mixing UID and sequence parameters' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Get-IMAPMessage -Client $info -SequenceStart 1 -UidStart 1 } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Get-IMAPMessage.WhatIf.Tests.ps1",
    "content": "Describe 'Get-IMAPMessage -Delete with WhatIf' {\n    It 'Throws when deleting without connection' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Get-IMAPMessage -Client $info -Delete -WhatIf } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Get-MimeMessageContent.Tests.ps1",
    "content": "Describe 'Get-MimeMessageContent' {\n    It 'Returns text and html bodies' {\n        $builder = [MimeKit.BodyBuilder]::new()\n        $builder.TextBody = 'plain'\n        $builder.HtmlBody = '<b>html</b>'\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Body = $builder.ToMessageBody()\n        $content = Get-MimeMessageContent -InputObject $msg\n        $content.TextBody | Should -Be 'plain'\n        $content.HtmlBody | Should -Be '<b>html</b>'\n    }\n}\n"
  },
  {
    "path": "Tests/Get-POP3Message.Tests.ps1",
    "content": "Describe 'Get-POP3Message' {\n    It 'Throws when POP connection missing' {\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        { Get-POP3Message -Client $info } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Get-SmtpConnectionPool.Tests.ps1",
    "content": "Describe 'Get-SmtpConnectionPool' {\n    BeforeAll { Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force }\n\n    It 'Returns empty snapshot for new pool' {\n        [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($true)\n        [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n        $snapshot = Get-SmtpConnectionPool\n        $snapshot.CurrentPoolSize | Should -Be 0\n        $snapshot.Entries.Count | Should -Be 0\n        [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n    }\n}\n"
  },
  {
    "path": "Tests/GraphApiErrorParser.Tests.ps1",
    "content": "Import-Module $PSScriptRoot/../Mailozaurr.psd1 -Force\nDescribe 'GraphApiErrorParser' {\n    It 'parses sample error string' {\n        $sample = @'\nPOST https://graph.microsoft.com/v1.0/users/przemyslaw.klys@company.pl/sendMail\nHTTP/2.0 404 Not Found\nrequest-id: 2ff18766-1395-4fb9-abd1-162774d4b063\nclient-request-id: 6c57f9e6-3cad-48ee-8f7a-d566dc92aca3\nx-ms-ags-diagnostic: {\"ServerInfo\":{\"DataCenter\":\"Poland Central\",\"Slice\":\"E\",\"Ring\":\"2\",\"ScaleUnit\":\"002\",\"RoleInstance\":\"WA3PEPF000004A2\"}}\nDate: Sun, 24 Aug 2025 12:55:18 GMT\nContent-Type: application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; charset=utf-8\n\n{\"error\":{\"code\":\"ErrorInvalidUser\",\"message\":\"The requested user 'przemyslaw.klys@company.pl' is invalid.\"}}\n'@\n        $parsed = [Mailozaurr.GraphApiErrorParser]::Parse($sample)\n        $parsed.Method | Should -Be ([Mailozaurr.GraphHttpMethod]::POST)\n        $parsed.Headers.RequestId | Should -Be '2ff18766-1395-4fb9-abd1-162774d4b063'\n        $parsed.Headers.Diagnostic.ServerInfo.DataCenter | Should -Be 'Poland Central'\n        $parsed.Error.Code | Should -Be 'ErrorInvalidUser'\n    }\n\n    It 'returns raw message for invalid input' {\n        $sample = 'not a graph error'\n        $parsed = [Mailozaurr.GraphApiErrorParser]::Parse($sample)\n        $parsed.Raw | Should -Be $sample\n        $parsed.Error | Should -Be $null\n    }\n}\n"
  },
  {
    "path": "Tests/GraphSendPolicy.Tests.ps1",
    "content": "Describe 'GraphSendPolicy and Options' {\n    It 'Allows setting default policy' {\n        $policy = [Mailozaurr.GraphSendPolicy]::new()\n        $policy.MaxConcurrency = 2\n        $policy.MaxRetries = 4\n        [Mailozaurr.MailozaurrOptions]::DefaultGraphPolicy = $policy\n        [Mailozaurr.MailozaurrOptions]::DefaultGraphPolicy | Should -Not -BeNullOrEmpty\n        [Mailozaurr.MailozaurrOptions]::DefaultGraphPolicy.MaxRetries | Should -Be 4\n    }\n\n    It 'Graph.WithSendPolicy adjusts global concurrency' {\n        $g = [Mailozaurr.Graph]::new()\n        $p = [Mailozaurr.GraphSendPolicy]::new()\n        $p.MaxConcurrency = 3\n        $null = $g.WithSendPolicy($p)\n        [Mailozaurr.MicrosoftGraphUtils]::MaxConcurrentRequests | Should -Be 3\n    }\n}\n\n"
  },
  {
    "path": "Tests/GraphUploadSessionResult.Tests.ps1",
    "content": "Describe 'GraphUploadSessionResult' {\n    It 'Deserializes uploadUrl correctly' {\n        $json = '{\"uploadUrl\":\"https://example.com/upload\"}'\n        $result = [System.Text.Json.JsonSerializer]::Deserialize($json, [Mailozaurr.GraphUploadSessionResult])\n        $result.UploadUrl | Should -Be 'https://example.com/upload'\n    }\n}\n"
  },
  {
    "path": "Tests/Import-Module.Tests.ps1",
    "content": "Describe 'Import-Module' {\n    BeforeAll {\n        $modulePath = Resolve-Path \"$PSScriptRoot/../Mailozaurr.psd1\"\n        Remove-Module Mailozaurr -Force -ErrorAction SilentlyContinue\n        $script:module = Import-Module $modulePath -Force -PassThru -ErrorAction Stop\n    }\n\n    It 'imports the manifest successfully' {\n        $module | Should -Not -BeNullOrEmpty\n        $module.Name | Should -Be 'Mailozaurr'\n    }\n\n    It 'exports representative commands after import' {\n        (Get-Command Get-SmtpConnectionPool -ErrorAction Stop).ModuleName | Should -Be 'Mailozaurr'\n        (Get-Command Send-EmailMessage -ErrorAction Stop).ModuleName | Should -Be 'Mailozaurr'\n    }\n}\n"
  },
  {
    "path": "Tests/Mailozaurr.Pester/Send-EmailPendingMessage.Filters.Tests.ps1",
    "content": "$script:FilterSenderReferencedAssemblies = $null\n\nfunction global:Get-FilterSenderReferencedAssemblies {\n    if ($null -eq $script:FilterSenderReferencedAssemblies) {\n        $referenceDirectory = Join-Path $PSHOME 'ref'\n        if (-not (Test-Path -Path $referenceDirectory)) {\n            throw 'Unable to locate PowerShell reference assemblies required for Add-Type.'\n        }\n\n        $script:FilterSenderReferencedAssemblies = @(\n            [Mailozaurr.PendingMessageRecord].Assembly.Location\n        ) + @(Get-ChildItem -Path $referenceDirectory -Filter '*.dll' -File | ForEach-Object { $_.FullName })\n    }\n\n    return $script:FilterSenderReferencedAssemblies\n}\n\nfunction global:New-PendingRecord {\n    param(\n        [Mailozaurr.EmailProvider]$Provider,\n        [string]$MessageId = ([Guid]::NewGuid().ToString())\n    )\n\n    $message = [MimeKit.MimeMessage]::new()\n    $message.From.Add([MimeKit.MailboxAddress]::Parse('sender@example.com'))\n    $message.To.Add([MimeKit.MailboxAddress]::Parse('recipient@example.com'))\n    $message.Subject = 'Queue item'\n    $message.Body = [MimeKit.TextPart]::new('plain', 'body')\n    $buffer = [System.IO.MemoryStream]::new()\n    $message.WriteTo($buffer)\n    $payload = [Convert]::ToBase64String($buffer.ToArray())\n\n    $record = [Mailozaurr.PendingMessageRecord]::new()\n    $record.MessageId = $MessageId\n    $record.MimeMessage = $payload\n    $record.Timestamp = [DateTimeOffset]::UtcNow\n    $record.NextAttemptAt = [DateTimeOffset]::UtcNow\n    $record.Provider = $Provider\n    if ($Provider -eq [Mailozaurr.EmailProvider]::None) {\n        $record.Server = 'smtp.server'\n        $record.Port = 25\n    }\n\n    return $record\n}\n\nDescribe 'Send-EmailPendingMessage provider and message filters' {\n    BeforeAll {\n        $moduleRoot = Join-Path (Join-Path $PSScriptRoot '..') '..'\n        $modulePath = Join-Path $moduleRoot 'Mailozaurr.psd1'\n        if (-not (Get-Module -Name Mailozaurr)) {\n            Import-Module $modulePath -Force\n        }\n\n        if (-not ('FilterRecordingPendingMessageSender' -as [type])) {\n            $assemblies = Get-FilterSenderReferencedAssemblies\n\n            Add-Type -ReferencedAssemblies $assemblies -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Concurrent;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Mailozaurr;\n\npublic sealed class FilterRecordingPendingMessageSender : IPendingMessageSender {\n    private readonly EmailProvider provider;\n    private static readonly ConcurrentDictionary<EmailProvider, ConcurrentBag<string>> Processed = new();\n\n    public FilterRecordingPendingMessageSender(EmailProvider provider) {\n        this.provider = provider;\n    }\n\n    public static void Reset() => Processed.Clear();\n\n    public static string[] GetProcessedMessageIds(EmailProvider provider) {\n        if (Processed.TryGetValue(provider, out var records)) {\n            return records.ToArray();\n        }\n\n        return Array.Empty<string>();\n    }\n\n    public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        var messages = Processed.GetOrAdd(provider, _ => new ConcurrentBag<string>());\n        messages.Add(record.MessageId ?? string.Empty);\n\n        return Task.CompletedTask;\n    }\n}\n\npublic static class FilterPendingMessageSenderFactory {\n    private static readonly EmailProvider[] Providers = new[] {\n        EmailProvider.None,\n        EmailProvider.SendGrid,\n        EmailProvider.Mailgun,\n        EmailProvider.SES,\n        EmailProvider.Gmail\n    };\n\n    public static PendingMessageSenderFactory Create() {\n        var entries = new KeyValuePair<EmailProvider, IPendingMessageSender>[Providers.Length];\n        for (var i = 0; i < Providers.Length; i++) {\n            var provider = Providers[i];\n            entries[i] = new KeyValuePair<EmailProvider, IPendingMessageSender>(provider, new FilterRecordingPendingMessageSender(provider));\n        }\n\n        return new PendingMessageSenderFactory(entries);\n    }\n}\n\"@\n        }\n    }\n\n    BeforeEach {\n        [FilterRecordingPendingMessageSender]::Reset()\n        $delegate = [System.Delegate]::CreateDelegate(\n            [System.Func[Mailozaurr.PendingMessageSenderFactory]],\n            [FilterPendingMessageSenderFactory],\n            'Create')\n        [Mailozaurr.PowerShell.CmdletSendEmailPendingMessage]::SenderFactoryProvider = $delegate\n    }\n\n    AfterEach {\n        [Mailozaurr.PowerShell.CmdletSendEmailPendingMessage]::SenderFactoryProvider = $null\n    }\n\n    It 'Processes only messages for the requested provider' {\n        $path = Join-Path $TestDrive 'filters-provider'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $smtpRecord = New-PendingRecord -Provider ([Mailozaurr.EmailProvider]::None)\n        $repo.SaveAsync($smtpRecord).GetAwaiter().GetResult()\n        $sesRecord = New-PendingRecord -Provider ([Mailozaurr.EmailProvider]::SES)\n        $repo.SaveAsync($sesRecord).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -Provider ([Mailozaurr.EmailProvider]::SES)\n\n        [FilterRecordingPendingMessageSender]::GetProcessedMessageIds([Mailozaurr.EmailProvider]::SES).Count | Should -Be 1\n        [FilterRecordingPendingMessageSender]::GetProcessedMessageIds([Mailozaurr.EmailProvider]::None).Count | Should -Be 0\n\n        $remaining = @(Get-EmailPendingMessage -PendingMessagesPath $path)\n        $remaining.Count | Should -Be 1\n        $remaining[0].MessageId | Should -Be $smtpRecord.MessageId\n    }\n\n    It 'Processes only the message matching both provider and message id filters' {\n        $path = Join-Path $TestDrive 'filters-combined'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $target = New-PendingRecord -Provider ([Mailozaurr.EmailProvider]::SendGrid)\n        $repo.SaveAsync($target).GetAwaiter().GetResult()\n        $other = New-PendingRecord -Provider ([Mailozaurr.EmailProvider]::SendGrid)\n        $repo.SaveAsync($other).GetAwaiter().GetResult()\n        $mailgun = New-PendingRecord -Provider ([Mailozaurr.EmailProvider]::Mailgun)\n        $repo.SaveAsync($mailgun).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -Provider ([Mailozaurr.EmailProvider]::SendGrid) -MessageId $target.MessageId\n\n        $processed = [FilterRecordingPendingMessageSender]::GetProcessedMessageIds([Mailozaurr.EmailProvider]::SendGrid)\n        $processed | Should -Contain $target.MessageId\n        $processed.Count | Should -Be 1\n        [FilterRecordingPendingMessageSender]::GetProcessedMessageIds([Mailozaurr.EmailProvider]::Mailgun).Count | Should -Be 0\n\n        $pending = @(Get-EmailPendingMessage -PendingMessagesPath $path)\n        $pending.Count | Should -Be 2\n        $pending.MessageId | Should -Contain $other.MessageId\n        $pending.MessageId | Should -Contain $mailgun.MessageId\n    }\n}\n"
  },
  {
    "path": "Tests/MaxConcurrentRequests.Tests.ps1",
    "content": "Describe 'MicrosoftGraphUtils.MaxConcurrentRequests' {\n    It 'Can change concurrency limit' {\n        [Mailozaurr.MicrosoftGraphUtils]::MaxConcurrentRequests = 7\n        [Mailozaurr.MicrosoftGraphUtils]::MaxConcurrentRequests | Should -Be 7\n    }\n}\n"
  },
  {
    "path": "Tests/MessageEncryption.Tests.ps1",
    "content": "Describe 'Message encryption detection' {\n    It 'Defaults to None for plain message' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Subject = 'test'\n        $imap = [Mailozaurr.ImapEmailMessage]::new([MailKit.UniqueId]::new(1), $msg)\n        $imap.Encryption.ToString() | Should -Be 'None'\n    }\n}\n"
  },
  {
    "path": "Tests/MessageTypes.Tests.ps1",
    "content": "Describe 'Message wrapper types' {\n    It 'Pop3EmailMessage type is available' {\n        [Mailozaurr.Pop3EmailMessage] | Should -Not -BeNullOrEmpty\n    }\n\n    It 'Pop3MessageInfo type is available' {\n        [Mailozaurr.Pop3MessageInfo] | Should -Not -BeNullOrEmpty\n    }\n\n    It 'ImapEmailMessage type is available' {\n        [Mailozaurr.ImapEmailMessage] | Should -Not -BeNullOrEmpty\n    }\n\n    It 'ImapMessageInfo type is available' {\n        [Mailozaurr.ImapMessageInfo] | Should -Not -BeNullOrEmpty\n    }\n\n    It 'GraphMessageInfo type is available' {\n        [Mailozaurr.GraphMessageInfo] | Should -Not -BeNullOrEmpty\n    }\n\n    It 'GraphEmailMessage type is available' {\n        [Mailozaurr.GraphEmailMessage] | Should -Not -BeNullOrEmpty\n    }\n}\n"
  },
  {
    "path": "Tests/Move-GraphMessage.Tests.ps1",
    "content": "Describe 'Move-GraphMessage' {\n    It 'Skips execution when using WhatIf' {\n        Move-GraphMessage -UserPrincipalName 'u' -MessageId 'id' -DestinationFolderId 'dest' -MgGraphRequest -WhatIf -ErrorVariable err\n        $err | Should -BeNullOrEmpty\n    }\n}\n"
  },
  {
    "path": "Tests/Move-IMAPMessage.Tests.ps1",
    "content": "Describe 'Move-IMAPMessage' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Move-IMAPMessage -Client $info -Uid 1 -DestinationFolder 'Archive' -WhatIf } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/New-GraphEvent.Tests.ps1",
    "content": "Describe 'New-GraphEvent' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        $ev = [Mailozaurr.GraphEvent]::new()\n        New-GraphEvent -UserPrincipalName 'u' -Event $ev -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'New-GraphEvent - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when connection missing with builder' {\n        $b = New-GraphEventBuilder -Subject 't' -Start (Get-Date) -End (Get-Date)\n        New-GraphEvent -UserPrincipalName 'u' -EventBuilder $b -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'New-GraphEvent - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/New-GraphInboxRule.Tests.ps1",
    "content": "Describe 'New-GraphInboxRule' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        New-GraphInboxRule -UserPrincipalName 'u' -Rule @{displayName='x'} -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'New-GraphInboxRule - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when connection missing with builder' {\n        $b = New-GraphInboxRuleBuilder -DisplayName 'Test' -Sequence 1 -SenderContains 'a@example.com'\n        New-GraphInboxRule -UserPrincipalName 'u' -RuleBuilder $b -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'New-GraphInboxRule - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/New-GraphInboxRuleObjectBuilder.Tests.ps1",
    "content": "Describe 'New-GraphInboxRuleObject builder parameter' {\n    It 'Builds rule from builder' {\n        $b = New-GraphInboxRuleBuilder -DisplayName 'Test' -Sequence 1 -SenderContains 'a@example.com'\n        $rule = New-GraphInboxRuleObject -Builder $b\n        $rule.DisplayName | Should -Be 'Test'\n        $rule.Sequence | Should -Be 1\n        $rule.Conditions.SenderContains | Should -Contain 'a@example.com'\n    }\n}\n"
  },
  {
    "path": "Tests/New-GraphMailboxPermissionObjectBuilder.Tests.ps1",
    "content": "Describe 'New-GraphMailboxPermissionObject builder parameter' {\n    It 'Builds permission from builder' {\n        $b = New-GraphMailboxPermissionBuilder -GrantedToUser 'a@example.com' -Roles Owner\n        $perm = New-GraphMailboxPermissionObject -Builder $b\n        $perm.GrantedTo.User | Should -Be 'a@example.com'\n        $perm.Roles | Should -Contain 'Owner'\n    }\n}\n"
  },
  {
    "path": "Tests/New-TemporaryMailCrypto.Tests.ps1",
    "content": "Describe 'New-TemporaryMailCrypto cmdlet' {\n    It 'Creates PGP keys with custom path' {\n        $dir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())\n        $keys = New-TemporaryMailCrypto -Pgp -OutputPath $dir -NoDispose\n        Test-Path $keys.PublicKeyPath | Should -BeTrue\n        $keys.Dispose()\n        Test-Path $keys.PublicKeyPath | Should -BeTrue\n        Remove-Item $dir -Recurse -Force\n    }\n    It 'Creates S/MIME certificate and saves to file' {\n        $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName() + '.pfx')\n        $cert = New-TemporaryMailCrypto -Smime -OutputPath $path\n        $cert | Should -BeOfType ([System.Security.Cryptography.X509Certificates.X509Certificate2])\n        Test-Path $path | Should -BeTrue\n        Remove-Item $path -Force\n    }\n}\n"
  },
  {
    "path": "Tests/Pop3Connector.Dispose.Tests.ps1",
    "content": "Describe 'Pop3Connector disposal' {\n    BeforeAll {\n        $refs = @(\n            [Mailozaurr.Pop3Connector].Assembly.Location,\n            [MailKit.Net.Pop3.Pop3Client].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n    }\n\n    It 'Disposes client after failed connect' {\n        if (-not ('FailingPop3Client' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class FailingPop3Client : Pop3Client {\n    public bool Disposed;\n    public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        throw new System.InvalidOperationException(\"fail\");\n    }\n    public override bool IsConnected => true;\n    public override bool IsAuthenticated => false;\n    protected override void Dispose(bool disposing) {\n        Disposed = true;\n        base.Dispose(disposing);\n    }\n}\npublic static class FailingPop3ClientFactory {\n    public static Pop3Client Client;\n    public static Pop3Client Create() => Client;\n    public static void Install() => Pop3Connector.ClientFactory = Create;\n}\n\"@\n        }\n\n        $fake = [FailingPop3Client]::new()\n        [FailingPop3ClientFactory]::Client = $fake\n        [FailingPop3ClientFactory]::Install()\n\n        try {\n            [Mailozaurr.Pop3Connector]::ConnectAsync('h', 995, [MailKit.Security.SecureSocketOptions]::SslOnConnect, 1000, $false, $false, { param($c) [Task]::CompletedTask }, 0, 0, 1.0).GetAwaiter().GetResult()\n        } catch {\n        }\n\n        $fake.Disposed | Should -BeTrue\n\n        [Mailozaurr.Pop3Connector]::ResetClientFactory()\n    }\n\n    It 'Disposes client even when DisconnectAsync throws' {\n        if (-not ('ThrowingDisconnectPop3Client' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit.Net.Pop3;\nusing MailKit.Security;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class ThrowingDisconnectPop3Client : Pop3Client {\n    public bool Disposed;\n    public bool DisconnectCalled;\n    public override Task ConnectAsync(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        throw new System.InvalidOperationException(\"fail\");\n    }\n    public override bool IsConnected => true;\n    public override bool IsAuthenticated => false;\n    public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = default) {\n        DisconnectCalled = true;\n        throw new System.InvalidOperationException(\"disconnect\");\n    }\n    protected override void Dispose(bool disposing) {\n        Disposed = true;\n        base.Dispose(disposing);\n    }\n}\npublic static class ThrowingDisconnectPop3ClientFactory {\n    public static Pop3Client Client;\n    public static Pop3Client Create() => Client;\n    public static void Install() => Pop3Connector.ClientFactory = Create;\n}\n\"@\n        }\n\n        $fake = [ThrowingDisconnectPop3Client]::new()\n        [ThrowingDisconnectPop3ClientFactory]::Client = $fake\n        [ThrowingDisconnectPop3ClientFactory]::Install()\n\n        try {\n            [Mailozaurr.Pop3Connector]::ConnectAsync('h', 995, [MailKit.Security.SecureSocketOptions]::SslOnConnect, 1000, $false, $false, { param($c) [Task]::CompletedTask }, 0, 0, 1.0).GetAwaiter().GetResult()\n        } catch {\n        }\n\n        $fake.DisconnectCalled | Should -BeTrue\n        $fake.Disposed | Should -BeTrue\n\n        [Mailozaurr.Pop3Connector]::ResetClientFactory()\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-EmailPendingMessage.Tests.ps1",
    "content": "Describe 'Remove-EmailPendingMessage' {\n    It 'Deletes message from repository' {\n        $path = Join-Path $TestDrive 'pending'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $msg.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $msg.Subject = 'Pending'\n        $msg.Body = [MimeKit.TextPart]::new('plain')\n        $ms = [System.IO.MemoryStream]::new()\n        $msg.WriteTo($ms)\n        $record = [Mailozaurr.PendingMessageRecord]::new()\n        $record.MessageId = $msg.MessageId\n        $record.MimeMessage = [Convert]::ToBase64String($ms.ToArray())\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $record.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $repo.SaveAsync($record).GetAwaiter().GetResult()\n\n        Remove-EmailPendingMessage -PendingMessagesPath $path -MessageId $record.MessageId -Confirm:$false\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 0\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GmailMessage.Tests.ps1",
    "content": "Describe 'Remove-GmailMessage cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletRemoveGmailMessage].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GraphEvent.Tests.ps1",
    "content": "Describe 'Remove-GraphEvent' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Remove-GraphEvent -UserPrincipalName 'u' -EventId 'id' -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Remove-GraphEvent - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GraphInboxRule.Tests.ps1",
    "content": "Describe 'Remove-GraphInboxRule' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Remove-GraphInboxRule -UserPrincipalName 'u' -RuleId 'id' -WhatIf -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Remove-GraphInboxRule - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GraphMailboxPermission.Tests.ps1",
    "content": "Describe 'Remove-GraphMailboxPermission' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Remove-GraphMailboxPermission -UserPrincipalName 'u' -PermissionId 'id' -WhatIf -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Remove-GraphMailboxPermission - Connection not provided and no default session available.'\n    }\n\n    It 'Warns when using role filter without connection' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Remove-GraphMailboxPermission -UserPrincipalName 'u' -Role Owner -WhatIf -WarningVariable warn2\n        $warn2 | Should -Not -BeNullOrEmpty\n        $warn2[0] | Should -Be 'Remove-GraphMailboxPermission - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GraphMessage.Tests.ps1",
    "content": "Describe 'Remove-GraphMessage' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Remove-GraphMessage -UserPrincipalName 'u' -MessageId 'id' -WhatIf -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Remove-GraphMessage - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-GraphMessageAttachment.Tests.ps1",
    "content": "Describe 'Remove-GraphMessageAttachment' {\n    It 'Clears attachments on GraphMessage' -Skip:$true {\n        $file = Join-Path $TestDrive 'file.txt'\n        'b' | Set-Content -Path $file\n        $graphMsg = [Mailozaurr.GraphMessage]::new()\n        $graphMsg.Subject = 's'\n        $graphMsg.Body = [Mailozaurr.GraphContent]::new()\n        $graphMsg.Attachments = @([Mailozaurr.GraphAttachment]::FromFile($file))\n        $result = Remove-GraphMessageAttachment -Message $graphMsg\n        $null -eq $result.Attachments | Should -BeTrue\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-IMAPMessage.Tests.ps1",
    "content": "Describe 'Remove-IMAPMessage' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Remove-IMAPMessage -Client $info -Uid 1 -WhatIf } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-IMAPMessageAttachment.Tests.ps1",
    "content": "Describe 'Remove-IMAPMessageAttachment' {\n    It 'Removes attachments from MimeMessage' -Skip:$true {\n        $path = Join-Path $TestDrive 'file.txt'\n        'a' | Set-Content -Path $path\n        $builder = [MimeKit.BodyBuilder]::new()\n        $builder.TextBody = 'body'\n        $builder.Attachments.Add($path) | Out-Null\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Body = $builder.ToMessageBody()\n        ($msg.Attachments | Measure-Object).Count | Should -Be 1\n        $result = Remove-IMAPMessageAttachment -Message $msg\n        ($result.Attachments | Measure-Object).Count | Should -Be 0\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-POP3Message.Tests.ps1",
    "content": "Describe 'Remove-POP3Message' {\n    It 'Throws when POP3 connection missing' {\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        { Remove-POP3Message -Client $info -Index 0 -WhatIf } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Remove-POP3MessageAttachment.Tests.ps1",
    "content": "Describe 'Remove-POP3MessageAttachment' {\n    It 'Removes attachments from MimeMessage' -Skip:$true {\n        $path = Join-Path $TestDrive 'file.txt'\n        'a' | Set-Content -Path $path\n        $builder = [MimeKit.BodyBuilder]::new()\n        $builder.TextBody = 'body'\n        $builder.Attachments.Add($path) | Out-Null\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Body = $builder.ToMessageBody()\n        ($msg.Attachments | Measure-Object).Count | Should -Be 1\n        $result = Remove-POP3MessageAttachment -Message $msg\n        ($result.Attachments | Measure-Object).Count | Should -Be 0\n    }\n}\n"
  },
  {
    "path": "Tests/Save-GmailMessageAttachment.Tests.ps1",
    "content": "Describe 'Save-GmailMessageAttachment cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletSaveGmailMessageAttachment].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n}\n"
  },
  {
    "path": "Tests/Save-IMAPMessage.Tests.ps1",
    "content": "Describe 'Save-IMAPMessage' {\n    It 'Creates target directory when missing' {\n        $dir = Join-Path $TestDrive 'imap'\n        $file = Join-Path $dir 'msg.eml'\n\n        # simulate logic of cmdlet for testing\n        $message = [MimeKit.MimeMessage]::new()\n        if (-not (Test-Path $dir)) { [System.IO.Directory]::CreateDirectory($dir) | Out-Null }\n        $message.WriteTo($file)\n\n        Test-Path $dir | Should -BeTrue\n    }\n\n    It 'Removes temp file when convert fails' {\n        $tempDir = Join-Path $TestDrive 'tmp'\n        [System.IO.Directory]::CreateDirectory($tempDir) | Out-Null\n        $oldTemp = $env:TEMP\n        $oldTmp = $env:TMP\n        $env:TEMP = $tempDir\n        $env:TMP = $tempDir\n\n        $message = [MimeKit.MimeMessage]::new()\n        $msgPath = '/sys/fail.msg'\n\n        $tmp = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString() + '.eml')\n        try {\n            $message.WriteTo($tmp)\n            [Mailozaurr.EmailMessage]::ConvertEmlToMsg([System.IO.FileInfo]$tmp, [System.IO.FileInfo]$msgPath, $true) | Out-Null\n        } finally {\n            if (Test-Path $tmp) { Remove-Item $tmp }\n        }\n\n        (Get-ChildItem $tempDir -Filter '*.eml') | Should -BeNullOrEmpty\n\n        $env:TEMP = $oldTemp\n        $env:TMP = $oldTmp\n    }\n}\n"
  },
  {
    "path": "Tests/Save-MimeMessage.Tests.ps1",
    "content": "Describe 'Save-MimeMessage' {\n    It 'Saves to EML file' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $file = Join-Path $TestDrive 'test.eml'\n        Save-MimeMessage -InputObject $msg -Path $file\n        Test-Path $file | Should -BeTrue\n    }\n\n    It 'Converts to MSG' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $file = Join-Path $TestDrive 'test.msg'\n        Save-MimeMessage -InputObject $msg -Path $file\n        Test-Path $file | Should -BeTrue\n    }\n}\n"
  },
  {
    "path": "Tests/Save-POP3Message.Tests.ps1",
    "content": "Describe 'Save-POP3Message' {\n    It 'Creates directory when saving message' {\n        $refs = @(\n            [MailKit.Net.Pop3.Pop3Client].Assembly.Location,\n            [MimeKit.MimeMessage].Assembly.Location\n        )\n        if (-not ('FakePop3MessageClient' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing MailKit.Net.Pop3;\nusing MimeKit;\npublic class FakePop3MessageClient : Pop3Client {\n    private readonly MimeMessage _message;\n    public FakePop3MessageClient() {\n        _message = new MimeMessage();\n        _message.From.Add(new MailboxAddress(\"a\", \"a@b.com\"));\n        _message.To.Add(new MailboxAddress(\"b\", \"b@c.com\"));\n        _message.Subject = \"test\";\n        _message.Body = new TextPart(\"plain\") { Text = \"body\" };\n    }\n    public override int Count => 1;\n    public override MimeMessage GetMessage(int index, System.Threading.CancellationToken cancellationToken = default, MailKit.ITransferProgress progress = null) {\n        return _message;\n    }\n}\n\"@\n        }\n\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        $info.Data = [FakePop3MessageClient]::new()\n        $filePath = Join-Path $TestDrive 'subdir/message.eml'\n        Save-POP3Message -Client $info -Index 0 -Path $filePath\n        Test-Path $filePath | Should -Be $true\n    }\n\n    It 'Cleans up temp file on conversion failure' {\n        $refs = @(\n            [MailKit.Net.Pop3.Pop3Client].Assembly.Location,\n            [MimeKit.MimeMessage].Assembly.Location\n        )\n        if (-not ('FakePop3MessageClient' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing MailKit.Net.Pop3;\nusing MimeKit;\npublic class FakePop3MessageClient : Pop3Client {\n    private readonly MimeMessage _message;\n    public FakePop3MessageClient() {\n        _message = new MimeMessage();\n        _message.From.Add(new MailboxAddress(\"a\", \"a@b.com\"));\n        _message.To.Add(new MailboxAddress(\"b\", \"b@c.com\"));\n        _message.Subject = \"test\";\n        _message.Body = new TextPart(\"plain\") { Text = \"body\" };\n    }\n    public override int Count => 1;\n    public override MimeMessage GetMessage(int index, System.Threading.CancellationToken cancellationToken = default, MailKit.ITransferProgress progress = null) {\n        return _message;\n    }\n}\n\"@\n        }\n\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        $info.Data = [FakePop3MessageClient]::new()\n\n        $tempDir = Join-Path $TestDrive 'tmp'\n        [System.IO.Directory]::CreateDirectory($tempDir) | Out-Null\n        $oldTemp = $env:TEMP\n        $oldTmp = $env:TMP\n        $env:TEMP = $tempDir\n        $env:TMP = $tempDir\n\n        $target = '/sys/fail.msg'\n\n        Save-POP3Message -Client $info -Index 0 -Path $target | Out-Null\n\n        (Get-ChildItem $tempDir -Filter '*.eml') | Should -BeNullOrEmpty\n\n        $env:TEMP = $oldTemp\n        $env:TMP = $oldTmp\n    }\n}\n"
  },
  {
    "path": "Tests/Search-IMAPMailbox.Tests.ps1",
    "content": "Describe 'Search-IMAPMailbox' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Search-IMAPMailbox -Client $info } | Should -Throw\n    }\n\n    It 'Supports BodyContains parameter' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Search-IMAPMailbox -Client $info -BodyContains 'test' } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Search-POP3Mailbox.Tests.ps1",
    "content": "Describe 'Search-POP3Mailbox' {\n    It 'Throws when POP3 connection missing' {\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        { Search-POP3Mailbox -Client $info } | Should -Throw\n    }\n\n    It 'Supports BodyContains parameter' {\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        { Search-POP3Mailbox -Client $info -BodyContains 'test' } | Should -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.AsyncLogging.Tests.ps1",
    "content": "Import-Module (Resolve-Path \"$PSScriptRoot/../Mailozaurr.psd1\") -Force\n\nDescribe 'Send-EmailMessage - Async Logging' {\n    It 'does not raise thread-bound Write warnings' {\n        $warnings = @()\n        Send-EmailMessage -From 'from@example.com' -To 'to@example.com' -Server 'smtp.example.com' `\n            -Subject 'Test' -Text 'Body' -Port 25 -WarningVariable warnings -WhatIf\n        $warnings | Should -Not -Contain 'WriteObject and WriteError methods cannot be called'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.Attachments.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Attachment Cleanup' {\n    It 'Warns and skips missing attachment paths' {\n        $missing1 = 'mailozaurr_missing1.txt'\n        $missing2 = 'mailozaurr_missing2.txt'\n        if (Test-Path $missing1) { Remove-Item $missing1 -Force }\n        if (Test-Path $missing2) { Remove-Item $missing2 -Force }\n        $result = Send-EmailMessage -From 'a@b.com' -To 'c@d.com' -Subject 'x' -Body 'y' -Server 'smtp.example.com' -Port 25 -Attachment $missing1 -InlineAttachment $missing2 -WarningAction Continue -WhatIf 3>&1\n        $warnings = $result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }\n        $warnings.Count | Should -Be 2\n        ($warnings[0].Message + $warnings[1].Message) | Should -Match 'mailozaurr_missing1.txt'\n        ($warnings[0].Message + $warnings[1].Message) | Should -Match 'mailozaurr_missing2.txt'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.GmailProvider.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Gmail provider' {\n    It 'Enumeration includes Gmail' {\n        [enum]::GetNames([Mailozaurr.EmailProvider]) | Should -Contain 'Gmail'\n    }\n    It 'WhatIf returns result' {\n        $cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'tok'\n        { Send-EmailMessage -EmailProvider Gmail -GmailAccount 'user@gmail.com' -From 'user@gmail.com' -To 'a@test.com' -Subject 's' -Body 'b' -Credential $cred -WhatIf } | Should -Not -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.GraphLimit.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Graph Size Limit' {\n    It 'Returns error when attachments exceed Graph limit' {\n        $file = [System.IO.Path]::GetTempFileName()\n        [byte[]]$bytes = New-Object byte[] 150000001\n        [System.IO.File]::WriteAllBytes($file, $bytes)\n        $cred = New-Object System.Management.Automation.PSCredential('a@b.com', (ConvertTo-SecureString 'x' -AsPlainText -Force))\n        $result = Send-EmailMessage -From 'a@b.com' -To 'c@d.com' -Subject 's' -Body 'b' -Graph -Attachment $file -Credential $cred -WhatIf 2>&1\n        Remove-Item $file\n        $error = $result | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }\n        $error.Exception.Message | Should -Match '150MB'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.Headers.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Headers' {\n    It 'Accepts custom headers parameter' {\n        $result = Send-EmailMessage -From 'a@b.com' -To 'c@d.com' -Subject 't' -Body 'b' -Server 'smtp.example.com' -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Accepts custom headers with Gmail provider' {\n        $cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'tok'\n        $result = Send-EmailMessage -EmailProvider Gmail -GmailAccount 'user@gmail.com' -From 'user@gmail.com' -To 'recipient@example.com' -Subject 't' -Body 'b' -Credential $cred -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Accepts custom headers with Graph provider' {\n        $cred = ConvertTo-GraphCredential -ClientID 'client' -ClientSecret 'secret' -DirectoryID 'tenant'\n        $result = Send-EmailMessage -From 'user@example.com' -To 'recipient@example.com' -Subject 't' -Body 'b' -Graph -Credential $cred -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Accepts custom headers with SendGrid provider' {\n        $cred = ConvertTo-SendGridCredential -ApiKey 'key'\n        $result = Send-EmailMessage -From 'user@example.com' -To 'recipient@example.com' -Subject 't' -Body 'b' -SendGrid -Credential $cred -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Accepts custom headers with Mailgun provider' {\n        $cred = ConvertTo-MailgunCredential -ApiKey 'key'\n        $result = Send-EmailMessage -From 'user@example.com' -To 'recipient@example.com' -Subject 't' -Body 'b' -EmailProvider Mailgun -Credential $cred -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Accepts custom headers with SES provider' {\n        $secure = ConvertTo-SecureString 'secret' -AsPlainText -Force\n        $cred = [PSCredential]::new('access', $secure)\n        $result = Send-EmailMessage -From 'user@example.com' -To 'recipient@example.com' -Subject 't' -Body 'b' -EmailProvider SES -Region 'us-east-1' -Credential $cred -Headers @{ 'X-Test'='123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.LoggerScope.Tests.ps1",
    "content": "Import-Module (Resolve-Path \"$PSScriptRoot/../Mailozaurr.psd1\") -Force\n\nDescribe 'Send-EmailMessage - Logger scope' {\n    It 'restores previous logger after execution' {\n        $original = [Mailozaurr.LoggingMessages]::Logger\n        Send-EmailMessage -From 'from@example.com' -To 'to@example.com' -Server 'smtp.example.com' `\n            -Subject 'Test' -Text 'Body' -Port 25 -WhatIf\n        [Mailozaurr.LoggingMessages]::Logger | Should -Be $original\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.Pgp.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - PGP' {\n    It 'Signs and encrypts via PGP (WhatIf)' {\n        $pub = Join-Path $PSScriptRoot '..\\Examples\\PGPKeys\\mimekit.gpg.pub'\n        $sec = Join-Path $PSScriptRoot '..\\Examples\\PGPKeys\\mimekit.gpg.sec'\n        $result = Send-EmailMessage -From 'mimekit@example.com' -To 'mimekit@example.com' -Server 'smtp.example.com' -Port 25 -Body 'test' -Subject 'test' -WhatIf `\n            -SignOrEncrypt PgpSignAndEncrypt -PublicKeyPath $pub -PrivateKeyPath $sec -PrivateKeyPassword 'no.secret'\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n\n    It 'Returns error when key files are missing' {\n        $smtp = [Mailozaurr.Smtp]::new()\n        $smtp.From = 'a@b.com'\n        $smtp.To = @('c@d.com')\n        $smtp.Subject = 'Test'\n        $smtp.TextBody = 'Body'\n        $smtp.CreateMessage()\n        $result = $smtp.PgpSignAndEncrypt('missing.pub', 'missing.sec', '', $false)\n        $result.Error | Should -Match 'file not found'\n        $result.Status | Should -Be $false\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.SentLogPath.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - SentLogPath OptIn' {\n    BeforeAll {\n        if (-not ('FakeClientSentLogPs' -as [type])) {\n            $refs = @(\n                [Mailozaurr.Smtp].Assembly.Location,\n                [Mailozaurr.ClientSmtp].Assembly.Location,\n                [MailKit.Security.SecureSocketOptions].Assembly.Location,\n                [MimeKit.MimeMessage].Assembly.Location\n            )\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\npublic class FakeClientSentLogPs : ClientSmtp {\n    private bool _connected;\n\n    public override bool IsConnected {\n        get { return _connected; }\n    }\n\n    public override void Connect(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken) {\n        _connected = true;\n    }\n\n    public override void Disconnect(bool quit, CancellationToken cancellationToken) {\n        _connected = false;\n    }\n\n    public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken, ITransferProgress progress) {\n        if (string.IsNullOrWhiteSpace(message.MessageId)) {\n            message.MessageId = \"fake-\" + Guid.NewGuid().ToString(\"N\") + \"@mailozaurr.test\";\n        }\n\n        return Task.FromResult(message.MessageId);\n    }\n}\npublic static class FakeClientSentLogPsFactory {\n    public static ClientSmtp Create(ProtocolLogger logger) => new FakeClientSentLogPs();\n    public static void Install() => Smtp.ClientFactory = Create;\n}\n\"@\n        }\n    }\n\n    It 'Does not create default temp sent log when SentLogPath is not provided' {\n        $oldTemp = $env:TEMP\n        $oldTmp = $env:TMP\n\n        try {\n            $tempDir = Join-Path $TestDrive 'tmp-default'\n            New-Item -ItemType Directory -Path $tempDir -Force | Out-Null\n            $env:TEMP = $tempDir\n            $env:TMP = $tempDir\n\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [FakeClientSentLogPsFactory]::Install()\n\n            $result = Send-EmailMessage -From 'from@example.com' -To 'to@example.com' -Server 'smtp.example.com' -Port 25 -Subject 'Subject' -Text 'Body'\n\n            $result.Status | Should -BeTrue\n            $defaultSentLogPath = Join-Path $tempDir 'Mailozaurr\\sentlog.json'\n            (Test-Path $defaultSentLogPath) | Should -BeFalse\n        } finally {\n            [Mailozaurr.Smtp]::ResetClientFactory()\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            $env:TEMP = $oldTemp\n            $env:TMP = $oldTmp\n        }\n    }\n\n    It 'Creates sent log when SentLogPath is provided' {\n        $oldTemp = $env:TEMP\n        $oldTmp = $env:TMP\n\n        try {\n            $tempDir = Join-Path $TestDrive 'tmp-optin'\n            New-Item -ItemType Directory -Path $tempDir -Force | Out-Null\n            $env:TEMP = $tempDir\n            $env:TMP = $tempDir\n\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [FakeClientSentLogPsFactory]::Install()\n\n            $sentLogPath = Join-Path $TestDrive 'sent\\sentlog.json'\n            $result = Send-EmailMessage -From 'from@example.com' -To 'to@example.com' -Server 'smtp.example.com' -Port 25 -Subject 'Subject' -Text 'Body' -SentLogPath $sentLogPath\n\n            $result.Status | Should -BeTrue\n            (Test-Path $sentLogPath) | Should -BeTrue\n\n            $records = Get-Content -Path $sentLogPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }\n            $records.Count | Should -BeGreaterThan 0\n        } finally {\n            [Mailozaurr.Smtp]::ResetClientFactory()\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            $env:TEMP = $oldTemp\n            $env:TMP = $oldTmp\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.SkipAuth.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Authentication Skipped' {\n    It 'Skips authentication when no credentials provided' {\n        $result = Send-EmailMessage -From 'noauth@example.com' -To 'recipient@example.com' -Subject 'Skip Auth Test' -Body 'test' -Server 'smtp.example.com' -Port 25 -WhatIf -Verbose 4>&1\n        $output = $result | Where-Object { $_ -isnot [System.Management.Automation.VerboseRecord] }\n        $verbose = $result | Where-Object { $_ -is [System.Management.Automation.VerboseRecord] }\n        ($verbose | Select-Object -ExpandProperty Message) | Should -Contain 'Send-EmailMessage - Skipping authentication'\n        $output.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.Tests.ps1",
    "content": "﻿Describe 'Send-EmailMessage' {\n    $Body = '<b>Test</b>'\n    $Text = 'Test'\n    It 'Send email using given parameters (SMTP)' {\n        $sendEmailMessageSplat = @{\n            From                       = @{ Name = 'Przemysław Kłys'; Email = 'test@evotec.pl' }\n            To                         = 'testing@test.pl', 'test@gmail.com'\n            Server                     = 'smtp.office365.com'\n            HTML                       = $Body\n            Text                       = $Text\n            DeliveryNotificationOption = 'OnSuccess'\n            Priority                   = 'High'\n            Subject                    = 'This is another test email 🤣😍😒💖✨🎁 我'\n            SecureSocketOptions        = 'Auto'\n            Password                   = 'TempPassword'\n            WhatIf                     = $True\n        }\n\n        $Output = Send-EmailMessage @sendEmailMessageSplat -ErrorAction Stop\n        $Output.Error | Should -Be 'Email not sent (WhatIf)'\n        $Output.SentTo | Should -Be 'testing@test.pl,test@gmail.com'\n        $Output.SentFrom | Should -Be 'test@evotec.pl'\n        $Output.Message | Should -Be ''\n        $Output.Server | Should -Be 'smtp.office365.com'\n        $Output.Port | Should -Be '587'\n        $Output.Status | Should -Be $false\n    }\n    It 'Send email using given parameters (Graph)' -Skip {\n        # Credentials for Graph\n        $ClientID = '0fb383f1-8bfe'\n        $DirectoryID = 'ceb371f6-8745'\n\n        $EncryptedClientSecret = ConvertTo-SecureString -String 'VKDM_2.eC2US7pFW1' -AsPlainText -Force | ConvertFrom-SecureString\n\n        $Credential = ConvertTo-GraphCredential -ClientID $ClientID -ClientSecretEncrypted $EncryptedClientSecret -DirectoryID $DirectoryID\n\n        # sending email\n        $sendEmailMessageSplat = @{\n            From                 = 'random@domain.pl'\n            To                   = 'newemail@domain.pl'\n            Credential           = $Credential\n            HTML                 = $Body\n            Subject              = 'This is another test email 2'\n            Graph                = $true\n            Verbose              = $true\n            Priority             = 'Low'\n            DoNotSaveToSentItems = $true\n            WhatIf               = $true\n        }\n        $GraphOutput = Send-EmailMessage @sendEmailMessageSplat\n        $GraphOutput.Error | Should -Be 'Email not sent (WhatIf)'\n        $GraphOutput.SentTo | Should -Be 'newemail@domain.pl'\n        $GraphOutput.SentFrom | Should -Be 'random@domain.pl'\n        $GraphOutput.Message | Should -Be ''\n        $GraphOutput.Status | Should -Be $False\n    }\n    It 'Send email using given parameters (SMTP no TLS, no login/password)' {\n        $Output = Send-EmailMessage -From 'test@evotec.pl' -To 'mailozaurr@evotec.pl' -Server 'smtp.freesmtpservers.com' -Port 25 -Body 'test me 🤣😍😒💖✨🎁 Przemysław Kłys' -Subject '😒💖 This is another test email 我' -Verbose -WhatIf\n        $Output.Error | Should -Be 'Email not sent (WhatIf)'\n        $Output.SentTo | Should -Be 'mailozaurr@evotec.pl'\n        $Output.SentFrom | Should -Be 'test@evotec.pl'\n        $Output.Message | Should -Be ''\n        $Output.Server | Should -Be 'smtp.freesmtpservers.com'\n        $Output.Port | Should -Be 25\n        $Output.Status | Should -Be $false\n    }\n    It 'Send email using given parameters (MgGraphRequest WhatIf)' {\n        $params = @{\n            From           = 'from@example.com'\n            To             = 'to@example.com'\n            Subject        = 'Test'\n            Body           = $Body\n            MgGraphRequest = $true\n            WhatIf         = $true\n        }\n        $result = Send-EmailMessage @params\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n        $result.Status | Should -Be $false\n    }\n\n    It 'Accepts RetryAlways switch' {\n        $params = @{ From = 'a@b.com'; To = 'c@d.com'; Server = 'smtp.example.com'; Subject = 't'; Text = 'b'; RetryAlways = $true; RetryCount = 1; WhatIf = $true }\n        $result = Send-EmailMessage @params\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessage.WildcardAttachment.Tests.ps1",
    "content": "Describe 'Send-EmailMessage - Wildcard Attachments' {\n    It 'Expands wildcard file patterns' {\n        $dir = Join-Path $TestDrive 'wc'\n        New-Item -ItemType Directory -Path $dir | Out-Null\n        $f1 = Join-Path $dir 'file1.txt'\n        $f2 = Join-Path $dir 'file2.txt'\n        'a' | Set-Content -Path $f1\n        'b' | Set-Content -Path $f2\n\n        $cmdlet = [Mailozaurr.PowerShell.CmdletSendEmailMessage]::new()\n        $method = $cmdlet.GetType().GetMethod('FilterExistingPaths', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        $result = $method.Invoke($cmdlet, @([object[]]@(\"$dir/*.txt\"), 'Attachment'))\n\n        $result.Count | Should -Be 2\n        ($result | ForEach-Object { $_.FullName }) | Should -Contain $f1\n        ($result | ForEach-Object { $_.FullName }) | Should -Contain $f2\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailMessageConnectionPool.Tests.ps1",
    "content": "Describe 'Send-EmailMessage connection pool' {\n    It 'Reuses connection when pool enabled' {\n        $refs = @(\n            [Mailozaurr.Smtp].Assembly.Location,\n            [Mailozaurr.ClientSmtp].Assembly.Location,\n            [MimeKit.MimeMessage].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n        if (-not ('FakeClientPsPool' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit;\nusing MailKit.Security;\nusing MimeKit;\nusing System.Threading;\nusing System.Threading.Tasks;\npublic class FakeClientPs : ClientSmtp {\n    public static int ConnectCalls;\n    private bool _connected;\n    public override bool IsConnected => _connected;\n    public override void Connect(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {\n        ConnectCalls++;\n        _connected = true;\n    }\n    public override void Disconnect(bool quit, CancellationToken cancellationToken = default) {\n        _connected = false;\n    }\n    public override Task<string> SendAsync(MimeMessage message, CancellationToken cancellationToken, ITransferProgress progress) {\n        message.MessageId = message.MessageId ?? \"fake-\" + ConnectCalls.ToString();\n        return Task.FromResult(message.MessageId);\n    }\n}\npublic static class FakeClientPsPool {\n    public static ClientSmtp Create(ProtocolLogger logger) => new FakeClientPs();\n    public static void Install() => Smtp.ClientFactory = Create;\n    public static void Reset() => FakeClientPs.ConnectCalls = 0;\n}\n\"@\n        }\n\n        try {\n            [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [FakeClientPsPool]::Reset()\n            [FakeClientPsPool]::Install()\n\n            $params = @{ From = 'a@b.com'; To = 'c@d.com'; Server = 'h'; Subject = 't'; Text = 'b'; UseConnectionPool = $true }\n            Send-EmailMessage @params | Out-Null\n            Send-EmailMessage @params | Out-Null\n\n            [FakeClientPs]::ConnectCalls | Should -Be 1\n        } finally {\n            [Mailozaurr.Smtp]::ResetClientFactory()\n            [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n            [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Send-EmailPendingMessage.Tests.ps1",
    "content": "Describe 'Send-EmailPendingMessage' {\n    BeforeAll {\n        if (-not ('Mailozaurr.PendingMessageRecord' -as [type])) {\n            $modulePath = Join-Path $PSScriptRoot '..' 'Mailozaurr.psd1'\n            Import-Module $modulePath -Force\n        }\n        if (-not ('FakePendingMessageSender' -as [type])) {\n            $assemblies = @(\n                [Mailozaurr.PendingMessageRecord].Assembly.Location\n            )\n            Add-Type -ReferencedAssemblies $assemblies -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\npublic sealed class FakePendingMessageSender : IPendingMessageSender {\n    private static int sendCount;\n\n    public static void Reset() => sendCount = 0;\n\n    public static int SendCount => sendCount;\n\n    public Task SendAsync(PendingMessageRecord record, CancellationToken ct) {\n        sendCount++;\n        return Task.CompletedTask;\n    }\n}\n\npublic static class FakePendingMessageSenderFactory {\n    public static PendingMessageSenderFactory Create() {\n        var entries = new [] {\n            new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.None, new FakePendingMessageSender()),\n            new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.SendGrid, new FakePendingMessageSender()),\n            new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.Mailgun, new FakePendingMessageSender()),\n            new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.SES, new FakePendingMessageSender()),\n            new KeyValuePair<EmailProvider, IPendingMessageSender>(EmailProvider.Gmail, new FakePendingMessageSender()),\n        };\n        return new PendingMessageSenderFactory(entries);\n    }\n}\n\"@\n        }\n    }\n\n    BeforeEach {\n        [FakePendingMessageSender]::Reset()\n        $delegate = [System.Delegate]::CreateDelegate([System.Func[Mailozaurr.PendingMessageSenderFactory]], [FakePendingMessageSenderFactory], 'Create')\n        [Mailozaurr.PowerShell.CmdletSendEmailPendingMessage]::SenderFactoryProvider = $delegate\n    }\n\n    AfterEach {\n        [Mailozaurr.PowerShell.CmdletSendEmailPendingMessage]::SenderFactoryProvider = $null\n    }\n\n    It 'WhatIf skips sending pending messages' {\n        $path = Join-Path $TestDrive 'pending-whatif'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $msg.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $msg.Subject = 'Pending'\n        $msg.Body = [MimeKit.TextPart]::new('plain')\n        $stream = [System.IO.MemoryStream]::new()\n        $msg.WriteTo($stream)\n\n        $record = [Mailozaurr.PendingMessageRecord]::new()\n        $record.MessageId = $msg.MessageId\n        $record.MimeMessage = [Convert]::ToBase64String($stream.ToArray())\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $record.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $record.Server = 'smtp.server'\n        $record.Port = 25\n        $record.Provider = [Mailozaurr.EmailProvider]::None\n        $repo.SaveAsync($record).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -WhatIf\n\n        [FakePendingMessageSender]::SendCount | Should -Be 0\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 1\n    }\n\n    It 'Resends selected messages by id regardless of schedule' {\n        $path = Join-Path $TestDrive 'pending-targeted'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $msg.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $msg.Subject = 'Pending'\n        $msg.Body = [MimeKit.TextPart]::new('plain')\n        $stream = [System.IO.MemoryStream]::new()\n        $msg.WriteTo($stream)\n\n        $record = [Mailozaurr.PendingMessageRecord]::new()\n        $record.MessageId = $msg.MessageId\n        $record.MimeMessage = [Convert]::ToBase64String($stream.ToArray())\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $record.NextAttemptAt = [DateTimeOffset]::UtcNow.AddHours(2)\n        $record.Server = 'smtp.server'\n        $record.Port = 25\n        $repo.SaveAsync($record).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -MessageId $record.MessageId\n\n        [FakePendingMessageSender]::SendCount | Should -Be 1\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 0\n    }\n\n    It 'Processes only the requested provider' {\n        $path = Join-Path $TestDrive 'pending-provider'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $message = [MimeKit.MimeMessage]::new()\n        $message.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $message.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $message.Subject = 'Provider filter'\n        $message.Body = [MimeKit.TextPart]::new('plain')\n        $buffer = [System.IO.MemoryStream]::new()\n        $message.WriteTo($buffer)\n        $payload = [Convert]::ToBase64String($buffer.ToArray())\n\n        $smtpRecord = [Mailozaurr.PendingMessageRecord]::new()\n        $smtpRecord.MessageId = [Guid]::NewGuid().ToString()\n        $smtpRecord.MimeMessage = $payload\n        $smtpRecord.Timestamp = [DateTimeOffset]::UtcNow\n        $smtpRecord.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $smtpRecord.Server = 'smtp.server'\n        $smtpRecord.Port = 25\n        $smtpRecord.Provider = [Mailozaurr.EmailProvider]::None\n        $repo.SaveAsync($smtpRecord).GetAwaiter().GetResult()\n\n        $apiRecord = [Mailozaurr.PendingMessageRecord]::new()\n        $apiRecord.MessageId = [Guid]::NewGuid().ToString()\n        $apiRecord.MimeMessage = $payload\n        $apiRecord.Timestamp = [DateTimeOffset]::UtcNow\n        $apiRecord.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $apiRecord.Provider = [Mailozaurr.EmailProvider]::Gmail\n        $repo.SaveAsync($apiRecord).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -Provider ([Mailozaurr.EmailProvider]::None)\n\n        [FakePendingMessageSender]::SendCount | Should -Be 1\n        $remaining = @(Get-EmailPendingMessage -PendingMessagesPath $path)\n        $remaining.Count | Should -Be 1\n        $remaining[0].Provider | Should -Be ([Mailozaurr.EmailProvider]::Gmail)\n    }\n\n    It 'Replays non-SMTP messages when filtered by provider' {\n        $path = Join-Path $TestDrive 'pending-api'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $message = [MimeKit.MimeMessage]::new()\n        $message.From.Add([MimeKit.MailboxAddress]::Parse('sender@example.com'))\n        $message.To.Add([MimeKit.MailboxAddress]::Parse('recipient@example.com'))\n        $message.Subject = 'queued'\n        $message.Body = [MimeKit.TextPart]::new('plain')\n        $buffer = [System.IO.MemoryStream]::new()\n        $message.WriteTo($buffer)\n        $payload = [Convert]::ToBase64String($buffer.ToArray())\n\n        $gmailRecord = [Mailozaurr.PendingMessageRecord]::new()\n        $gmailRecord.MessageId = [Guid]::NewGuid().ToString()\n        $gmailRecord.MimeMessage = $payload\n        $gmailRecord.Timestamp = [DateTimeOffset]::UtcNow\n        $gmailRecord.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $gmailRecord.Provider = [Mailozaurr.EmailProvider]::Gmail\n        $repo.SaveAsync($gmailRecord).GetAwaiter().GetResult()\n\n        $sendGridRecord = [Mailozaurr.PendingMessageRecord]::new()\n        $sendGridRecord.MessageId = [Guid]::NewGuid().ToString()\n        $sendGridRecord.MimeMessage = $payload\n        $sendGridRecord.Timestamp = [DateTimeOffset]::UtcNow\n        $sendGridRecord.NextAttemptAt = [DateTimeOffset]::UtcNow\n        $sendGridRecord.Provider = [Mailozaurr.EmailProvider]::SendGrid\n        $repo.SaveAsync($sendGridRecord).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path -Provider ([Mailozaurr.EmailProvider]::Gmail)\n\n        [FakePendingMessageSender]::SendCount | Should -Be 1\n        $remaining = @(Get-EmailPendingMessage -PendingMessagesPath $path)\n        $remaining.Count | Should -Be 1\n        $remaining[0].Provider | Should -Be ([Mailozaurr.EmailProvider]::SendGrid)\n    }\n\n    It 'Honors schedule unless ProcessAll is specified' {\n        $path = Join-Path $TestDrive 'pending-schedule'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $message = [MimeKit.MimeMessage]::new()\n        $message.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $message.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $message.Subject = 'Future delivery'\n        $message.Body = [MimeKit.TextPart]::new('plain')\n        $dataStream = [System.IO.MemoryStream]::new()\n        $message.WriteTo($dataStream)\n\n        $scheduled = [Mailozaurr.PendingMessageRecord]::new()\n        $scheduled.MessageId = $message.MessageId\n        $scheduled.MimeMessage = [Convert]::ToBase64String($dataStream.ToArray())\n        $scheduled.Timestamp = [DateTimeOffset]::UtcNow\n        $scheduled.NextAttemptAt = [DateTimeOffset]::UtcNow.AddHours(4)\n        $scheduled.Server = 'smtp.server'\n        $scheduled.Port = 25\n        $repo.SaveAsync($scheduled).GetAwaiter().GetResult()\n\n        Send-EmailPendingMessage -PendingMessagesPath $path\n\n        [FakePendingMessageSender]::SendCount | Should -Be 0\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 1\n\n        [FakePendingMessageSender]::Reset()\n        Send-EmailPendingMessage -PendingMessagesPath $path -ProcessAll\n\n        [FakePendingMessageSender]::SendCount | Should -Be 1\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 0\n    }\n\n    It 'Processes due messages without hanging when using the file repository' {\n        $path = Join-Path $TestDrive 'pending-hang-check'\n        $options = [Mailozaurr.PendingMessageRepositoryOptions]::new()\n        $options.DirectoryPath = $path\n        $repo = [Mailozaurr.FilePendingMessageRepository]::new($options)\n\n        $message = [MimeKit.MimeMessage]::new()\n        $message.From.Add([MimeKit.MailboxAddress]::Parse('a@example.com'))\n        $message.To.Add([MimeKit.MailboxAddress]::Parse('b@example.com'))\n        $message.Subject = 'Immediate delivery'\n        $message.Body = [MimeKit.TextPart]::new('plain')\n        $data = [System.IO.MemoryStream]::new()\n        $message.WriteTo($data)\n\n        $record = [Mailozaurr.PendingMessageRecord]::new()\n        $record.MessageId = $message.MessageId\n        $record.MimeMessage = [Convert]::ToBase64String($data.ToArray())\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $record.NextAttemptAt = [DateTimeOffset]::UtcNow.AddMinutes(-1)\n        $record.Provider = [Mailozaurr.EmailProvider]::None\n        $repo.SaveAsync($record).GetAwaiter().GetResult()\n\n        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()\n        Send-EmailPendingMessage -PendingMessagesPath $path\n        $stopwatch.Stop()\n\n        [FakePendingMessageSender]::SendCount | Should -Be 1\n        (Get-EmailPendingMessage -PendingMessagesPath $path | Measure-Object).Count | Should -Be 0\n        $stopwatch.Elapsed.TotalSeconds | Should -BeLessThan 5\n    }\n}\n"
  },
  {
    "path": "Tests/Send-GmailMessage.Tests.ps1",
    "content": "Describe 'Send-GmailMessage cmdlet' {\n    It 'Cmdlet derives from AsyncPSCmdlet' {\n        $base = [Mailozaurr.PowerShell.CmdletSendGmailMessage].BaseType\n        $base.FullName | Should -Be 'Mailozaurr.PowerShell.AsyncPSCmdlet'\n    }\n\n    It 'Accepts custom headers parameter' {\n        $cred = ConvertTo-OAuth2Credential -UserName 'user@gmail.com' -Token 'tok'\n        $result = Send-GmailMessage -GmailAccount 'user@gmail.com' -Credential $cred -From 'user@gmail.com' -To 'recipient@example.com' -Subject 'h' -TextBody 'b' -Headers @{ 'X-Test' = '123' } -WhatIf\n        $result.Error | Should -Be 'Email not sent (WhatIf)'\n    }\n}\n"
  },
  {
    "path": "Tests/SendLogResolver.Tests.ps1",
    "content": "Describe 'SendLogResolver' {\n    It 'Resolves saved record' {\n        $path = Join-Path $TestDrive 'sentlog.json'\n        $repo = [Mailozaurr.FileSentMessageRepository]::new($path)\n        $record = [Mailozaurr.SentMessageRecord]::new()\n        $record.MessageId = 'id1'\n        $record.Recipients = 'c@d.com'\n        $record.Subject = 'subject'\n        $record.Timestamp = [DateTimeOffset]::UtcNow\n        $repo.SaveAsync($record).GetAwaiter().GetResult() | Out-Null\n        $resolver = [Mailozaurr.SendLogResolver]::new($repo)\n        $ndr = [Mailozaurr.NonDeliveryReports.NonDeliveryReport]::new()\n        $ndr.OriginalMessageId = 'id1'\n        $ndr.FinalRecipient = 'c@d.com'\n        $resolved = $resolver.ResolveAsync($ndr).GetAwaiter().GetResult()\n        $resolved.Subject | Should -Be 'subject'\n    }\n}\n"
  },
  {
    "path": "Tests/Set-GraphEvent.Tests.ps1",
    "content": "Describe 'Set-GraphEvent' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        $ev = [Mailozaurr.GraphEvent]::new()\n        Set-GraphEvent -UserPrincipalName 'u' -EventId 'id' -Event $ev -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Set-GraphEvent - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Set-GraphInboxRule.Tests.ps1",
    "content": "Describe 'Set-GraphInboxRule' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Set-GraphInboxRule -UserPrincipalName 'u' -RuleId 'id' -Rule @{displayName='x'} -WhatIf -WarningVariable warn\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Set-GraphInboxRule - Connection not provided and no default session available.'\n    }\n}\n"
  },
  {
    "path": "Tests/Test-MimeMessageSignature.Tests.ps1",
    "content": "Describe 'Test-MimeMessageSignature' {\n    It 'Returns false for unsigned message' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Subject = 'plain'\n        $result = $msg | Test-MimeMessageSignature\n        $result | Should -Be $false\n    }\n\n    It 'Verifies PGP signature' -Skip:(-not $IsWindows) {\n        $base = Join-Path $PSScriptRoot '..'\n        $pub = Join-Path $base 'Examples/PGPKeys/mimekit.gpg.pub'\n        $sec = Join-Path $base 'Examples/PGPKeys/mimekit.gpg.sec'\n        $smtp = [Mailozaurr.Smtp]::new()\n        $smtp.From = 'mimekit@example.com'\n        $smtp.To = @('b@c.com')\n        $smtp.Subject = 't'\n        $smtp.TextBody = 'body'\n        $smtp.CreateMessage()\n        $smtp.PgpSign($pub,$sec,'no.secret',$false) | Out-Null\n        $msg = $smtp.Message\n        $result = $msg | Test-MimeMessageSignature -PublicKeyPath $pub\n        $result | Should -Be $true\n    }\n\n    It 'Verifies SMIME signature' -Skip:(-not $IsWindows) {\n        $cert = [Mailozaurr.TemporarySmimeCertificate]::CreateSelfSigned('CN=test@example.com')\n        $smtp = [Mailozaurr.Smtp]::new()\n        $smtp.From = 'x@y.com'\n        $smtp.To = @('z@a.com')\n        $smtp.Subject = 's'\n        $smtp.TextBody = 'text'\n        $smtp.CreateMessage()\n        $smtp.Sign($cert) | Out-Null\n        $msg = $smtp.Message\n        $result = $msg | Test-MimeMessageSignature -Certificate $cert\n        $result | Should -Be $true\n    }\n}\n"
  },
  {
    "path": "Tests/Test-SmtpConnection.Tests.ps1",
    "content": "Describe 'Test-SmtpConnection' {\n    It 'Returns capabilities object' {\n        $refs = @(\n            [Mailozaurr.Smtp].Assembly.Location,\n            [Mailozaurr.ClientSmtp].Assembly.Location,\n            [MailKit.Security.SecureSocketOptions].Assembly.Location\n        )\n        if (-not ('FakeClientPs3' -as [type])) {\n            Add-Type -ReferencedAssemblies $refs -CompilerOptions '/nowarn:1701,1702' -TypeDefinition @\"\nusing Mailozaurr;\nusing MailKit;\nusing MailKit.Net.Smtp;\nusing MailKit.Security;\nusing System.Threading;\npublic class FakeClientPs3 : ClientSmtp {\n    public override bool IsConnected => true;\n    public override SmtpCapabilities GetCapabilitiesSnapshot() => SmtpCapabilities.Pipelining;\n    public override void Connect(string host, int port, SecureSocketOptions options, CancellationToken cancellationToken = default) {}\n    public override void Disconnect(bool quit, CancellationToken cancellationToken = default) {}\n    public override void NoOp(CancellationToken cancellationToken = default) {}\n}\npublic static class FakeClientPs3Factory {\n    public static ClientSmtp Create(ProtocolLogger logger) => new FakeClientPs3();\n    public static void Install() => Smtp.ClientFactory = Create;\n}\n\"@\n        }\n        try {\n            [FakeClientPs3Factory]::Install()\n            $result = Test-SmtpConnection -Server 'h'\n            $result | Should -BeOfType 'Mailozaurr.SmtpConnectionInfo'\n            $result.Capabilities | Should -Match 'Pipelining'\n        } finally {\n            [Mailozaurr.Smtp]::ResetClientFactory()\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/Unprotect-MimeMessage.Tests.ps1",
    "content": "Describe 'Unprotect-MimeMessage' {\n    It 'Returns original message when not encrypted' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Subject = 'plain'\n        $result = $msg | Unprotect-MimeMessage\n        $result | Should -Be $msg\n    }\n\n    It 'Decrypts PGP message' {\n        $base = Join-Path $PSScriptRoot '..'\n        $pub = Join-Path $base 'Examples/PGPKeys/mimekit.gpg.pub'\n        $sec = Join-Path $base 'Examples/PGPKeys/mimekit.gpg.sec'\n        $smtp = [Mailozaurr.Smtp]::new()\n        $smtp.From = 'a@b.com'\n        $smtp.To = @('b@c.com')\n        $smtp.Subject = 't'\n        $smtp.TextBody = 'body'\n        $smtp.CreateMessage()\n        $smtp.PgpEncrypt($pub) | Out-Null\n        $msg = $smtp.Message\n        $out = $msg | Unprotect-MimeMessage -PrivateKeyPath $sec -PrivateKeyPassword 'no.secret'\n        ($out.TextBody) | Should -Be 'body'\n    }\n\n    It 'Decrypts SMIME message' -Skip:(-not $IsWindows) {\n        $cert = [Mailozaurr.TemporarySmimeCertificate]::CreateSelfSigned('CN=test@example.com')\n        $smtp = [Mailozaurr.Smtp]::new()\n        $smtp.From = 'x@y.com'\n        $smtp.To = @('z@a.com')\n        $smtp.Subject = 'enc'\n        $smtp.TextBody = 'data'\n        $smtp.CreateMessage()\n        $smtp.Encrypt($cert) | Out-Null\n        $msg = $smtp.Message\n        $out = $msg | Unprotect-MimeMessage -Certificate $cert\n        ($out.TextBody) | Should -Be 'data'\n    }\n\n    It 'Handles wrapper objects' {\n        $msg = [MimeKit.MimeMessage]::new()\n        $msg.Subject = 'plain'\n        $imap = [Mailozaurr.ImapEmailMessage]::new([MailKit.UniqueId]::new(1), $msg)\n        $info = [Mailozaurr.ImapMessageInfo]::new($imap)\n        $out1 = Unprotect-MimeMessage -InputObject $imap\n        $out2 = Unprotect-MimeMessage -InputObject $info\n        $graph = [Mailozaurr.GraphEmailMessage]::new('1', $msg)\n        $out3 = Unprotect-MimeMessage -InputObject $graph\n        $out1.Subject | Should -Be $msg.Subject\n        $out2.Subject | Should -Be $msg.Subject\n        $out3.Subject | Should -Be $msg.Subject\n    }\n}\n"
  },
  {
    "path": "Tests/Wait-GraphMessage.Tests.ps1",
    "content": "Describe 'Wait-GraphMessage' {\n    It 'Warns when Graph connection missing' {\n        $info = [Mailozaurr.PowerShell.GraphConnectionInfo]::new()\n        Wait-GraphMessage -Connection $info -UserPrincipalName 'u' -WarningVariable warn -Action {} -ErrorAction SilentlyContinue\n        $warn | Should -Not -BeNullOrEmpty\n        $warn[0] | Should -Be 'Wait-GraphMessage - Connection not provided and no default session available.'\n    }\n\n    It 'Invokes action and cancels on match' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitGraphMessage]::new()\n        $script:wasCalled = $false\n        $cmd.Action = { $script:wasCalled = $true }\n        $cmd.Until = { $true }\n        $cmd.StopOnMatch = $true\n        $method = $cmd.GetType().GetMethod('OnMessageArrived', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        $msg = [System.Collections.Generic.Dictionary[string,object]]::new()\n        $msg.Add('id','1')\n        $method.Invoke($cmd, @($null, $msg))\n        $wasCalled | Should -Be $true\n        $field = $cmd.GetType().BaseType.GetField('_cancelSource', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        ($field.GetValue($cmd)).IsCancellationRequested | Should -Be $true\n    }\n\n    It 'Dispose can be called multiple times' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitGraphMessage]::new()\n        $cmd.Dispose()\n        { $cmd.Dispose() } | Should -Not -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Wait-IMAPMessage.Tests.ps1",
    "content": "Describe 'Wait-IMAPMessage' {\n    It 'Throws when IMAP connection missing' {\n        $info = [Mailozaurr.PowerShell.ImapConnectionInfo]::new()\n        { Wait-IMAPMessage -Client $info -Action {} } | Should -Throw\n    }\n\n    It 'Invokes action and cancels on match' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitIMAPMessage]::new()\n        $script:wasCalled = $false\n        $cmd.Action = { $script:wasCalled = $true }\n        $cmd.Until = { $true }\n        $cmd.StopOnMatch = $true\n        $method = $cmd.GetType().GetMethod('OnMessageArrived', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        $msg = [Mailozaurr.ImapEmailMessage]::new([MailKit.UniqueId]::MinValue, [MimeKit.MimeMessage]::new())\n        $method.Invoke($cmd, @($null, $msg))\n        $wasCalled | Should -Be $true\n        $field = $cmd.GetType().BaseType.GetField('_cancelSource', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        ($field.GetValue($cmd)).IsCancellationRequested | Should -Be $true\n    }\n\n    It 'Dispose can be called multiple times' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitIMAPMessage]::new()\n        $cmd.Dispose()\n        { $cmd.Dispose() } | Should -Not -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Wait-POP3Message.Tests.ps1",
    "content": "Describe 'Wait-POP3Message' {\n    It 'Throws when POP3 connection missing' {\n        $info = [Mailozaurr.PowerShell.PopConnectionInfo]::new()\n        { Wait-POP3Message -Client $info -Action {} } | Should -Throw\n    }\n\n    It 'Invokes action and cancels on match' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitPOP3Message]::new()\n        $script:wasCalled = $false\n        $cmd.Action = { $script:wasCalled = $true }\n        $cmd.Until = { $true }\n        $cmd.StopOnMatch = $true\n        $method = $cmd.GetType().GetMethod('OnMessageArrived', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        $msg = [Mailozaurr.Pop3EmailMessage]::new(0, [MimeKit.MimeMessage]::new())\n        $method.Invoke($cmd, @($null, $msg))\n        $wasCalled | Should -Be $true\n        $field = $cmd.GetType().BaseType.GetField('_cancelSource', [System.Reflection.BindingFlags] 'NonPublic, Instance')\n        ($field.GetValue($cmd)).IsCancellationRequested | Should -Be $true\n    }\n\n    It 'Dispose can be called multiple times' {\n        $cmd = [Mailozaurr.PowerShell.CmdletWaitPOP3Message]::new()\n        $cmd.Dispose()\n        { $cmd.Dispose() } | Should -Not -Throw\n    }\n}\n"
  },
  {
    "path": "Tests/Watch-SmtpConnectionPool.Tests.ps1",
    "content": "Describe 'Watch-SmtpConnectionPool' {\n    BeforeAll { Import-Module $PSScriptRoot\\..\\Mailozaurr.psd1 -Force }\n\n    It 'Registers watcher for pool updates' {\n        [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($true)\n        [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n        $values = [System.Collections.Generic.List[int]]::new()\n        $watcher = Watch-SmtpConnectionPool -Action { param($s) $values.Add($s.CurrentPoolSize) }\n        [Mailozaurr.SmtpConnectionPool]::ClearConnectionPool()\n        [Mailozaurr.SmtpConnectionPool]::remove_PoolSizeChanged($watcher)\n        [Mailozaurr.SmtpConnectionPool]::SetPoolingEnabled($false)\n        $values.Count | Should -BeGreaterThan 0\n    }\n}\n"
  },
  {
    "path": "Website/README.md",
    "content": "# Mailozaurr Website Content\n\nThis folder contains curated source content that the Evotec website imports for the Mailozaurr project page.\n\n## Layout\n\n- `content/project-docs/` contains short project documentation pages shown under /projects/mailozaurr/docs/.\n- `content/examples/` contains curated website examples shown under /projects/mailozaurr/examples/.\n- API artifacts are not enabled from this folder yet. Add `WebsiteArtifacts/apidocs` and wire it in the website catalog only when external help/API metadata is generated and reviewed.\n\n## Editing Rules\n\n- Keep this folder intentional and small.\n- Do not mirror raw `Examples/`, `Example/`, or generated output folders into the public website.\n- Add only examples that have a clear explanation, a small code sample, and a link back to the original source file.\n- Keep links rooted at /projects/mailozaurr/ so the same content works on localhost, evotec.xyz, and evotec.pl.\n\nThe website pipeline prefers this `Website/content/...` layout over legacy root-level content folders.\n\n"
  },
  {
    "path": "Website/content/examples/_index.md",
    "content": "---\ntitle: \"Mailozaurr Examples\"\ndescription: \"Curated examples for Mailozaurr.\"\nlayout: docs\n---\n\nThese examples are maintained with the Mailozaurr repository and selected for the website because they show safe, reviewable usage patterns.\n\n## Featured examples\n\n<div class=\"ev-example-card-grid\">\n  <a class=\"ev-example-card\" href=\"./test-smtp-connection-before-sending/\">\n    <span class=\"ev-example-card__eyebrow\">SMTP</span>\n    <h3>Test SMTP before sending</h3>\n    <p>Check whether a server keeps connections open before deciding whether to use pooling.</p>\n  </a>\n  <a class=\"ev-example-card\" href=\"./validate-email-addresses/\">\n    <span class=\"ev-example-card__eyebrow\">Validation</span>\n    <h3>Validate email addresses</h3>\n    <p>Run a quick validation pass over user input or imported address lists.</p>\n  </a>\n</div>\n"
  },
  {
    "path": "Website/content/examples/test-smtp-connection-before-sending.md",
    "content": "---\ntitle: \"Test SMTP before sending\"\ndescription: \"Use Mailozaurr to test SMTP capabilities and choose a safer send pattern.\"\nlayout: docs\n---\n\nThis pattern is useful before adding SMTP sending to an operational script.\n\nIt comes from the source example at `Examples/Example-TestSmtpConnection.ps1`.\n\n## When to use this pattern\n\n- You need to confirm SMTP connectivity before sending mail.\n- You want to decide whether connection pooling is safe.\n- You are adding a WhatIf-protected send path to a script.\n\n## Example\n\n```powershell\nImport-Module .\\Mailozaurr.psd1 -Force\n\n$info = Test-SmtpConnection -Server 'smtp.example.com' -Port 587\n\n$poolSettings = if ($info.Persistent) {\n    @{ UseConnectionPool = $true; ConnectionPoolSize = 2 }\n} else {\n    @{}\n}\n\nSend-EmailMessage -From 'sender@example.com' -To 'recipient@example.com' -Server 'smtp.example.com' @poolSettings -WhatIf\n```\n\n## What this demonstrates\n\n- testing the transport before sending\n- using SMTP capability data to shape the send path\n- keeping the example safe with WhatIf\n\n## Source\n\n- [Example-TestSmtpConnection.ps1](https://github.com/EvotecIT/Mailozaurr/blob/v2-speedygonzales/Examples/Example-TestSmtpConnection.ps1)\n\n"
  },
  {
    "path": "Website/content/examples/validate-email-addresses.md",
    "content": "---\ntitle: \"Validate email addresses\"\ndescription: \"Use Mailozaurr to validate email address strings from parameters or pipeline input.\"\nlayout: docs\n---\n\nThis pattern is useful when an automation accepts address lists from files, forms, or another system.\n\nIt comes from the source example at `Examples/Example-ValidateEmail.ps1`.\n\n## When to use this pattern\n\n- You accept email addresses from user input.\n- You import recipients from CSV or another system.\n- You want invalid values to be visible before sending.\n\n## Example\n\n```powershell\nImport-Module .\\Mailozaurr.psd1 -Force\n\n$addresses = 'admin@example.com', 'broken-address', 'helpdesk@example.org'\n$addresses | Test-EmailAddress -Verbose | Format-Table\n```\n\n## What this demonstrates\n\n- validating parameter and pipeline input\n- making bad addresses visible early\n- keeping send workflows separate from validation\n\n## Source\n\n- [Example-ValidateEmail.ps1](https://github.com/EvotecIT/Mailozaurr/blob/v2-speedygonzales/Examples/Example-ValidateEmail.ps1)\n\n"
  },
  {
    "path": "Website/content/project-docs/docs/_index.md",
    "content": "---\ntitle: \"Mailozaurr Docs\"\ndescription: \"Curated documentation workspace for Mailozaurr.\"\nlayout: docs\n---\n\nMailozaurr provides SMTP, POP3, IMAP, Microsoft Graph mail, and mail-format tooling for PowerShell and .NET.\n\n## Start here\n\n- [Installation](./install/)\n- [Project overview](./overview/)\n- [Back to project overview](/projects/mailozaurr/)\n"
  },
  {
    "path": "Website/content/project-docs/docs/install.md",
    "content": "---\ntitle: \"Install Mailozaurr\"\ndescription: \"Install Mailozaurr from the package source used by this project.\"\nlayout: docs\n---\n\nUse this page when you need the shortest setup path before trying the curated examples.\n\n## PowerShell Gallery\n\n```powershell\nInstall-Module Mailozaurr -Scope CurrentUser\n```\n\n## Next steps\n\n- Review the [project overview](../overview/)\n- Browse the [curated examples](/projects/mailozaurr/examples/)\n"
  },
  {
    "path": "Website/content/project-docs/docs/overview.md",
    "content": "---\ntitle: \"Mailozaurr overview\"\ndescription: \"Mailozaurr provides SMTP, POP3, IMAP, Microsoft Graph mail, and mail-format tooling for PowerShell and .NET.\"\nlayout: docs\n---\n\nUse Mailozaurr when email workflows need repeatable automation: sending messages, validating addresses, working with Microsoft Graph, or processing mailbox data.\n\n## Typical use\n\n- SMTP and Graph-based email sending\n- mailbox and message automation\n- email validation, attachment handling, and protocol diagnostics\n\n## Related project pages\n\n- [Project overview](/projects/mailozaurr/)\n- [Examples](/projects/mailozaurr/examples/)\n"
  },
  {
    "path": "Website/content/project-docs/docs/toc.yml",
    "content": "- title: Docs Home\n  href: ./\n- title: Install\n  href: install/\n- title: Overview\n  href: overview/\n"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        informational: true\n    patch:\n      default:\n        informational: true\n    changes:\n      default:\n        informational: true\n"
  }
]