├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── pages.yml │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── .mailmap ├── BSDL ├── COPYING ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmarks ├── .gitignore ├── stringprep.yml └── table-regexps.yml ├── bin ├── check-regexps ├── console └── setup ├── docs └── styles.css ├── lib └── net │ ├── imap.rb │ └── imap │ ├── authenticators.rb │ ├── command_data.rb │ ├── config.rb │ ├── config │ ├── attr_accessors.rb │ ├── attr_inheritance.rb │ └── attr_type_coercion.rb │ ├── connection_state.rb │ ├── data_encoding.rb │ ├── data_lite.rb │ ├── deprecated_client_options.rb │ ├── errors.rb │ ├── esearch_result.rb │ ├── fetch_data.rb │ ├── flags.rb │ ├── response_data.rb │ ├── response_parser.rb │ ├── response_parser │ └── parser_utils.rb │ ├── response_reader.rb │ ├── sasl.rb │ ├── sasl │ ├── anonymous_authenticator.rb │ ├── authentication_exchange.rb │ ├── authenticators.rb │ ├── client_adapter.rb │ ├── cram_md5_authenticator.rb │ ├── digest_md5_authenticator.rb │ ├── external_authenticator.rb │ ├── gs2_header.rb │ ├── login_authenticator.rb │ ├── oauthbearer_authenticator.rb │ ├── plain_authenticator.rb │ ├── protocol_adapters.rb │ ├── scram_algorithm.rb │ ├── scram_authenticator.rb │ ├── stringprep.rb │ └── xoauth2_authenticator.rb │ ├── sasl_adapter.rb │ ├── search_result.rb │ ├── sequence_set.rb │ ├── stringprep.rb │ ├── stringprep │ ├── nameprep.rb │ ├── saslprep.rb │ ├── saslprep_tables.rb │ ├── tables.rb │ └── trace.rb │ ├── uidplus_data.rb │ └── vanished_data.rb ├── net-imap.gemspec ├── rakelib ├── benchmarks.rake ├── rdoc.rake ├── rfcs.rake ├── saslprep.rake └── string_prep_tables_generator.rb ├── rfcs └── rfc3454.txt ├── sample └── net-imap.rb └── test ├── lib └── helper.rb └── net ├── fixtures ├── Makefile ├── cacert.pem ├── dhparams.pem ├── server.crt └── server.key └── imap ├── fake_server.rb ├── fake_server ├── command.rb ├── command_reader.rb ├── command_response_writer.rb ├── command_router.rb ├── configuration.rb ├── connection.rb ├── connection_state.rb ├── response_writer.rb ├── session.rb ├── socket.rb └── test_helper.rb ├── fixtures └── response_parser │ ├── acl_responses.yml │ ├── body_structure_responses.yml │ ├── capability_responses.yml │ ├── continuation_requests.yml │ ├── enabled_responses.yml │ ├── esearch_responses.yml │ ├── expunge_responses.yml │ ├── fetch_responses.yml │ ├── flags_responses.yml │ ├── id_responses.yml │ ├── list_responses.yml │ ├── mailbox_size_responses.yml │ ├── namespace_responses.yml │ ├── quirky_behaviors.yml │ ├── resp_code_examples.yml │ ├── resp_cond_examples.yml │ ├── resp_text_responses.yml │ ├── rfc3501_examples.yml │ ├── rfc7162_condstore_qresync_responses.yml │ ├── rfc8474_objectid_responses.yml │ ├── rfc9208_quota_responses.yml │ ├── rfc9394_partial.yml │ ├── rfc9586_uidonly_responses.yml │ ├── ruby.png │ ├── search_responses.yml │ ├── status_responses.yml │ ├── thread_responses.yml │ ├── uidplus_extension.yml │ └── utf8_responses.yml ├── net_imap_test_helpers.rb ├── regexp_collector.rb ├── test_config.rb ├── test_data_lite.rb ├── test_deprecated_client_options.rb ├── test_errors.rb ├── test_esearch_result.rb ├── test_fetch_data.rb ├── test_imap.rb ├── test_imap_authenticators.rb ├── test_imap_capabilities.rb ├── test_imap_connection_state.rb ├── test_imap_data_encoding.rb ├── test_imap_login.rb ├── test_imap_max_response_size.rb ├── test_imap_response_handlers.rb ├── test_imap_response_parser.rb ├── test_imap_responses.rb ├── test_regexps.rb ├── test_response_reader.rb ├── test_saslprep.rb ├── test_search_result.rb ├── test_sequence_set.rb ├── test_stringprep_nameprep.rb ├── test_stringprep_profiles.rb ├── test_stringprep_tables.rb ├── test_thread_member.rb ├── test_uidplus_data.rb └── test_vanished_data.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 7 | labels: ["breaking-change"] 8 | - title: Added 9 | labels: ["enhancement"] 10 | - title: Deprecated 11 | labels: ["deprecation"] 12 | - title: Fixed 13 | labels: ["bug"] 14 | - title: Documentation 15 | labels: ["documentation"] 16 | - title: Other Changes 17 | labels: ["*"] 18 | exclude: 19 | labels: 20 | - "tests-only" 21 | - "dependencies" 22 | - "workflows" 23 | authors: 24 | - "dependabot" 25 | - title: Miscellaneous 26 | labels: ["*"] 27 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy RDoc site to Pages 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Setup Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: '3.3' 27 | bundler-cache: true 28 | - name: Setup Pages 29 | id: pages 30 | uses: actions/configure-pages@v5 31 | - name: Build with RDoc 32 | run: bundle exec rake rdoc 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: { path: 'doc' } 36 | 37 | deploy: 38 | environment: 39 | name: github-pages 40 | url: ${{ steps.deployment.outputs.page_url }} 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/net-imap' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/net-imap 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | # Set up 26 | - name: Harden Runner 27 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 28 | with: 29 | egress-policy: audit 30 | 31 | - uses: actions/checkout@v4 32 | 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | bundler-cache: true 37 | ruby-version: ruby 38 | 39 | # Release 40 | - name: Publish to RubyGems 41 | uses: rubygems/release-gem@v1 42 | 43 | - name: Create GitHub release 44 | run: | 45 | tag_name="$(git describe --tags --abbrev=0)" 46 | gh release create "${tag_name}" --verify-tag --draft --generate-notes 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 3.1 11 | 12 | build: 13 | needs: ruby-versions 14 | permissions: 15 | contents: read 16 | checks: write 17 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 18 | strategy: 19 | matrix: 20 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 21 | os: [ ubuntu-latest, macos-latest, windows-latest ] 22 | experimental: [false] 23 | exclude: 24 | - { ruby: head, os: windows-latest } 25 | include: 26 | - { ruby: head, os: windows-latest, experimental: true } 27 | runs-on: ${{ matrix.os }} 28 | continue-on-error: ${{ matrix.experimental }} 29 | timeout-minutes: 15 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: ${{ matrix.ruby }} 36 | bundler-cache: true 37 | rubygems: 3.5.14 38 | - name: Run test 39 | run: bundle exec rake test 40 | timeout-minutes: 5 # _should_ finish in under a minute 41 | 42 | - uses: joshmfrankel/simplecov-check-action@main 43 | if: matrix.os == 'ubuntu-latest' && github.event_name != 'pull_request' 44 | with: 45 | check_job_name: "SimpleCov - ${{ matrix.ruby }}" 46 | minimum_suite_coverage: 90 47 | minimum_file_coverage: 40 # TODO: increase this after switching to SASL::AuthenticationExchange 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /rfcs 8 | /spec/reports/ 9 | /tmp/ 10 | /Gemfile.lock 11 | /benchmarks/Gemfile* 12 | /benchmarks/parser.yml 13 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | nicholas a. evans 2 | nicholas a. evans 3 | nicholas a. evans 4 | nicholas a. evans 5 | 6 | Shugo Maeda 7 | Shugo Maeda 8 | 9 | Nobuyoshi Nakada 10 | Nobuyoshi Nakada 11 | 12 | Hiroshi SHIBATA 13 | Hiroshi SHIBATA 14 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "digest" 8 | gem "strscan" 9 | gem "base64" 10 | 11 | gem "irb" 12 | gem "rake" 13 | gem "rdoc" 14 | gem "test-unit" 15 | gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core" 16 | 17 | gem "benchmark-driver", require: false 18 | 19 | group :test do 20 | gem "simplecov", require: false 21 | gem "simplecov-html", require: false 22 | gem "simplecov-json", require: false 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All the files in this distribution are covered under either the Ruby license or 2 | the BSD-2-Clause license (see the file COPYING) except some documentation mentioned 3 | below. 4 | 5 | ------------------------------------------------------------------------- 6 | 7 | This software includes documentation which has been copied from the relevant 8 | RFCs. The copied documentation is covered by the following licenses: 9 | 10 | RFC 3501 (Editor: M. Crispin) 11 | Full Copyright Statement 12 | 13 | Copyright (C) The Internet Society (2003). All Rights Reserved. 14 | 15 | This document and translations of it may be copied and furnished to 16 | others, and derivative works that comment on or otherwise explain it 17 | or assist in its implementation may be prepared, copied, published 18 | and distributed, in whole or in part, without restriction of any 19 | kind, provided that the above copyright notice and this paragraph are 20 | included on all such copies and derivative works. However, this 21 | document itself may not be modified in any way, such as by removing 22 | the copyright notice or references to the Internet Society or other 23 | Internet organizations, except as needed for the purpose of 24 | developing Internet standards in which case the procedures for 25 | copyrights defined in the Internet Standards process must be 26 | followed, or as required to translate it into languages other than 27 | English. 28 | 29 | The limited permissions granted above are perpetual and will not be 30 | revoked by the Internet Society or its successors or assigns. v This 31 | document and the information contained herein is provided on an "AS 32 | IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK 33 | FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT 34 | LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL 35 | NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY 36 | OR FITNESS FOR A PARTICULAR PURPOSE. 37 | 38 | 39 | RFC9051 (Editors: A. Melnikov, B. Leiba) 40 | Copyright Notice 41 | 42 | Copyright (c) 2021 IETF Trust and the persons identified as the 43 | document authors. All rights reserved. 44 | 45 | This document is subject to BCP 78 and the IETF Trust's Legal 46 | Provisions Relating to IETF Documents 47 | (https://trustee.ietf.org/license-info) in effect on the date of 48 | publication of this document. Please review these documents 49 | carefully, as they describe your rights and restrictions with respect 50 | to this document. Code Components extracted from this document must 51 | include Simplified BSD License text as described in Section 4.e of 52 | the Trust Legal Provisions and are provided without warranty as 53 | described in the Simplified BSD License. 54 | 55 | This document may contain material from IETF Documents or IETF 56 | Contributions published or made publicly available before November 57 | 10, 2008. The person(s) controlling the copyright in some of this 58 | material may not have granted the IETF Trust the right to allow 59 | modifications of such material outside the IETF Standards Process. 60 | Without obtaining an adequate license from the person(s) controlling 61 | the copyright in such materials, this document may not be modified 62 | outside the IETF Standards Process, and derivative works of it may 63 | not be created outside the IETF Standards Process, except to format 64 | it for publication as an RFC or to translate it into languages other 65 | than English. 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::IMAP 2 | 3 | Net::IMAP implements Internet Message Access Protocol (IMAP) client 4 | functionality. The protocol is described in 5 | [RFC3501](https://www.rfc-editor.org/rfc/rfc3501), 6 | [RFC9051](https://www.rfc-editor.org/rfc/rfc9051) and various extensions. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'net-imap' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle install 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install net-imap 23 | 24 | ## Usage 25 | 26 | ### Connect with TLS to port 993 27 | 28 | ```ruby 29 | imap = Net::IMAP.new('mail.example.com', ssl: true) 30 | imap.port => 993 31 | imap.tls_verified? => true 32 | case imap.greeting.name 33 | in /OK/i 34 | # The client is connected in the "Not Authenticated" state. 35 | imap.authenticate("PLAIN", "joe_user", "joes_password") 36 | in /PREAUTH/i 37 | # The client is connected in the "Authenticated" state. 38 | end 39 | ``` 40 | 41 | ### List sender and subject of all recent messages in the default mailbox 42 | 43 | ```ruby 44 | imap.examine('INBOX') 45 | imap.search(["RECENT"]).each do |message_id| 46 | envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] 47 | puts "#{envelope.from[0].name}: \t#{envelope.subject}" 48 | end 49 | ``` 50 | 51 | ### Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" 52 | 53 | ```ruby 54 | imap.select('Mail/sent-mail') 55 | if imap.list('Mail/', 'sent-apr03').empty? 56 | imap.create('Mail/sent-apr03') 57 | end 58 | imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| 59 | if imap.capable?(:move) || imap.capable?(:IMAP4rev2) 60 | imap.move(message_id, "Mail/sent-apr03") 61 | else 62 | imap.copy(message_id, "Mail/sent-apr03") 63 | imap.store(message_id, "+FLAGS", [:Deleted]) 64 | end 65 | end 66 | imap.expunge 67 | ``` 68 | 69 | ## Development 70 | 71 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 72 | 73 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 74 | 75 | ## Contributing 76 | 77 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-imap. 78 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | require "rake/clean" 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << "test/lib" 9 | t.ruby_opts << "-rhelper" 10 | t.test_files = FileList["test/**/test_*.rb"] 11 | end 12 | 13 | task :default => :test 14 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile-*.lock 2 | -------------------------------------------------------------------------------- /benchmarks/stringprep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | prelude: | 3 | begin 4 | require "mongo" # gem install mongo 5 | require "idn" # gem install idn-ruby 6 | rescue LoadError 7 | warn "You must 'gem install mongo idn-ruby' for this benchmark." 8 | raise 9 | end 10 | 11 | MStrPrep = Mongo::Auth::StringPrep 12 | 13 | # this indirection will slow it down a little bit 14 | def mongo_saslprep(string) 15 | MStrPrep.prepare(string, 16 | MStrPrep::Profiles::SASL::MAPPINGS, 17 | MStrPrep::Profiles::SASL::PROHIBITED, 18 | normalize: true, 19 | bidi: true) 20 | rescue Mongo::Error::FailedStringPrepValidation 21 | nil 22 | end 23 | 24 | $LOAD_PATH.unshift "./lib" 25 | require "net/imap" 26 | def net_imap_saslprep(string) 27 | Net::IMAP::StringPrep::SASLprep.saslprep string, exception: false 28 | end 29 | 30 | def libidn_saslprep(string) 31 | IDN::Stringprep.with_profile(string, "SASLprep") 32 | rescue IDN::Stringprep::StringprepError 33 | nil 34 | end 35 | 36 | benchmark: 37 | - net_imap_saslprep "I\u00ADX" # RFC example 1. IX 38 | - net_imap_saslprep "user" # RFC example 2. user 39 | - net_imap_saslprep "USER" # RFC example 3. user 40 | - net_imap_saslprep "\u00aa" # RFC example 4. a 41 | - net_imap_saslprep "\u2168" # RFC example 5. IX 42 | - net_imap_saslprep "\u0007" # RFC example 6. Error - prohibited character 43 | - net_imap_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check 44 | - net_imap_saslprep "I\u2000X" # map to space: I X 45 | - net_imap_saslprep "a longer string, e.g. a password" 46 | 47 | - libidn_saslprep "I\u00ADX" # RFC example 1. IX 48 | - libidn_saslprep "user" # RFC example 2. user 49 | - libidn_saslprep "USER" # RFC example 3. user 50 | - libidn_saslprep "\u00aa" # RFC example 4. a 51 | - libidn_saslprep "\u2168" # RFC example 5. IX 52 | - libidn_saslprep "\u0007" # RFC example 6. Error - prohibited character 53 | - libidn_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check 54 | - libidn_saslprep "I\u2000X" # map to space: I X 55 | - libidn_saslprep "a longer string, e.g. a password" 56 | 57 | - mongo_saslprep "I\u00ADX" # RFC example 1. IX 58 | - mongo_saslprep "user" # RFC example 2. user 59 | - mongo_saslprep "USER" # RFC example 3. user 60 | - mongo_saslprep "\u00aa" # RFC example 4. a 61 | - mongo_saslprep "\u2168" # RFC example 5. IX 62 | - mongo_saslprep "\u0007" # RFC example 6. Error - prohibited character 63 | - mongo_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check 64 | - mongo_saslprep "I\u2000X" # map to space: I X 65 | - mongo_saslprep "a longer string, e.g. a password" 66 | -------------------------------------------------------------------------------- /benchmarks/table-regexps.yml: -------------------------------------------------------------------------------- 1 | prelude: | 2 | require "json" 3 | require "set" unless defined?(::Set) 4 | 5 | all_codepoints = (0..0x10ffff).map{_1.chr("UTF-8") rescue nil}.compact 6 | 7 | rfc3454_tables = Dir["rfcs/rfc3454*.json"] 8 | .first 9 | .then{File.read _1} 10 | .then{JSON.parse _1} 11 | titles = rfc3454_tables.delete("titles") 12 | 13 | sets = rfc3454_tables 14 | .transform_values{|t|t.keys rescue t} 15 | .transform_values{|table| 16 | table 17 | .map{_1.split(?-).map{|i|Integer i, 16}} 18 | .flat_map{_2 ? (_1.._2).to_a : _1} 19 | .to_set 20 | } 21 | 22 | TABLE_A1_SET = sets.fetch "A.1" 23 | ASSIGNED_3_2 = /\p{AGE=3.2}/ 24 | UNASSIGNED_3_2 = /\P{AGE=3.2}/ 25 | TABLE_A1_REGEX = /(?-mix:[\u{0000}-\u{001f}\u{007f}-\u{00a0}\u{0340}-\u{0341}\u{06dd}\u{070f}\u{1680}\u{180e}\u{2000}-\u{200f}\u{2028}-\u{202f}\u{205f}-\u{2063}\u{206a}-\u{206f}\u{2ff0}-\u{2ffb}\u{3000}\u{e000}-\u{f8ff}\u{fdd0}-\u{fdef}\u{feff}\u{fff9}-\u{ffff}\u{1d173}-\u{1d17a}\u{1fffe}-\u{1ffff}\u{2fffe}-\u{2ffff}\u{3fffe}-\u{3ffff}\u{4fffe}-\u{4ffff}\u{5fffe}-\u{5ffff}\u{6fffe}-\u{6ffff}\u{7fffe}-\u{7ffff}\u{8fffe}-\u{8ffff}\u{9fffe}-\u{9ffff}\u{afffe}-\u{affff}\u{bfffe}-\u{bffff}\u{cfffe}-\u{cffff}\u{dfffe}-\u{dffff}\u{e0001}\u{e0020}-\u{e007f}\u{efffe}-\u{10ffff}])|(?-mix:\p{Cs})/.freeze 26 | 27 | benchmark: 28 | 29 | # matches A.1 30 | - script: "all_codepoints.grep(TABLE_A1_SET)" 31 | - script: "all_codepoints.grep(TABLE_A1_REGEX)" 32 | - script: "all_codepoints.grep(UNASSIGNED_3_2)" 33 | - script: "all_codepoints.grep_v(ASSIGNED_3_2)" 34 | 35 | # doesn't match A.1 36 | - script: "all_codepoints.grep_v(TABLE_A1_SET)" 37 | - script: "all_codepoints.grep_v(TABLE_A1_REGEX)" 38 | - script: "all_codepoints.grep_v(UNASSIGNED_3_2)" 39 | - script: "all_codepoints.grep(ASSIGNED_3_2)" 40 | -------------------------------------------------------------------------------- /bin/check-regexps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # See also: test/net/imap/regexp_collector.rb 5 | # See also: test/net/imap/test_regexps.rb 6 | 7 | def traverse(m=Object, s=Set.new, &b) 8 | m.constants(false).map{m.const_get _1 rescue nil}.select{_1 in Module}.each do 9 | next if s.include?(_1); s << _1 10 | b and b[_1] 11 | traverse(_1, s, &b) 12 | end 13 | end 14 | 15 | def collect_regexps = ObjectSpace 16 | .each_object(Regexp) 17 | .reject{Regexp.linear_time? _1} 18 | 19 | 2.times{traverse} 20 | before = collect_regexps 21 | 22 | $LOAD_PATH.unshift "./lib" 23 | require 'net/imap' 24 | 2.times{traverse} 25 | traverse(Net::IMAP) { puts _1.name } 26 | after = collect_regexps - before 27 | p before: before.count, count: after.count, after:; 28 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/imap" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | /* this is a work in progress. :) */ 2 | 3 | /*********************************************** 4 | * Method descriptions 5 | ***********************************************/ 6 | 7 | main .method-detail { 8 | display: grid; 9 | grid-template-columns: 1fr auto; 10 | justify-content: space-between; 11 | } 12 | 13 | main .method-header, 14 | main .method-controls, 15 | .attribute-method-heading { 16 | padding: 0.5em; 17 | /* border: 1px solid var(--highlight-color); */ 18 | background: var(--table-header-background-color); 19 | line-height: 1.6; 20 | } 21 | 22 | .attribute-method-heading .attribute-access-type { 23 | float: right; 24 | } 25 | 26 | main .method-header { 27 | border-right: none; 28 | border-radius: 4px 0 0 4px; 29 | } 30 | 31 | main .method-heading :any-link { 32 | text-decoration: none; 33 | } 34 | 35 | main .method-controls { 36 | border-left: none; 37 | border-radius: 0 4px 4px 0; 38 | } 39 | 40 | main .method-description, main .aliases { 41 | grid-column: 1 / span 2; 42 | padding-left: 1em; 43 | } 44 | 45 | @media (max-width: 700px) { 46 | main .method-header, main .method-controls, main .method-description { 47 | grid-column: 1 / span 2; 48 | margin: 0; 49 | } 50 | main .method-controls { 51 | background: none; 52 | } 53 | } 54 | 55 | /*********************************************** 56 | * Description lists 57 | ***********************************************/ 58 | 59 | main dt { 60 | margin-bottom: 0; /* override rdoc 6.8 */ 61 | float: unset; /* override rdoc 6.8 */ 62 | line-height: 1.5; /* matches `main p` */ 63 | } 64 | 65 | main dl.note-list dt { 66 | margin-right: 1em; 67 | float: left; 68 | } 69 | 70 | main dl.note-list dt:has(+ dt) { 71 | margin-right: 0.25em; 72 | } 73 | 74 | main dl.note-list dt:has(+ dt)::after { 75 | content: ', '; 76 | font-weight: normal; 77 | } 78 | 79 | main dd { 80 | margin: 0 0 1em 1em; 81 | } 82 | 83 | main dd p:first-child { 84 | margin-top: 0; 85 | } 86 | -------------------------------------------------------------------------------- /lib/net/imap/authenticators.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Backward compatible delegators from Net::IMAP to Net::IMAP::SASL. 4 | module Net::IMAP::Authenticators 5 | 6 | # Deprecated. Use Net::IMAP::SASL.add_authenticator instead. 7 | def add_authenticator(...) 8 | warn( 9 | "%s.%s is deprecated. Use %s.%s instead." % [ 10 | Net::IMAP, __method__, Net::IMAP::SASL, __method__ 11 | ], 12 | uplevel: 1, category: :deprecated 13 | ) 14 | Net::IMAP::SASL.add_authenticator(...) 15 | end 16 | 17 | # Deprecated. Use Net::IMAP::SASL.authenticator instead. 18 | def authenticator(...) 19 | warn( 20 | "%s.%s is deprecated. Use %s.%s instead." % [ 21 | Net::IMAP, __method__, Net::IMAP::SASL, __method__ 22 | ], 23 | uplevel: 1, category: :deprecated 24 | ) 25 | Net::IMAP::SASL.authenticator(...) 26 | end 27 | 28 | Net::IMAP.extend self 29 | end 30 | 31 | class Net::IMAP 32 | PlainAuthenticator = SASL::PlainAuthenticator # :nodoc: 33 | deprecate_constant :PlainAuthenticator 34 | 35 | XOauth2Authenticator = SASL::XOAuth2Authenticator # :nodoc: 36 | deprecate_constant :XOauth2Authenticator 37 | end 38 | -------------------------------------------------------------------------------- /lib/net/imap/config/attr_accessors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Net 6 | class IMAP 7 | class Config 8 | # >>> 9 | # *NOTE:* This module is an internal implementation detail, with no 10 | # guarantee of backward compatibility. 11 | # 12 | # +attr_accessor+ values are stored in a struct rather than ivars, making 13 | # it simpler to ensure that all config objects share a single object 14 | # shape. This also simplifies iteration over all defined attributes. 15 | module AttrAccessors 16 | module Macros # :nodoc: internal API 17 | def attr_accessor(name) AttrAccessors.attr_accessor(name) end 18 | end 19 | private_constant :Macros 20 | 21 | def self.included(mod) 22 | mod.extend Macros 23 | end 24 | private_class_method :included 25 | 26 | extend Forwardable 27 | 28 | def self.attr_accessor(name) # :nodoc: internal API 29 | name = name.to_sym 30 | def_delegators :data, name, :"#{name}=" 31 | end 32 | 33 | def self.attributes 34 | instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym } 35 | end 36 | private_class_method :attributes 37 | 38 | def self.struct # :nodoc: internal API 39 | unless defined?(self::Struct) 40 | const_set :Struct, Struct.new(*attributes) 41 | end 42 | self::Struct 43 | end 44 | 45 | def initialize # :notnew: 46 | super() 47 | @data = AttrAccessors.struct.new 48 | end 49 | 50 | # Freezes the internal attributes struct, in addition to +self+. 51 | def freeze 52 | data.freeze 53 | super 54 | end 55 | 56 | protected 57 | 58 | attr_reader :data # :nodoc: internal API 59 | 60 | private 61 | 62 | def initialize_clone(other) 63 | super 64 | @data = other.data.clone 65 | end 66 | 67 | def initialize_dup(other) 68 | super 69 | @data = other.data.dup 70 | end 71 | 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/net/imap/config/attr_inheritance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | class Config 6 | # >>> 7 | # *NOTE:* The public methods on this module are part of the stable 8 | # public API of Net::IMAP::Config. But the module itself is an internal 9 | # implementation detail, with no guarantee of backward compatibility. 10 | # 11 | # +attr_accessor+ methods will delegate to their #parent when the local 12 | # value does not contain an override. Inheritance forms a singly linked 13 | # list, so lookup will be O(n) on the number of ancestors. In 14 | # practice, the ancestor chain is not expected to be long. Without 15 | # customization, it is only three deep: 16 | # >>> 17 | # IMAP#config → Config.global → Config.default 18 | # 19 | # When creating a client with the +config+ keyword, for example to use 20 | # the appropriate defaults for an application or a library while still 21 | # relying on global for configuration of +debug+ or +logger+, most likely 22 | # the ancestor chain is still only four deep: 23 | # >>> 24 | # IMAP#config → alternate defaults → Config.global → Config.default 25 | module AttrInheritance 26 | INHERITED = Module.new.freeze 27 | private_constant :INHERITED 28 | 29 | module Macros # :nodoc: internal API 30 | def attr_accessor(name) super; AttrInheritance.attr_accessor(name) end 31 | end 32 | private_constant :Macros 33 | 34 | def self.included(mod) 35 | mod.extend Macros 36 | end 37 | private_class_method :included 38 | 39 | def self.attr_accessor(name) # :nodoc: internal API 40 | module_eval <<~RUBY, __FILE__, __LINE__ + 1 41 | def #{name}; (val = super) == INHERITED ? parent&.#{name} : val end 42 | RUBY 43 | end 44 | 45 | # The parent Config object 46 | attr_reader :parent 47 | 48 | def initialize(parent = nil) # :notnew: 49 | super() 50 | @parent = Config[parent] 51 | reset 52 | end 53 | 54 | # Creates a new config, which inherits from +self+. 55 | def new(**attrs) self.class.new(self, **attrs) end 56 | 57 | # Returns +true+ if +attr+ is inherited from #parent and not overridden 58 | # by this config. 59 | def inherited?(attr) data[attr] == INHERITED end 60 | 61 | # :call-seq: 62 | # reset -> self 63 | # reset(attr) -> attribute value 64 | # 65 | # Resets an +attr+ to inherit from the #parent config. 66 | # 67 | # When +attr+ is nil or not given, all attributes are reset. 68 | def reset(attr = nil) 69 | if attr.nil? 70 | data.members.each do |attr| data[attr] = INHERITED end 71 | self 72 | elsif inherited?(attr) 73 | nil 74 | else 75 | old, data[attr] = data[attr], INHERITED 76 | old 77 | end 78 | end 79 | 80 | private 81 | 82 | def initialize_copy(other) 83 | super 84 | @parent ||= other # only default has nil parent 85 | end 86 | 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/net/imap/config/attr_type_coercion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | class Config 6 | # >>> 7 | # *NOTE:* This module is an internal implementation detail, with no 8 | # guarantee of backward compatibility. 9 | # 10 | # Adds a +type+ keyword parameter to +attr_accessor+, to enforce that 11 | # config attributes have valid types, for example: boolean, numeric, 12 | # enumeration, non-nullable, etc. 13 | module AttrTypeCoercion 14 | # :stopdoc: internal APIs only 15 | 16 | module Macros # :nodoc: internal API 17 | def attr_accessor(attr, type: nil) 18 | super(attr) 19 | AttrTypeCoercion.attr_accessor(attr, type: type) 20 | end 21 | 22 | module_function def Integer? = NilOrInteger 23 | end 24 | private_constant :Macros 25 | 26 | def self.included(mod) 27 | mod.extend Macros 28 | end 29 | private_class_method :included 30 | 31 | if defined?(Ractor.make_shareable) 32 | def self.safe(...) Ractor.make_shareable nil.instance_eval(...).freeze end 33 | else 34 | def self.safe(...) nil.instance_eval(...).freeze end 35 | end 36 | private_class_method :safe 37 | 38 | Types = Hash.new do |h, type| type => Proc | nil; safe{type} end 39 | Types[:boolean] = Boolean = safe{-> {!!_1}} 40 | Types[Integer] = safe{->{Integer(_1)}} 41 | 42 | def self.attr_accessor(attr, type: nil) 43 | type = Types[type] or return 44 | define_method :"#{attr}=" do |val| super type[val] end 45 | define_method :"#{attr}?" do send attr end if type == Boolean 46 | end 47 | 48 | NilOrInteger = safe{->val { Integer val unless val.nil? }} 49 | 50 | Enum = ->(*enum) { 51 | enum = safe{enum} 52 | expected = -"one of #{enum.map(&:inspect).join(", ")}" 53 | safe{->val { 54 | return val if enum.include?(val) 55 | raise ArgumentError, "expected %s, got %p" % [expected, val] 56 | }} 57 | } 58 | 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/net/imap/connection_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | class ConnectionState < Net::IMAP::Data # :nodoc: 6 | def self.define(symbol, *attrs) 7 | symbol => Symbol 8 | state = super(*attrs) 9 | state.const_set :NAME, symbol 10 | state 11 | end 12 | 13 | def symbol; self.class::NAME end 14 | def name; self.class::NAME.name end 15 | alias to_sym symbol 16 | 17 | def deconstruct; [symbol, *super] end 18 | 19 | def deconstruct_keys(names) 20 | hash = super 21 | hash[:symbol] = symbol if names.nil? || names.include?(:symbol) 22 | hash[:name] = name if names.nil? || names.include?(:name) 23 | hash 24 | end 25 | 26 | def to_h(&block) 27 | hash = deconstruct_keys(nil) 28 | block ? hash.to_h(&block) : hash 29 | end 30 | 31 | def not_authenticated?; to_sym == :not_authenticated end 32 | def authenticated?; to_sym == :authenticated end 33 | def selected?; to_sym == :selected end 34 | def logout?; to_sym == :logout end 35 | 36 | NotAuthenticated = define(:not_authenticated) 37 | Authenticated = define(:authenticated) 38 | Selected = define(:selected) 39 | Logout = define(:logout) 40 | 41 | class << self 42 | undef :define 43 | end 44 | freeze 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/net/imap/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP < Protocol 5 | 6 | # Superclass of IMAP errors. 7 | class Error < StandardError 8 | end 9 | 10 | class LoginDisabledError < Error 11 | def initialize(msg = "Remote server has disabled the LOGIN command", ...) 12 | super 13 | end 14 | end 15 | 16 | # Error raised when data is in the incorrect format. 17 | class DataFormatError < Error 18 | end 19 | 20 | # Error raised when the socket cannot be read, due to a Config limit. 21 | class ResponseReadError < Error 22 | end 23 | 24 | # Error raised when a response is larger than IMAP#max_response_size. 25 | class ResponseTooLargeError < ResponseReadError 26 | attr_reader :bytes_read, :literal_size 27 | attr_reader :max_response_size 28 | 29 | def initialize(msg = nil, *args, 30 | bytes_read: nil, 31 | literal_size: nil, 32 | max_response_size: nil, 33 | **kwargs) 34 | @bytes_read = bytes_read 35 | @literal_size = literal_size 36 | @max_response_size = max_response_size 37 | msg ||= [ 38 | "Response size", response_size_msg, "exceeds max_response_size", 39 | max_response_size && "(#{max_response_size}B)", 40 | ].compact.join(" ") 41 | super(msg, *args, **kwargs) 42 | end 43 | 44 | private 45 | 46 | def response_size_msg 47 | if bytes_read && literal_size 48 | "(#{bytes_read}B read + #{literal_size}B literal)" 49 | end 50 | end 51 | end 52 | 53 | # Error raised when a response from the server is non-parsable. 54 | class ResponseParseError < Error 55 | end 56 | 57 | # Superclass of all errors used to encapsulate "fail" responses 58 | # from the server. 59 | class ResponseError < Error 60 | 61 | # The response that caused this error 62 | attr_accessor :response 63 | 64 | def initialize(response) 65 | @response = response 66 | 67 | super @response.data.text 68 | end 69 | 70 | end 71 | 72 | # Error raised upon a "NO" response from the server, indicating 73 | # that the client command could not be completed successfully. 74 | class NoResponseError < ResponseError 75 | end 76 | 77 | # Error raised upon a "BAD" response from the server, indicating 78 | # that the client command violated the IMAP protocol, or an internal 79 | # server failure has occurred. 80 | class BadResponseError < ResponseError 81 | end 82 | 83 | # Error raised upon a "BYE" response from the server, indicating 84 | # that the client is not being allowed to login, or has been timed 85 | # out due to inactivity. 86 | class ByeResponseError < ResponseError 87 | end 88 | 89 | # Error raised when the server sends an invalid response. 90 | # 91 | # This is different from UnknownResponseError: the response has been 92 | # rejected. Although it may be parsable, the server is forbidden from 93 | # sending it in the current context. The client should automatically 94 | # disconnect, abruptly (without logout). 95 | # 96 | # Note that InvalidResponseError does not inherit from ResponseError: it 97 | # can be raised before the response is fully parsed. A related 98 | # ResponseParseError or ResponseError may be the #cause. 99 | class InvalidResponseError < Error 100 | end 101 | 102 | # Error raised upon an unknown response from the server. 103 | # 104 | # This is different from InvalidResponseError: the response may be a 105 | # valid extension response and the server may be allowed to send it in 106 | # this context, but Net::IMAP either does not know how to parse it or 107 | # how to handle it. This could result from enabling unknown or 108 | # unhandled extensions. The connection may still be usable, 109 | # but—depending on context—it may be prudent to disconnect. 110 | class UnknownResponseError < ResponseError 111 | end 112 | 113 | RESPONSE_ERRORS = Hash.new(ResponseError) # :nodoc: 114 | RESPONSE_ERRORS["NO"] = NoResponseError 115 | RESPONSE_ERRORS["BAD"] = BadResponseError 116 | 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/net/imap/response_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2 6 | class ResponseReader # :nodoc: 7 | attr_reader :client 8 | 9 | def initialize(client, sock) 10 | @client, @sock = client, sock 11 | end 12 | 13 | def read_response_buffer 14 | @buff = String.new 15 | catch :eof do 16 | while true 17 | read_line 18 | break unless (@literal_size = get_literal_size) 19 | read_literal 20 | end 21 | end 22 | buff 23 | ensure 24 | @buff = nil 25 | end 26 | 27 | private 28 | 29 | attr_reader :buff, :literal_size 30 | 31 | def bytes_read = buff.bytesize 32 | def empty? = buff.empty? 33 | def done? = line_done? && !get_literal_size 34 | def line_done? = buff.end_with?(CRLF) 35 | def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i 36 | 37 | def read_line 38 | buff << (@sock.gets(CRLF, read_limit) or throw :eof) 39 | max_response_remaining! unless line_done? 40 | end 41 | 42 | def read_literal 43 | # check before allocating memory for literal 44 | max_response_remaining! 45 | literal = String.new(capacity: literal_size) 46 | buff << (@sock.read(read_limit(literal_size), literal) or throw :eof) 47 | ensure 48 | @literal_size = nil 49 | end 50 | 51 | def read_limit(limit = nil) 52 | [limit, max_response_remaining!].compact.min 53 | end 54 | 55 | def max_response_size = client.max_response_size 56 | def max_response_remaining = max_response_size &.- bytes_read 57 | def response_too_large? = max_response_size &.< min_response_size 58 | def min_response_size = bytes_read + min_response_remaining 59 | 60 | def min_response_remaining 61 | empty? ? 3 : done? ? 0 : (literal_size || 0) + 2 62 | end 63 | 64 | def max_response_remaining! 65 | return max_response_remaining unless response_too_large? 66 | raise ResponseTooLargeError.new( 67 | max_response_size:, bytes_read:, literal_size:, 68 | ) 69 | end 70 | 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/anonymous_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP < Protocol 5 | module SASL 6 | 7 | # Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by 8 | # RFC-4505[https://www.rfc-editor.org/rfc/rfc4505]. See 9 | # Net::IMAP#authenticate. 10 | class AnonymousAuthenticator 11 | 12 | # An optional token sent for the +ANONYMOUS+ mechanism., up to 255 UTF-8 13 | # characters in length. 14 | # 15 | # If it contains an "@" sign, the message must be a valid email address 16 | # (+addr-spec+ from RFC-2822[https://www.rfc-editor.org/rfc/rfc2822]). 17 | # Email syntax is _not_ validated by AnonymousAuthenticator. 18 | # 19 | # Otherwise, it can be any UTF8 string which is permitted by the 20 | # StringPrep::Trace profile. 21 | attr_reader :anonymous_message 22 | 23 | # :call-seq: 24 | # new(anonymous_message = "", **) -> authenticator 25 | # new(anonymous_message: "", **) -> authenticator 26 | # 27 | # Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as 28 | # specified in RFC-4505[https://www.rfc-editor.org/rfc/rfc4505]. To use 29 | # this, see Net::IMAP#authenticate or your client's authentication 30 | # method. 31 | # 32 | # ==== Parameters 33 | # 34 | # * _optional_ #anonymous_message — a message to send to the server. 35 | # 36 | # Any other keyword arguments are silently ignored. 37 | def initialize(anon_msg = nil, anonymous_message: nil, **) 38 | message = (anonymous_message || anon_msg || "").to_str 39 | @anonymous_message = StringPrep::Trace.stringprep_trace message 40 | if (size = @anonymous_message&.length)&.> 255 41 | raise ArgumentError, 42 | "anonymous_message is too long. (%d codepoints)" % [size] 43 | end 44 | @done = false 45 | end 46 | 47 | # :call-seq: 48 | # initial_response? -> true 49 | # 50 | # +ANONYMOUS+ can send an initial client response. 51 | def initial_response?; true end 52 | 53 | # Returns #anonymous_message. 54 | def process(_server_challenge_string) 55 | anonymous_message 56 | ensure 57 | @done = true 58 | end 59 | 60 | # Returns true when the initial client response was sent. 61 | # 62 | # The authentication should not succeed unless this returns true, but it 63 | # does *not* indicate success. 64 | def done?; @done end 65 | 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/authenticators.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net::IMAP::SASL 4 | 5 | # Registry for SASL authenticators 6 | # 7 | # Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or 8 | # a proc), receiving any credentials and options and returning an 9 | # authenticator instance. The returned object represents a single 10 | # authentication exchange and must not be reused for multiple 11 | # authentication attempts. 12 | # 13 | # An authenticator instance object must respond to +#process+, receiving the 14 | # server's challenge and returning the client's response. Optionally, it may 15 | # also respond to +#initial_response?+ and +#done?+. When 16 | # +#initial_response?+ returns +true+, +#process+ may be called the first 17 | # time with +nil+. When +#done?+ returns +false+, the exchange is incomplete 18 | # and an exception should be raised if the exchange terminates prematurely. 19 | # 20 | # See the source for PlainAuthenticator, XOAuth2Authenticator, and 21 | # ScramSHA1Authenticator for examples. 22 | class Authenticators 23 | 24 | # Normalize the mechanism name as an uppercase string, with underscores 25 | # converted to dashes. 26 | def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end 27 | 28 | # Create a new Authenticators registry. 29 | # 30 | # This class is usually not instantiated directly. Use SASL.authenticators 31 | # to reuse the default global registry. 32 | # 33 | # When +use_defaults+ is +false+, the registry will start empty. When 34 | # +use_deprecated+ is +false+, deprecated authenticators will not be 35 | # included with the defaults. 36 | def initialize(use_defaults: true, use_deprecated: true) 37 | @authenticators = {} 38 | return unless use_defaults 39 | add_authenticator "Anonymous" 40 | add_authenticator "External" 41 | add_authenticator "OAuthBearer" 42 | add_authenticator "Plain" 43 | add_authenticator "Scram-SHA-1" 44 | add_authenticator "Scram-SHA-256" 45 | add_authenticator "XOAuth2" 46 | return unless use_deprecated 47 | add_authenticator "Login" # deprecated 48 | add_authenticator "Cram-MD5" # deprecated 49 | add_authenticator "Digest-MD5" # deprecated 50 | end 51 | 52 | # Returns the names of all registered SASL mechanisms. 53 | def names; @authenticators.keys end 54 | 55 | # :call-seq: 56 | # add_authenticator(mechanism) 57 | # add_authenticator(mechanism, authenticator_class) 58 | # add_authenticator(mechanism, authenticator_proc) 59 | # 60 | # Registers an authenticator for #authenticator to use. +mechanism+ is the 61 | # name of the 62 | # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] 63 | # implemented by +authenticator_class+ (for instance, "PLAIN"). 64 | # 65 | # If +mechanism+ refers to an existing authenticator, 66 | # the old authenticator will be replaced. 67 | # 68 | # When only a single argument is given, the authenticator class will be 69 | # lazily loaded from Net::IMAP::SASL::#{name}Authenticator (case is 70 | # preserved and non-alphanumeric characters are removed.. 71 | def add_authenticator(name, authenticator = nil) 72 | authenticator ||= begin 73 | class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym 74 | auth_class = nil 75 | ->(*creds, **props, &block) { 76 | auth_class ||= Net::IMAP::SASL.const_get(class_name) 77 | auth_class.new(*creds, **props, &block) 78 | } 79 | end 80 | key = Authenticators.normalize_name(name) 81 | @authenticators[key] = authenticator 82 | end 83 | 84 | # Removes the authenticator registered for +name+ 85 | def remove_authenticator(name) 86 | key = Authenticators.normalize_name(name) 87 | @authenticators.delete(key) 88 | end 89 | 90 | def mechanism?(name) 91 | key = Authenticators.normalize_name(name) 92 | @authenticators.key?(key) 93 | end 94 | 95 | # :call-seq: 96 | # authenticator(mechanism, ...) -> auth_session 97 | # 98 | # Builds an authenticator instance using the authenticator registered to 99 | # +mechanism+. The returned object represents a single authentication 100 | # exchange and must not be reused for multiple authentication 101 | # attempts. 102 | # 103 | # All arguments (except +mechanism+) are forwarded to the registered 104 | # authenticator's +#new+ or +#call+ method. Each authenticator must 105 | # document its own arguments. 106 | # 107 | # [Note] 108 | # This method is intended for internal use by connection protocol code 109 | # only. Protocol client users should see refer to their client's 110 | # documentation, e.g. Net::IMAP#authenticate. 111 | def authenticator(mechanism, ...) 112 | key = Authenticators.normalize_name(mechanism) 113 | auth = @authenticators.fetch(key) do 114 | raise ArgumentError, 'unknown auth type - "%s"' % key 115 | end 116 | auth.respond_to?(:new) ? auth.new(...) : auth.call(...) 117 | end 118 | alias new authenticator 119 | 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/client_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Net 6 | class IMAP 7 | module SASL 8 | 9 | # This API is *experimental*, and may change. 10 | # 11 | # TODO: use with more clients, to verify the API can accommodate them. 12 | # 13 | # Represents the client to a SASL::AuthenticationExchange. By default, 14 | # most methods simply delegate to #client. Clients should subclass 15 | # SASL::ClientAdapter and override methods as needed to match the 16 | # semantics of this API to their API. 17 | # 18 | # Subclasses should also include a protocol adapter mixin when the default 19 | # ProtocolAdapters::Generic isn't sufficient. 20 | # 21 | # === Protocol Requirements 22 | # 23 | # {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4] 24 | # lists requirements for protocol specifications to offer SASL. Where 25 | # possible, ClientAdapter delegates the handling of these requirements to 26 | # SASL::ProtocolAdapters. 27 | class ClientAdapter 28 | extend Forwardable 29 | 30 | include ProtocolAdapters::Generic 31 | 32 | # The client that handles communication with the protocol server. 33 | # 34 | # Most ClientAdapter methods are simply delegated to #client by default. 35 | attr_reader :client 36 | 37 | # +command_proc+ can used to avoid exposing private methods on #client. 38 | # It's value is set by the block that is passed to ::new, and it is used 39 | # by the default implementation of #run_command. Subclasses that 40 | # override #run_command may use #command_proc for any other purpose they 41 | # find useful. 42 | # 43 | # In the default implementation of #run_command, command_proc is called 44 | # with the protocols authenticate +command+ name, the +mechanism+ name, 45 | # an _optional_ +initial_response+ argument, and a +continuations+ 46 | # block. command_proc must run the protocol command with the arguments 47 | # sent to it, _yield_ the payload of each continuation, respond to the 48 | # continuation with the result of each _yield_, and _return_ the 49 | # command's successful result. Non-successful results *MUST* raise 50 | # an exception. 51 | attr_reader :command_proc 52 | 53 | # By default, this simply sets the #client and #command_proc attributes. 54 | # Subclasses may override it, for example: to set the appropriate 55 | # command_proc automatically. 56 | def initialize(client, &command_proc) 57 | @client, @command_proc = client, command_proc 58 | end 59 | 60 | # Attempt to authenticate #client to the server. 61 | # 62 | # By default, this simply delegates to 63 | # AuthenticationExchange.authenticate. 64 | def authenticate(...) AuthenticationExchange.authenticate(self, ...) end 65 | 66 | ## 67 | # method: sasl_ir_capable? 68 | # Do the protocol, server, and client all support an initial response? 69 | def_delegator :client, :sasl_ir_capable? 70 | 71 | ## 72 | # method: auth_capable? 73 | # call-seq: auth_capable?(mechanism) 74 | # 75 | # Does the server advertise support for the +mechanism+? 76 | def_delegator :client, :auth_capable? 77 | 78 | # Calls command_proc with +command_name+ (see 79 | # SASL::ProtocolAdapters::Generic#command_name), 80 | # +mechanism+, +initial_response+, and a +continuations_handler+ block. 81 | # The +initial_response+ is optional; when it's nil, it won't be sent to 82 | # command_proc. 83 | # 84 | # Yields each continuation payload, responds to the server with the 85 | # result of each yield, and returns the result. Non-successful results 86 | # *MUST* raise an exception. Exceptions in the block *MUST* cause the 87 | # command to fail. 88 | # 89 | # Subclasses that override this may use #command_proc differently. 90 | def run_command(mechanism, initial_response = nil, &continuations_handler) 91 | command_proc or raise Error, "initialize with block or override" 92 | args = [command_name, mechanism, initial_response].compact 93 | command_proc.call(*args, &continuations_handler) 94 | end 95 | 96 | ## 97 | # method: host 98 | # The hostname to which the client connected. 99 | def_delegator :client, :host 100 | 101 | ## 102 | # method: port 103 | # The destination port to which the client connected. 104 | def_delegator :client, :port 105 | 106 | # Returns an array of server responses errors raised by run_command. 107 | # Exceptions in this array won't drop the connection. 108 | def response_errors; [] end 109 | 110 | ## 111 | # method: drop_connection 112 | # Drop the connection gracefully, sending a "LOGOUT" command as needed. 113 | def_delegator :client, :drop_connection 114 | 115 | ## 116 | # method: drop_connection! 117 | # Drop the connection abruptly, closing the socket without logging out. 118 | def_delegator :client, :drop_connection! 119 | 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/cram_md5_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Authenticator for the "+CRAM-MD5+" SASL mechanism, specified in 4 | # RFC2195[https://www.rfc-editor.org/rfc/rfc2195]. See Net::IMAP#authenticate. 5 | # 6 | # == Deprecated 7 | # 8 | # +CRAM-MD5+ is obsolete and insecure. It is included for compatibility with 9 | # existing servers. 10 | # {draft-ietf-sasl-crammd5-to-historic}[https://www.rfc-editor.org/rfc/draft-ietf-sasl-crammd5-to-historic-00.html] 11 | # recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead. 12 | # 13 | # Additionally, RFC8314[https://www.rfc-editor.org/rfc/rfc8314] discourage the use 14 | # of cleartext and recommends TLS version 1.2 or greater be used for all 15 | # traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+ 16 | class Net::IMAP::SASL::CramMD5Authenticator 17 | def initialize(user = nil, pass = nil, 18 | authcid: nil, username: nil, 19 | password: nil, secret: nil, 20 | warn_deprecation: true, 21 | **) 22 | if warn_deprecation 23 | warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated 24 | end 25 | require "digest/md5" 26 | @user = authcid || username || user 27 | @password = password || secret || pass 28 | @done = false 29 | end 30 | 31 | def initial_response?; false end 32 | 33 | def process(challenge) 34 | digest = hmac_md5(challenge, @password) 35 | return @user + " " + digest 36 | ensure 37 | @done = true 38 | end 39 | 40 | def done?; @done end 41 | 42 | private 43 | 44 | def hmac_md5(text, key) 45 | if key.length > 64 46 | key = Digest::MD5.digest(key) 47 | end 48 | 49 | k_ipad = key + "\0" * (64 - key.length) 50 | k_opad = key + "\0" * (64 - key.length) 51 | for i in 0..63 52 | k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr 53 | k_opad[i] = (k_opad[i].ord ^ 0x5c).chr 54 | end 55 | 56 | digest = Digest::MD5.digest(k_ipad + text) 57 | 58 | return Digest::MD5.hexdigest(k_opad + digest) 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/external_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP < Protocol 5 | module SASL 6 | 7 | # Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by 8 | # RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. See 9 | # Net::IMAP#authenticate. 10 | # 11 | # The EXTERNAL mechanism requests that the server use client credentials 12 | # established external to SASL, for example by TLS certificate or IPSec. 13 | class ExternalAuthenticator 14 | 15 | # Authorization identity: an identity to act as or on behalf of. The 16 | # identity form is application protocol specific. If not provided or 17 | # left blank, the server derives an authorization identity from the 18 | # authentication identity. The server is responsible for verifying the 19 | # client's credentials and verifying that the identity it associates 20 | # with the client's authentication identity is allowed to act as (or on 21 | # behalf of) the authorization identity. 22 | # 23 | # For example, an administrator or superuser might take on another role: 24 | # 25 | # imap.authenticate "PLAIN", "root", passwd, authzid: "user" 26 | # 27 | attr_reader :authzid 28 | alias username authzid 29 | 30 | # :call-seq: 31 | # new(authzid: nil, **) -> authenticator 32 | # new(username: nil, **) -> authenticator 33 | # new(username = nil, **) -> authenticator 34 | # 35 | # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as 36 | # specified in RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. To use 37 | # this, see Net::IMAP#authenticate or your client's authentication 38 | # method. 39 | # 40 | # ==== Parameters 41 | # 42 | # * _optional_ #authzid ― Authorization identity to act as or on behalf of. 43 | # 44 | # _optional_ #username ― An alias for #authzid. 45 | # 46 | # Note that, unlike some other authenticators, +username+ sets the 47 | # _authorization_ identity and not the _authentication_ identity. The 48 | # authentication identity is established for the client by the 49 | # external credentials. 50 | # 51 | # Any other keyword parameters are quietly ignored. 52 | def initialize(user = nil, authzid: nil, username: nil, **) 53 | authzid ||= username || user 54 | @authzid = authzid&.to_str&.encode "UTF-8" 55 | if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding 56 | raise ArgumentError, "contains NULL" 57 | end 58 | @done = false 59 | end 60 | 61 | # :call-seq: 62 | # initial_response? -> true 63 | # 64 | # +EXTERNAL+ can send an initial client response. 65 | def initial_response?; true end 66 | 67 | # Returns #authzid, or an empty string if there is no authzid. 68 | def process(_) 69 | authzid || "" 70 | ensure 71 | @done = true 72 | end 73 | 74 | # Returns true when the initial client response was sent. 75 | # 76 | # The authentication should not succeed unless this returns true, but it 77 | # does *not* indicate success. 78 | def done?; @done end 79 | 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/gs2_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP < Protocol 5 | module SASL 6 | 7 | # Originally defined for the GS2 mechanism family in 8 | # RFC5801[https://www.rfc-editor.org/rfc/rfc5801], 9 | # several different mechanisms start with a GS2 header: 10 | # * +GS2-*+ --- RFC5801[https://www.rfc-editor.org/rfc/rfc5801] 11 | # * +SCRAM-*+ --- RFC5802[https://www.rfc-editor.org/rfc/rfc5802] 12 | # (ScramAuthenticator) 13 | # * +SAML20+ --- RFC6595[https://www.rfc-editor.org/rfc/rfc6595] 14 | # * +OPENID20+ --- RFC6616[https://www.rfc-editor.org/rfc/rfc6616] 15 | # * +OAUTH10A+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628] 16 | # * +OAUTHBEARER+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628] 17 | # (OAuthBearerAuthenticator) 18 | # 19 | # Classes that include this module must implement +#authzid+. 20 | module GS2Header 21 | NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc: 22 | 23 | ## 24 | # Matches {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4] 25 | # +saslname+. The output from gs2_saslname_encode matches this Regexp. 26 | RFC5801_SASLNAME = /\A(?:[^,=\x00]|=2C|=3D)+\z/u.freeze 27 | 28 | # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4] 29 | # +gs2-header+, which prefixes the #initial_client_response. 30 | # 31 | # >>> 32 | # Note: the actual GS2 header includes an optional flag to 33 | # indicate that the GSS mechanism is not "standard", but since all of 34 | # the SASL mechanisms using GS2 are "standard", we don't include that 35 | # flag. A class for a nonstandard GSSAPI mechanism should prefix with 36 | # "+F,+". 37 | def gs2_header 38 | "#{gs2_cb_flag},#{gs2_authzid}," 39 | end 40 | 41 | # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4] 42 | # +gs2-cb-flag+: 43 | # 44 | # "+n+":: The client doesn't support channel binding. 45 | # "+y+":: The client does support channel binding 46 | # but thinks the server does not. 47 | # "+p+":: The client requires channel binding. 48 | # The selected channel binding follows "+p=+". 49 | # 50 | # The default always returns "+n+". A mechanism that supports channel 51 | # binding must override this method. 52 | # 53 | def gs2_cb_flag; "n" end 54 | 55 | # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4] 56 | # +gs2-authzid+ header, when +#authzid+ is not empty. 57 | # 58 | # If +#authzid+ is empty or +nil+, an empty string is returned. 59 | def gs2_authzid 60 | return "" if authzid.nil? || authzid == "" 61 | "a=#{gs2_saslname_encode(authzid)}" 62 | end 63 | 64 | module_function 65 | 66 | # Encodes +str+ to match RFC5801_SASLNAME. 67 | def gs2_saslname_encode(str) 68 | str = str.encode("UTF-8") 69 | # Regexp#match raises "invalid byte sequence" for invalid UTF-8 70 | NO_NULL_CHARS.match str or 71 | raise ArgumentError, "invalid saslname: %p" % [str] 72 | str 73 | .gsub(?=, "=3D") 74 | .gsub(?,, "=2C") 75 | end 76 | 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/login_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate. 4 | # 5 | # +LOGIN+ authentication sends the password in cleartext. 6 | # RFC3501[https://www.rfc-editor.org/rfc/rfc3501] encourages servers to disable 7 | # cleartext authentication until after TLS has been negotiated. 8 | # RFC8314[https://www.rfc-editor.org/rfc/rfc8314] recommends TLS version 1.2 or 9 | # greater be used for all traffic, and deprecate cleartext access ASAP. +LOGIN+ 10 | # can be secured by TLS encryption. 11 | # 12 | # == Deprecated 13 | # 14 | # The {SASL mechanisms 15 | # registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml] 16 | # marks "LOGIN" as obsoleted in favor of "PLAIN". It is included here for 17 | # compatibility with existing servers. See 18 | # {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login] 19 | # for both specification and deprecation. 20 | class Net::IMAP::SASL::LoginAuthenticator 21 | STATE_USER = :USER 22 | STATE_PASSWORD = :PASSWORD 23 | STATE_DONE = :DONE 24 | private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE 25 | 26 | def initialize(user = nil, pass = nil, 27 | authcid: nil, username: nil, 28 | password: nil, secret: nil, 29 | warn_deprecation: true, 30 | **) 31 | if warn_deprecation 32 | warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.", 33 | category: :deprecated 34 | end 35 | @user = authcid || username || user 36 | @password = password || secret || pass 37 | @state = STATE_USER 38 | end 39 | 40 | def initial_response?; false end 41 | 42 | def process(data) 43 | case @state 44 | when STATE_USER 45 | @state = STATE_PASSWORD 46 | return @user 47 | when STATE_PASSWORD 48 | @state = STATE_DONE 49 | return @password 50 | when STATE_DONE 51 | raise ResponseParseError, data 52 | end 53 | end 54 | 55 | def done?; @state == STATE_DONE end 56 | end 57 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/plain_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Authenticator for the "+PLAIN+" SASL mechanism, specified in 4 | # RFC-4616[https://www.rfc-editor.org/rfc/rfc4616]. See Net::IMAP#authenticate. 5 | # 6 | # +PLAIN+ authentication sends the password in cleartext. 7 | # RFC-3501[https://www.rfc-editor.org/rfc/rfc3501] encourages servers to disable 8 | # cleartext authentication until after TLS has been negotiated. 9 | # RFC-8314[https://www.rfc-editor.org/rfc/rfc8314] recommends TLS version 1.2 or 10 | # greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+ 11 | # can be secured by TLS encryption. 12 | class Net::IMAP::SASL::PlainAuthenticator 13 | 14 | NULL = -"\0".b 15 | private_constant :NULL 16 | 17 | # Authentication identity: the identity that matches the #password. 18 | # 19 | # RFC-2831[https://www.rfc-editor.org/rfc/rfc2831] uses the term +username+. 20 | # "Authentication identity" is the generic term used by 21 | # RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. 22 | # RFC-4616[https://www.rfc-editor.org/rfc/rfc4616] and many later RFCs 23 | # abbreviate this to +authcid+. 24 | attr_reader :username 25 | alias authcid username 26 | 27 | # A password or passphrase that matches the #username. 28 | attr_reader :password 29 | alias secret password 30 | 31 | # Authorization identity: an identity to act as or on behalf of. The identity 32 | # form is application protocol specific. If not provided or left blank, the 33 | # server derives an authorization identity from the authentication identity. 34 | # The server is responsible for verifying the client's credentials and 35 | # verifying that the identity it associates with the client's authentication 36 | # identity is allowed to act as (or on behalf of) the authorization identity. 37 | # 38 | # For example, an administrator or superuser might take on another role: 39 | # 40 | # imap.authenticate "PLAIN", "root", passwd, authzid: "user" 41 | # 42 | attr_reader :authzid 43 | 44 | # :call-seq: 45 | # new(username, password, authzid: nil, **) -> authenticator 46 | # new(username:, password:, authzid: nil, **) -> authenticator 47 | # new(authcid:, password:, authzid: nil, **) -> authenticator 48 | # 49 | # Creates an Authenticator for the "+PLAIN+" SASL mechanism. 50 | # 51 | # Called by Net::IMAP#authenticate and similar methods on other clients. 52 | # 53 | # ==== Parameters 54 | # 55 | # * #authcid ― Authentication identity that is associated with #password. 56 | # 57 | # #username ― An alias for #authcid. 58 | # 59 | # * #password ― A password or passphrase associated with the #authcid. 60 | # 61 | # * _optional_ #authzid ― Authorization identity to act as or on behalf of. 62 | # 63 | # When +authzid+ is not set, the server should derive the authorization 64 | # identity from the authentication identity. 65 | # 66 | # Any other keyword parameters are quietly ignored. 67 | def initialize(user = nil, pass = nil, 68 | authcid: nil, secret: nil, 69 | username: nil, password: nil, authzid: nil, **) 70 | username ||= authcid || user or 71 | raise ArgumentError, "missing username (authcid)" 72 | password ||= secret || pass or raise ArgumentError, "missing password" 73 | raise ArgumentError, "username contains NULL" if username.include?(NULL) 74 | raise ArgumentError, "password contains NULL" if password.include?(NULL) 75 | raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL) 76 | @username = username 77 | @password = password 78 | @authzid = authzid 79 | @done = false 80 | end 81 | 82 | # :call-seq: 83 | # initial_response? -> true 84 | # 85 | # +PLAIN+ can send an initial client response. 86 | def initial_response?; true end 87 | 88 | # Responds with the client's credentials. 89 | def process(data) 90 | return "#@authzid\0#@username\0#@password" 91 | ensure 92 | @done = true 93 | end 94 | 95 | # Returns true when the initial client response was sent. 96 | # 97 | # The authentication should not succeed unless this returns true, but it 98 | # does *not* indicate success. 99 | def done?; @done end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/protocol_adapters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | module SASL 6 | 7 | # SASL::ProtocolAdapters modules are meant to be used as mixins for 8 | # SASL::ClientAdapter and its subclasses. Where the client adapter must 9 | # be customized for each client library, the protocol adapter mixin 10 | # handles \SASL requirements that are part of the protocol specification, 11 | # but not specific to any particular client library. In particular, see 12 | # {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4] 13 | # 14 | # === Interface 15 | # 16 | # >>> 17 | # NOTE: This API is experimental, and may change. 18 | # 19 | # - {#command_name}[rdoc-ref:Generic#command_name] -- The name of the 20 | # command used to to initiate an authentication exchange. 21 | # - {#service}[rdoc-ref:Generic#service] -- The GSSAPI service name. 22 | # - {#encode_ir}[rdoc-ref:Generic#encode_ir]--Encodes an initial response. 23 | # - {#decode}[rdoc-ref:Generic#decode] -- Decodes a server challenge. 24 | # - {#encode}[rdoc-ref:Generic#encode] -- Encodes a client response. 25 | # - {#cancel_response}[rdoc-ref:Generic#cancel_response] -- The encoded 26 | # client response used to cancel an authentication exchange. 27 | # 28 | # Other protocol requirements of the \SASL authentication exchange are 29 | # handled by SASL::ClientAdapter. 30 | # 31 | # === Included protocol adapters 32 | # 33 | # - Generic -- a basic implementation of all of the methods listed above. 34 | # - IMAP -- An adapter for the IMAP4 protocol. 35 | # - SMTP -- An adapter for the \SMTP protocol with the +AUTH+ capability. 36 | # - POP -- An adapter for the POP3 protocol with the +SASL+ capability. 37 | module ProtocolAdapters 38 | # See SASL::ProtocolAdapters@Interface. 39 | module Generic 40 | # The name of the protocol command used to initiate a \SASL 41 | # authentication exchange. 42 | # 43 | # The generic implementation returns "AUTHENTICATE". 44 | def command_name; "AUTHENTICATE" end 45 | 46 | # A service name from the {GSSAPI/Kerberos/SASL Service Names 47 | # registry}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]. 48 | # 49 | # The generic implementation returns "host", which is the 50 | # generic GSSAPI host-based service name. 51 | def service; "host" end 52 | 53 | # Encodes an initial response string. 54 | # 55 | # The generic implementation returns the result of #encode, or returns 56 | # "=" when +string+ is empty. 57 | def encode_ir(string) string.empty? ? "=" : encode(string) end 58 | 59 | # Encodes a client response string. 60 | # 61 | # The generic implementation returns the Base64 encoding of +string+. 62 | def encode(string) [string].pack("m0") end 63 | 64 | # Decodes a server challenge string. 65 | # 66 | # The generic implementation returns the Base64 decoding of +string+. 67 | def decode(string) string.unpack1("m0") end 68 | 69 | # Returns the message used by the client to abort an authentication 70 | # exchange. 71 | # 72 | # The generic implementation returns "*". 73 | def cancel_response; "*" end 74 | end 75 | 76 | # See RFC-3501 (IMAP4rev1), RFC-4959 (SASL-IR capability), 77 | # and RFC-9051 (IMAP4rev2). 78 | module IMAP 79 | include Generic 80 | def service; "imap" end 81 | end 82 | 83 | # See RFC-4954 (AUTH capability). 84 | module SMTP 85 | include Generic 86 | def command_name; "AUTH" end 87 | def service; "smtp" end 88 | end 89 | 90 | # See RFC-5034 (SASL capability). 91 | module POP 92 | include Generic 93 | def command_name; "AUTH" end 94 | def service; "pop" end 95 | end 96 | 97 | end 98 | 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/scram_algorithm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | module SASL 6 | 7 | # For method descriptions, 8 | # see {RFC5802 §2.2}[https://www.rfc-editor.org/rfc/rfc5802#section-2.2] 9 | # and {RFC5802 §3}[https://www.rfc-editor.org/rfc/rfc5802#section-3]. 10 | module ScramAlgorithm 11 | def Normalize(str) SASL.saslprep(str) end 12 | 13 | def Hi(str, salt, iterations) 14 | length = digest.digest_length 15 | OpenSSL::KDF.pbkdf2_hmac( 16 | str, 17 | salt: salt, 18 | iterations: iterations, 19 | length: length, 20 | hash: digest, 21 | ) 22 | end 23 | 24 | def H(str) digest.digest str end 25 | 26 | def HMAC(key, data) OpenSSL::HMAC.digest(digest, key, data) end 27 | 28 | def XOR(str1, str2) 29 | str1.unpack("C*") 30 | .zip(str2.unpack("C*")) 31 | .map {|a, b| a ^ b } 32 | .pack("C*") 33 | end 34 | 35 | def auth_message 36 | [ 37 | client_first_message_bare, 38 | server_first_message, 39 | client_final_message_without_proof, 40 | ] 41 | .join(",") 42 | end 43 | 44 | def salted_password 45 | Hi(Normalize(password), salt, iterations) 46 | end 47 | 48 | def client_key; HMAC(salted_password, "Client Key") end 49 | def server_key; HMAC(salted_password, "Server Key") end 50 | def stored_key; H(client_key) end 51 | def client_signature; HMAC(stored_key, auth_message) end 52 | def server_signature; HMAC(server_key, auth_message) end 53 | def client_proof; XOR(client_key, client_signature) end 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/stringprep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net::IMAP::SASL 4 | 5 | # Alias for Net::IMAP::StringPrep::SASLprep. 6 | SASLprep = Net::IMAP::StringPrep::SASLprep 7 | StringPrep = Net::IMAP::StringPrep # :nodoc: 8 | BidiStringError = Net::IMAP::StringPrep::BidiStringError # :nodoc: 9 | ProhibitedCodepoint = Net::IMAP::StringPrep::ProhibitedCodepoint # :nodoc: 10 | StringPrepError = Net::IMAP::StringPrep::StringPrepError # :nodoc: 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/net/imap/sasl/xoauth2_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Authenticator for the "+XOAUTH2+" SASL mechanism. This mechanism was 4 | # originally created for GMail and widely adopted by hosted email providers. 5 | # +XOAUTH2+ has been documented by 6 | # Google[https://developers.google.com/gmail/imap/xoauth2-protocol] and 7 | # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth]. 8 | # 9 | # This mechanism requires an OAuth2 access token which has been authorized 10 | # with the appropriate OAuth2 scopes to access the user's services. Most of 11 | # these scopes are not standardized---consult each service provider's 12 | # documentation for their scopes. 13 | # 14 | # Although this mechanism was never standardized and has been obsoleted by 15 | # "+OAUTHBEARER+", it is still very widely supported. 16 | # 17 | # See Net::IMAP::SASL::OAuthBearerAuthenticator. 18 | class Net::IMAP::SASL::XOAuth2Authenticator 19 | 20 | # It is unclear from {Google's original XOAUTH2 21 | # documentation}[https://developers.google.com/gmail/imap/xoauth2-protocol], 22 | # whether "User" refers to the authentication identity (+authcid+) or the 23 | # authorization identity (+authzid+). The authentication identity is 24 | # established for the client by the OAuth token, so it seems that +username+ 25 | # must be the authorization identity. 26 | # 27 | # {Microsoft's documentation for shared 28 | # mailboxes}[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#sasl-xoauth2-authentication-for-shared-mailboxes-in-office-365] 29 | # _clearly_ indicates that the Office 365 server interprets it as the 30 | # authorization identity. 31 | # 32 | # Although they _should_ validate that the token has been authorized to access 33 | # the service for +username+, _some_ servers appear to ignore this field, 34 | # relying only the identity and scope authorized by the token. 35 | attr_reader :username 36 | 37 | # Note that, unlike most other authenticators, #username is an alias for the 38 | # authorization identity and not the authentication identity. The 39 | # authenticated identity is established for the client by the #oauth2_token. 40 | alias authzid username 41 | 42 | # An OAuth2 access token which has been authorized with the appropriate OAuth2 43 | # scopes to use the service for #username. 44 | attr_reader :oauth2_token 45 | alias secret oauth2_token 46 | 47 | # :call-seq: 48 | # new(username, oauth2_token, **) -> authenticator 49 | # new(username:, oauth2_token:, **) -> authenticator 50 | # new(authzid:, oauth2_token:, **) -> authenticator 51 | # 52 | # Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by 53 | # Google[https://developers.google.com/gmail/imap/xoauth2-protocol], 54 | # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth] 55 | # and Yahoo[https://senders.yahooinc.com/developer/documentation]. 56 | # 57 | # === Properties 58 | # 59 | # * #username --- the username for the account being accessed. 60 | # 61 | # #authzid --- an alias for #username. 62 | # 63 | # Note that, unlike some other authenticators, +username+ sets the 64 | # _authorization_ identity and not the _authentication_ identity. The 65 | # authenticated identity is established for the client with the OAuth token. 66 | # 67 | # * #oauth2_token --- An OAuth2.0 access token which is authorized to access 68 | # the service for #username. 69 | # 70 | # Any other keyword parameters are quietly ignored. 71 | def initialize(user = nil, token = nil, username: nil, oauth2_token: nil, 72 | authzid: nil, secret: nil, **) 73 | @username = authzid || username || user or 74 | raise ArgumentError, "missing username (authzid)" 75 | @oauth2_token = oauth2_token || secret || token or 76 | raise ArgumentError, "missing oauth2_token" 77 | @done = false 78 | end 79 | 80 | # :call-seq: 81 | # initial_response? -> true 82 | # 83 | # +XOAUTH2+ can send an initial client response. 84 | def initial_response?; true end 85 | 86 | # Returns the XOAUTH2 formatted response, which combines the +username+ 87 | # with the +oauth2_token+. 88 | def process(_data) 89 | build_oauth2_string(@username, @oauth2_token) 90 | ensure 91 | @done = true 92 | end 93 | 94 | # Returns true when the initial client response was sent. 95 | # 96 | # The authentication should not succeed unless this returns true, but it 97 | # does *not* indicate success. 98 | def done?; @done end 99 | 100 | private 101 | 102 | def build_oauth2_string(username, oauth2_token) 103 | format("user=%s\1auth=Bearer %s\1\1", username, oauth2_token) 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/net/imap/sasl_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | 6 | # Experimental 7 | class SASLAdapter < SASL::ClientAdapter 8 | include SASL::ProtocolAdapters::IMAP 9 | 10 | RESPONSE_ERRORS = [NoResponseError, BadResponseError, ByeResponseError] 11 | .freeze 12 | 13 | def response_errors; RESPONSE_ERRORS end 14 | def sasl_ir_capable?; client.capable?("SASL-IR") end 15 | def drop_connection; client.logout! end 16 | def drop_connection!; client.disconnect end 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/net/imap/search_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | 6 | # An array of sequence numbers returned by Net::IMAP#search, or unique 7 | # identifiers returned by Net::IMAP#uid_search. 8 | # 9 | # For backward compatibility, SearchResult inherits from Array. 10 | class SearchResult < Array 11 | 12 | # Returns a SearchResult populated with the given +seq_nums+. 13 | # 14 | # Net::IMAP::SearchResult[1, 3, 5, modseq: 9] 15 | # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9] 16 | def self.[](*seq_nums, modseq: nil) 17 | new(seq_nums, modseq: modseq) 18 | end 19 | 20 | # A modification sequence number, as described by the +CONDSTORE+ 21 | # extension in {[RFC7162 22 | # §3.1.6]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1.6]. 23 | attr_reader :modseq 24 | 25 | # Returns a SearchResult populated with the given +seq_nums+. 26 | # 27 | # Net::IMAP::SearchResult.new([1, 3, 5], modseq: 9) 28 | # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9] 29 | def initialize(seq_nums, modseq: nil) 30 | super(seq_nums.to_ary.map { Integer _1 }) 31 | @modseq = Integer modseq if modseq 32 | end 33 | 34 | # Returns whether +other+ is a SearchResult with the same values and the 35 | # same #modseq. The order of numbers is irrelevant. 36 | # 37 | # Net::IMAP::SearchResult[123, 456, modseq: 789] == 38 | # Net::IMAP::SearchResult[123, 456, modseq: 789] 39 | # # => true 40 | # Net::IMAP::SearchResult[123, 456, modseq: 789] == 41 | # Net::IMAP::SearchResult[456, 123, modseq: 789] 42 | # # => true 43 | # 44 | # Net::IMAP::SearchResult[123, 456, modseq: 789] == 45 | # Net::IMAP::SearchResult[987, 654, modseq: 789] 46 | # # => false 47 | # Net::IMAP::SearchResult[123, 456, modseq: 789] == 48 | # Net::IMAP::SearchResult[1, 2, 3, modseq: 9999] 49 | # # => false 50 | # 51 | # SearchResult can be compared directly with Array, if #modseq is nil and 52 | # the array is sorted. 53 | # 54 | # Net::IMAP::SearchResult[9, 8, 6, 4, 1] == [1, 4, 6, 8, 9] # => true 55 | # Net::IMAP::SearchResult[3, 5, 7, modseq: 99] == [3, 5, 7] # => false 56 | # 57 | # Note that Array#== does require matching order and ignores #modseq. 58 | # 59 | # [9, 8, 6, 4, 1] == Net::IMAP::SearchResult[1, 4, 6, 8, 9] # => false 60 | # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true 61 | # 62 | def ==(other) 63 | (modseq ? 64 | other.is_a?(self.class) && modseq == other.modseq : 65 | other.is_a?(Array)) && 66 | size == other.size && 67 | sort == other.sort 68 | end 69 | 70 | # Hash equality. Unlike #==, order will be taken into account. 71 | def hash 72 | return super if modseq.nil? 73 | [super, self.class, modseq].hash 74 | end 75 | 76 | # Hash equality. Unlike #==, order will be taken into account. 77 | def eql?(other) 78 | return super if modseq.nil? 79 | self.class == other.class && hash == other.hash 80 | end 81 | 82 | # Returns a string that represents the SearchResult. 83 | # 84 | # Net::IMAP::SearchResult[123, 456, 789].inspect 85 | # # => "[123, 456, 789]" 86 | # 87 | # Net::IMAP::SearchResult[543, 210, 678, modseq: 2048].inspect 88 | # # => "Net::IMAP::SearchResult[543, 210, 678, modseq: 2048]" 89 | # 90 | def inspect 91 | return super if modseq.nil? 92 | "%s[%s, modseq: %p]" % [self.class, join(", "), modseq] 93 | end 94 | 95 | # Returns a string that follows the formal \IMAP syntax. 96 | # 97 | # data = Net::IMAP::SearchResult[2, 8, 32, 128, 256, 512] 98 | # data.to_s # => "* SEARCH 2 8 32 128 256 512" 99 | # data.to_s("SEARCH") # => "* SEARCH 2 8 32 128 256 512" 100 | # data.to_s("SORT") # => "* SORT 2 8 32 128 256 512" 101 | # data.to_s(nil) # => "2 8 32 128 256 512" 102 | # 103 | # data = Net::IMAP::SearchResult[1, 3, 16, 1024, modseq: 2048] 104 | # data.to_s # => "* SEARCH 1 3 16 1024 (MODSEQ 2048)" 105 | # data.to_s("SORT") # => "* SORT 1 3 16 1024 (MODSEQ 2048)" 106 | # data.to_s(nil) # => "1 3 16 1024 (MODSEQ 2048)" 107 | # 108 | def to_s(type = "SEARCH") 109 | str = +"" 110 | str << "* %s " % [type.to_str] unless type.nil? 111 | str << join(" ") 112 | str << " (MODSEQ %d)" % [modseq] if modseq 113 | -str 114 | end 115 | 116 | # Converts the SearchResult into a SequenceSet. 117 | # 118 | # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456] 119 | # .to_sequence_set 120 | # # => Net::IMAP::SequenceSet["1:4,9:10,12"] 121 | def to_sequence_set; SequenceSet[*self] end 122 | 123 | def pretty_print(pp) 124 | return super if modseq.nil? 125 | pp.text self.class.name + "[" 126 | pp.group_sub do 127 | pp.nest(2) do 128 | pp.breakable "" 129 | each do |num| 130 | pp.pp num 131 | pp.text "," 132 | pp.fill_breakable 133 | end 134 | pp.breakable "" 135 | pp.text "modseq: " 136 | pp.pp modseq 137 | end 138 | pp.breakable "" 139 | pp.text "]" 140 | end 141 | end 142 | 143 | end 144 | 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/net/imap/stringprep/nameprep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | module StringPrep 6 | 7 | # Defined in RFC3491[https://www.rfc-editor.org/rfc/rfc3491], the +nameprep+ 8 | # profile of "Stringprep" is: 9 | # >>> 10 | # used by the IDNA protocol for preparing domain names; it is not 11 | # designed for any other purpose. It is explicitly not designed for 12 | # processing arbitrary free text and SHOULD NOT be used for that 13 | # purpose. 14 | # 15 | # ... 16 | # 17 | # This profile specifies prohibiting using the following tables...: 18 | # 19 | # - C.1.2 (Non-ASCII space characters) 20 | # - C.2.2 (Non-ASCII control characters) 21 | # - C.3 (Private use characters) 22 | # - C.4 (Non-character code points) 23 | # - C.5 (Surrogate codes) 24 | # - C.6 (Inappropriate for plain text) 25 | # - C.7 (Inappropriate for canonical representation) 26 | # - C.8 (Change display properties are deprecated) 27 | # - C.9 (Tagging characters) 28 | # 29 | # IMPORTANT NOTE: This profile MUST be used with the IDNA protocol. 30 | # The IDNA protocol has additional prohibitions that are checked 31 | # outside of this profile. 32 | module NamePrep 33 | 34 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §10 35 | STRINGPREP_PROFILE = "nameprep" 36 | 37 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §2 38 | UNASSIGNED_TABLE = "A.1" 39 | 40 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §3 41 | MAPPING_TABLES = %w[B.1 B.2].freeze 42 | 43 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §4 44 | NORMALIZATION = :nfkc 45 | 46 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §5 47 | PROHIBITED_TABLES = %w[C.1.2 C.2.2 C.3 C.4 C.5 C.6 C.7 C.8 C.9].freeze 48 | 49 | # From RFC3491[https://www.rfc-editor.org/rfc/rfc3491.html] §6 50 | CHECK_BIDI = true 51 | 52 | module_function 53 | 54 | def nameprep(string, **opts) 55 | StringPrep.stringprep( 56 | string, 57 | unassigned: UNASSIGNED_TABLE, 58 | maps: MAPPING_TABLES, 59 | prohibited: PROHIBITED_TABLES, 60 | normalization: NORMALIZATION, 61 | bidi: CHECK_BIDI, 62 | profile: STRINGPREP_PROFILE, 63 | **opts, 64 | ) 65 | end 66 | end 67 | 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/net/imap/stringprep/saslprep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | module StringPrep 6 | 7 | # SASLprep#saslprep can be used to prepare a string according to [RFC4013]. 8 | # 9 | # \SASLprep maps characters three ways: to nothing, to space, and Unicode 10 | # normalization form KC. \SASLprep prohibits codepoints from nearly all 11 | # standard StringPrep tables (RFC3454, Appendix "C"), and uses 12 | # \StringPrep's standard bidirectional characters requirements (Appendix 13 | # "D"). \SASLprep also uses \StringPrep's definition of "Unassigned" 14 | # codepoints (Appendix "A"). 15 | module SASLprep 16 | 17 | # Used to short-circuit strings that don't need preparation. 18 | ASCII_NO_CTRLS = /\A[\x20-\x7e]*\z/u.freeze 19 | 20 | # Avoid loading these tables unless they are needed (they are only 21 | # needed for non-ASCII). 22 | saslprep_tables = File.expand_path("saslprep_tables", __dir__) 23 | autoload :MAP_TO_NOTHING, saslprep_tables 24 | autoload :MAP_TO_SPACE, saslprep_tables 25 | autoload :PROHIBITED, saslprep_tables 26 | autoload :PROHIBITED_STORED, saslprep_tables 27 | autoload :TABLES_PROHIBITED, saslprep_tables 28 | autoload :TABLES_PROHIBITED_STORED, saslprep_tables 29 | 30 | module_function 31 | 32 | # Prepares a UTF-8 +string+ for comparison, using the \SASLprep profile 33 | # RFC4013 of the StringPrep algorithm RFC3454. 34 | # 35 | # By default, prohibited strings will return +nil+. When +exception+ is 36 | # +true+, a StringPrepError describing the violation will be raised. 37 | # 38 | # When +stored+ is +true+, "unassigned" codepoints will be prohibited. 39 | # For \StringPrep and the \SASLprep profile, "unassigned" refers to 40 | # Unicode 3.2, and not later versions. See RFC3454 §7 for more 41 | # information. 42 | def saslprep(str, stored: false, exception: false) 43 | return str if ASCII_NO_CTRLS.match?(str) # incompatible encoding raises 44 | str = str.encode("UTF-8") # also dups (and raises for invalid encoding) 45 | str.gsub!(MAP_TO_SPACE, " ") 46 | str.gsub!(MAP_TO_NOTHING, "") 47 | str.unicode_normalize!(:nfkc) 48 | # These regexps combine the prohibited and bidirectional checks 49 | return str unless str.match?(stored ? PROHIBITED_STORED : PROHIBITED) 50 | return nil unless exception 51 | # raise helpful errors to indicate *why* it failed: 52 | tables = stored ? TABLES_PROHIBITED_STORED : TABLES_PROHIBITED 53 | StringPrep.check_prohibited! str, *tables, bidi: true, profile: "SASLprep" 54 | raise InvalidStringError.new( 55 | "unknown error", string: string, profile: "SASLprep" 56 | ) 57 | rescue ArgumentError, Encoding::CompatibilityError => ex 58 | if /invalid byte sequence|incompatible encoding/.match? ex.message 59 | return nil unless exception 60 | raise StringPrepError.new(ex.message, string: str, profile: "saslprep") 61 | end 62 | raise ex 63 | end 64 | 65 | end 66 | 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/net/imap/stringprep/trace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP 5 | module StringPrep 6 | 7 | # Defined in RFC-4505[https://www.rfc-editor.org/rfc/rfc4505] §3, The +trace+ 8 | # profile of \StringPrep is used by the +ANONYMOUS+ \SASL mechanism. 9 | module Trace 10 | 11 | # Defined in RFC-4505[https://www.rfc-editor.org/rfc/rfc4505] §3. 12 | STRINGPREP_PROFILE = "trace" 13 | 14 | # >>> 15 | # The character repertoire of this profile is Unicode 3.2 [Unicode]. 16 | UNASSIGNED_TABLE = "A.1" 17 | 18 | # >>> 19 | # No mapping is required by this profile. 20 | MAPPING_TABLES = nil 21 | 22 | # >>> 23 | # No Unicode normalization is required by this profile. 24 | NORMALIZATION = nil 25 | 26 | # From RFC-4505[https://www.rfc-editor.org/rfc/rfc4505] §3, The "trace" 27 | # Profile of "Stringprep": 28 | # >>> 29 | # Characters from the following tables of [StringPrep] are prohibited: 30 | # 31 | # - C.2.1 (ASCII control characters) 32 | # - C.2.2 (Non-ASCII control characters) 33 | # - C.3 (Private use characters) 34 | # - C.4 (Non-character code points) 35 | # - C.5 (Surrogate codes) 36 | # - C.6 (Inappropriate for plain text) 37 | # - C.8 (Change display properties are deprecated) 38 | # - C.9 (Tagging characters) 39 | # 40 | # No additional characters are prohibited. 41 | PROHIBITED_TABLES = %w[C.2.1 C.2.2 C.3 C.4 C.5 C.6 C.8 C.9].freeze 42 | 43 | # >>> 44 | # This profile requires bidirectional character checking per Section 6 45 | # of [StringPrep]. 46 | CHECK_BIDI = true 47 | 48 | module_function 49 | 50 | # From RFC-4505[https://www.rfc-editor.org/rfc/rfc4505] §3, The "trace" 51 | # Profile of "Stringprep": 52 | # >>> 53 | # The character repertoire of this profile is Unicode 3.2 [Unicode]. 54 | # 55 | # No mapping is required by this profile. 56 | # 57 | # No Unicode normalization is required by this profile. 58 | # 59 | # The list of unassigned code points for this profile is that provided 60 | # in Appendix A of [StringPrep]. Unassigned code points are not 61 | # prohibited. 62 | # 63 | # Characters from the following tables of [StringPrep] are prohibited: 64 | # (documented on PROHIBITED_TABLES) 65 | # 66 | # This profile requires bidirectional character checking per Section 6 67 | # of [StringPrep]. 68 | def stringprep_trace(string, **opts) 69 | StringPrep.stringprep( 70 | string, 71 | unassigned: UNASSIGNED_TABLE, 72 | maps: MAPPING_TABLES, 73 | prohibited: PROHIBITED_TABLES, 74 | normalization: NORMALIZATION, 75 | bidi: CHECK_BIDI, 76 | profile: STRINGPREP_PROFILE, 77 | **opts, 78 | ) 79 | end 80 | 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/net/imap/vanished_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Net 4 | class IMAP < Protocol 5 | 6 | # Net::IMAP::VanishedData represents the contents of a +VANISHED+ response, 7 | # which is described by the 8 | # {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] extension. 9 | # [{RFC7162 §3.2.10}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.2.10]]. 10 | # 11 | # +VANISHED+ responses replace +EXPUNGE+ responses when either the 12 | # {QRESYNC}[https://www.rfc-editor.org/rfc/rfc7162.html] or the 13 | # {UIDONLY}[https://www.rfc-editor.org/rfc/rfc9586.html] extension has been 14 | # enabled. 15 | class VanishedData < Data.define(:uids, :earlier) 16 | 17 | # Returns a new VanishedData object. 18 | # 19 | # * +uids+ will be converted by SequenceSet.[]. 20 | # * +earlier+ will be converted to +true+ or +false+ 21 | def initialize(uids:, earlier:) 22 | uids = SequenceSet[uids] 23 | earlier = !!earlier 24 | super 25 | end 26 | 27 | ## 28 | # :attr_reader: uids 29 | # 30 | # SequenceSet of UIDs that have been permanently removed from the mailbox. 31 | 32 | ## 33 | # :attr_reader: earlier 34 | # 35 | # +true+ when the response was caused by Net::IMAP#uid_fetch with 36 | # vanished: true or Net::IMAP#select/Net::IMAP#examine with 37 | # qresync: true. 38 | # 39 | # +false+ when the response is used to announce message removals within an 40 | # already selected mailbox. 41 | 42 | # rdoc doesn't handle attr aliases nicely. :( 43 | alias earlier? earlier # :nodoc: 44 | ## 45 | # :attr_reader: earlier? 46 | # 47 | # Alias for #earlier. 48 | 49 | # Returns an Array of all of the UIDs in #uids. 50 | # 51 | # See SequenceSet#numbers. 52 | def to_a; uids.numbers end 53 | 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /net-imap.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb"), :encoding=> 'utf-8') do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Shugo Maeda", "nicholas a. evans"] 14 | spec.email = ["shugo@ruby-lang.org", "nick@rubinick.dev"] 15 | 16 | spec.summary = %q{Ruby client api for Internet Message Access Protocol} 17 | spec.description = %q{Ruby client api for Internet Message Access Protocol} 18 | spec.homepage = "https://github.com/ruby/net-imap" 19 | spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0") 20 | spec.licenses = ["Ruby", "BSD-2-Clause"] 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | spec.metadata["changelog_uri"] = spec.homepage + "/releases" 25 | 26 | # Specify which files should be added to the gem when it is released. 27 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 28 | spec.files = Dir.chdir(__dir__) do 29 | `git ls-files -z 2>/dev/null`.split("\x0") 30 | .grep_v(%r{^(\.git(ignore)?|\.mailmap|(\.github|bin|test|spec|benchmarks|features|rfcs)/)}) 31 | end 32 | spec.bindir = "exe" 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ["lib"] 35 | 36 | spec.add_dependency "net-protocol" 37 | spec.add_dependency "date" 38 | end 39 | -------------------------------------------------------------------------------- /rakelib/benchmarks.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | PARSER_TEST_FIXTURES = FileList.new "test/net/imap/fixtures/response_parser/*.yml" 4 | CLOBBER.include "benchmarks/parser.yml" 5 | CLEAN.include "benchmarks/Gemfile-*" 6 | 7 | BENCHMARK_INIT = < PARSER_TEST_FIXTURES do |t| 21 | require "yaml" 22 | require "pathname" 23 | require "net/imap" 24 | 25 | path = Pathname.new(__dir__) / "../test/net/imap/fixtures/response_parser" 26 | files = path.glob("*.yml") 27 | tests = files.flat_map {|file| 28 | file.read 29 | .gsub(%r{([-:]) !ruby/(object|struct|array):\S+}) { $1 } 30 | .then { 31 | YAML.safe_load(_1, filename: file, 32 | permitted_classes: [Symbol, Regexp], aliases: true) 33 | } 34 | .fetch(:tests) 35 | .select {|test_name, test| 36 | :parser_assert_equal == test.fetch(:test_type) { 37 | test.key?(:expected) ? :parser_assert_equal : :parser_pending 38 | } 39 | } 40 | .map {|test_name, test| [test_name.to_s, test.fetch(:response)] } 41 | } 42 | 43 | benchmarks = tests.map {|fixture_name, response| 44 | {"name" => fixture_name.delete_prefix("test_"), 45 | "prelude" => "response = -%s.b" % [response.dump], 46 | "script" => "parser.parse(response)"} 47 | } 48 | .sort_by { _1["name"] } 49 | 50 | YAML.dump({"prelude" => BENCHMARK_INIT, "benchmark" => benchmarks}) 51 | .then { File.write t.name, _1 } 52 | end 53 | 54 | namespace :benchmarks do 55 | desc "Generate benchmarks from fixture data" 56 | task :generate => "benchmarks/parser.yml" 57 | 58 | desc "run the parser benchmarks comparing multiple gem versions" 59 | task :compare => :generate do |task, args| 60 | cd Pathname.new(__dir__) + ".." 61 | current = `git describe --tags --dirty`.chomp 62 | current = "dev" if current.empty? 63 | versions = args.to_a 64 | if versions.empty? 65 | latest = %x{git describe --tags --abbrev=0 --match 'v*.*.*'}.chomp 66 | versions = latest.empty? ? [] : [latest.delete_prefix("v")] 67 | end 68 | versions = versions.to_h { [_1, "Gemfile-v#{_1}"] } 69 | cd "benchmarks" do 70 | versions.each do |version, gemfile| 71 | File.write gemfile, <<~RUBY 72 | # frozen_string_literal: true 73 | source "https://rubygems.org" 74 | gem "net-imap", #{version.dump} 75 | RUBY 76 | end 77 | versions = {current => "../Gemfile" , **versions}.map { 78 | "%s::/usr/bin/env BUNDLE_GEMFILE=%s ruby" % _1 79 | }.join(";") 80 | 81 | extra = ENV.fetch("BENCHMARK_ARGS", "").shellsplit 82 | 83 | sh("benchmark-driver", 84 | "--bundler", 85 | "-e", versions, 86 | "parser.yml", 87 | *extra) 88 | end 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /rakelib/rdoc.rake: -------------------------------------------------------------------------------- 1 | # require "sdoc" 2 | require "rdoc/task" 3 | require_relative "../lib/net/imap" 4 | require 'rdoc/rdoc' unless defined?(RDoc::Markup::ToHtml) 5 | 6 | module RDoc::Generator 7 | module NetIMAP 8 | 9 | module RemoveRedundantParens 10 | def param_seq 11 | super.sub(/^\(\)\s*/, "") 12 | end 13 | end 14 | 15 | # See https://github.com/ruby/rdoc/pull/936 16 | module FixSectionComments 17 | def markup(text) 18 | @store ||= @parent&.store 19 | super 20 | end 21 | def description; markup comment end 22 | def comment; super || @comments&.first end 23 | def parse(_comment_location = nil) super() end 24 | end 25 | 26 | # render "[label] data" lists as tables. adapted from "hanna-nouveau" gem. 27 | module LabelListTable 28 | def list_item_start(list_item, list_type) 29 | case list_type 30 | when :NOTE 31 | %(#{Array(list_item.label).map{|label| to_html(label)}.join("
")}) 32 | else 33 | super 34 | end 35 | end 36 | 37 | def list_end_for(list_type) 38 | case list_type 39 | when :NOTE then 40 | "" 41 | else 42 | super 43 | end 44 | end 45 | end 46 | 47 | end 48 | end 49 | 50 | class RDoc::AnyMethod 51 | prepend RDoc::Generator::NetIMAP::RemoveRedundantParens 52 | end 53 | 54 | class RDoc::Context::Section 55 | prepend RDoc::Generator::NetIMAP::FixSectionComments 56 | end 57 | 58 | class RDoc::Markup::ToHtml 59 | LIST_TYPE_TO_HTML[:NOTE] = ['', '
'] 60 | prepend RDoc::Generator::NetIMAP::LabelListTable 61 | end 62 | 63 | RDoc::Task.new do |doc| 64 | doc.main = "README.md" 65 | doc.title = "net-imap #{Net::IMAP::VERSION}" 66 | doc.rdoc_dir = "doc" 67 | doc.rdoc_files = FileList.new %w[lib/**/*.rb *.rdoc *.md] 68 | doc.options << "--template-stylesheets" << "docs/styles.css" 69 | # doc.generator = "hanna" 70 | end 71 | -------------------------------------------------------------------------------- /rakelib/saslprep.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "string_prep_tables_generator" 4 | 5 | generator = StringPrepTablesGenerator.new 6 | 7 | file generator.json_filename => generator.json_deps do |t| 8 | generator.generate_json_data_file 9 | end 10 | 11 | directory "lib/net/imap/sasl" 12 | 13 | file "lib/net/imap/stringprep/tables.rb" => generator.rb_deps do |t| 14 | File.write t.name, generator.stringprep_rb 15 | end 16 | 17 | file "lib/net/imap/stringprep/saslprep_tables.rb" => generator.rb_deps do |t| 18 | File.write t.name, generator.saslprep_rb 19 | end 20 | 21 | GENERATED_RUBY = FileList.new( 22 | "lib/net/imap/stringprep/tables.rb", 23 | "lib/net/imap/stringprep/saslprep_tables.rb", 24 | ) 25 | 26 | CLEAN.include generator.clean_deps 27 | CLOBBER.include GENERATED_RUBY 28 | 29 | task saslprep_rb: GENERATED_RUBY 30 | task test: :saslprep_rb 31 | -------------------------------------------------------------------------------- /sample/net-imap.rb: -------------------------------------------------------------------------------- 1 | require 'net/imap' 2 | require "getoptlong" 3 | 4 | $stdout.sync = true 5 | $port = nil 6 | $user = ENV["USER"] || ENV["LOGNAME"] 7 | $auth = "login" 8 | $ssl = false 9 | $starttls = false 10 | 11 | def usage 12 | < 14 | 15 | --help print this message 16 | --port=PORT specifies port 17 | --user=USER specifies user 18 | --auth=AUTH specifies auth type 19 | --starttls use starttls 20 | --ssl use ssl 21 | EOF 22 | end 23 | 24 | begin 25 | require 'io/console' 26 | rescue LoadError 27 | def _noecho(&block) 28 | system("stty", "-echo") 29 | begin 30 | yield STDIN 31 | ensure 32 | system("stty", "echo") 33 | end 34 | end 35 | else 36 | def _noecho(&block) 37 | STDIN.noecho(&block) 38 | end 39 | end 40 | 41 | def get_password 42 | print "password: " 43 | begin 44 | return _noecho(&:gets).chomp 45 | ensure 46 | puts 47 | end 48 | end 49 | 50 | def get_command 51 | printf("%s@%s> ", $user, $host) 52 | if line = gets 53 | return line.strip.split(/\s+/) 54 | else 55 | return nil 56 | end 57 | end 58 | 59 | parser = GetoptLong.new 60 | parser.set_options(['--debug', GetoptLong::NO_ARGUMENT], 61 | ['--help', GetoptLong::NO_ARGUMENT], 62 | ['--port', GetoptLong::REQUIRED_ARGUMENT], 63 | ['--user', GetoptLong::REQUIRED_ARGUMENT], 64 | ['--auth', GetoptLong::REQUIRED_ARGUMENT], 65 | ['--starttls', GetoptLong::NO_ARGUMENT], 66 | ['--ssl', GetoptLong::NO_ARGUMENT]) 67 | begin 68 | parser.each_option do |name, arg| 69 | case name 70 | when "--port" 71 | $port = arg 72 | when "--user" 73 | $user = arg 74 | when "--auth" 75 | $auth = arg 76 | when "--ssl" 77 | $ssl = true 78 | when "--starttls" 79 | $starttls = true 80 | when "--debug" 81 | Net::IMAP.debug = true 82 | when "--help" 83 | usage 84 | exit 85 | end 86 | end 87 | rescue 88 | abort usage 89 | end 90 | 91 | $host = ARGV.shift 92 | unless $host 93 | abort usage 94 | end 95 | 96 | imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl) 97 | begin 98 | imap.starttls if $starttls 99 | class << password = method(:get_password) 100 | alias to_str call 101 | end 102 | imap.authenticate($auth, $user, password) 103 | while true 104 | cmd, *args = get_command 105 | break unless cmd 106 | begin 107 | case cmd 108 | when "list" 109 | for mbox in imap.list("", args[0] || "*") 110 | if mbox.attr.include?(Net::IMAP::NOSELECT) 111 | prefix = "!" 112 | elsif mbox.attr.include?(Net::IMAP::MARKED) 113 | prefix = "*" 114 | else 115 | prefix = " " 116 | end 117 | print prefix, mbox.name, "\n" 118 | end 119 | when "select" 120 | imap.select(args[0] || "inbox") 121 | print "ok\n" 122 | when "close" 123 | imap.close 124 | print "ok\n" 125 | when "summary" 126 | unless messages = imap.responses["EXISTS"][-1] 127 | puts "not selected" 128 | next 129 | end 130 | if messages > 0 131 | for data in imap.fetch(1..-1, ["ENVELOPE"]) 132 | print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n" 133 | end 134 | else 135 | puts "no message" 136 | end 137 | when "fetch" 138 | if args[0] 139 | data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0] 140 | puts data.attr["RFC822.HEADER"] 141 | puts data.attr["RFC822.TEXT"] 142 | else 143 | puts "missing argument" 144 | end 145 | when "logout", "exit", "quit" 146 | break 147 | when "help", "?" 148 | print < e 53 | flunk e.message 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/net/fixtures/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | regen_certs: 4 | touch server.key 5 | make server.crt 6 | 7 | cacert.pem: server.key 8 | openssl req -new -x509 -days 3650 -key server.key -out cacert.pem -subj "/C=JP/ST=Shimane/L=Matz-e city/O=Ruby Core Team/CN=Ruby Test CA/emailAddress=security@ruby-lang.org" 9 | 10 | server.csr: 11 | openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Shimane/O=Ruby Core Team/OU=Ruby Test/CN=localhost" 12 | 13 | server.crt: server.csr cacert.pem 14 | openssl x509 -days 3650 -CA cacert.pem -CAkey server.key -set_serial 00 -in server.csr -req -out server.crt 15 | rm server.csr 16 | -------------------------------------------------------------------------------- /test/net/fixtures/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIUGMvHl3EhtKPKcgc3NQSAYfFuC+8wDQYJKoZIhvcNAQEL 3 | BQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRQwEgYDVQQHDAtN 4 | YXR6LWUgY2l0eTEXMBUGA1UECgwOUnVieSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1 5 | YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAcnVieS1sYW5nLm9y 6 | ZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEyMjkxMTQ3MjNaMIGMMQswCQYDVQQGEwJK 7 | UDEQMA4GA1UECAwHU2hpbWFuZTEUMBIGA1UEBwwLTWF0ei1lIGNpdHkxFzAVBgNV 8 | BAoMDlJ1YnkgQ29yZSBUZWFtMRUwEwYDVQQDDAxSdWJ5IFRlc3QgQ0ExJTAjBgkq 9 | hkiG9w0BCQEWFnNlY3VyaXR5QHJ1YnktbGFuZy5vcmcwggEiMA0GCSqGSIb3DQEB 10 | AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI 11 | +1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f 12 | qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 13 | yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe 14 | NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 15 | JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjUzBRMB0GA1Ud 16 | DgQWBBSJGVleDvFp9cu9R+E0/OKYzGkwkTAfBgNVHSMEGDAWgBSJGVleDvFp9cu9 17 | R+E0/OKYzGkwkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBl 18 | 8GLB8skAWlkSw/FwbUmEV3zyqu+p7PNP5YIYoZs0D74e7yVulGQ6PKMZH5hrZmHo 19 | orFSQU+VUUirG8nDGj7Rzce8WeWBxsaDGC8CE2dq6nC6LuUwtbdMnBrH0LRWAz48 20 | jGFF3jHtVz8VsGfoZTZCjukWqNXvU6hETT9GsfU+PZqbqcTVRPH52+XgYayKdIbD 21 | r97RM4X3+aXBHcUW0b76eyyi65RR/Xtvn8ioZt2AdX7T2tZzJyXJN3Hupp77s6Ui 22 | AZR35SToHCZeTZD12YBvLBdaTPLZN7O/Q/aAO9ZiJaZ7SbFOjz813B2hxXab4Fob 23 | 2uJX6eMWTVxYK5D4M9lm 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /test/net/fixtures/dhparams.pem: -------------------------------------------------------------------------------- 1 | DH Parameters: (2048 bit) 2 | prime: 3 | 00:ec:4e:a4:06:b6:22:ca:f9:8a:00:cc:d0:ee:2f: 4 | 16:bf:05:64:f5:8f:fe:7f:c4:bb:b0:24:cd:ef:5d: 5 | 8a:90:ad:dc:a9:dd:63:84:90:d8:25:ba:d8:78:d5: 6 | 77:91:42:0a:84:fc:56:1e:13:9b:1c:aa:43:d5:1f: 7 | 38:52:92:fe:b3:66:f9:e7:e8:8c:77:a1:a6:2f:b3: 8 | 98:98:d2:13:fc:57:1c:2a:14:dc:bd:e6:9b:54:19: 9 | 99:4f:ce:81:64:a6:32:7f:8e:61:50:5f:45:3a:e5: 10 | 0c:f7:13:f3:b8:ad:d5:77:ca:09:42:f7:d8:30:27: 11 | 7b:2c:f0:b4:b5:a0:04:96:34:0b:47:81:1d:7f:c1: 12 | 3a:62:86:8e:7d:f8:13:7f:9a:b1:8b:09:23:9e:55: 13 | 59:41:cd:f0:86:09:c4:b7:d1:69:54:cb:d0:f5:e9: 14 | 27:c9:e1:81:e4:a1:df:6b:20:1c:df:e8:54:02:f2: 15 | 37:fc:2a:f7:d5:b3:6f:79:7e:70:22:78:79:18:3c: 16 | 75:14:68:4a:05:9f:ac:d4:7f:9a:79:db:9d:0a:6e: 17 | ec:0a:04:70:bf:c9:4a:59:81:a2:1f:33:9b:4a:66: 18 | bc:03:ce:8a:1b:e3:03:ec:ba:39:26:ab:90:dc:39: 19 | 41:a1:d8:f7:20:3c:8f:af:12:2f:f7:a9:6f:44:f1: 20 | 6d:03 21 | generator: 2 (0x2) 22 | -----BEGIN DH PARAMETERS----- 23 | MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY 24 | JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab 25 | VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6 26 | YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3 27 | 1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD 28 | 7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg== 29 | -----END DH PARAMETERS----- 30 | -------------------------------------------------------------------------------- /test/net/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYTCCAkkCAQAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYD 3 | VQQIDAdTaGltYW5lMRQwEgYDVQQHDAtNYXR6LWUgY2l0eTEXMBUGA1UECgwOUnVi 4 | eSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJ 5 | ARYWc2VjdXJpdHlAcnVieS1sYW5nLm9yZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEy 6 | MjkxMTQ3MjNaMGAxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRcwFQYD 7 | VQQKDA5SdWJ5IENvcmUgVGVhbTESMBAGA1UECwwJUnVieSBUZXN0MRIwEAYDVQQD 8 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw+egZ 9 | Q6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI+1GSqyi1bFBgsRjM0THllIdMbKmJ 10 | tWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0fqXmG8UTz0VTWdlAXXmhUs6lSADvA 11 | aIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0yg+801SXzoFTTa+UGIRLE66jH51a 12 | a5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIeNWMF32wHqIOOPvQcWV3M5D2vxJEj 13 | 702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1JNPc/n3dVUm+fM6NoDXPoLP7j55G 14 | 9zKyqGtGAWXAj1MTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACtGNdj5TEtnJBYp 15 | M+LhBeU3oNteldfycEm993gJp6ghWZFg23oX8fVmyEeJr/3Ca9bAgDqg0t9a0npN 16 | oWKEY6wVKqcHgu3gSvThF5c9KhGbeDDmlTSVVNQmXWX0K2d4lS2cwZHH8mCm2mrY 17 | PDqlEkSc7k4qSiqigdS8i80Yk+lDXWsm8CjsiC93qaRM7DnS0WPQR0c16S95oM6G 18 | VklFKUSDAuFjw9aVWA/nahOucjn0w5fVW6lyIlkBslC1ChlaDgJmvhz+Ol3iMsE0 19 | kAmFNu2KKPVrpMWaBID49QwQTDyhetNLaVVFM88iUdA9JDoVMEuP1mm39JqyzHTu 20 | uBrdP4Q= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/net/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso 3 | tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE 4 | 89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU 5 | l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s 6 | B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59 7 | 3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+ 8 | dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI 9 | FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J 10 | aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2 11 | BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx 12 | IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/ 13 | fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u 14 | pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT 15 | Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl 16 | u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD 17 | fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X 18 | Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE 19 | k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo 20 | qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS 21 | CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ 22 | XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw 23 | AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r 24 | UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0 25 | 2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5 26 | 7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/net/imap/fake_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | 5 | # NOTE: API is experimental and may change without deprecation or warning. 6 | # 7 | # FakeServer is simple fake IMAP server that is used for testing Net::IMAP. It 8 | # contains simple implementations of many IMAP commands and allows customization 9 | # of server responses. This allow tests to assume a more-or-less "normal" IMAP 10 | # server implementation, so as to focus on what's important for what's being 11 | # tested without needing to fuss over the details of a TCPServer script. 12 | # 13 | # Although the API is not (yet) stable, Net::IMAP::FakeServer is also intended 14 | # to be useful for testing libraries and applications which themselves use 15 | # Net::IMAP. 16 | # 17 | # ## Limitations 18 | # 19 | # FakeServer cannot be a complete replacement for exploratory testing or 20 | # integration testing with actual IMAP servers. Simple default behaviors will 21 | # be provided for many commands, and tests may simulate specific server 22 | # responses by assigning handlers (using #on). 23 | # 24 | # And FakeServer is significantly more complex than simply creating a socket IO 25 | # script in a separate thread. This complexity may obscure the focus of some 26 | # tests or make it more difficult to debug them. Use with discretion. 27 | # 28 | # Currently, the server will shutdown after a single connection has been 29 | # accepted and closed. This may change in the future, but only if tests can be 30 | # simplified or made significantly faster by allowing multiple connections to 31 | # the same TCPServer. 32 | # 33 | class Net::IMAP::FakeServer 34 | dir = "#{__dir__}/fake_server" 35 | autoload :Command, "#{dir}/command" 36 | autoload :CommandReader, "#{dir}/command_reader" 37 | autoload :CommandRouter, "#{dir}/command_router" 38 | autoload :CommandResponseWriter, "#{dir}/command_response_writer" 39 | autoload :Configuration, "#{dir}/configuration" 40 | autoload :Connection, "#{dir}/connection" 41 | autoload :ConnectionState, "#{dir}/connection_state" 42 | autoload :ResponseWriter, "#{dir}/response_writer" 43 | autoload :Socket, "#{dir}/socket" 44 | autoload :Session, "#{dir}/session" 45 | autoload :TestHelper, "#{dir}/test_helper" 46 | 47 | # Returns the server's FakeServer::Configuration 48 | attr_reader :config 49 | 50 | # All arguments to FakeServer#initialize are forwarded to 51 | # FakeServer::Configuration#initialize, to define the FakeServer#config. 52 | # 53 | # The server will immediately bind to a port, so any non-default +hostname+ 54 | # and +port+ must be specified as parameters. Changing them after creating 55 | # the server will have no effect. The default values are hostname: 56 | # "localhost", port: 0, which binds to a random port. Use 57 | # FakeServer#port to learn which port was chosen. 58 | # 59 | # The server does not accept any incoming connections until #run is called. 60 | def initialize(...) 61 | @config = Configuration.new(...) 62 | @tcp_server = TCPServer.new(config.hostname, config.port) 63 | @connection = nil 64 | @mutex = Thread::Mutex.new 65 | end 66 | 67 | def host; tcp_server.addr[2] end 68 | def port; tcp_server.addr[1] end 69 | 70 | # Accept a client connection and run a server loop to handle incoming 71 | # commands. #run will block until that connection has closed, and must be 72 | # called in a different Thread (or Fiber) from the client connection. 73 | def run 74 | Timeout.timeout(config.timeout) do 75 | tcp_socket = tcp_server.accept 76 | tcp_socket.timeout = config.read_timeout if tcp_socket.respond_to? :timeout 77 | @connection = Connection.new(self, tcp_socket: tcp_socket) 78 | @connection.run 79 | ensure 80 | shutdown 81 | end 82 | end 83 | 84 | # Currently, the server will shutdown after a single connection has been 85 | # accepted and closed. This may change in the future. Call #shutdown 86 | # explicitly to ensure the server socket is unbound. 87 | def shutdown 88 | @mutex.synchronize do 89 | connection&.close 90 | commands&.close if connection&.commands&.closed?&.! 91 | tcp_server.close 92 | end 93 | end 94 | 95 | # A Queue that contains every command the server has received. 96 | # 97 | # NOTE: This is not available until the connection has been accepted. 98 | def commands; connection.commands end 99 | 100 | # A Queue that contains every command the server has received. 101 | def state; connection.state end 102 | 103 | # See CommandRouter#on 104 | def on(...) connection&.on(...) end 105 | 106 | # See Connection#unsolicited 107 | def unsolicited(...) @mutex.synchronize { connection&.unsolicited(...) } end 108 | 109 | private 110 | 111 | attr_reader :tcp_server, :connection 112 | 113 | end 114 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | 5 | class Net::IMAP::FakeServer 6 | Command = Struct.new(:tag, :name, :args, :raw) 7 | end 8 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/command_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | 5 | class Net::IMAP::FakeServer 6 | CommandParseError = RuntimeError 7 | 8 | class CommandReader 9 | attr_reader :last_command 10 | 11 | def initialize(socket) 12 | @socket = socket 13 | @last_comma0 = nil 14 | end 15 | 16 | def get_command 17 | buf = "".b 18 | while true 19 | s = socket.gets("\r\n") or break 20 | buf << s 21 | break unless /\{(\d+)(\+)?\}\r\n\z/n =~ buf 22 | $2 or socket.print "+ Continue\r\n" 23 | buf << socket.read(Integer($1)) 24 | end 25 | throw :eof if buf.empty? 26 | @last_command = parse(buf) 27 | rescue CommandParseError => err 28 | raise IOError, err.message if socket.eof? && !buf.end_with?("\r\n") 29 | end 30 | 31 | private 32 | 33 | attr_reader :socket 34 | 35 | # TODO: convert bad command exception to tagged BAD response, when possible 36 | def parse(buf) 37 | /\A([^ ]+) ((?:UID )?\w+)(?: (.+))?\r\n\z/min =~ buf or 38 | raise CommandParseError, "bad request: %p" [buf] 39 | case $2.upcase 40 | when "LOGIN", "SELECT", "EXAMINE", "ENABLE", "AUTHENTICATE" 41 | Command.new $1, $2, scan_astrings($3), buf 42 | else 43 | Command.new $1, $2, $3, buf # TODO... 44 | end 45 | end 46 | 47 | # TODO: this is not the correct regexp, and literals aren't handled either 48 | def scan_astrings(str) 49 | str 50 | .scan(/"((?:[^"\\]|\\["\\])+)"|(\S+)/n) 51 | .map {|quoted, astr| astr || quoted.gsub(/\\([\\"])/n, '\1') } 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/command_response_writer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | 5 | class Net::IMAP::FakeServer 6 | 7 | class CommandResponseWriter < ResponseWriter 8 | attr_reader :command 9 | 10 | def initialize(parent, command) 11 | super(parent.socket, config: parent.config, state: parent.state) 12 | @command = command 13 | end 14 | 15 | def tag; command.tag end 16 | def name; command.name end 17 | def args; command.args end 18 | 19 | def tagged(cond, code:, text:) 20 | puts [tag, resp_cond(cond, text: text, code: code)].join(" ") 21 | end 22 | 23 | def done_ok(text = "#{name} done", code: nil) 24 | tagged :OK, text: text, code: code 25 | end 26 | 27 | def fail_bad(text = "Invalid command or args", code: nil) 28 | tagged :BAD, code: code, text: text 29 | end 30 | 31 | def fail_no(text, code: nil) 32 | tagged :NO, code: code, text: text 33 | end 34 | 35 | def fail_bad_state(state) 36 | fail_bad "Wrong state for command %s (%s)" % [name, state.name] 37 | end 38 | 39 | def fail_bad_args 40 | fail_bad "invalid args for #{name}" 41 | end 42 | 43 | def fail_no_command 44 | fail_no "%s command is not implemented" % [name] 45 | end 46 | 47 | private 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # shareable_constant_value: experimental_everything 3 | 4 | class Net::IMAP::FakeServer 5 | 6 | # NOTE: The API is experimental and may change without deprecation or warning. 7 | # 8 | class Configuration 9 | CA_FILE = File.expand_path("../../fixtures/cacert.pem", __dir__) 10 | SERVER_KEY = File.expand_path("../../fixtures/server.key", __dir__) 11 | SERVER_CERT = File.expand_path("../../fixtures/server.crt", __dir__) 12 | 13 | DEFAULTS = { 14 | hostname: "localhost", port: 0, 15 | timeout: 10, connect_timeout: 2, read_timeout: 2, write_timeout: 2, 16 | 17 | implicit_tls: false, 18 | starttls: true, 19 | tls: { ca_file: CA_FILE, key: SERVER_KEY, cert: SERVER_CERT }.freeze, 20 | 21 | cleartext_login: false, 22 | encrypted_login: true, 23 | cleartext_auth: false, 24 | sasl_mechanisms: %i[PLAIN].freeze, 25 | sasl_ir: false, 26 | 27 | rev1: true, 28 | rev2: false, 29 | 30 | # TODO: use these to enable or disable actual commands 31 | extensions: %i[NAMESPACE MOVE IDLE UTF8=ACCEPT].freeze, 32 | 33 | capabilities_enablable: %i[UTF8=ACCEPT].freeze, 34 | 35 | preauth: true, 36 | greeting_bye: false, 37 | greeting_capabilities: true, 38 | greeting_text: "ruby Net::IMAP test server v#{Net::IMAP::VERSION}", 39 | 40 | user: { 41 | username: "test_user", 42 | password: "test-password", 43 | }.freeze, 44 | 45 | mailboxes: { 46 | "INBOX" => { name: "INBOX" }.freeze, 47 | }.freeze, 48 | } 49 | 50 | def initialize(with_extensions: [], without_extensions: [], **opts, &block) 51 | DEFAULTS.merge(opts).each do send :"#{_1}=", _2 end 52 | @handlers = {} 53 | self.extensions += with_extensions 54 | self.extensions -= without_extensions 55 | self.mailboxes = mailboxes.dup.transform_values(&:dup) 56 | end 57 | 58 | attr_reader :handlers 59 | attr_accessor(*DEFAULTS.keys) 60 | alias preauth? preauth 61 | alias implicit_tls? implicit_tls 62 | alias starttls? starttls 63 | alias rev1? rev1 64 | alias rev2? rev2 65 | alias cleartext_login? cleartext_login 66 | alias encrypted_login? encrypted_login 67 | alias cleartext_auth? cleartext_auth 68 | alias greeting_bye? greeting_bye 69 | alias greeting_capabilities? greeting_capabilities 70 | alias sasl_ir? sasl_ir 71 | 72 | def on(event, &handler) 73 | handler or raise ArgumentError 74 | handlers[event.to_sym.downcase] = handler 75 | end 76 | 77 | def greeting_cond; preauth? ? :PREAUTH : greeting_bye ? :BYE : :OK end 78 | 79 | def greeting_code 80 | return unless greeting_capabilities? 81 | capabilities = 82 | if preauth? then capabilities_post_auth 83 | elsif implicit_tls? then capabilities_pre_auth 84 | else capabilities_pre_tls 85 | end 86 | [:CAPABILITY, *capabilities] 87 | end 88 | 89 | def auth_capabilities; sasl_mechanisms.map { "AUTH=#{_1}" } end 90 | 91 | def valid_username_and_password 92 | users 93 | .map { _1.slice(:username, :password) } 94 | .find { _1.compact.length == 2 } 95 | end 96 | 97 | def basic_capabilities 98 | capa = [] 99 | capa << "IMAP4rev1" if rev1? 100 | capa << "IMAP4rev2" if rev2? 101 | capa 102 | end 103 | 104 | def capabilities_pre_tls 105 | capa = basic_capabilities 106 | capa << "STARTTLS" if starttls? 107 | capa << "LOGINDISABLED" unless cleartext_login? 108 | capa.concat auth_capabilities if cleartext_auth? 109 | capa << "SASL-IR" if sasl_ir? && cleartext_auth? 110 | capa 111 | end 112 | 113 | def capabilities_pre_auth 114 | capa = basic_capabilities 115 | capa << "LOGINDISABLED" unless encrypted_login? 116 | capa.concat auth_capabilities 117 | capa << "SASL-IR" if sasl_ir? 118 | capa 119 | end 120 | 121 | def capabilities_post_auth 122 | capa = basic_capabilities 123 | capa.concat extensions 124 | capa 125 | end 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Net::IMAP::FakeServer 4 | # > "Connection" refers to the entire sequence of client/server interaction 5 | # > from the initial establishment of the network connection until its 6 | # > termination. 7 | # --- https://www.rfc-editor.org/rfc/rfc9051#name-conventions-used-in-this-do 8 | class Connection 9 | attr_reader :config, :state 10 | 11 | def initialize(server, tcp_socket:) 12 | @config = server.config 13 | @socket = Socket.new tcp_socket, config: config 14 | @state = ConnectionState.new socket: socket, config: config 15 | @reader = CommandReader.new socket 16 | @writer = ResponseWriter.new socket, config: config, state: state 17 | @router = CommandRouter.new writer, config: config, state: state 18 | end 19 | 20 | def commands; state.commands end 21 | def on(...) router.on(...) end 22 | def unsolicited(...) writer.untagged(...) end 23 | 24 | def run 25 | writer.greeting 26 | catch(:eof) do 27 | router << reader.get_command until state.logout? 28 | end 29 | ensure 30 | close 31 | end 32 | 33 | def close 34 | unless state.logout? 35 | state.logout 36 | writer.bye 37 | end 38 | socket&.close unless socket&.closed? 39 | end 40 | 41 | private 42 | 43 | attr_reader :socket, :reader, :writer, :router 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/connection_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Net::IMAP::FakeServer 4 | 5 | class ConnectionState 6 | attr_reader :user 7 | attr_reader :session 8 | attr_reader :enabled 9 | attr_reader :commands 10 | 11 | def initialize(config:, socket: nil) 12 | @socket = socket # for managing the TLS state 13 | @logout = false 14 | @user = nil 15 | @session = nil 16 | @commands = Queue.new 17 | @enabled = [] 18 | 19 | if config.preauth? then authenticate config.user 20 | elsif config.greeting_bye then logout 21 | end 22 | end 23 | 24 | def capabilities(config) 25 | if user then config.capabilities_post_auth 26 | elsif tls? then config.capabilities_pre_auth 27 | else config.capabilities_pre_tls 28 | end 29 | end 30 | 31 | def tls?; @socket.tls? end 32 | def use_tls; @socket.use_tls end 33 | def closed?; @socket.closed? end 34 | 35 | def name 36 | if @logout then :logout 37 | elsif @session then :selected 38 | elsif @user then :authenticated 39 | else :not_authenticated 40 | end 41 | end 42 | 43 | def not_authenticated?; name == :not_authenticated end 44 | def authenticated?; name == :authenticated end 45 | def selected?; name == :selected end 46 | def logout?; name == :logout end 47 | 48 | def authenticate(user) 49 | not_authenticated? or raise "invalid state change" 50 | user or raise ArgumentError 51 | @user = user 52 | end 53 | 54 | def select(mbox:, **options) 55 | authenticated? || selected? or raise "invalid state change" 56 | mbox or raise ArgumentError 57 | @session = Session.new mbox: mbox, **options 58 | end 59 | 60 | def unselect 61 | selected? or raise "invalid state change" 62 | @session = nil 63 | end 64 | 65 | def unauthenticate 66 | authenticated? || selected? or raise "invalid state change" 67 | @user = @selected = nil 68 | end 69 | 70 | def logout 71 | !logout? or raise "already logged out" 72 | @logout = true 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/response_writer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | 5 | class Net::IMAP::FakeServer 6 | 7 | # :nodoc: 8 | class ResponseWriter 9 | def initialize(socket, config:, state:) 10 | @socket = socket 11 | @config = config 12 | @state = state 13 | end 14 | 15 | def for_command(command) CommandResponseWriter.new(self, command) end 16 | 17 | def request_continuation(message, length = nil) 18 | socket.print "+ #{message}\r\n" unless message.nil? 19 | length ? socket.read(Integer(length)) : socket.gets("\r\n") 20 | end 21 | 22 | def puts(*lines) lines.each do |msg| print "#{msg}\r\n" end end 23 | def print(...); socket.print(...) end 24 | 25 | def greeting 26 | untagged resp_cond(config.greeting_cond, 27 | text: config.greeting_text, 28 | code: config.greeting_code) 29 | end 30 | 31 | def bye(message = "Closing connection") 32 | untagged :BYE, message 33 | end 34 | 35 | def untagged(name_or_text, text = nil) 36 | puts [?*, name_or_text, text].compact.join(" ") 37 | end 38 | 39 | protected 40 | 41 | attr_reader :socket, :config, :state 42 | 43 | private 44 | 45 | def resp_code(code) 46 | case code 47 | in Array then resp_code code.join(" ") 48 | in String then code.match?(/\A\[/) ? code : "[#{code}]" 49 | in nil then nil 50 | end 51 | end 52 | 53 | def resp_cond(cond, text:, code: nil) 54 | case cond when :OK, :NO, :BAD, :BYE, :PREAUTH 55 | [cond, resp_code(code), text].compact.join " " 56 | else 57 | raise ArgumentError, "invalid resp-cond" 58 | end 59 | end 60 | 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Net::IMAP::FakeServer 4 | # > "Session" refers to the sequence of client/server interaction from the 5 | # > time that a mailbox is selected (SELECT or EXAMINE command) until the time 6 | # > that selection ends (SELECT or EXAMINE of another mailbox, CLOSE command, 7 | # > UNSELECT command, or connection termination). 8 | # --- https://www.rfc-editor.org/rfc/rfc9051#name-conventions-used-in-this-do 9 | Session = Struct.new(:mbox, :args, keyword_init: true) 10 | end 11 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Net::IMAP::FakeServer 4 | 5 | # :nodoc: 6 | class Socket 7 | attr_reader :config 8 | attr_reader :tcp_socket, :tls_socket 9 | 10 | def initialize(tcp_socket, config:) 11 | @config = config 12 | @tcp_socket = tcp_socket 13 | @tls_socket = nil 14 | @closed = false 15 | use_tls if config.implicit_tls && tcp_socket 16 | end 17 | 18 | def tls?; !!@tls_socket end 19 | def closed?; @closed end 20 | 21 | def eof?; socket.eof? end 22 | def gets(...) socket.gets(...) end 23 | def read(...) socket.read(...) end 24 | def print(...) socket.print(...) end 25 | 26 | def use_tls 27 | @tls_socket ||= OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_ctx).tap do |s| 28 | s.sync_close = true 29 | s.accept 30 | end 31 | end 32 | 33 | def close 34 | @tls_socket&.close unless @tls_socket&.closed? 35 | @tcp_socket&.close unless @tcp_socket&.closed? 36 | @closed = true 37 | end 38 | 39 | private 40 | 41 | def socket; @tls_socket || @tcp_socket end 42 | 43 | def ssl_ctx 44 | @ssl_ctx ||= OpenSSL::SSL::SSLContext.new.tap do |ctx| 45 | ctx.ca_file = config.tls[:ca_file] 46 | ctx.key = OpenSSL::PKey::RSA.new File.read config.tls.fetch :key 47 | ctx.cert = OpenSSL::X509::Certificate.new File.read config.tls.fetch :cert 48 | end 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/net/imap/fake_server/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../fake_server" 4 | 5 | module Net::IMAP::FakeServer::TestHelper 6 | 7 | IO_ERRORS = [ 8 | IOError, 9 | EOFError, 10 | Errno::ECONNABORTED, 11 | Errno::ECONNRESET, 12 | Errno::EPIPE, 13 | Errno::ETIMEDOUT, 14 | ].freeze 15 | 16 | def run_fake_server_in_thread(ignore_io_error: false, 17 | report_on_exception: true, 18 | timeout: 10, **opts) 19 | Timeout.timeout(timeout) do 20 | server = Net::IMAP::FakeServer.new(timeout: timeout, **opts) 21 | @threads << Thread.new do 22 | Thread.current.abort_on_exception = false 23 | Thread.current.report_on_exception = report_on_exception 24 | server.run 25 | rescue *IO_ERRORS 26 | raise unless ignore_io_error 27 | end 28 | yield server 29 | ensure 30 | begin 31 | server&.shutdown 32 | rescue *IO_ERRORS 33 | raise unless ignore_io_error 34 | end 35 | end 36 | end 37 | 38 | def with_client(*args, **kwargs) 39 | client = Net::IMAP.new(*args, **kwargs) 40 | yield client 41 | ensure 42 | if client && !client.disconnected? 43 | client.logout! 44 | end 45 | end 46 | 47 | def with_fake_server(select: nil, **opts) 48 | run_fake_server_in_thread(**opts) do |server| 49 | tls = opts[:implicit_tls] 50 | tls = {ca_file: server.config.tls[:ca_file]} if tls == true 51 | with_client("localhost", port: server.port, ssl: tls) do |client| 52 | if select 53 | client.select(select) 54 | server.commands.pop 55 | assert server.state.selected? 56 | end 57 | yield server, client 58 | end 59 | end 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/acl_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | test_acl_response: 4 | :comment: | 5 | [Bug #8281] 6 | :response: "* ACL \"INBOX/share\" \"imshare2copy1366146467@xxxxxxxxxxxxxxxxxx.com\" 7 | lrswickxteda\r\n" 8 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 9 | name: ACL 10 | data: 11 | - !ruby/struct:Net::IMAP::MailboxACLItem 12 | user: imshare2copy1366146467@xxxxxxxxxxxxxxxxxx.com 13 | rights: lrswickxteda 14 | mailbox: INBOX/share 15 | raw_data: "* ACL \"INBOX/share\" \"imshare2copy1366146467@xxxxxxxxxxxxxxxxxx.com\" 16 | lrswickxteda\r\n" 17 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/capability_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | # 6.1.1. CAPABILITY Command 5 | # 6 | # Example: C: abcd CAPABILITY 7 | # S: * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI 8 | # LOGINDISABLED 9 | # S: abcd OK CAPABILITY completed 10 | # C: efgh STARTTLS 11 | # S: efgh OK STARTLS completed 12 | # 13 | # C: ijkl CAPABILITY 14 | # S: * CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN 15 | # S: ijkl OK CAPABILITY completed 16 | 17 | rfc3501_6.1.1_example_1_capability_response: 18 | :response: "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n" 19 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 20 | name: CAPABILITY 21 | data: 22 | - IMAP4REV1 23 | - STARTTLS 24 | - AUTH=GSSAPI 25 | - LOGINDISABLED 26 | raw_data: "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n" 27 | 28 | rfc3501_6.1.1_example_2_capability_response: 29 | :response: "* CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n" 30 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 31 | name: CAPABILITY 32 | data: 33 | - IMAP4REV1 34 | - AUTH=GSSAPI 35 | - AUTH=PLAIN 36 | raw_data: "* CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n" 37 | 38 | # 7.2.1. CAPABILITY Response 39 | # 40 | # Example: S: * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI XPIG-LATIN 41 | rfc3501_7.2.1_CAPABILITY_response_example: 42 | :response: "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI XPIG-LATIN\r\n" 43 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 44 | name: CAPABILITY 45 | data: 46 | - IMAP4REV1 47 | - STARTTLS 48 | - AUTH=GSSAPI 49 | - XPIG-LATIN 50 | raw_data: "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI XPIG-LATIN\r\n" 51 | 52 | # The RFC9051 examples aren't significantly different from RFC3501. 53 | # Including only this one, for completeness: 54 | rfc9051_7.2.2_capability_example: 55 | :response: "* CAPABILITY STARTTLS AUTH=GSSAPI IMAP4rev2 LOGINDISABLED XPIG-LATIN\r\n" 56 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 57 | name: CAPABILITY 58 | data: 59 | - STARTTLS 60 | - AUTH=GSSAPI 61 | - IMAP4REV2 62 | - LOGINDISABLED 63 | - XPIG-LATIN 64 | raw_data: "* CAPABILITY STARTTLS AUTH=GSSAPI IMAP4rev2 LOGINDISABLED XPIG-LATIN\r\n" 65 | 66 | test_invalid_capability_extra_space_at_end: 67 | :comment: | 68 | [Bug #8415] 69 | :quirky_server: Apple iCloud 70 | :response: "* CAPABILITY st11p00mm-iscream009 1Q49 XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 71 | SASL-IR AUTH=ATOKEN AUTH=PLAIN \r\n" 72 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 73 | name: CAPABILITY 74 | data: 75 | - ST11P00MM-ISCREAM009 76 | - 1Q49 77 | - XAPPLEPUSHSERVICE 78 | - IMAP4 79 | - IMAP4REV1 80 | - SASL-IR 81 | - AUTH=ATOKEN 82 | - AUTH=PLAIN 83 | raw_data: "* CAPABILITY st11p00mm-iscream009 1Q49 XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 84 | SASL-IR AUTH=ATOKEN AUTH=PLAIN \r\n" 85 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/continuation_requests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | # n.b: The RFC9051 examples would be identical to these RFC3501 examples. 4 | 5 | rfc3501_6.2.2_example_continuation_request: 6 | :response: "+ YGgGCSqGSIb3EgECAgIAb1kwV6ADAgEFoQMCAQ+iSzBJoAMCAQGiQgRAtHTEuOP2BXb9sBYFR4SJlDZxmg39IxmRBOhXRKdDA0uHTCOT9Bq3OsUTXUlk0CsFLoa8j+gvGDlgHuqzWHPSQg==\r\n" 7 | :expected: !ruby/struct:Net::IMAP::ContinuationRequest 8 | data: !ruby/struct:Net::IMAP::ResponseText 9 | code: 10 | text: "YGgGCSqGSIb3EgECAgIAb1kwV6ADAgEFoQMCAQ+iSzBJoAMCAQGiQgRAtHTEuOP2BXb9sBYFR4SJlDZxmg39IxmRBOhXRKdDA0uHTCOT9Bq3OsUTXUlk0CsFLoa8j+gvGDlgHuqzWHPSQg==" 11 | raw_data: "+ YGgGCSqGSIb3EgECAgIAb1kwV6ADAgEFoQMCAQ+iSzBJoAMCAQGiQgRAtHTEuOP2BXb9sBYFR4SJlDZxmg39IxmRBOhXRKdDA0uHTCOT9Bq3OsUTXUlk0CsFLoa8j+gvGDlgHuqzWHPSQg==\r\n" 12 | 13 | rfc3501_6.3.11_example_continuation_request: 14 | :response: "+ Ready for literal data\r\n" 15 | :expected: !ruby/struct:Net::IMAP::ContinuationRequest 16 | data: !ruby/struct:Net::IMAP::ResponseText 17 | code: 18 | text: "Ready for literal data" 19 | raw_data: "+ Ready for literal data\r\n" 20 | 21 | # 7.5. Server Responses - Command Continuation Request 22 | # 23 | # The command continuation request response is indicated by a "+" token 24 | # instead of a tag. This form of response indicates that the server is 25 | # ready to accept the continuation of a command from the client. The 26 | # remainder of this response is a line of text. 27 | # ... 28 | # Example: C: A001 LOGIN {11} 29 | # S: + Ready for additional command text 30 | # C: FRED FOOBAR {7} 31 | # S: + Ready for additional command text 32 | # C: fat man 33 | # S: A001 OK LOGIN completed 34 | # C: A044 BLURDYBLOOP {102856} 35 | # S: A044 BAD No such command as "BLURDYBLOOP" 36 | rfc3501_7.5_continuation_request_example: 37 | :response: "+ Ready for additional command text\r\n" 38 | :expected: !ruby/struct:Net::IMAP::ContinuationRequest 39 | data: !ruby/struct:Net::IMAP::ResponseText 40 | code: 41 | text: "Ready for additional command text" 42 | raw_data: "+ Ready for additional command text\r\n" 43 | 44 | test_continuation_request_without_response_text: 45 | :response: "+\r\n" 46 | :expected: !ruby/struct:Net::IMAP::ContinuationRequest 47 | data: !ruby/struct:Net::IMAP::ResponseText 48 | code: 49 | text: '' 50 | raw_data: "+\r\n" 51 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/enabled_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | rfc9051_7.2.1_ENABLED_example: 5 | :response: "* ENABLED CONDSTORE QRESYNC\r\n" 6 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 7 | name: ENABLED 8 | data: 9 | - CONDSTORE 10 | - QRESYNC 11 | :raw_data: "* ENABLED CONDSTORE QRESYNC\r\n" 12 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/expunge_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | # 7.4.1. EXPUNGE Response 5 | # 6 | # Example: S: * 44 EXPUNGE 7 | rfc3501_7.4.1_EXPUNGE_response_example: 8 | :response: "* 44 EXPUNGE\r\n" 9 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 10 | name: EXPUNGE 11 | data: 44 12 | raw_data: "* 44 EXPUNGE\r\n" 13 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/flags_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | # 7.2.6. FLAGS Response 4 | # 5 | # Example: S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) 6 | rfc3501_7.2.6_FLAGS_response_example: 7 | :response: "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n" 8 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 9 | name: FLAGS 10 | data: 11 | - :Answered 12 | - :Flagged 13 | - :Deleted 14 | - :Seen 15 | - :Draft 16 | raw_data: "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n" 17 | 18 | test_flags_with_various_flag_types: 19 | :response: "* FLAGS (\\foo bAR $Etc \\baz)\r\n" 20 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 21 | name: FLAGS 22 | data: 23 | - :Foo 24 | - bAR 25 | - $Etc 26 | - :Baz 27 | raw_data: "* FLAGS (\\foo bAR $Etc \\baz)\r\n" 28 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/id_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | test_id_rfc2971_example_3.1_nil: 4 | :response: "* ID NIL\r\n" 5 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 6 | name: ID 7 | data: 8 | raw_data: "* ID NIL\r\n" 9 | test_id_rfc2971_example_3.2_cyrus: 10 | :response: "* ID (\"name\" \"Cyrus\" \"version\" \"1.5\" \"os\" \"sunos\" \"os-version\" \"5.5\" \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")\r\n" 11 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 12 | name: ID 13 | data: 14 | name: Cyrus 15 | version: "1.5" 16 | os: sunos 17 | os-version: "5.5" 18 | support-url: "mailto:cyrus-bugs+@andrew.cmu.edu" 19 | raw_data: "* ID (\"name\" \"Cyrus\" \"version\" \"1.5\" \"os\" \"sunos\" \"os-version\" \"5.5\" \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")\r\n" 20 | test_id_gmail: 21 | :response: "* ID (\"name\" \"GImap\" \"vendor\" \"Google, Inc.\" \"support-url\" 22 | NIL)\r\n" 23 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 24 | name: ID 25 | data: 26 | name: GImap 27 | vendor: Google, Inc. 28 | support-url: 29 | raw_data: "* ID (\"name\" \"GImap\" \"vendor\" \"Google, Inc.\" \"support-url\" 30 | NIL)\r\n" 31 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/list_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | # 7.2. Server Responses - Server and Mailbox Status 5 | # 6 | # These responses are always untagged. This is how server and mailbox 7 | # status data are transmitted from the server to the client. Many of 8 | # these responses typically result from a command with the same name. 9 | 10 | # 7.2.2. LIST Response 11 | # 12 | # Example: S: * LIST (\Noselect) "/" ~/Mail/foo 13 | rfc3501_7.2.2_LIST_response_example: 14 | :response: "* LIST (\\Noselect) \"/\" ~/Mail/foo\r\n" 15 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 16 | name: LIST 17 | data: !ruby/struct:Net::IMAP::MailboxList 18 | attr: 19 | - :Noselect 20 | delim: "/" 21 | name: "~/Mail/foo" 22 | raw_data: "* LIST (\\Noselect) \"/\" ~/Mail/foo\r\n" 23 | 24 | # 7.2.3. LSUB Response 25 | # 26 | # Example: S: * LSUB () "." #news.comp.mail.misc 27 | rfc3501_7.2.3_LSUB_response_example: 28 | :response: "* LSUB () \".\" #news.comp.mail.misc\r\n" 29 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 30 | name: LSUB 31 | data: !ruby/struct:Net::IMAP::MailboxList 32 | attr: [] 33 | delim: "." 34 | name: "#news.comp.mail.misc" 35 | raw_data: "* LSUB () \".\" #news.comp.mail.misc\r\n" 36 | 37 | test_list_with_various_flag_capitalizations: 38 | :response: "* LIST (\\foo \\bAR \\Etc \\baz) \".\" \"INBOX\"\r\n" 39 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 40 | name: LIST 41 | data: !ruby/struct:Net::IMAP::MailboxList 42 | attr: 43 | - :Foo 44 | - :Bar 45 | - :Etc 46 | - :Baz 47 | delim: "." 48 | name: INBOX 49 | raw_data: "* LIST (\\foo \\bAR \\Etc \\baz) \".\" \"INBOX\"\r\n" 50 | 51 | test_xlist_inbox: 52 | :response: "* XLIST (\\Inbox) \".\" \"INBOX\"\r\n" 53 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 54 | name: XLIST 55 | data: !ruby/struct:Net::IMAP::MailboxList 56 | attr: 57 | - :Inbox 58 | delim: "." 59 | name: INBOX 60 | raw_data: "* XLIST (\\Inbox) \".\" \"INBOX\"\r\n" 61 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/mailbox_size_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | # 7.3. Server Responses - Mailbox Size 4 | # 5 | # These responses are always untagged. This is how changes in the size 6 | # of the mailbox are transmitted from the server to the client. 7 | # Immediately following the "*" token is a number that represents a 8 | # message count. 9 | 10 | # 7.3.1. EXISTS Response 11 | # 12 | # Example: S: * 23 EXISTS 13 | rfc3501_7.3.1_EXISTS_response_example: 14 | :response: "* 23 EXISTS\r\n" 15 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 16 | name: EXISTS 17 | data: 23 18 | raw_data: "* 23 EXISTS\r\n" 19 | 20 | # 7.3.2. RECENT Response 21 | # 22 | # Example: S: * 5 RECENT 23 | rfc3501_7.3.2_RECENT_response_example: 24 | :response: "* 5 RECENT\r\n" 25 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 26 | name: RECENT 27 | data: 5 28 | raw_data: "* 5 RECENT\r\n" 29 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/resp_text_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | test_resp_text_with_T_LBRA: 5 | :response: "RUBY0004 OK [READ-WRITE] [Gmail]/Sent Mail selected. (Success)\r\n" 6 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 7 | tag: RUBY0004 8 | name: OK 9 | data: !ruby/struct:Net::IMAP::ResponseText 10 | code: !ruby/struct:Net::IMAP::ResponseCode 11 | name: READ-WRITE 12 | data: 13 | text: "[Gmail]/Sent Mail selected. (Success)" 14 | raw_data: "RUBY0004 OK [READ-WRITE] [Gmail]/Sent Mail selected. (Success)\r\n" 15 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/rfc8474_objectid_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | rfc8474_example_4.1_MAILBOXID_response_to_CREATE: 4 | :response: "3 OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Completed\r\n" 5 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 6 | tag: '3' 7 | name: OK 8 | data: !ruby/struct:Net::IMAP::ResponseText 9 | code: !ruby/struct:Net::IMAP::ResponseCode 10 | name: MAILBOXID 11 | data: F2212ea87-6097-4256-9d51-71338625 12 | text: Completed 13 | raw_data: "3 OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Completed\r\n" 14 | 15 | rfc8474_example_4.2_MAILBOXID_untagged_response_to_SELECT: 16 | :response: "* OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Ok\r\n" 17 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 18 | name: OK 19 | data: !ruby/struct:Net::IMAP::ResponseText 20 | code: !ruby/struct:Net::IMAP::ResponseCode 21 | name: MAILBOXID 22 | data: F2212ea87-6097-4256-9d51-71338625 23 | text: Ok 24 | raw_data: "* OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Ok\r\n" 25 | 26 | rfc8474_example_4.3_MAILBOXID_attribute_for_STATUS: 27 | :response: "* STATUS foo (MAILBOXID (F2212ea87-6097-4256-9d51-71338625))\r\n" 28 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 29 | name: STATUS 30 | data: !ruby/struct:Net::IMAP::StatusData 31 | mailbox: foo 32 | attr: 33 | MAILBOXID: F2212ea87-6097-4256-9d51-71338625 34 | raw_data: "* STATUS foo (MAILBOXID (F2212ea87-6097-4256-9d51-71338625))\r\n" 35 | 36 | rfc8474_example_5.3_EMAILID_and_THREADID: 37 | :response: "* 3 FETCH (EMAILID (M5fdc09b49ea703) THREADID (T11863d02dd95b5))\r\n" 38 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 39 | name: FETCH 40 | data: !ruby/struct:Net::IMAP::FetchData 41 | seqno: 3 42 | attr: 43 | EMAILID: M5fdc09b49ea703 44 | THREADID: T11863d02dd95b5 45 | raw_data: "* 3 FETCH (EMAILID (M5fdc09b49ea703) THREADID (T11863d02dd95b5))\r\n" 46 | 47 | rfc8474_example_5.3_no_THREADID_support: 48 | :response: "* 2 FETCH (EMAILID (M00000002) THREADID NIL)\r\n" 49 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 50 | name: FETCH 51 | data: !ruby/struct:Net::IMAP::FetchData 52 | seqno: 2 53 | attr: 54 | EMAILID: M00000002 55 | THREADID: 56 | raw_data: "* 2 FETCH (EMAILID (M00000002) THREADID NIL)\r\n" 57 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/rfc9208_quota_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | rfc9208_4.1.1_example: 5 | :response: "* QUOTA \"!partition/sda4\" (STORAGE 104 10923847)\r\n" 6 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 7 | name: QUOTA 8 | data: !ruby/struct:Net::IMAP::MailboxQuota 9 | mailbox: "!partition/sda4" 10 | usage: '104' 11 | quota: '10923847' 12 | raw_data: "* QUOTA \"!partition/sda4\" (STORAGE 104 10923847)\r\n" 13 | 14 | rfc9208_4.1.2_example_1: 15 | :response: "* QUOTAROOT INBOX \"#user/alice\" \"!partition/sda4\"\r\n" 16 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 17 | name: QUOTAROOT 18 | data: !ruby/struct:Net::IMAP::MailboxQuotaRoot 19 | mailbox: INBOX 20 | quotaroots: 21 | - "#user/alice" 22 | - "!partition/sda4" 23 | raw_data: "* QUOTAROOT INBOX \"#user/alice\" \"!partition/sda4\"\r\n" 24 | 25 | rfc9208_4.1.2_example_2: 26 | :response: "* QUOTA \"#user/alice\" (MESSAGE 42 1000)\r\n" 27 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 28 | name: QUOTA 29 | data: !ruby/struct:Net::IMAP::MailboxQuota 30 | mailbox: "#user/alice" 31 | usage: '42' 32 | quota: '1000' 33 | raw_data: "* QUOTA \"#user/alice\" (MESSAGE 42 1000)\r\n" 34 | 35 | # rfc9208_4.1.3_example_1: 36 | # :response: "* QUOTA \"#user/alice\" (STORAGE 54 111 MESSAGE 42 1000)\r\n" 37 | 38 | rfc9208_4.1.4_example: 39 | :response: "* STATUS INBOX (MESSAGES 12 DELETED 4 DELETED-STORAGE 8)\r\n" 40 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 41 | name: STATUS 42 | data: !ruby/struct:Net::IMAP::StatusData 43 | mailbox: INBOX 44 | attr: 45 | MESSAGES: 12 46 | DELETED: 4 47 | DELETED-STORAGE: 8 48 | raw_data: "* STATUS INBOX (MESSAGES 12 DELETED 4 DELETED-STORAGE 8)\r\n" 49 | 50 | rfc9208_4.2.1_example: 51 | :response: "* QUOTA \"\" (STORAGE 10 512)\r\n" 52 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 53 | name: QUOTA 54 | data: !ruby/struct:Net::IMAP::MailboxQuota 55 | mailbox: '' 56 | usage: '10' 57 | quota: '512' 58 | raw_data: "* QUOTA \"\" (STORAGE 10 512)\r\n" 59 | 60 | rfc9208_4.2.2_example_1: 61 | :response: "* QUOTAROOT INBOX \"\"\r\n" 62 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 63 | name: QUOTAROOT 64 | data: !ruby/struct:Net::IMAP::MailboxQuotaRoot 65 | mailbox: INBOX 66 | quotaroots: 67 | - '' 68 | raw_data: "* QUOTAROOT INBOX \"\"\r\n" 69 | 70 | rfc9208_4.2.2_example_2: 71 | :response: "* QUOTAROOT comp.mail.mime\r\n" 72 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 73 | name: QUOTAROOT 74 | data: !ruby/struct:Net::IMAP::MailboxQuotaRoot 75 | mailbox: comp.mail.mime 76 | quotaroots: [] 77 | raw_data: "* QUOTAROOT comp.mail.mime\r\n" 78 | 79 | rfc9208_4.3.1_example_1: 80 | :response: "A003 NO [OVERQUOTA] APPEND Failed\r\n" 81 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 82 | tag: A003 83 | name: 'NO' 84 | data: !ruby/struct:Net::IMAP::ResponseText 85 | code: !ruby/struct:Net::IMAP::ResponseCode 86 | name: OVERQUOTA 87 | data: 88 | text: APPEND Failed 89 | raw_data: "A003 NO [OVERQUOTA] APPEND Failed\r\n" 90 | 91 | rfc9208_4.3.1_example_2: 92 | :response: "* NO [OVERQUOTA] Soft quota has been exceeded\r\n" 93 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 94 | name: 'NO' 95 | data: !ruby/struct:Net::IMAP::ResponseText 96 | code: !ruby/struct:Net::IMAP::ResponseCode 97 | name: OVERQUOTA 98 | data: 99 | text: Soft quota has been exceeded 100 | raw_data: "* NO [OVERQUOTA] Soft quota has been exceeded\r\n" 101 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/rfc9394_partial.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | "RFC9394 PARTIAL 3.1. example 1": 5 | comment: | 6 | Neither RFC9394 nor RFC5267 contain any examples of a normal unelided 7 | sequence-set result. I've edited it to include a sequence-set here. 8 | :response: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" 9 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 10 | name: ESEARCH 11 | data: !ruby/object:Net::IMAP::ESearchResult 12 | tag: A01 13 | uid: true 14 | data: 15 | - - PARTIAL 16 | - !ruby/object:Net::IMAP::ESearchResult::PartialResult 17 | range: !ruby/range 18 | begin: -100 19 | end: -1 20 | excl: false 21 | results: !ruby/object:Net::IMAP::SequenceSet 22 | string: 200:250,252:300 23 | raw_data: "* ESEARCH (TAG \"A01\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" 24 | 25 | "RFC9394 PARTIAL 3.1. example 2": 26 | :response: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n" 27 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 28 | name: ESEARCH 29 | data: !ruby/object:Net::IMAP::ESearchResult 30 | tag: A02 31 | uid: true 32 | data: 33 | - - PARTIAL 34 | - !ruby/object:Net::IMAP::ESearchResult::PartialResult 35 | range: !ruby/range 36 | begin: 23500 37 | end: 24000 38 | excl: false 39 | results: !ruby/object:Net::IMAP::SequenceSet 40 | string: 55500:56000 41 | raw_data: "* ESEARCH (TAG \"A02\") UID PARTIAL (23500:24000 55500:56000)\r\n" 42 | 43 | "RFC9394 PARTIAL 3.1. example 3": 44 | :response: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n" 45 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 46 | name: ESEARCH 47 | data: !ruby/object:Net::IMAP::ESearchResult 48 | tag: A04 49 | uid: true 50 | data: 51 | - - PARTIAL 52 | - !ruby/object:Net::IMAP::ESearchResult::PartialResult 53 | range: !ruby/range 54 | begin: 24000 55 | end: 24500 56 | excl: false 57 | results: 58 | raw_data: "* ESEARCH (TAG \"A04\") UID PARTIAL (24000:24500 NIL)\r\n" 59 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | "RFC9586 UIDONLY 3. UIDREQUIRED response code": 4 | :response: "07 BAD [UIDREQUIRED] Message numbers are not allowed once UIDONLY is enabled\r\n" 5 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 6 | tag: '07' 7 | name: BAD 8 | data: !ruby/struct:Net::IMAP::ResponseText 9 | code: !ruby/struct:Net::IMAP::ResponseCode 10 | name: UIDREQUIRED 11 | data: 12 | text: Message numbers are not allowed once UIDONLY is enabled 13 | raw_data: "07 BAD [UIDREQUIRED] Message numbers are not allowed once UIDONLY 14 | is enabled\r\n" 15 | 16 | "RFC9586 UIDONLY 3.3 UIDFETCH response": 17 | :response: "* 25997 UIDFETCH (FLAGS (\\Flagged \\Answered))\r\n" 18 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 19 | name: UIDFETCH 20 | data: !ruby/struct:Net::IMAP::UIDFetchData 21 | uid: 25997 22 | attr: 23 | FLAGS: 24 | - :Flagged 25 | - :Answered 26 | raw_data: "* 25997 UIDFETCH (FLAGS (\\Flagged \\Answered))\r\n" -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby/net-imap/29aab576d2e8c5164d9fea54659e4e2e6bb2c677/test/net/imap/fixtures/response_parser/ruby.png -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/search_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | # 7.2.5. SEARCH Response 4 | # 5 | # Example: S: * SEARCH 2 3 6 6 | rfc3501_7.2.5_SEARCH_response_example: 7 | :response: "* SEARCH 2 3 6\r\n" 8 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 9 | name: SEARCH 10 | data: 11 | - 2 12 | - 3 13 | - 6 14 | raw_data: "* SEARCH 2 3 6\r\n" 15 | 16 | test_search_response_empty: 17 | :response: "* SEARCH\r\n" 18 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 19 | name: SEARCH 20 | data: !ruby/array:Net::IMAP::SearchResult 21 | internal: [] 22 | ivars: 23 | "@modseq": 24 | raw_data: "* SEARCH\r\n" 25 | 26 | test_search_response_single_seq_nums_returned: 27 | :response: "* SEARCH 1\r\n" 28 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 29 | name: SEARCH 30 | data: !ruby/array:Net::IMAP::SearchResult 31 | internal: 32 | - 1 33 | ivars: 34 | "@modseq": 35 | raw_data: "* SEARCH 1\r\n" 36 | 37 | test_search_response_multiple_seq_nums_returned: 38 | :response: "* SEARCH 1 2 3\r\n" 39 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 40 | name: SEARCH 41 | data: !ruby/array:Net::IMAP::SearchResult 42 | internal: 43 | - 1 44 | - 2 45 | - 3 46 | ivars: 47 | "@modseq": 48 | raw_data: "* SEARCH 1 2 3\r\n" 49 | 50 | test_invalid_search_response_single_result_with_trailing_space: 51 | :quirky_servers: Yahoo 52 | :response: "* SEARCH 1 \r\n" 53 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 54 | name: SEARCH 55 | data: !ruby/array:Net::IMAP::SearchResult 56 | internal: 57 | - 1 58 | ivars: 59 | "@modseq": 60 | raw_data: "* SEARCH 1 \r\n" 61 | 62 | test_invalid_search_response_multiple_result_with_trailing_space: 63 | :quirky_servers: Yahoo 64 | :response: "* SEARCH 1 2 3 \r\n" 65 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 66 | name: SEARCH 67 | data: !ruby/array:Net::IMAP::SearchResult 68 | internal: 69 | - 1 70 | - 2 71 | - 3 72 | ivars: 73 | "@modseq": 74 | raw_data: "* SEARCH 1 2 3 \r\n" 75 | 76 | test_search_response_with_condstore_modseq: 77 | :comment: | 78 | [Bug #10112] 79 | :response: "* SEARCH 87216 87221 (MODSEQ 7667567)\r\n" 80 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 81 | name: SEARCH 82 | data: !ruby/array:Net::IMAP::SearchResult 83 | internal: 84 | - 87216 85 | - 87221 86 | ivars: 87 | "@modseq": 7667567 88 | raw_data: "* SEARCH 87216 87221 (MODSEQ 7667567)\r\n" 89 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/status_responses.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | # 7.2.4 STATUS Response 4 | # 5 | # Example: S: * STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292) 6 | rfc3501_7.2.4_STATUS_response_example: 7 | :response: "* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n" 8 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 9 | name: STATUS 10 | data: !ruby/struct:Net::IMAP::StatusData 11 | mailbox: blurdybloop 12 | attr: 13 | MESSAGES: 231 14 | UIDNEXT: 44292 15 | raw_data: "* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n" 16 | 17 | test_status_response_uidnext_uidvalidity: 18 | :response: "* STATUS INBOX (UIDNEXT 1 UIDVALIDITY 1234)\r\n" 19 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 20 | name: STATUS 21 | data: !ruby/struct:Net::IMAP::StatusData 22 | mailbox: INBOX 23 | attr: 24 | UIDNEXT: 1 25 | UIDVALIDITY: 1234 26 | raw_data: "* STATUS INBOX (UIDNEXT 1 UIDVALIDITY 1234)\r\n" 27 | 28 | test_invalid_status_response_trailing_space: 29 | :comments: | 30 | [Bug #13649] 31 | :response: "* STATUS INBOX (UIDNEXT 1 UIDVALIDITY 1234) \r\n" 32 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 33 | name: STATUS 34 | data: !ruby/struct:Net::IMAP::StatusData 35 | mailbox: INBOX 36 | attr: 37 | UIDNEXT: 1 38 | UIDVALIDITY: 1234 39 | raw_data: "* STATUS INBOX (UIDNEXT 1 UIDVALIDITY 1234) \r\n" 40 | 41 | test_imaginary_status_response_using_tagged-ext-val: 42 | :response: &test_imaginary_status_response_using_tagged_ext_val 43 | "* STATUS mbox (num 1 seq 1234:5,*:789654 comp-empty () 44 | comp-quoted (\"quoted string\") comp-astring (nil) comp-multi (1 \"str\" 45 | 2:3,7:77 nil (nested (several (layers)))))\r\n" 46 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 47 | name: STATUS 48 | data: !ruby/struct:Net::IMAP::StatusData 49 | mailbox: mbox 50 | attr: 51 | NUM: 1 52 | SEQ: !ruby/struct:Net::IMAP::ExtensionData 53 | data: !ruby/object:Net::IMAP::SequenceSet 54 | string: 1234:5,*:789654 55 | COMP-EMPTY: !ruby/struct:Net::IMAP::ExtensionData 56 | data: [] 57 | COMP-QUOTED: !ruby/struct:Net::IMAP::ExtensionData 58 | data: 59 | - quoted string 60 | COMP-ASTRING: !ruby/struct:Net::IMAP::ExtensionData 61 | data: 62 | - nil 63 | COMP-MULTI: !ruby/struct:Net::IMAP::ExtensionData 64 | data: 65 | - 1 66 | - str 67 | - 2:3,7:77 68 | - nil 69 | - - nested 70 | - - several 71 | - - layers 72 | raw_data: *test_imaginary_status_response_using_tagged_ext_val 73 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/uidplus_extension.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :tests: 3 | 4 | # Identical to the example in RFC4315 5 | test_resp_code_APPENDUID_rfc9051_6.3.12_example: 6 | :response: "A003 OK [APPENDUID 38505 3955] APPEND completed\r\n" 7 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 8 | tag: A003 9 | name: OK 10 | data: !ruby/struct:Net::IMAP::ResponseText 11 | code: !ruby/struct:Net::IMAP::ResponseCode 12 | name: APPENDUID 13 | data: !ruby/struct:Net::IMAP::UIDPlusData 14 | uidvalidity: 38505 15 | source_uids: 16 | assigned_uids: 17 | - 3955 18 | text: APPEND completed 19 | raw_data: "A003 OK [APPENDUID 38505 3955] APPEND completed\r\n" 20 | 21 | # Identical to the example in RFC4315 22 | test_resp_code_COPYUID_rfc9051_6.3.12_example: 23 | :response: "A004 OK [COPYUID 38505 304,319:320 3956:3958] Done\r\n" 24 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 25 | tag: A004 26 | name: OK 27 | data: !ruby/struct:Net::IMAP::ResponseText 28 | code: !ruby/struct:Net::IMAP::ResponseCode 29 | name: COPYUID 30 | data: !ruby/struct:Net::IMAP::UIDPlusData 31 | uidvalidity: 38505 32 | source_uids: 33 | - 304 34 | - 319 35 | - 320 36 | assigned_uids: 37 | - 3956 38 | - 3957 39 | - 3958 40 | text: Done 41 | raw_data: "A004 OK [COPYUID 38505 304,319:320 3956:3958] Done\r\n" 42 | 43 | test_resp_code_APPENDUID_with_MULTIAPPEND_compatibility: 44 | :response: "A003 OK [APPENDUID 2 4,6:7,9] APPEND completed\r\n" 45 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 46 | tag: A003 47 | name: OK 48 | data: !ruby/struct:Net::IMAP::ResponseText 49 | code: !ruby/struct:Net::IMAP::ResponseCode 50 | name: APPENDUID 51 | data: !ruby/struct:Net::IMAP::UIDPlusData 52 | uidvalidity: 2 53 | source_uids: 54 | assigned_uids: 55 | - 4 56 | - 6 57 | - 7 58 | - 9 59 | text: APPEND completed 60 | raw_data: "A003 OK [APPENDUID 2 4,6:7,9] APPEND completed\r\n" 61 | 62 | test_resp_code_COPYUID_with_reversed_ranges_and_mixed_case: 63 | comment: | 64 | From RFC4315 ABNF: 65 | > and all values between these two *regardless of order*. 66 | > Example: 2:4 and 4:2 are equivalent. 67 | :response: "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n" 68 | :expected: !ruby/struct:Net::IMAP::TaggedResponse 69 | tag: A004 70 | name: OK 71 | data: !ruby/struct:Net::IMAP::ResponseText 72 | code: !ruby/struct:Net::IMAP::ResponseCode 73 | name: COPYUID 74 | data: !ruby/struct:Net::IMAP::UIDPlusData 75 | uidvalidity: 9999 76 | source_uids: 77 | - 19 78 | - 20 79 | - 495 80 | - 496 81 | - 497 82 | - 498 83 | - 499 84 | - 500 85 | assigned_uids: 86 | - 92 87 | - 93 88 | - 94 89 | - 95 90 | - 96 91 | - 97 92 | - 100 93 | - 101 94 | text: Done 95 | raw_data: "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n" 96 | -------------------------------------------------------------------------------- /test/net/imap/fixtures/response_parser/utf8_responses.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | :tests: 4 | 5 | test_utf8_in_list_mailbox: 6 | :response: "* LIST () \"/\" \"☃️&☺️\"\r\n" 7 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 8 | name: LIST 9 | data: !ruby/struct:Net::IMAP::MailboxList 10 | attr: [] 11 | delim: "/" 12 | name: "☃️&☺️" 13 | raw_data: !binary |- 14 | KiBMSVNUICgpICIvIiAi4piD77iPJuKYuu+4jyINCg== 15 | 16 | test_utf8_in_resp_text: 17 | :response: "* OK 𝖀𝖓𝖎𝖈𝖔𝖉𝖊 «α-ω» ほげ ふが ʇɐɥʍ\r\n" 18 | :expected: !ruby/struct:Net::IMAP::UntaggedResponse 19 | name: OK 20 | data: !ruby/struct:Net::IMAP::ResponseText 21 | text: "𝖀𝖓𝖎𝖈𝖔𝖉𝖊 «α-ω» ほげ ふが ʇɐɥʍ" 22 | raw_data: !binary |- 23 | KiBPSyDwnZaA8J2Wk/Cdlo7wnZaI8J2WlPCdlonwnZaKIMKrzrEtz4nCuyDjgbvjgZIg44G144 GMIMqHyZDJpcqNDQo= 24 | -------------------------------------------------------------------------------- /test/net/imap/net_imap_test_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | require "yaml" 6 | 7 | module NetIMAPTestHelpers 8 | module TestFixtureGenerators 9 | 10 | attr_reader :fixtures 11 | 12 | def load_fixture_data(*test_fixture_path) 13 | dir = self::TEST_FIXTURE_PATH 14 | YAML.unsafe_load_file File.join(dir, *test_fixture_path) 15 | end 16 | 17 | def generate_tests_from(fixture_data: nil, fixture_file: nil) 18 | fixture_data ||= load_fixture_data fixture_file 19 | tests = fixture_data.fetch(:tests) 20 | 21 | tests.each do |name, test| 22 | type = test.fetch(:test_type) { 23 | test.key?(:expected) ? :parser_assert_equal : :parser_pending 24 | } 25 | name = "test_#{name}" unless name.start_with? "test_" 26 | name = name.to_sym 27 | raise "#{name} is already defined" if instance_methods.include?(name) 28 | # warn "define_method :#{name} = #{type}..." 29 | 30 | case type 31 | 32 | when :parser_assert_equal 33 | response = test.fetch(:response).force_encoding "ASCII-8BIT" 34 | expected = test.fetch(:expected) 35 | debug = test.fetch(:debug, false) 36 | 37 | define_method name do 38 | with_debug do 39 | parser = Net::IMAP::ResponseParser.new 40 | actual = parser.parse response 41 | binding.irb if debug 42 | assert_equal expected, actual 43 | rescue Test::Unit::AssertionFailedError 44 | puts YAML.dump name => {response: response, expected: actual} 45 | raise 46 | end 47 | end 48 | 49 | when :parser_pending 50 | response = test.fetch(:response) 51 | 52 | define_method name do 53 | with_debug do 54 | parser = Net::IMAP::ResponseParser.new 55 | actual = parser.parse response 56 | puts YAML.dump "tests" => { 57 | name => {response: response, expected: actual} 58 | } 59 | pend "update tests with expected data..." 60 | end 61 | end 62 | 63 | when :assert_parse_failure 64 | response = test.fetch(:response) 65 | message = test.fetch(:message) 66 | 67 | define_method name do 68 | err = assert_raise(Net::IMAP::ResponseParseError) do 69 | Net::IMAP::ResponseParser.new.parse response 70 | end 71 | assert_match(message, err.message) 72 | end 73 | 74 | end 75 | end 76 | 77 | end 78 | end 79 | 80 | def with_debug(bool = true) 81 | Net::IMAP.debug, original = bool, Net::IMAP.debug 82 | yield 83 | ensure 84 | Net::IMAP.debug = original 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /test/net/imap/regexp_collector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RegexpCollector 4 | Data = Net::IMAP::Data 5 | 6 | ConstantRegexp = Data.define(:mod, :const_name, :regexp) do 7 | def name = "%s::%s" % [mod, const_name] 8 | end 9 | 10 | InstanceMethodRegexp = Data.define(:mod, :method_name, :regexp) do 11 | def name = "%s#%s: %p" % [mod, method_name, regexp] 12 | end 13 | 14 | SingletonMethodRegexp = Data.define(:mod, :method_name, :regexp) do 15 | def name = "%s.%s: %p" % [mod, method_name, regexp] 16 | end 17 | 18 | attr_reader :mod, :exclude, :exclude_map 19 | 20 | def initialize(mod, exclude: [], exclude_map: {}) 21 | @mod = mod 22 | @exclude = exclude 23 | @exclude_map = exclude_map 24 | end 25 | 26 | def to_a = (consts + code).flat_map { collect_regexps _1 } 27 | def to_h = to_a.group_by(&:name).transform_values(&:first) 28 | 29 | def excluded?(name_or_obj) = 30 | exclude&.include?(name_or_obj) || exclude_map[mod]&.include?(name_or_obj) 31 | 32 | def consts 33 | @consts = mod 34 | .constants(false) 35 | .reject { excluded? _1 } 36 | .map { [_1, mod.const_get(_1)] } 37 | .select { _2 in Regexp | Module } 38 | .reject { excluded? _2 } 39 | .reject { _2 in Module and "%s::%s" % [mod, _1] != _2.name } 40 | end 41 | 42 | def code 43 | return [] unless defined?(RubyVM::InstructionSequence) 44 | [ 45 | *(mod.methods(false) + mod.private_methods(false)) 46 | .map { mod.method _1 }, 47 | *(mod.instance_methods(false) + mod.private_instance_methods(false)) 48 | .map { mod.instance_method _1 }, 49 | ] 50 | .reject { excluded?(_1) || excluded?(_2) } 51 | end 52 | 53 | protected attr_writer :mod 54 | 55 | private 56 | 57 | def collect_regexps(obj) = case obj 58 | in name, Module => submod then dup.tap do _1.mod = submod end.to_a 59 | in name, Regexp => regexp then ConstantRegexp.new mod, name, regexp 60 | in Method => method then method_regexps SingletonMethodRegexp, method 61 | in UnboundMethod => method then method_regexps InstanceMethodRegexp, method 62 | end 63 | 64 | def method_regexps(klass, method) 65 | iseq_regexps(RubyVM::InstructionSequence.of(method)) 66 | .map { klass.new mod, method.name, _1 } 67 | end 68 | 69 | def iseq_regexps(obj) = case obj 70 | in RubyVM::InstructionSequence then iseq_regexps obj.to_a 71 | in Array then obj.flat_map { iseq_regexps _1 }.uniq 72 | in Regexp then excluded?(obj) ? [] : obj 73 | else [] 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/net/imap/test_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class IMAPErrorsTest < Test::Unit::TestCase 7 | 8 | test "ResponseTooLargeError" do 9 | err = Net::IMAP::ResponseTooLargeError.new 10 | assert_nil err.bytes_read 11 | assert_nil err.literal_size 12 | assert_nil err.max_response_size 13 | 14 | err = Net::IMAP::ResponseTooLargeError.new("manually set message") 15 | assert_equal "manually set message", err.message 16 | assert_nil err.bytes_read 17 | assert_nil err.literal_size 18 | assert_nil err.max_response_size 19 | 20 | err = Net::IMAP::ResponseTooLargeError.new(max_response_size: 1024) 21 | assert_equal "Response size exceeds max_response_size (1024B)", err.message 22 | assert_nil err.bytes_read 23 | assert_nil err.literal_size 24 | assert_equal 1024, err.max_response_size 25 | 26 | err = Net::IMAP::ResponseTooLargeError.new(bytes_read: 1200, 27 | max_response_size: 1200) 28 | assert_equal 1200, err.bytes_read 29 | assert_equal "Response size exceeds max_response_size (1200B)", err.message 30 | 31 | err = Net::IMAP::ResponseTooLargeError.new(bytes_read: 800, 32 | literal_size: 1000, 33 | max_response_size: 1200) 34 | assert_equal 800, err.bytes_read 35 | assert_equal 1000, err.literal_size 36 | assert_equal("Response size (800B read + 1000B literal) " \ 37 | "exceeds max_response_size (1200B)", err.message) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/net/imap/test_esearch_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class ESearchResultTest < Test::Unit::TestCase 7 | ESearchResult = Net::IMAP::ESearchResult 8 | SequenceSet = Net::IMAP::SequenceSet 9 | ExtensionData = Net::IMAP::ExtensionData 10 | 11 | test "#to_a" do 12 | esearch = ESearchResult.new(nil, true, []) 13 | assert_equal [], esearch.to_a 14 | esearch = ESearchResult.new(nil, false, []) 15 | assert_equal [], esearch.to_a 16 | esearch = ESearchResult.new(nil, false, [["ALL", SequenceSet["1,5:8"]]]) 17 | assert_equal [1, 5, 6, 7, 8], esearch.to_a 18 | esearch = ESearchResult.new(nil, false, [ 19 | ["PARTIAL", ESearchResult::PartialResult[1..5, "1,5:8"]] 20 | ]) 21 | assert_equal [1, 5, 6, 7, 8], esearch.to_a 22 | end 23 | 24 | test "#tag" do 25 | esearch = ESearchResult.new("A0001", false, [["count", 0]]) 26 | assert_equal "A0001", esearch.tag 27 | esearch = ESearchResult.new("A0002", false, [["count", 0]]) 28 | assert_equal "A0002", esearch.tag 29 | end 30 | 31 | test "#uid" do 32 | esearch = ESearchResult.new("A0003", true, [["count", 0]]) 33 | assert_equal true, esearch.uid 34 | assert_equal true, esearch.uid? 35 | esearch = ESearchResult.new("A0004", false, [["count", 0]]) 36 | assert_equal false, esearch.uid 37 | assert_equal false, esearch.uid? 38 | end 39 | 40 | test "#data.assoc('UNKNOWN') returns ExtensionData value" do 41 | result = Net::IMAP::ResponseParser.new.parse( 42 | "* ESEARCH (TAG \"A0006\") UID UNKNOWN 1\r\n" 43 | ).data 44 | result => ESearchResult[data:] 45 | assert_equal(["UNKNOWN", ExtensionData[1]], 46 | data.assoc("UNKNOWN")) 47 | result = Net::IMAP::ResponseParser.new.parse( 48 | "* ESEARCH (TAG \"A0006\") UID UNKNOWN 1:2\r\n" 49 | ).data 50 | result => ESearchResult[data:] 51 | assert_equal(["UNKNOWN", ExtensionData[SequenceSet[1..2]]], 52 | data.assoc("UNKNOWN")) 53 | result = Net::IMAP::ResponseParser.new.parse( 54 | "* ESEARCH (TAG \"A0006\") UID UNKNOWN (-1:-100 200:250,252:300)\r\n" 55 | ).data 56 | result => ESearchResult[data:] 57 | assert_equal( 58 | [ 59 | "UNKNOWN", 60 | ExtensionData.new(["-1:-100", "200:250,252:300"]), 61 | ], 62 | data.assoc("UNKNOWN") 63 | ) 64 | end 65 | 66 | # "simple" result da¨a return exactly what is in the data.assoc 67 | test "simple RFC4731 and RFC9051 return data accessors" do 68 | seqset = SequenceSet["5:9,101:105,151:152"] 69 | esearch = ESearchResult.new( 70 | "A0005", 71 | true, 72 | [ 73 | ["MIN", 5], 74 | ["MAX", 152], 75 | ["COUNT", 12], 76 | ["ALL", seqset], 77 | ["MODSEQ", 12345], 78 | ] 79 | ) 80 | assert_equal 5, esearch.min 81 | assert_equal 152, esearch.max 82 | assert_equal 12, esearch.count 83 | assert_equal seqset, esearch.all 84 | assert_equal 12345, esearch.modseq 85 | end 86 | 87 | test "#partial returns PARTIAL value (RFC9394: PARTIAL)" do 88 | result = Net::IMAP::ResponseParser.new.parse( 89 | "* ESEARCH (TAG \"A0006\") UID PARTIAL (-1:-100 200:250,252:300)\r\n" 90 | ).data 91 | assert_equal(ESearchResult, result.class) 92 | assert_equal( 93 | ESearchResult::PartialResult.new( 94 | -100..-1, SequenceSet[200..250, 252..300] 95 | ), 96 | result.partial 97 | ) 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /test/net/imap/test_imap_data_encoding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class IMAPDataEncodingTest < Test::Unit::TestCase 7 | 8 | def test_encode_utf7 9 | assert_equal("foo", Net::IMAP.encode_utf7("foo")) 10 | assert_equal("&-", Net::IMAP.encode_utf7("&")) 11 | 12 | utf8 = "\357\274\241\357\274\242\357\274\243".dup.force_encoding("UTF-8") 13 | s = Net::IMAP.encode_utf7(utf8) 14 | assert_equal("&,yH,Iv8j-", s) 15 | s = Net::IMAP.encode_utf7("foo&#{utf8}-bar".encode("EUC-JP")) 16 | assert_equal("foo&-&,yH,Iv8j--bar", s) 17 | 18 | utf8 = "\343\201\202&".dup.force_encoding("UTF-8") 19 | s = Net::IMAP.encode_utf7(utf8) 20 | assert_equal("&MEI-&-", s) 21 | s = Net::IMAP.encode_utf7(utf8.encode("EUC-JP")) 22 | assert_equal("&MEI-&-", s) 23 | end 24 | 25 | def test_decode_utf7 26 | assert_equal("&", Net::IMAP.decode_utf7("&-")) 27 | assert_equal("&-", Net::IMAP.decode_utf7("&--")) 28 | 29 | s = Net::IMAP.decode_utf7("&,yH,Iv8j-") 30 | utf8 = "\357\274\241\357\274\242\357\274\243".dup.force_encoding("UTF-8") 31 | assert_equal(utf8, s) 32 | 33 | assert_linear_performance([1, 10, 100], pre: ->(n) {'&'*(n*1_000)}) do |s| 34 | Net::IMAP.decode_utf7(s) 35 | end 36 | end 37 | 38 | def test_encode_date 39 | assert_equal("24-Jul-2009", Net::IMAP.encode_date(Time.mktime(2009, 7, 24))) 40 | assert_equal("24-Jul-2009", Net::IMAP.format_date(Time.mktime(2009, 7, 24))) 41 | assert_equal("06-Oct-2022", Net::IMAP.encode_date(Date.new(2022, 10, 6))) 42 | end 43 | 44 | def test_decode_date 45 | assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date("06-Oct-2022") 46 | assert_equal Date.new(2022, 10, 6), Net::IMAP.decode_date('"06-Oct-2022"') 47 | assert_equal Date.new(2022, 10, 6), Net::IMAP.parse_date("06-Oct-2022") 48 | end 49 | 50 | def test_encode_datetime 51 | time = Time.new(2009, 7, 24, 1, 3, 5, "+05:00") 52 | assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_datetime(time)) 53 | # assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_datetime(time)) 54 | assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.format_time(time)) 55 | assert_equal('"24-Jul-2009 01:03:05 +0500"', Net::IMAP.encode_time(time)) 56 | end 57 | 58 | def test_decode_datetime 59 | expected = DateTime.new(2022, 10, 6, 1, 2, 3, "-04:00") 60 | actual = Net::IMAP.decode_datetime('"06-Oct-2022 01:02:03 -0400"') 61 | assert_equal expected, actual 62 | actual = Net::IMAP.decode_datetime("06-Oct-2022 01:02:03 -0400") 63 | assert_equal expected, actual 64 | actual = Net::IMAP.parse_datetime '" 6-Oct-2022 01:02:03 -0400"' 65 | assert_equal expected, actual 66 | end 67 | 68 | def test_decode_time 69 | expected = DateTime.new(2020, 11, 7, 1, 2, 3, "-04:00").to_time 70 | actual = Net::IMAP.parse_time '"07-Nov-2020 01:02:03 -0400"' 71 | assert_equal expected, actual 72 | actual = Net::IMAP.decode_time '" 7-Nov-2020 01:02:03 -0400"' 73 | assert_equal expected, actual 74 | actual = Net::IMAP.parse_time "07-Nov-2020 01:02:03 -0400" 75 | assert_equal expected, actual 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /test/net/imap/test_imap_login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | require_relative "fake_server" 6 | 7 | class IMAPLoginTest < Test::Unit::TestCase 8 | include Net::IMAP::FakeServer::TestHelper 9 | 10 | def setup 11 | Net::IMAP.config.reset 12 | @do_not_reverse_lookup = Socket.do_not_reverse_lookup 13 | Socket.do_not_reverse_lookup = true 14 | @threads = [] 15 | end 16 | 17 | def teardown 18 | if !@threads.empty? 19 | assert_join_threads(@threads) 20 | end 21 | ensure 22 | Socket.do_not_reverse_lookup = @do_not_reverse_lookup 23 | end 24 | 25 | test "#login doesn't send CAPABILITY when it is already cached" do 26 | with_fake_server( 27 | preauth: false, cleartext_login: true, greeting_capabilities: true 28 | ) do |server, imap| 29 | imap.login("test_user", "test-password") 30 | cmd = server.commands.pop 31 | assert_equal "LOGIN", cmd.name 32 | assert_empty server.commands 33 | end 34 | end 35 | 36 | test "#login raises LoginDisabledError when LOGINDISABLED" do 37 | with_fake_server(preauth: false, cleartext_login: false) do |server, imap| 38 | assert imap.capabilities_cached? 39 | assert_raise(Net::IMAP::LoginDisabledError) do 40 | imap.login("test_user", "test-password") 41 | end 42 | assert_empty server.commands 43 | end 44 | end 45 | 46 | test "#login first checks capabilities for LOGINDISABLED (success)" do 47 | with_fake_server( 48 | preauth: false, cleartext_login: true, greeting_capabilities: false 49 | ) do |server, imap| 50 | imap.login("test_user", "test-password") 51 | cmd = server.commands.pop 52 | assert_equal "CAPABILITY", cmd.name 53 | cmd = server.commands.pop 54 | assert_equal "LOGIN", cmd.name 55 | assert_empty server.commands 56 | end 57 | end 58 | 59 | test "#login first checks capabilities for LOGINDISABLED (failure)" do 60 | with_fake_server( 61 | preauth: false, cleartext_login: false, greeting_capabilities: false 62 | ) do |server, imap| 63 | assert_raise(Net::IMAP::LoginDisabledError) do 64 | imap.login("test_user", "test-password") 65 | end 66 | cmd = server.commands.pop 67 | assert_equal "CAPABILITY", cmd.name 68 | assert_empty server.commands 69 | end 70 | end 71 | 72 | test("#login sends LOGIN without asking CAPABILITY " \ 73 | "when config.enforce_logindisabled is false") do 74 | with_fake_server( 75 | preauth: false, cleartext_login: false, greeting_capabilities: false 76 | ) do |server, imap| 77 | imap.config.enforce_logindisabled = false 78 | imap.login("test_user", "test-password") 79 | cmd = server.commands.pop 80 | assert_equal "LOGIN", cmd.name 81 | end 82 | end 83 | 84 | test("#login raises LoginDisabledError without sending CAPABILITY " \ 85 | "when config.enforce_logindisabled is :when_capabilities_cached") do 86 | with_fake_server( 87 | preauth: false, cleartext_login: false, greeting_capabilities: true 88 | ) do |server, imap| 89 | imap.config.enforce_logindisabled = :when_capabilities_cached 90 | assert_raise(Net::IMAP::LoginDisabledError) do 91 | imap.login("test_user", "test-password") 92 | end 93 | assert_empty server.commands 94 | end 95 | end 96 | 97 | test("#login sends LOGIN without asking CAPABILITY " \ 98 | "when config.enforce_logindisabled is :when_capabilities_cached") do 99 | with_fake_server( 100 | preauth: false, cleartext_login: false, greeting_capabilities: false 101 | ) do |server, imap| 102 | imap.config.enforce_logindisabled = :when_capabilities_cached 103 | imap.login("test_user", "test-password") 104 | cmd = server.commands.pop 105 | assert_equal "LOGIN", cmd.name 106 | assert_empty server.commands 107 | end 108 | with_fake_server( 109 | preauth: false, cleartext_login: true, greeting_capabilities: true 110 | ) do |server, imap| 111 | imap.config.enforce_logindisabled = :when_capabilities_cached 112 | imap.login("test_user", "test-password") 113 | cmd = server.commands.pop 114 | assert_equal "LOGIN", cmd.name 115 | assert_empty server.commands 116 | end 117 | end 118 | 119 | end 120 | -------------------------------------------------------------------------------- /test/net/imap/test_imap_max_response_size.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | require_relative "fake_server" 6 | 7 | class IMAPMaxResponseSizeTest < Test::Unit::TestCase 8 | include Net::IMAP::FakeServer::TestHelper 9 | 10 | def setup 11 | Net::IMAP.config.reset 12 | @do_not_reverse_lookup = Socket.do_not_reverse_lookup 13 | Socket.do_not_reverse_lookup = true 14 | @threads = [] 15 | end 16 | 17 | def teardown 18 | if !@threads.empty? 19 | assert_join_threads(@threads) 20 | end 21 | ensure 22 | Socket.do_not_reverse_lookup = @do_not_reverse_lookup 23 | end 24 | 25 | test "#max_response_size reading literals" do 26 | with_fake_server(preauth: true) do |server, imap| 27 | imap.max_response_size = 12_345 + 30 28 | server.on("NOOP") do |resp| 29 | resp.untagged("1 FETCH (BODY[] {12345}\r\n" + "a" * 12_345 + ")") 30 | resp.done_ok 31 | end 32 | imap.noop 33 | assert_equal "a" * 12_345, imap.responses("FETCH").first.message 34 | end 35 | end 36 | 37 | test "#max_response_size closes connection for too long line" do 38 | Net::IMAP.config.max_response_size = 10 39 | run_fake_server_in_thread(preauth: false, ignore_io_error: true) do |server| 40 | assert_raise_with_message( 41 | Net::IMAP::ResponseTooLargeError, /exceeds max_response_size .*\b10B\b/ 42 | ) do 43 | with_client("localhost", port: server.port) do 44 | fail "should not get here (greeting longer than max_response_size)" 45 | end 46 | end 47 | end 48 | end 49 | 50 | test "#max_response_size closes connection for too long literal" do 51 | Net::IMAP.config.max_response_size = 1<<20 52 | with_fake_server(preauth: false, ignore_io_error: true) do |server, client| 53 | client.max_response_size = 50 54 | server.on("NOOP") do |resp| 55 | resp.untagged("1 FETCH (BODY[] {1000}\r\n" + "a" * 1000 + ")") 56 | end 57 | assert_raise_with_message( 58 | Net::IMAP::ResponseTooLargeError, 59 | /\d+B read \+ 1000B literal.* exceeds max_response_size .*\b50B\b/ 60 | ) do 61 | client.noop 62 | fail "should not get here (FETCH literal longer than max_response_size)" 63 | end 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/net/imap/test_imap_response_handlers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | require_relative "fake_server" 6 | 7 | class IMAPResponseHandlersTest < Test::Unit::TestCase 8 | include Net::IMAP::FakeServer::TestHelper 9 | 10 | def setup 11 | Net::IMAP.config.reset 12 | @do_not_reverse_lookup = Socket.do_not_reverse_lookup 13 | Socket.do_not_reverse_lookup = true 14 | @threads = [] 15 | end 16 | 17 | def teardown 18 | if !@threads.empty? 19 | assert_join_threads(@threads) 20 | end 21 | ensure 22 | Socket.do_not_reverse_lookup = @do_not_reverse_lookup 23 | end 24 | 25 | test "#add_response_handlers" do 26 | responses = [] 27 | with_fake_server do |server, imap| 28 | server.on("NOOP") do |resp| 29 | 3.times do resp.untagged("#{_1 + 1} EXPUNGE") end 30 | resp.done_ok 31 | end 32 | 33 | assert_equal 0, imap.response_handlers.length 34 | imap.add_response_handler do responses << [:block, _1] end 35 | assert_equal 1, imap.response_handlers.length 36 | imap.add_response_handler(->{ responses << [:proc, _1] }) 37 | assert_equal 2, imap.response_handlers.length 38 | 39 | imap.noop 40 | assert_pattern do 41 | responses => [ 42 | [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], 43 | [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 1]], 44 | [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], 45 | [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 2]], 46 | [:block, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], 47 | [:proc, Net::IMAP::UntaggedResponse[name: "EXPUNGE", data: 3]], 48 | ] 49 | end 50 | end 51 | end 52 | 53 | test "::new with response_handlers kwarg" do 54 | greeting = nil 55 | expunges = [] 56 | alerts = [] 57 | untagged = 0 58 | handler0 = ->{ greeting ||= _1 } 59 | handler1 = ->{ alerts << _1.data.text if _1 in {data: {code: {name: "ALERT"}}} } 60 | handler2 = ->{ expunges << _1.data if _1 in {name: "EXPUNGE"} } 61 | handler3 = ->{ untagged += 1 if _1.is_a?(Net::IMAP::UntaggedResponse) } 62 | response_handlers = [handler0, handler1, handler2, handler3] 63 | 64 | run_fake_server_in_thread do |server| 65 | port = server.port 66 | imap = Net::IMAP.new("localhost", port:, response_handlers:) 67 | assert_equal response_handlers, imap.response_handlers 68 | refute_same response_handlers, imap.response_handlers 69 | 70 | # handler0 recieved the greeting and handler3 counted it 71 | assert_equal imap.greeting, greeting 72 | assert_equal 1, untagged 73 | 74 | server.on("NOOP") do |resp| 75 | resp.untagged "1 EXPUNGE" 76 | resp.untagged "1 EXPUNGE" 77 | resp.untagged "OK [ALERT] The first alert." 78 | resp.done_ok "[ALERT] Did you see the alert?" 79 | end 80 | 81 | imap.noop 82 | assert_equal 4, untagged 83 | assert_equal [1, 1], expunges # from handler2 84 | assert_equal ["The first alert.", "Did you see the alert?"], alerts 85 | ensure 86 | imap&.logout! unless imap&.disconnected? 87 | end 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /test/net/imap/test_regexps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | begin 7 | require_relative "regexp_collector" 8 | rescue LoadError 9 | warn "Can't collect regexps to test Regexp.linear_time?(...)" 10 | end 11 | 12 | unless defined?(RegexpCollector) 13 | class RegexpCollector # :nodoc: 14 | def initialize(...) end 15 | def to_a; [] end 16 | def to_h; {} end 17 | end 18 | end 19 | 20 | class IMAPRegexpsTest < Test::Unit::TestCase 21 | 22 | data( 23 | RegexpCollector.new( 24 | Net::IMAP, 25 | exclude_map: { 26 | Net::IMAP => %i[ 27 | PlainAuthenticator 28 | XOauth2Authenticator 29 | ], # deprecated 30 | }, 31 | ).to_h 32 | ) 33 | 34 | def test_linear_time(data) 35 | regexp = data.regexp 36 | assert Regexp.linear_time?(regexp), "%p might backtrack" % [regexp] 37 | rescue NoMethodError 38 | pend "Regexp.linear_time? not implemented by #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION}" 39 | rescue Test::Unit::AssertionFailedError 40 | raise if RUBY_ENGINE == "ruby" 41 | pend "%p might backtrack in %s %s" % [regexp, RUBY_ENGINE, RUBY_ENGINE_VERSION] 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/net/imap/test_response_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "stringio" 5 | require "test/unit" 6 | 7 | class ResponseReaderTest < Test::Unit::TestCase 8 | def setup 9 | Net::IMAP.config.reset 10 | end 11 | 12 | class FakeClient 13 | def config = @config ||= Net::IMAP.config.new 14 | def max_response_size = config.max_response_size 15 | end 16 | 17 | def literal(str) = "{#{str.bytesize}}\r\n#{str}" 18 | 19 | test "#read_response_buffer" do 20 | client = FakeClient.new 21 | aaaaaaaaa = "a" * (20 << 10) 22 | many_crs = "\r" * 1000 23 | many_crlfs = "\r\n" * 500 24 | simple = "* OK greeting\r\n" 25 | long_line = "tag ok #{aaaaaaaaa} #{aaaaaaaaa}\r\n" 26 | literal_aaaa = "* fake #{literal aaaaaaaaa}\r\n" 27 | literal_crlf = "tag ok #{literal many_crlfs} #{literal many_crlfs}\r\n" 28 | zero_literal = "tag ok #{literal ""} #{literal ""}\r\n" 29 | illegal_crs = "tag ok #{many_crs} #{many_crs}\r\n" 30 | illegal_lfs = "tag ok #{literal "\r"}\n#{literal "\r"}\n\r\n" 31 | io = StringIO.new([ 32 | simple, 33 | long_line, 34 | literal_aaaa, 35 | literal_crlf, 36 | zero_literal, 37 | illegal_crs, 38 | illegal_lfs, 39 | simple, 40 | ].join) 41 | rcvr = Net::IMAP::ResponseReader.new(client, io) 42 | assert_equal simple, rcvr.read_response_buffer.to_str 43 | assert_equal long_line, rcvr.read_response_buffer.to_str 44 | assert_equal literal_aaaa, rcvr.read_response_buffer.to_str 45 | assert_equal literal_crlf, rcvr.read_response_buffer.to_str 46 | assert_equal zero_literal, rcvr.read_response_buffer.to_str 47 | assert_equal illegal_crs, rcvr.read_response_buffer.to_str 48 | assert_equal illegal_lfs, rcvr.read_response_buffer.to_str 49 | assert_equal simple, rcvr.read_response_buffer.to_str 50 | assert_equal "", rcvr.read_response_buffer.to_str 51 | end 52 | 53 | test "#read_response_buffer with max_response_size" do 54 | client = FakeClient.new 55 | client.config.max_response_size = 10 56 | under = "+ 3456\r\n" 57 | exact = "+ 345678\r\n" 58 | very_over = "+ 3456789 #{?a * (16<<10)}}\r\n" 59 | slightly_over = "+ 34567890\r\n" # CRLF after the limit 60 | io = StringIO.new([under, exact, very_over, slightly_over].join) 61 | rcvr = Net::IMAP::ResponseReader.new(client, io) 62 | assert_equal under, rcvr.read_response_buffer.to_str 63 | assert_equal exact, rcvr.read_response_buffer.to_str 64 | assert_raise Net::IMAP::ResponseTooLargeError do 65 | result = rcvr.read_response_buffer 66 | flunk "Got result: %p" % [result] 67 | end 68 | io = StringIO.new(slightly_over) 69 | rcvr = Net::IMAP::ResponseReader.new(client, io) 70 | assert_raise Net::IMAP::ResponseTooLargeError do 71 | result = rcvr.read_response_buffer 72 | flunk "Got result: %p" % [result] 73 | end 74 | end 75 | 76 | test "#read_response_buffer max_response_size straddling CRLF" do 77 | barely_over = "+ 3456789\r\n" # CRLF straddles the boundary 78 | client = FakeClient.new 79 | client.config.max_response_size = 10 80 | io = StringIO.new(barely_over) 81 | rcvr = Net::IMAP::ResponseReader.new(client, io) 82 | assert_raise Net::IMAP::ResponseTooLargeError do 83 | result = rcvr.read_response_buffer 84 | flunk "Got result: %p" % [result] 85 | end 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/net/imap/test_saslprep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class SASLprepTest < Test::Unit::TestCase 7 | include Net::IMAP::StringPrep 8 | 9 | # Test cases from RFC-4013 §3: 10 | # 11 | # # Input Output Comments 12 | # - ----- ------ -------- 13 | # 1 IX IX SOFT HYPHEN mapped to nothing 14 | # 2 user user no transformation 15 | # 3 USER USER case preserved, will not match #2 16 | # 4 a output is NFKC, input in ISO 8859-1 17 | # 5 IX output is NFKC, will match #1 18 | def test_saslprep_valid_inputs 19 | { 20 | "I\u00ADX" => "IX", # SOFT HYPHEN mapped to nothing 21 | "user" => "user", # no transformation 22 | "USER" => "USER", # case preserved, will not match #2 23 | "\u00aa" => "a", # output is NFKC, input in ISO 8859-1 24 | "\u2168" => "IX", # output is NFKC, will match #1 25 | # some more tests: 26 | "foo\u00a0bar" => "foo bar", # map to space 27 | "foo\u2000bar" => "foo bar", # map to space 28 | "foo\u3000bar" => "foo bar", # map to space 29 | "\u0627" => "\u0627", # single RandALCat char is okay 30 | "\u{1f468}\u200d\u{1f469}\u200d\u{1f467}" => 31 | "\u{1f468}\u{1f469}\u{1f467}" # map ZWJ to nothing 32 | }.each do |input, output| 33 | assert_equal output, Net::IMAP.saslprep(input) 34 | end 35 | end 36 | 37 | # Test cases from RFC-4013 §3: 38 | # 39 | # # Input Output Comments 40 | # - ----- ------ -------- 41 | # 6 Error - prohibited character 42 | # 7 Error - bidirectional check 43 | def test_saslprep_invalid_inputs 44 | { 45 | # from the RFC examples table 46 | "\u0007" => [ProhibitedCodepoint, /ASCII control character/], 47 | "\u0627\u0031" => [BidiStringError, /must start.*end with RandAL/], 48 | # some more prohibited codepoints 49 | "\x7f" => [ProhibitedCodepoint, /ASCII control character/i], 50 | "\ufff9" => [ProhibitedCodepoint, /Non-ASCII control character/i], 51 | "\ue000" => [ProhibitedCodepoint, /private use.*C.3/i], 52 | "\u{f0000}" => [ProhibitedCodepoint, /private use.*C.3/i], 53 | "\u{100000}" => [ProhibitedCodepoint, /private use.*C.3/i], 54 | "\ufffe" => [ProhibitedCodepoint, /Non-character code point.*C.4/i], 55 | "\xed\xa0\x80" => [StringPrepError, /invalid byte seq\w+ in UTF-8/i], 56 | "\ufffd" => [ProhibitedCodepoint, /inapprop.* plain text.*C.6/i], 57 | "\u2FFb" => [ProhibitedCodepoint, /inapprop.* canonical rep.*C.7/i], 58 | "\u202c" => [ProhibitedCodepoint, /change display.*deprecate.*C.8/i], 59 | "\u{e0001}" => [ProhibitedCodepoint, /tagging character/i], 60 | # some more invalid bidirectional characters 61 | "\u0627abc\u0627" => [BidiStringError, /must not contain.* Lcat/i], 62 | "\u0627123" => [BidiStringError, /must start.*end with RandAL/i], 63 | }.each do |input, (err, msg)| 64 | assert_nil Net::IMAP.saslprep input 65 | assert_raise_with_message err, msg do 66 | Net::IMAP.saslprep(input, exception: true) 67 | end 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/net/imap/test_search_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class SearchDataTests < Test::Unit::TestCase 7 | SearchResult = Net::IMAP::SearchResult 8 | 9 | test "#modseq" do 10 | assert_nil SearchResult[12, 34].modseq 11 | assert_equal 123_456_789, SearchResult[12, 34, modseq: 123_456_789].modseq 12 | end 13 | 14 | test "#== ignores the order of elements" do 15 | unsorted = SearchResult[4, 2, 2048, 99] 16 | sorted = SearchResult[2, 4, 99, 2048] 17 | array = [2, 4, 99, 2048] 18 | assert_equal sorted, array 19 | assert_equal unsorted, array 20 | end 21 | 22 | test "#== checks modseq" do 23 | unsorted = SearchResult[4, 2, 2048, 99, modseq: 99_999] 24 | sorted = SearchResult[2, 4, 99, 2048, modseq: 99_999] 25 | assert_equal unsorted, sorted 26 | assert_equal sorted, unsorted 27 | end 28 | 29 | test "SearchResult[*nz_numbers] == Array[*nz_numbers]" do 30 | array = [1, 5, 20, 3, 98] 31 | result = SearchResult[*array] 32 | assert_equal array, result 33 | assert_equal result, array 34 | end 35 | 36 | test "SearchResult.new(nz_numbers) == Array.new(nz_numbers)" do 37 | nz_numbers = [11, 35, 39, 1083, 958] 38 | result = SearchResult.new(nz_numbers) 39 | array = Array.new(nz_numbers) 40 | assert_equal array, result 41 | assert_equal result, array 42 | end 43 | 44 | test "SearchResult[*nz_numbers, modseq: nz_number] != Array[*nz_numbers]" do 45 | array = [1, 5, 20, 3, 98] 46 | result = SearchResult[*array, modseq: 123456] 47 | refute_equal result, array 48 | end 49 | 50 | test "Array[*nz_numbers] == SearchResult[*nz_numbers, modseq: nz_number]" do 51 | array = [1, 5, 20, 3, 98] 52 | result = SearchResult[*array, modseq: 123456] 53 | assert_equal array, result 54 | end 55 | 56 | test "SearchResult[*nz_numbers] == Array[*differently_sorted]" do 57 | array = [1, 5, 20, 3, 98] 58 | result = SearchResult[*array.reverse] 59 | assert_equal result, array 60 | end 61 | 62 | test "Array[*nz_numbers] != SearchResult[*differently_sorted]" do 63 | array = [1, 5, 20, 3, 98] 64 | result = SearchResult[*array.reverse] 65 | refute_equal array, result 66 | end 67 | 68 | test "#inspect" do 69 | assert_equal "[1, 2, 3]", Net::IMAP::SearchResult[1, 2, 3].inspect 70 | assert_equal("Net::IMAP::SearchResult[1, 3, modseq: 9]", 71 | Net::IMAP::SearchResult[1, 3, modseq: 9].inspect) 72 | end 73 | 74 | test "#to_s" do 75 | assert_equal "* SEARCH 1 2 3", Net::IMAP::SearchResult[1, 2, 3].to_s 76 | assert_equal("* SEARCH 3 2 1 (MODSEQ 9)", 77 | Net::IMAP::SearchResult[3, 2, 1, modseq: 9].to_s) 78 | end 79 | 80 | test "#to_s(type)" do 81 | assert_equal "* SEARCH 1 3", Net::IMAP::SearchResult[1, 3].to_s("SEARCH") 82 | assert_equal "* SORT 1 2 3", Net::IMAP::SearchResult[1, 2, 3].to_s("SORT") 83 | assert_equal("* SORT 99 111 44 (MODSEQ 999)", 84 | Net::IMAP::SearchResult[99, 111, 44, modseq: 999].to_s("SORT")) 85 | assert_equal("99 111 44 (MODSEQ 999)", 86 | Net::IMAP::SearchResult[99, 111, 44, modseq: 999].to_s(nil)) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/net/imap/test_stringprep_profiles.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class StringPrepProfilesTest < Test::Unit::TestCase 7 | include Net::IMAP::StringPrep 8 | include Net::IMAP::StringPrep::Trace 9 | 10 | def test_trace_profile_prohibit_ctrl_chars 11 | assert_raise(ProhibitedCodepoint) { 12 | stringprep_trace("no\ncontrol\rchars") 13 | } 14 | end 15 | 16 | def test_trace_profile_prohibit_tagging_chars 17 | assert_raise(ProhibitedCodepoint) { 18 | stringprep_trace("regional flags use tagging chars: e.g." \ 19 | "🏴󠁧󠁢󠁥󠁮󠁧󠁿 England, " \ 20 | "🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland, " \ 21 | "🏴󠁧󠁢󠁷󠁬󠁳󠁿 Wales.") 22 | } 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /test/net/imap/test_stringprep_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | require "json" 6 | 7 | require_relative "../../../rakelib/string_prep_tables_generator" 8 | 9 | class StringPrepTablesTest < Test::Unit::TestCase 10 | include Net::IMAP::StringPrep 11 | 12 | # Surrogates are excluded. They are handled by enforcing valid UTF8 encoding. 13 | VALID_CODEPOINTS = (0..0x10_ffff).map{|cp| cp.chr("UTF-8") rescue nil}.compact 14 | 15 | rfc3454_generator = StringPrepTablesGenerator.new 16 | 17 | # testing with set inclusion, just in case the regexp generation is buggy 18 | RFC3454_TABLE_SETS = rfc3454_generator.sets 19 | 20 | # The library regexps are a mixture of generated vs handcrafted, in order to 21 | # reduce load-time and memory footprint of the largest tables. 22 | # 23 | # These are the simple generated regexps, which directly translate each table 24 | # into a character class with every codepoint. These can be used to verify 25 | # the hand-crafted regexps are correct, for every supported version of ruby. 26 | RFC3454_TABLE_REGEXPS = rfc3454_generator.regexps 27 | 28 | # C.5 (surrogates) aren't really tested here. 29 | # D.2 includes surrogates... which also aren't tested here. 30 | # 31 | # This is ok: valid UTF-8 encoding is enforced and cannot contain surrogates. 32 | 33 | def test_rfc3454_table_A_1; assert_rfc3454_table_compliance "A.1" end 34 | def test_rfc3454_table_B_1; assert_rfc3454_table_compliance "B.1" end 35 | def test_rfc3454_table_B_2; assert_rfc3454_table_compliance "B.2" end 36 | def test_rfc3454_table_C_1_1; assert_rfc3454_table_compliance "C.1.1" end 37 | def test_rfc3454_table_C_1_2; assert_rfc3454_table_compliance "C.1.2" end 38 | def test_rfc3454_table_C_2_1; assert_rfc3454_table_compliance "C.2.1" end 39 | def test_rfc3454_table_C_2_2; assert_rfc3454_table_compliance "C.2.2" end 40 | def test_rfc3454_table_C_3; assert_rfc3454_table_compliance "C.3" end 41 | def test_rfc3454_table_C_4; assert_rfc3454_table_compliance "C.4" end 42 | def test_rfc3454_table_C_5; assert_rfc3454_table_compliance "C.5" end 43 | def test_rfc3454_table_C_6; assert_rfc3454_table_compliance "C.6" end 44 | def test_rfc3454_table_C_7; assert_rfc3454_table_compliance "C.7" end 45 | def test_rfc3454_table_C_8; assert_rfc3454_table_compliance "C.8" end 46 | def test_rfc3454_table_C_9; assert_rfc3454_table_compliance "C.9" end 47 | def test_rfc3454_table_D_1; assert_rfc3454_table_compliance "D.1" end 48 | def test_rfc3454_table_D_2; assert_rfc3454_table_compliance "D.2" end 49 | 50 | def assert_rfc3454_table_compliance(name) 51 | set = RFC3454_TABLE_SETS .fetch(name) 52 | regexp = RFC3454_TABLE_REGEXPS.fetch(name) 53 | coded = Tables::REGEXPS .fetch(name) 54 | matched_set = VALID_CODEPOINTS 55 | .map{|cp| cp.unpack1 "U"} 56 | .grep(set) 57 | .map{|cp| [cp].pack "U"} 58 | matched_reg = VALID_CODEPOINTS.grep(regexp) 59 | matched_lib = VALID_CODEPOINTS.grep(coded) 60 | assert_not_empty matched_lib unless name == "C.5" 61 | # assert_equal freezes up on errors; too much data to pretty print. 62 | # and printing out weird unicode control characters (etc) isn't very useful. 63 | if matched_lib != matched_reg 64 | missing = (matched_reg - matched_lib).map{|s|"%04x" % s.codepoints.first} 65 | extra = (matched_lib - matched_reg).map{|s|"%04x" % s.codepoints.first} 66 | assert_empty missing.first(100), "missing some codepoints" 67 | assert_empty extra.first(100), "some extra codepoints" 68 | else 69 | assert_equal matched_lib, matched_reg 70 | assert_equal matched_lib, matched_set 71 | end 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /test/net/imap/test_thread_member.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class ThreadMemberTest < Test::Unit::TestCase 7 | 8 | test "#to_sequence_set" do 9 | # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96)) 10 | thmember = Net::IMAP::ThreadMember.method :new 11 | thread = thmember.(3, [ 12 | thmember.(6, [ 13 | thmember.(4, [ 14 | thmember.(23, []) 15 | ]), 16 | thmember.(44, [ 17 | thmember.(7, [ 18 | thmember.(96, []) 19 | ]) 20 | ]) 21 | ]) 22 | ]) 23 | expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96") 24 | assert_equal(expected, thread.to_sequence_set) 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /test/net/imap/test_vanished_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/imap" 4 | require "test/unit" 5 | 6 | class VanishedDataTest < Test::Unit::TestCase 7 | VanishedData = Net::IMAP::VanishedData 8 | SequenceSet = Net::IMAP::SequenceSet 9 | DataFormatError = Net::IMAP::DataFormatError 10 | 11 | test ".new(uids: string, earlier: bool)" do 12 | vanished = VanishedData.new(uids: "1,3:5,7", earlier: true) 13 | assert_equal SequenceSet["1,3:5,7"], vanished.uids 14 | assert vanished.earlier? 15 | vanished = VanishedData.new(uids: "99,111", earlier: false) 16 | assert_equal SequenceSet["99,111"], vanished.uids 17 | refute vanished.earlier? 18 | end 19 | 20 | test ".new, missing args raises ArgumentError" do 21 | assert_raise ArgumentError do VanishedData.new end 22 | assert_raise ArgumentError do VanishedData.new "1234" end 23 | assert_raise ArgumentError do VanishedData.new uids: "1234" end 24 | assert_raise ArgumentError do VanishedData.new earlier: true end 25 | end 26 | 27 | test ".new, nil uids raises DataFormatError" do 28 | assert_raise DataFormatError do VanishedData.new uids: nil, earlier: true end 29 | assert_raise DataFormatError do VanishedData.new nil, true end 30 | end 31 | 32 | test ".[uids: string, earlier: bool]" do 33 | vanished = VanishedData[uids: "1,3:5,7", earlier: true] 34 | assert_equal SequenceSet["1,3:5,7"], vanished.uids 35 | assert vanished.earlier? 36 | vanished = VanishedData[uids: "99,111", earlier: false] 37 | assert_equal SequenceSet["99,111"], vanished.uids 38 | refute vanished.earlier? 39 | end 40 | 41 | test ".[uids, earlier]" do 42 | vanished = VanishedData["1,3:5,7", true] 43 | assert_equal SequenceSet["1,3:5,7"], vanished.uids 44 | assert vanished.earlier? 45 | vanished = VanishedData["99,111", false] 46 | assert_equal SequenceSet["99,111"], vanished.uids 47 | refute vanished.earlier? 48 | end 49 | 50 | test ".[], mixing args raises ArgumentError" do 51 | assert_raise ArgumentError do 52 | VanishedData[1, true, uids: "1", earlier: true] 53 | end 54 | assert_raise ArgumentError do VanishedData["1234", earlier: true] end 55 | assert_raise ArgumentError do VanishedData[nil, true, uids: "1"] end 56 | end 57 | 58 | test ".[], missing args raises ArgumentError" do 59 | assert_raise ArgumentError do VanishedData[] end 60 | assert_raise ArgumentError do VanishedData["1234"] end 61 | end 62 | 63 | test ".[], nil uids raises DataFormatError" do 64 | assert_raise DataFormatError do VanishedData[nil, true] end 65 | assert_raise DataFormatError do VanishedData[nil, nil] end 66 | end 67 | 68 | test "#to_a delegates to uids (SequenceSet#to_a)" do 69 | assert_equal [1, 2, 3, 4], VanishedData["1:4", true].to_a 70 | end 71 | 72 | test "#deconstruct_keys returns uids and earlier" do 73 | assert_equal({uids: SequenceSet[1,9], earlier: true}, 74 | VanishedData["1,9", true].deconstruct_keys([:uids, :earlier])) 75 | VanishedData["1:5", false] => VanishedData[uids: SequenceSet, earlier: false] 76 | end 77 | 78 | test "#==" do 79 | assert_equal VanishedData[123, false], VanishedData["123", false] 80 | assert_equal VanishedData["3:1", false], VanishedData["1:3", false] 81 | end 82 | 83 | test "#eql?" do 84 | assert VanishedData["1:3", false].eql?(VanishedData[1..3, false]) 85 | refute VanishedData["3:1", false].eql?(VanishedData["1:3", false]) 86 | refute VanishedData["1:5", false].eql?(VanishedData["1:3", false]) 87 | refute VanishedData["1:3", true].eql?(VanishedData["1:3", false]) 88 | end 89 | 90 | end 91 | --------------------------------------------------------------------------------