├── test ├── lib │ └── helper.rb └── net │ └── pop │ └── test_pop.rb ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── Gemfile ├── bin ├── setup └── console ├── Rakefile ├── net-pop.gemspec ├── LICENSE.txt ├── README.md └── lib └── net └── pop.rb /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development do 4 | gem "bundler" 5 | gem "rake" 6 | gem "test-unit" 7 | gem "test-unit-ruby-core" 8 | end 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList['test/**/test_*.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/pop" 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 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 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 | min_version: 2.4 10 | 11 | test: 12 | needs: ruby-versions 13 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 14 | strategy: 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.4 } 20 | - { os: macos-latest, ruby: 2.5 } 21 | - { os: windows-latest, ruby: truffleruby-head } 22 | - { os: windows-latest, ruby: truffleruby } 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v6 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Install dependencies 31 | run: bundle install 32 | - name: Run test 33 | run: bundle exec rake test 34 | -------------------------------------------------------------------------------- /net-pop.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")) 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 = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Ruby client library for POP3.} 17 | spec.description = %q{Ruby client library for POP3.} 18 | spec.homepage = "https://github.com/ruby/net-pop" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 25 | `git ls-files -z 2>/dev/null`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features)/}) } 26 | end 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency "net-protocol" 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::POP3 2 | 3 | This library provides functionality for retrieving 4 | email via POP3, the Post Office Protocol version 3. For details 5 | of POP3, see [RFC1939](http://www.ietf.org/rfc/rfc1939.txt). 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'net-pop' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install net-pop 22 | 23 | ## Usage 24 | 25 | This example retrieves messages from the server and deletes them 26 | on the server. 27 | 28 | Messages are written to files named 'inbox/1', 'inbox/2', .... 29 | Replace 'pop.example.com' with your POP3 server address, and 30 | 'YourAccount' and 'YourPassword' with the appropriate account 31 | details. 32 | 33 | ```ruby 34 | require 'net/pop' 35 | 36 | pop = Net::POP3.new('pop.example.com') 37 | pop.start('YourAccount', 'YourPassword') # (1) 38 | if pop.mails.empty? 39 | puts 'No mail.' 40 | else 41 | i = 0 42 | pop.each_mail do |m| # or "pop.mails.each ..." # (2) 43 | File.open("inbox/#{i}", 'w') do |f| 44 | f.write m.pop 45 | end 46 | m.delete 47 | i += 1 48 | end 49 | puts "#{pop.mails.size} mails popped." 50 | end 51 | pop.finish # (3) 52 | ``` 53 | 54 | 1. Call Net::POP3#start and start POP session. 55 | 2. Access messages by using POP3#each_mail and/or POP3#mails. 56 | 3. Close POP session by calling POP3#finish or use the block form of #start. 57 | 58 | ## Development 59 | 60 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 61 | 62 | 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). 63 | 64 | ## Contributing 65 | 66 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-pop. 67 | -------------------------------------------------------------------------------- /test/net/pop/test_pop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'net/pop' 3 | require 'test/unit' 4 | require 'digest/md5' 5 | 6 | class TestPOP < Test::Unit::TestCase 7 | def setup 8 | @users = {'user' => 'pass' } 9 | @ok_user = 'user' 10 | @stamp_base = "#{$$}.#{Time.now.to_i}@localhost" 11 | end 12 | 13 | def test_pop_auth_ok 14 | pop_test(false) do |pop| 15 | assert_instance_of Net::POP3, pop 16 | assert_nothing_raised do 17 | pop.start(@ok_user, @users[@ok_user]) 18 | end 19 | end 20 | end 21 | 22 | def test_pop_auth_ng 23 | pop_test(false) do |pop| 24 | assert_instance_of Net::POP3, pop 25 | assert_raise Net::POPAuthenticationError do 26 | pop.start(@ok_user, 'bad password') 27 | end 28 | end 29 | end 30 | 31 | def test_apop_ok 32 | pop_test(@stamp_base) do |pop| 33 | assert_instance_of Net::APOP, pop 34 | assert_nothing_raised do 35 | pop.start(@ok_user, @users[@ok_user]) 36 | end 37 | end 38 | end 39 | 40 | def test_apop_ng 41 | pop_test(@stamp_base) do |pop| 42 | assert_instance_of Net::APOP, pop 43 | assert_raise Net::POPAuthenticationError do 44 | pop.start(@ok_user, 'bad password') 45 | end 46 | end 47 | end 48 | 49 | def test_apop_invalid 50 | pop_test("\x80"+@stamp_base) do |pop| 51 | assert_instance_of Net::APOP, pop 52 | assert_raise Net::POPAuthenticationError do 53 | pop.start(@ok_user, @users[@ok_user]) 54 | end 55 | end 56 | end 57 | 58 | def test_apop_invalid_at 59 | pop_test(@stamp_base.sub('@', '.')) do |pop| 60 | assert_instance_of Net::APOP, pop 61 | assert_raise Net::POPAuthenticationError do 62 | pop.start(@ok_user, @users[@ok_user]) 63 | end 64 | end 65 | end 66 | 67 | def test_popmail 68 | # totally not representative of real messages, but 69 | # enough to test frozen bugs 70 | lines = [ "[ruby-core:85210]" , "[Bug #14416]" ].freeze 71 | command = Object.new 72 | command.instance_variable_set(:@lines, lines) 73 | 74 | def command.retr(n) 75 | @lines.each { |l| yield "#{l}\r\n" } 76 | end 77 | 78 | def command.top(number, nl) 79 | @lines.each do |l| 80 | yield "#{l}\r\n" 81 | break if (nl -= 1) <= 0 82 | end 83 | end 84 | 85 | net_pop = :unused 86 | popmail = Net::POPMail.new(1, 123, net_pop, command) 87 | res = popmail.pop 88 | assert_equal "[ruby-core:85210]\r\n[Bug #14416]\r\n", res 89 | assert_not_predicate res, :frozen? 90 | 91 | res = popmail.top(1) 92 | assert_equal "[ruby-core:85210]\r\n", res 93 | assert_not_predicate res, :frozen? 94 | end 95 | 96 | def pop_test(apop=false) 97 | host = 'localhost' 98 | server = TCPServer.new(host, 0) 99 | port = server.addr[1] 100 | server_thread = Thread.start do 101 | sock = server.accept 102 | begin 103 | pop_server_loop(sock, apop) 104 | ensure 105 | sock.close 106 | end 107 | end 108 | client_thread = Thread.start do 109 | begin 110 | begin 111 | pop = Net::POP3::APOP(apop).new(host, port) 112 | #pop.set_debug_output $stderr 113 | yield pop 114 | ensure 115 | begin 116 | pop.finish 117 | rescue IOError 118 | raise unless $!.message == "POP session not yet started" 119 | end 120 | end 121 | ensure 122 | server.close 123 | end 124 | end 125 | assert_join_threads([client_thread, server_thread]) 126 | end 127 | 128 | def pop_server_loop(sock, apop) 129 | if apop 130 | sock.print "+OK ready <#{apop}>\r\n" 131 | else 132 | sock.print "+OK ready\r\n" 133 | end 134 | user = nil 135 | while line = sock.gets 136 | case line 137 | when /^USER (.+)\r\n/ 138 | user = $1 139 | if @users.key?(user) 140 | sock.print "+OK\r\n" 141 | else 142 | sock.print "-ERR unknown user\r\n" 143 | end 144 | when /^PASS (.+)\r\n/ 145 | if @users[user] == $1 146 | sock.print "+OK\r\n" 147 | else 148 | sock.print "-ERR invalid password\r\n" 149 | end 150 | when /^APOP (.+) (.+)\r\n/ 151 | user = $1 152 | if apop && Digest::MD5.hexdigest("<#{apop}>#{@users[user]}") == $2 153 | sock.print "+OK\r\n" 154 | else 155 | sock.print "-ERR authentication failed\r\n" 156 | end 157 | when /^QUIT/ 158 | sock.print "+OK bye\r\n" 159 | return 160 | else 161 | sock.print "-ERR command not recognized\r\n" 162 | return 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/net/pop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # = net/pop.rb 3 | # 4 | # Copyright (c) 1999-2007 Yukihiro Matsumoto. 5 | # 6 | # Copyright (c) 1999-2007 Minero Aoki. 7 | # 8 | # Written & maintained by Minero Aoki . 9 | # 10 | # Documented by William Webber and Minero Aoki. 11 | # 12 | # This program is free software. You can re-distribute and/or 13 | # modify this program under the same terms as Ruby itself, 14 | # Ruby Distribute License. 15 | # 16 | # NOTE: You can find Japanese version of this document at: 17 | # http://docs.ruby-lang.org/ja/latest/library/net=2fpop.html 18 | # 19 | # $Id$ 20 | # 21 | # See Net::POP3 for documentation. 22 | # 23 | 24 | require 'net/protocol' 25 | require 'digest/md5' 26 | require 'timeout' 27 | 28 | begin 29 | require "openssl" 30 | rescue LoadError 31 | end 32 | 33 | module Net 34 | 35 | # Non-authentication POP3 protocol error 36 | # (reply code "-ERR", except authentication). 37 | class POPError < ProtocolError; end 38 | 39 | # POP3 authentication error. 40 | class POPAuthenticationError < ProtoAuthError; end 41 | 42 | # Unexpected response from the server. 43 | class POPBadResponse < POPError; end 44 | 45 | # 46 | # == What is This Library? 47 | # 48 | # This library provides functionality for retrieving 49 | # email via POP3, the Post Office Protocol version 3. For details 50 | # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt). 51 | # 52 | # == Examples 53 | # 54 | # === Retrieving Messages 55 | # 56 | # This example retrieves messages from the server and deletes them 57 | # on the server. 58 | # 59 | # Messages are written to files named 'inbox/1', 'inbox/2', .... 60 | # Replace 'pop.example.com' with your POP3 server address, and 61 | # 'YourAccount' and 'YourPassword' with the appropriate account 62 | # details. 63 | # 64 | # require 'net/pop' 65 | # 66 | # pop = Net::POP3.new('pop.example.com') 67 | # pop.start('YourAccount', 'YourPassword') # (1) 68 | # if pop.mails.empty? 69 | # puts 'No mail.' 70 | # else 71 | # i = 0 72 | # pop.each_mail do |m| # or "pop.mails.each ..." # (2) 73 | # File.open("inbox/#{i}", 'w') do |f| 74 | # f.write m.pop 75 | # end 76 | # m.delete 77 | # i += 1 78 | # end 79 | # puts "#{pop.mails.size} mails popped." 80 | # end 81 | # pop.finish # (3) 82 | # 83 | # 1. Call Net::POP3#start and start POP session. 84 | # 2. Access messages by using POP3#each_mail and/or POP3#mails. 85 | # 3. Close POP session by calling POP3#finish or use the block form of #start. 86 | # 87 | # === Shortened Code 88 | # 89 | # The example above is very verbose. You can shorten the code by using 90 | # some utility methods. First, the block form of Net::POP3.start can 91 | # be used instead of POP3.new, POP3#start and POP3#finish. 92 | # 93 | # require 'net/pop' 94 | # 95 | # Net::POP3.start('pop.example.com', 110, 96 | # 'YourAccount', 'YourPassword') do |pop| 97 | # if pop.mails.empty? 98 | # puts 'No mail.' 99 | # else 100 | # i = 0 101 | # pop.each_mail do |m| # or "pop.mails.each ..." 102 | # File.open("inbox/#{i}", 'w') do |f| 103 | # f.write m.pop 104 | # end 105 | # m.delete 106 | # i += 1 107 | # end 108 | # puts "#{pop.mails.size} mails popped." 109 | # end 110 | # end 111 | # 112 | # POP3#delete_all is an alternative for #each_mail and #delete. 113 | # 114 | # require 'net/pop' 115 | # 116 | # Net::POP3.start('pop.example.com', 110, 117 | # 'YourAccount', 'YourPassword') do |pop| 118 | # if pop.mails.empty? 119 | # puts 'No mail.' 120 | # else 121 | # i = 1 122 | # pop.delete_all do |m| 123 | # File.open("inbox/#{i}", 'w') do |f| 124 | # f.write m.pop 125 | # end 126 | # i += 1 127 | # end 128 | # end 129 | # end 130 | # 131 | # And here is an even shorter example. 132 | # 133 | # require 'net/pop' 134 | # 135 | # i = 0 136 | # Net::POP3.delete_all('pop.example.com', 110, 137 | # 'YourAccount', 'YourPassword') do |m| 138 | # File.open("inbox/#{i}", 'w') do |f| 139 | # f.write m.pop 140 | # end 141 | # i += 1 142 | # end 143 | # 144 | # === Memory Space Issues 145 | # 146 | # All the examples above get each message as one big string. 147 | # This example avoids this. 148 | # 149 | # require 'net/pop' 150 | # 151 | # i = 1 152 | # Net::POP3.delete_all('pop.example.com', 110, 153 | # 'YourAccount', 'YourPassword') do |m| 154 | # File.open("inbox/#{i}", 'w') do |f| 155 | # m.pop do |chunk| # get a message little by little. 156 | # f.write chunk 157 | # end 158 | # i += 1 159 | # end 160 | # end 161 | # 162 | # === Using APOP 163 | # 164 | # The net/pop library supports APOP authentication. 165 | # To use APOP, use the Net::APOP class instead of the Net::POP3 class. 166 | # You can use the utility method, Net::POP3.APOP(). For example: 167 | # 168 | # require 'net/pop' 169 | # 170 | # # Use APOP authentication if $isapop == true 171 | # pop = Net::POP3.APOP($isapop).new('apop.example.com', 110) 172 | # pop.start('YourAccount', 'YourPassword') do |pop| 173 | # # Rest of the code is the same. 174 | # end 175 | # 176 | # === Fetch Only Selected Mail Using 'UIDL' POP Command 177 | # 178 | # If your POP server provides UIDL functionality, 179 | # you can grab only selected mails from the POP server. 180 | # e.g. 181 | # 182 | # def need_pop?( id ) 183 | # # determine if we need pop this mail... 184 | # end 185 | # 186 | # Net::POP3.start('pop.example.com', 110, 187 | # 'Your account', 'Your password') do |pop| 188 | # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m| 189 | # do_something(m.pop) 190 | # end 191 | # end 192 | # 193 | # The POPMail#unique_id() method returns the unique-id of the message as a 194 | # String. Normally the unique-id is a hash of the message. 195 | # 196 | class POP3 < Protocol 197 | # version of this library 198 | VERSION = "0.1.2" 199 | 200 | # 201 | # Class Parameters 202 | # 203 | 204 | # returns the port for POP3 205 | def POP3.default_port 206 | default_pop3_port() 207 | end 208 | 209 | # The default port for POP3 connections, port 110 210 | def POP3.default_pop3_port 211 | 110 212 | end 213 | 214 | # The default port for POP3S connections, port 995 215 | def POP3.default_pop3s_port 216 | 995 217 | end 218 | 219 | def POP3.socket_type #:nodoc: obsolete 220 | Net::InternetMessageIO 221 | end 222 | 223 | # 224 | # Utilities 225 | # 226 | 227 | # Returns the APOP class if +isapop+ is true; otherwise, returns 228 | # the POP class. For example: 229 | # 230 | # # Example 1 231 | # pop = Net::POP3::APOP($is_apop).new(addr, port) 232 | # 233 | # # Example 2 234 | # Net::POP3::APOP($is_apop).start(addr, port) do |pop| 235 | # .... 236 | # end 237 | # 238 | def POP3.APOP(isapop) 239 | isapop ? APOP : POP3 240 | end 241 | 242 | # Starts a POP3 session and iterates over each POPMail object, 243 | # yielding it to the +block+. 244 | # This method is equivalent to: 245 | # 246 | # Net::POP3.start(address, port, account, password) do |pop| 247 | # pop.each_mail do |m| 248 | # yield m 249 | # end 250 | # end 251 | # 252 | # This method raises a POPAuthenticationError if authentication fails. 253 | # 254 | # === Example 255 | # 256 | # Net::POP3.foreach('pop.example.com', 110, 257 | # 'YourAccount', 'YourPassword') do |m| 258 | # file.write m.pop 259 | # m.delete if $DELETE 260 | # end 261 | # 262 | def POP3.foreach(address, port = nil, 263 | account = nil, password = nil, 264 | isapop = false, &block) # :yields: message 265 | start(address, port, account, password, isapop) {|pop| 266 | pop.each_mail(&block) 267 | } 268 | end 269 | 270 | # Starts a POP3 session and deletes all messages on the server. 271 | # If a block is given, each POPMail object is yielded to it before 272 | # being deleted. 273 | # 274 | # This method raises a POPAuthenticationError if authentication fails. 275 | # 276 | # === Example 277 | # 278 | # Net::POP3.delete_all('pop.example.com', 110, 279 | # 'YourAccount', 'YourPassword') do |m| 280 | # file.write m.pop 281 | # end 282 | # 283 | def POP3.delete_all(address, port = nil, 284 | account = nil, password = nil, 285 | isapop = false, &block) 286 | start(address, port, account, password, isapop) {|pop| 287 | pop.delete_all(&block) 288 | } 289 | end 290 | 291 | # Opens a POP3 session, attempts authentication, and quits. 292 | # 293 | # This method raises POPAuthenticationError if authentication fails. 294 | # 295 | # === Example: normal POP3 296 | # 297 | # Net::POP3.auth_only('pop.example.com', 110, 298 | # 'YourAccount', 'YourPassword') 299 | # 300 | # === Example: APOP 301 | # 302 | # Net::POP3.auth_only('pop.example.com', 110, 303 | # 'YourAccount', 'YourPassword', true) 304 | # 305 | def POP3.auth_only(address, port = nil, 306 | account = nil, password = nil, 307 | isapop = false) 308 | new(address, port, isapop).auth_only account, password 309 | end 310 | 311 | # Starts a pop3 session, attempts authentication, and quits. 312 | # This method must not be called while POP3 session is opened. 313 | # This method raises POPAuthenticationError if authentication fails. 314 | def auth_only(account, password) 315 | raise IOError, 'opening previously opened POP session' if started? 316 | start(account, password) { 317 | ; 318 | } 319 | end 320 | 321 | # 322 | # SSL 323 | # 324 | 325 | @ssl_params = nil 326 | 327 | # :call-seq: 328 | # Net::POP.enable_ssl(params = {}) 329 | # 330 | # Enable SSL for all new instances. 331 | # +params+ is passed to OpenSSL::SSLContext#set_params. 332 | def POP3.enable_ssl(*args) 333 | @ssl_params = create_ssl_params(*args) 334 | end 335 | 336 | # Constructs proper parameters from arguments 337 | def POP3.create_ssl_params(verify_or_params = {}, certs = nil) 338 | begin 339 | params = verify_or_params.to_hash 340 | rescue NoMethodError 341 | params = {} 342 | params[:verify_mode] = verify_or_params 343 | if certs 344 | if File.file?(certs) 345 | params[:ca_file] = certs 346 | elsif File.directory?(certs) 347 | params[:ca_path] = certs 348 | end 349 | end 350 | end 351 | return params 352 | end 353 | 354 | # Disable SSL for all new instances. 355 | def POP3.disable_ssl 356 | @ssl_params = nil 357 | end 358 | 359 | # returns the SSL Parameters 360 | # 361 | # see also POP3.enable_ssl 362 | def POP3.ssl_params 363 | return @ssl_params 364 | end 365 | 366 | # returns +true+ if POP3.ssl_params is set 367 | def POP3.use_ssl? 368 | return !@ssl_params.nil? 369 | end 370 | 371 | # returns whether verify_mode is enable from POP3.ssl_params 372 | def POP3.verify 373 | return @ssl_params[:verify_mode] 374 | end 375 | 376 | # returns the :ca_file or :ca_path from POP3.ssl_params 377 | def POP3.certs 378 | return @ssl_params[:ca_file] || @ssl_params[:ca_path] 379 | end 380 | 381 | # 382 | # Session management 383 | # 384 | 385 | # Creates a new POP3 object and open the connection. Equivalent to 386 | # 387 | # Net::POP3.new(address, port, isapop).start(account, password) 388 | # 389 | # If +block+ is provided, yields the newly-opened POP3 object to it, 390 | # and automatically closes it at the end of the session. 391 | # 392 | # === Example 393 | # 394 | # Net::POP3.start(addr, port, account, password) do |pop| 395 | # pop.each_mail do |m| 396 | # file.write m.pop 397 | # m.delete 398 | # end 399 | # end 400 | # 401 | def POP3.start(address, port = nil, 402 | account = nil, password = nil, 403 | isapop = false, &block) # :yield: pop 404 | new(address, port, isapop).start(account, password, &block) 405 | end 406 | 407 | # Creates a new POP3 object. 408 | # 409 | # +address+ is the hostname or ip address of your POP3 server. 410 | # 411 | # The optional +port+ is the port to connect to. 412 | # 413 | # The optional +isapop+ specifies whether this connection is going 414 | # to use APOP authentication; it defaults to +false+. 415 | # 416 | # This method does *not* open the TCP connection. 417 | def initialize(addr, port = nil, isapop = false) 418 | @address = addr 419 | @ssl_params = POP3.ssl_params 420 | @port = port 421 | @apop = isapop 422 | 423 | @command = nil 424 | @socket = nil 425 | @started = false 426 | @open_timeout = 30 427 | @read_timeout = 60 428 | @debug_output = nil 429 | 430 | @mails = nil 431 | @n_mails = nil 432 | @n_bytes = nil 433 | end 434 | 435 | # Does this instance use APOP authentication? 436 | def apop? 437 | @apop 438 | end 439 | 440 | # does this instance use SSL? 441 | def use_ssl? 442 | return !@ssl_params.nil? 443 | end 444 | 445 | # :call-seq: 446 | # Net::POP#enable_ssl(params = {}) 447 | # 448 | # Enables SSL for this instance. Must be called before the connection is 449 | # established to have any effect. 450 | # +params[:port]+ is port to establish the SSL connection on; Defaults to 995. 451 | # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params. 452 | def enable_ssl(verify_or_params = {}, certs = nil, port = nil) 453 | begin 454 | @ssl_params = verify_or_params.to_hash.dup 455 | @port = @ssl_params.delete(:port) || @port 456 | rescue NoMethodError 457 | @ssl_params = POP3.create_ssl_params(verify_or_params, certs) 458 | @port = port || @port 459 | end 460 | end 461 | 462 | # Disable SSL for all new instances. 463 | def disable_ssl 464 | @ssl_params = nil 465 | end 466 | 467 | # Provide human-readable stringification of class state. 468 | def inspect 469 | +"#<#{self.class} #{@address}:#{@port} open=#{@started}>" 470 | end 471 | 472 | # *WARNING*: This method causes a serious security hole. 473 | # Use this method only for debugging. 474 | # 475 | # Set an output stream for debugging. 476 | # 477 | # === Example 478 | # 479 | # pop = Net::POP.new(addr, port) 480 | # pop.set_debug_output $stderr 481 | # pop.start(account, passwd) do |pop| 482 | # .... 483 | # end 484 | # 485 | def set_debug_output(arg) 486 | @debug_output = arg 487 | end 488 | 489 | # The address to connect to. 490 | attr_reader :address 491 | 492 | # The port number to connect to. 493 | def port 494 | return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port) 495 | end 496 | 497 | # Seconds to wait until a connection is opened. 498 | # If the POP3 object cannot open a connection within this time, 499 | # it raises a Net::OpenTimeout exception. The default value is 30 seconds. 500 | attr_accessor :open_timeout 501 | 502 | # Seconds to wait until reading one block (by one read(1) call). 503 | # If the POP3 object cannot complete a read() within this time, 504 | # it raises a Net::ReadTimeout exception. The default value is 60 seconds. 505 | attr_reader :read_timeout 506 | 507 | # Set the read timeout. 508 | def read_timeout=(sec) 509 | @command.socket.read_timeout = sec if @command 510 | @read_timeout = sec 511 | end 512 | 513 | # +true+ if the POP3 session has started. 514 | def started? 515 | @started 516 | end 517 | 518 | alias active? started? #:nodoc: obsolete 519 | 520 | # Starts a POP3 session. 521 | # 522 | # When called with block, gives a POP3 object to the block and 523 | # closes the session after block call finishes. 524 | # 525 | # This method raises a POPAuthenticationError if authentication fails. 526 | def start(account, password) # :yield: pop 527 | raise IOError, 'POP session already started' if @started 528 | if block_given? 529 | begin 530 | do_start account, password 531 | return yield(self) 532 | ensure 533 | do_finish 534 | end 535 | else 536 | do_start account, password 537 | return self 538 | end 539 | end 540 | 541 | # internal method for Net::POP3.start 542 | def do_start(account, password) # :nodoc: 543 | s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do 544 | TCPSocket.open(@address, port) 545 | end 546 | if use_ssl? 547 | raise 'openssl library not installed' unless defined?(OpenSSL) 548 | context = OpenSSL::SSL::SSLContext.new 549 | context.set_params(@ssl_params) 550 | s = OpenSSL::SSL::SSLSocket.new(s, context) 551 | s.hostname = @address 552 | s.sync_close = true 553 | ssl_socket_connect(s, @open_timeout) 554 | if context.verify_mode != OpenSSL::SSL::VERIFY_NONE 555 | s.post_connection_check(@address) 556 | end 557 | end 558 | @socket = InternetMessageIO.new(s, 559 | read_timeout: @read_timeout, 560 | debug_output: @debug_output) 561 | logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})" 562 | on_connect 563 | @command = POP3Command.new(@socket) 564 | if apop? 565 | @command.apop account, password 566 | else 567 | @command.auth account, password 568 | end 569 | @started = true 570 | ensure 571 | # Authentication failed, clean up connection. 572 | unless @started 573 | s.close if s 574 | @socket = nil 575 | @command = nil 576 | end 577 | end 578 | private :do_start 579 | 580 | # Does nothing 581 | def on_connect # :nodoc: 582 | end 583 | private :on_connect 584 | 585 | # Finishes a POP3 session and closes TCP connection. 586 | def finish 587 | raise IOError, 'POP session not yet started' unless started? 588 | do_finish 589 | end 590 | 591 | # nil's out the: 592 | # - mails 593 | # - number counter for mails 594 | # - number counter for bytes 595 | # - quits the current command, if any 596 | def do_finish # :nodoc: 597 | @mails = nil 598 | @n_mails = nil 599 | @n_bytes = nil 600 | @command.quit if @command 601 | ensure 602 | @started = false 603 | @command = nil 604 | @socket.close if @socket 605 | @socket = nil 606 | end 607 | private :do_finish 608 | 609 | # Returns the current command. 610 | # 611 | # Raises IOError if there is no active socket 612 | def command # :nodoc: 613 | raise IOError, 'POP session not opened yet' \ 614 | if not @socket or @socket.closed? 615 | @command 616 | end 617 | private :command 618 | 619 | # 620 | # POP protocol wrapper 621 | # 622 | 623 | # Returns the number of messages on the POP server. 624 | def n_mails 625 | return @n_mails if @n_mails 626 | @n_mails, @n_bytes = command().stat 627 | @n_mails 628 | end 629 | 630 | # Returns the total size in bytes of all the messages on the POP server. 631 | def n_bytes 632 | return @n_bytes if @n_bytes 633 | @n_mails, @n_bytes = command().stat 634 | @n_bytes 635 | end 636 | 637 | # Returns an array of Net::POPMail objects, representing all the 638 | # messages on the server. This array is renewed when the session 639 | # restarts; otherwise, it is fetched from the server the first time 640 | # this method is called (directly or indirectly) and cached. 641 | # 642 | # This method raises a POPError if an error occurs. 643 | def mails 644 | return @mails.dup if @mails 645 | if n_mails() == 0 646 | # some popd raises error for LIST on the empty mailbox. 647 | @mails = [] 648 | return [] 649 | end 650 | 651 | @mails = command().list.map {|num, size| 652 | POPMail.new(num, size, self, command()) 653 | } 654 | @mails.dup 655 | end 656 | 657 | # Yields each message to the passed-in block in turn. 658 | # Equivalent to: 659 | # 660 | # pop3.mails.each do |popmail| 661 | # .... 662 | # end 663 | # 664 | # This method raises a POPError if an error occurs. 665 | def each_mail(&block) # :yield: message 666 | mails().each(&block) 667 | end 668 | 669 | alias each each_mail 670 | 671 | # Deletes all messages on the server. 672 | # 673 | # If called with a block, yields each message in turn before deleting it. 674 | # 675 | # === Example 676 | # 677 | # n = 1 678 | # pop.delete_all do |m| 679 | # File.open("inbox/#{n}") do |f| 680 | # f.write m.pop 681 | # end 682 | # n += 1 683 | # end 684 | # 685 | # This method raises a POPError if an error occurs. 686 | # 687 | def delete_all # :yield: message 688 | mails().each do |m| 689 | yield m if block_given? 690 | m.delete unless m.deleted? 691 | end 692 | end 693 | 694 | # Resets the session. This clears all "deleted" marks from messages. 695 | # 696 | # This method raises a POPError if an error occurs. 697 | def reset 698 | command().rset 699 | mails().each do |m| 700 | m.instance_eval { 701 | @deleted = false 702 | } 703 | end 704 | end 705 | 706 | def set_all_uids #:nodoc: internal use only (called from POPMail#uidl) 707 | uidl = command().uidl 708 | @mails.each {|m| m.uid = uidl[m.number] } 709 | end 710 | 711 | # debugging output for +msg+ 712 | def logging(msg) 713 | @debug_output << msg + "\n" if @debug_output 714 | end 715 | 716 | end # class POP3 717 | 718 | # class aliases 719 | POP = POP3 # :nodoc: 720 | POPSession = POP3 # :nodoc: 721 | POP3Session = POP3 # :nodoc: 722 | 723 | # 724 | # This class is equivalent to POP3, except that it uses APOP authentication. 725 | # 726 | class APOP < POP3 727 | # Always returns true. 728 | def apop? 729 | true 730 | end 731 | end 732 | 733 | # class aliases 734 | APOPSession = APOP 735 | 736 | # 737 | # This class represents a message which exists on the POP server. 738 | # Instances of this class are created by the POP3 class; they should 739 | # not be directly created by the user. 740 | # 741 | class POPMail 742 | 743 | def initialize(num, len, pop, cmd) #:nodoc: 744 | @number = num 745 | @length = len 746 | @pop = pop 747 | @command = cmd 748 | @deleted = false 749 | @uid = nil 750 | end 751 | 752 | # The sequence number of the message on the server. 753 | attr_reader :number 754 | 755 | # The length of the message in octets. 756 | attr_reader :length 757 | alias size length 758 | 759 | # Provide human-readable stringification of class state. 760 | def inspect 761 | +"#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" 762 | end 763 | 764 | # 765 | # This method fetches the message. If called with a block, the 766 | # message is yielded to the block one chunk at a time. If called 767 | # without a block, the message is returned as a String. The optional 768 | # +dest+ argument will be prepended to the returned String; this 769 | # argument is essentially obsolete. 770 | # 771 | # === Example without block 772 | # 773 | # POP3.start('pop.example.com', 110, 774 | # 'YourAccount', 'YourPassword') do |pop| 775 | # n = 1 776 | # pop.mails.each do |popmail| 777 | # File.open("inbox/#{n}", 'w') do |f| 778 | # f.write popmail.pop 779 | # end 780 | # popmail.delete 781 | # n += 1 782 | # end 783 | # end 784 | # 785 | # === Example with block 786 | # 787 | # POP3.start('pop.example.com', 110, 788 | # 'YourAccount', 'YourPassword') do |pop| 789 | # n = 1 790 | # pop.mails.each do |popmail| 791 | # File.open("inbox/#{n}", 'w') do |f| 792 | # popmail.pop do |chunk| #### 793 | # f.write chunk 794 | # end 795 | # end 796 | # n += 1 797 | # end 798 | # end 799 | # 800 | # This method raises a POPError if an error occurs. 801 | # 802 | def pop( dest = +'', &block ) # :yield: message_chunk 803 | if block_given? 804 | @command.retr(@number, &block) 805 | nil 806 | else 807 | @command.retr(@number) do |chunk| 808 | dest << chunk 809 | end 810 | dest 811 | end 812 | end 813 | 814 | alias all pop #:nodoc: obsolete 815 | alias mail pop #:nodoc: obsolete 816 | 817 | # Fetches the message header and +lines+ lines of body. 818 | # 819 | # The optional +dest+ argument is obsolete. 820 | # 821 | # This method raises a POPError if an error occurs. 822 | def top(lines, dest = +'') 823 | @command.top(@number, lines) do |chunk| 824 | dest << chunk 825 | end 826 | dest 827 | end 828 | 829 | # Fetches the message header. 830 | # 831 | # The optional +dest+ argument is obsolete. 832 | # 833 | # This method raises a POPError if an error occurs. 834 | def header(dest = +'') 835 | top(0, dest) 836 | end 837 | 838 | # Marks a message for deletion on the server. Deletion does not 839 | # actually occur until the end of the session; deletion may be 840 | # cancelled for _all_ marked messages by calling POP3#reset(). 841 | # 842 | # This method raises a POPError if an error occurs. 843 | # 844 | # === Example 845 | # 846 | # POP3.start('pop.example.com', 110, 847 | # 'YourAccount', 'YourPassword') do |pop| 848 | # n = 1 849 | # pop.mails.each do |popmail| 850 | # File.open("inbox/#{n}", 'w') do |f| 851 | # f.write popmail.pop 852 | # end 853 | # popmail.delete #### 854 | # n += 1 855 | # end 856 | # end 857 | # 858 | def delete 859 | @command.dele @number 860 | @deleted = true 861 | end 862 | 863 | alias delete! delete #:nodoc: obsolete 864 | 865 | # True if the mail has been deleted. 866 | def deleted? 867 | @deleted 868 | end 869 | 870 | # Returns the unique-id of the message. 871 | # Normally the unique-id is a hash string of the message. 872 | # 873 | # This method raises a POPError if an error occurs. 874 | def unique_id 875 | return @uid if @uid 876 | @pop.set_all_uids 877 | @uid 878 | end 879 | 880 | alias uidl unique_id 881 | 882 | def uid=(uid) #:nodoc: internal use only 883 | @uid = uid 884 | end 885 | 886 | end # class POPMail 887 | 888 | 889 | class POP3Command #:nodoc: internal use only 890 | 891 | def initialize(sock) 892 | @socket = sock 893 | @error_occurred = false 894 | res = check_response(critical { recv_response() }) 895 | @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/) 896 | end 897 | 898 | attr_reader :socket 899 | 900 | def inspect 901 | +"#<#{self.class} socket=#{@socket}>" 902 | end 903 | 904 | def auth(account, password) 905 | check_response_auth(critical { 906 | check_response_auth(get_response('USER %s', account)) 907 | get_response('PASS %s', password) 908 | }) 909 | end 910 | 911 | def apop(account, password) 912 | raise POPAuthenticationError, 'not APOP server; cannot login' \ 913 | unless @apop_stamp 914 | check_response_auth(critical { 915 | get_response('APOP %s %s', 916 | account, 917 | Digest::MD5.hexdigest(@apop_stamp + password)) 918 | }) 919 | end 920 | 921 | def list 922 | critical { 923 | getok 'LIST' 924 | list = [] 925 | @socket.each_list_item do |line| 926 | m = /\A(\d+)[ \t]+(\d+)/.match(line) or 927 | raise POPBadResponse, "bad response: #{line}" 928 | list.push [m[1].to_i, m[2].to_i] 929 | end 930 | return list 931 | } 932 | end 933 | 934 | def stat 935 | res = check_response(critical { get_response('STAT') }) 936 | m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or 937 | raise POPBadResponse, "wrong response format: #{res}" 938 | [m[1].to_i, m[2].to_i] 939 | end 940 | 941 | def rset 942 | check_response(critical { get_response('RSET') }) 943 | end 944 | 945 | def top(num, lines = 0, &block) 946 | critical { 947 | getok('TOP %d %d', num, lines) 948 | @socket.each_message_chunk(&block) 949 | } 950 | end 951 | 952 | def retr(num, &block) 953 | critical { 954 | getok('RETR %d', num) 955 | @socket.each_message_chunk(&block) 956 | } 957 | end 958 | 959 | def dele(num) 960 | check_response(critical { get_response('DELE %d', num) }) 961 | end 962 | 963 | def uidl(num = nil) 964 | if num 965 | res = check_response(critical { get_response('UIDL %d', num) }) 966 | return res.split(/ /)[1] 967 | else 968 | critical { 969 | getok('UIDL') 970 | table = {} 971 | @socket.each_list_item do |line| 972 | num, uid = line.split(' ') 973 | table[num.to_i] = uid 974 | end 975 | return table 976 | } 977 | end 978 | end 979 | 980 | def quit 981 | check_response(critical { get_response('QUIT') }) 982 | end 983 | 984 | private 985 | 986 | def getok(fmt, *fargs) 987 | @socket.writeline sprintf(fmt, *fargs) 988 | check_response(recv_response()) 989 | end 990 | 991 | def get_response(fmt, *fargs) 992 | @socket.writeline sprintf(fmt, *fargs) 993 | recv_response() 994 | end 995 | 996 | def recv_response 997 | @socket.readline 998 | end 999 | 1000 | def check_response(res) 1001 | raise POPError, res unless /\A\+OK/i =~ res 1002 | res 1003 | end 1004 | 1005 | def check_response_auth(res) 1006 | raise POPAuthenticationError, res unless /\A\+OK/i =~ res 1007 | res 1008 | end 1009 | 1010 | def critical 1011 | return '+OK dummy ok response' if @error_occurred 1012 | begin 1013 | return yield() 1014 | rescue Exception 1015 | @error_occurred = true 1016 | raise 1017 | end 1018 | end 1019 | 1020 | end # class POP3Command 1021 | 1022 | end # module Net 1023 | --------------------------------------------------------------------------------