1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/net/pop.rb
aamine 819e8e6c82 * lib/net/http.rb (finish): does not raise IOError even if !started?, to allow duplicated #finish call.
* lib/net/pop.rb (finish): ditto.
* lib/net/smtp.rb (finish): ditto.


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4625 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2003-09-29 14:39:54 +00:00

850 lines
22 KiB
Ruby

# = net/pop.rb
#
#--
# Copyright (c) 1999-2003 Yukihiro Matsumoto
# Copyright (c) 1999-2003 Minero Aoki
#
# written & maintained by Minero Aoki <aamine@loveruby.net>
#
# This program is free software. You can re-distribute and/or
# modify this program under the same terms as Ruby itself,
# Ruby Distribute License or GNU General Public License.
#
# NOTE: You can find Japanese version of this document in
# the doc/net directory of the standard ruby interpreter package.
#
# $Id$
#++
#
# == What is This Library?
#
# This library provides functionality for retrieving
# email via POP3, the Post Office Protocol version 3. For details
# of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
#
# == Examples
#
# === Retrieving Messages
#
# This example retrieves messages from the server and deletes them
# on the server.
# Messages are written to files named 'inbox/1', 'inbox/2', ....
# Replace 'pop.example.com' with your POP3 server address, and
# 'YourAccount' and 'YourPassword' with the appropriate account
# details.
#
# require 'net/pop'
#
# pop = Net::POP3.new('pop.example.com')
# pop.start('YourAccount', 'YourPassword') # (1)
# if pop.mails.empty?
# puts 'no mail.'
# else
# i = 0
# pop.each_mail do |m| # or "pop.mails.each ..." # (2)
# File.open("inbox/#{i}", 'w') {|f|
# f.write m.pop
# }
# m.delete
# i += 1
# end
# puts "#{pop.mails.size} mails popped."
# end
# pop.finish # (3)
#
# 1. call Net::POP3#start and start POP session
# 2. access messages by using POP3#each_mail and/or POP3#mails
# 3. close POP session by calling POP3#finish or use the block form of #start.
#
# === Shortened Code
#
# The example above is very verbose. You can shorten the code by using
# some utility methods. First, the block form of Net::POP3.start can
# be used instead of POP3.new, POP3#start and POP3#finish.
#
# require 'net/pop'
#
# Net::POP3.start('pop.example.com', 110,
# 'YourAccount', 'YourPassword') {|pop|
# if pop.mails.empty?
# puts 'no mail.'
# else
# i = 0
# pop.each_mail do |m| # or "pop.mails.each ..."
# File.open("inbox/#{i}", 'w') {|f|
# f.write m.pop
# }
# m.delete
# i += 1
# end
# puts "#{pop.mails.size} mails popped."
# end
# }
#
# POP3#delete_all alternates #each_mail and m.delete.
#
# require 'net/pop'
#
# Net::POP3.start('pop.example.com', 110,
# 'YourAccount', 'YourPassword') {|pop|
# if pop.mails.empty?
# puts 'no mail.'
# else
# i = 1
# pop.delete_all do |m|
# File.open("inbox/#{i}", 'w') {|f|
# f.write m.pop
# }
# i += 1
# end
# end
# }
#
# And here is an even shorter example.
#
# require 'net/pop'
#
# i = 0
# Net::POP3.delete_all('pop.example.com', 110,
# 'YourAccount', 'YourPassword') do |m|
# File.open("inbox/#{i}", 'w') {|f|
# f.write m.pop
# }
# i += 1
# end
#
# === Memory Space Issues
#
# All the examples above get each message as one big string.
# This example avoids this.
#
# require 'net/pop'
#
# i = 1
# Net::POP3.delete_all('pop.example.com', 110,
# 'YourAccount', 'YourPassword') do |m|
# File.open("inbox/#{i}", 'w') {|f|
# m.pop do |chunk| # get a message little by little.
# f.write chunk
# end
# i += 1
# }
# end
#
# === Using APOP
#
# The net/pop library supports APOP authentication.
# To use APOP, use the Net::APOP class instead of the Net::POP3 class.
# You can use the utility method, Net::POP3.APOP(). For example:
#
# require 'net/pop'
#
# # Use APOP authentication if $isapop == true
# pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
# pop.start(YourAccount', 'YourPassword') {|pop|
# # Rest code is same.
# }
#
# === Fetch Only Selected Mail Using `UIDL' POP Command
#
# If your POP server provides UIDL functionality,
# you can grab only selected mails from the POP server.
# e.g.
#
# def need_pop?( id )
# # determine if we need pop this mail...
# end
#
# Net::POP3.start('pop.example.com', 110,
# 'Your account', 'Your password') {|pop|
# pop.mails.select {|m| need_pop?(m.unique_id) }.each do |m|
# do_something(m.pop)
# end
# }
#
# The POPMail#unique_id() method returns the unique-id of the message as a
# String. Normally the unique-id is a hash of the message.
#
require 'net/protocol'
require 'digest/md5'
module Net
# Non-authentication POP3 protocol error
# (reply code "-ERR", except authentication).
class POPError < ProtocolError; end
# POP3 authentication error.
class POPAuthenticationError < ProtoAuthError; end
# Unexpected response from the server.
class POPBadResponse < POPError; end
#
# Class providing POP3 client functionality.
#
# See documentation for the file pop.rb for examples of usage.
#
class POP3 < Protocol
Revision = %q$Revision$.split[1]
#
# Class Parameters
#
# The default port for POP3 connections, port 110
def POP3.default_port
110
end
def POP3.socket_type #:nodoc: obsolete
Net::InternetMessageIO
end
#
# Utilities
#
# Returns the APOP class if +isapop+ is true; otherwise, returns
# the POP class. For example:
#
# # Example 1
# pop = Net::POP3::APOP($is_apop).new(addr, port)
#
# # Example 2
# Net::POP3::APOP($is_apop).start(addr, port) {|pop|
# ....
# }
#
def POP3.APOP( isapop )
isapop ? APOP : POP3
end
# Starts a POP3 session and iterates over each POPMail object,
# yielding it to the +block+.
# This method is equivalent to:
#
# Net::POP3.start(address, port, account, password) {|pop|
# pop.each_mail do |m|
# yield m
# end
# }
#
# This method raises a POPAuthenticationError if authentication fails.
#
# # Typical usage
# Net::POP3.foreach('pop.example.com', 110,
# 'YourAccount', 'YourPassword') do |m|
# file.write m.pop
# m.delete if $DELETE
# end
#
def POP3.foreach( address, port = nil,
account = nil, password = nil,
isapop = false, &block ) # :yields: message
start(address, port, account, password, isapop) {|pop|
pop.each_mail(&block)
}
end
# Starts a POP3 session and deletes all messages on the server.
# If a block is given, each POPMail object is yielded to it before
# being deleted.
#
# This method raises a POPAuthenticationError if authentication fails.
#
# # Example
# Net::POP3.delete_all('pop.example.com', 110,
# 'YourAccount', 'YourPassword') do |m|
# file.write m.pop
# end
#
def POP3.delete_all( address, port = nil,
account = nil, password = nil,
isapop = false, &block )
start(address, port, account, password, isapop) {|pop|
pop.delete_all(&block)
}
end
# Opens a POP3 session, attempts authentication, and quits.
#
# This method raises POPAuthenticationError if authentication fails.
#
# # Example 1: normal POP3
# Net::POP3.auth_only('pop.example.com', 110,
# 'YourAccount', 'YourPassword')
#
# # Example 2: APOP
# Net::POP3.auth_only('pop.example.com', 110,
# 'YourAccount', 'YourPassword', true)
#
def POP3.auth_only( address, port = nil,
account = nil, password = nil,
isapop = false )
new(address, port, isapop).auth_only account, password
end
# Starts a pop3 session, attempts authentication, and quits.
# This method must not be called while POP3 session is opened.
# This method raises POPAuthenticationError if authentication fails.
def auth_only( account, password )
raise IOError, 'opening already opened POP session' if started?
start(account, password) {
;
}
end
#
# Session management
#
# Creates a new POP3 object and open the connection. Equivalent to
# Net::POP3.new(address, port, isapop).start(account, password)
#
# If +block+ is provided, yields the newly-opened POP3 object to it,
# and automatically closes it at the end of the session.
#
# Typical usage:
#
# Net::POP3.start(addr, port, account, password) {|pop|
# pop.each_mail do |m|
# file.write m.pop
# m.delete
# end
# }
#
def POP3.start( address, port = nil,
account = nil, password = nil,
isapop = false, &block ) # :yield: pop
new(address, port, isapop).start(account, password, &block)
end
# Creates a new POP3 object.
# +address+ is the hostname or ip address of your POP3 server.
# The optional +port+ is the port to connect to; it defaults to 110.
# The optional +isapop+ specifies whether this connection is going
# to use APOP authentication; it defaults to +false+.
# This method does *not* open the TCP connection.
def initialize( addr, port = nil, isapop = false )
@address = addr
@port = port || self.class.default_port
@apop = isapop
@command = nil
@socket = nil
@started = false
@open_timeout = 30
@read_timeout = 60
@debug_output = nil
@mails = nil
@n_mails = nil
@n_bytes = nil
end
# Does this instance use APOP authentication?
def apop?
@apop
end
# Provide human-readable stringification of class state.
def inspect
"#<#{self.class} #{@address}:#{@port} open=#{@started}>"
end
# *WARNING*: This method causes a serious security hole.
# Use this method only for debugging.
#
# Set an output stream for debugging.
#
# # Example
# pop = Net::POP.new(addr, port)
# pop.set_debug_output $stderr
# pop.start(account, passwd) {
# ....
# }
#
def set_debug_output( arg )
@debug_output = arg
end
# The address to connect to.
attr_reader :address
# The port number to connect to.
attr_reader :port
# Seconds to wait until a connection is opened.
# If the POP3 object cannot open a connection within this time,
# it raises a TimeoutError exception.
attr_accessor :open_timeout
# Seconds to wait until reading one block (by one read(1) call).
# If the POP3 object cannot complete a read() within this time,
# it raises a TimeoutError exception.
attr_reader :read_timeout
# Set the read timeout.
def read_timeout=( sec )
@command.socket.read_timeout = sec if @command
@read_timeout = sec
end
# +true+ if the POP3 session has started.
def started?
@started
end
alias active? started? #:nodoc: obsolete
# Starts a POP3 session.
#
# When called with block, gives a POP3 object to the block and
# closes the session after block call finishes.
#
# This method raises a POPAuthenticationError if authentication fails.
def start( account, password ) # :yield: pop
raise IOError, 'POP session already started' if @started
if block_given?
begin
do_start account, password
return yield(self)
ensure
finish
end
else
do_start account, password
return self
end
end
def do_start( account, password )
@socket = self.class.socket_type.open(@address, @port,
@open_timeout, @read_timeout, @debug_output)
on_connect
@command = POP3Command.new(@socket)
if apop?
@command.apop account, password
else
@command.auth account, password
end
@started = true
end
private :do_start
def on_connect
end
private :on_connect
# Finishes a POP3 session and closes TCP connection.
def finish
@mails = nil
@command.quit if @command
@command = nil
@socket.close if @socket and not @socket.closed?
@socket = nil
@started = false
end
def command
raise IOError, 'POP session not opened yet' \
if not @socket or @socket.closed?
@command
end
private :command
#
# POP protocol wrapper
#
# Returns the number of messages on the POP server.
def n_mails
return @n_mails if @n_mails
@n_mails, @n_bytes = command().stat
@n_mails
end
# Returns the total size in bytes of all the messages on the POP server.
def n_bytes
return @n_bytes if @n_bytes
@n_mails, @n_bytes = command().stat
@n_bytes
end
# Returns an array of Net::POPMail objects, representing all the
# messages on the server. This array is renewed when the session
# restarts; otherwise, it is fetched from the server the first time
# this method is called (directly or indirectly) and cached.
#
# This method raises a POPError if an error occurs.
def mails
return @mails.dup if @mails
if n_mails() == 0
# some popd raises error for LIST on the empty mailbox.
@mails = []
return []
end
@mails = command().list.map {|num, size|
POPMail.new(num, size, self, command())
}
@mails.dup
end
# Yields each message to the passed-in block in turn.
# Equivalent to:
#
# pop3.mails.each do |popmail|
# ....
# end
#
# This method raises a POPError if an error occurs.
def each_mail( &block ) # :yield: message
mails().each(&block)
end
alias each each_mail
# Deletes all messages on the server.
#
# If called with a block, yields each message in turn before deleting it.
#
# # Example
# n = 1
# pop.delete_all do |m|
# File.open("inbox/#{n}") {|f|
# f.write m.pop
# }
# n += 1
# end
#
# This method raises a POPError if an error occurs.
def delete_all # :yield: message
mails().each do |m|
yield m if block_given?
m.delete unless m.deleted?
end
end
# Resets the session. This clears all "deleted" marks from messages.
#
# This method raises a POPError if an error occurs.
def reset
command().rset
mails().each do |m|
m.instance_eval {
@deleted = false
}
end
end
def set_all_uids #:nodoc: internal use only (called from POPMail#uidl)
command().uidl.each do |num, uid|
@mails.find {|m| m.number == num }.uid = uid
end
end
end # class POP3
# class aliases
POP = POP3
POPSession = POP3
POP3Session = POP3
#
# This class is equivalent to POP3, except that it uses APOP authentication.
#
class APOP < POP3
# Always returns true.
def apop?
true
end
end
# class aliases
APOPSession = APOP
#
# This class represents a message which exists on the POP server.
# Instances of this class are created by the POP3 class; they should
# not be directly created by the user.
#
class POPMail
def initialize( num, len, pop, cmd ) #:nodoc:
@number = num
@length = len
@pop = pop
@command = cmd
@deleted = false
@uid = nil
end
# The sequence number of the message on the server.
attr_reader :number
# The length of the message in octets.
attr_reader :length
alias size length
# Provide human-readable stringification of class state.
def inspect
"#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
end
# This method fetches the message. If called with a block, the
# message is yielded to the block one chunk at a time. If called
# without a block, the message is returned as a String. The optional
# +dest+ argument will be prepended to the returned String; this
# argument is essentially obsolete.
#
# # Example without block
# POP3.start('pop.example.com', 110,
# 'YourAccount, 'YourPassword') {|pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') {|f|
# f.write popmail.pop
# }
# popmail.delete
# n += 1
# end
# }
#
# # Example with block
# POP3.start('pop.example.com', 110,
# 'YourAccount, 'YourPassword') {|pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') {|f|
# popmail.pop do |chunk| ####
# f.write chunk
# end
# }
# n += 1
# end
# }
#
# This method raises a POPError if an error occurs.
def pop( dest = '', &block ) # :yield: message_chunk
if block_given?
@command.retr(@number, &block)
nil
else
@command.retr(@number) do |chunk|
dest << chunk
end
dest
end
end
alias all pop #:nodoc: obsolete
alias mail pop #:nodoc: obsolete
# Fetches the message header and +lines+ lines of body.
# The optional +dest+ argument is obsolete.
#
# This method raises a POPError if an error occurs.
def top( lines, dest = '' )
@command.top(@number, lines) do |chunk|
dest << chunk
end
dest
end
# Fetches the message header.
# The optional +dest+ argument is obsolete.
#
# This method raises a POPError if an error occurs.
def header( dest = '' )
top(0, dest)
end
# Marks a message for deletion on the server. Deletion does not
# actually occur until the end of the session; deletion may be
# cancelled for _all_ marked messages by calling POP3#reset().
#
# This method raises a POPError if an error occurs.
#
# # Example
# POP3.start('pop.example.com', 110,
# 'YourAccount, 'YourPassword') {|pop|
# n = 1
# pop.mails.each do |popmail|
# File.open("inbox/#{n}", 'w') {|f|
# f.write popmail.pop
# }
# popmail.delete ####
# n += 1
# end
# }
#
def delete
@command.dele @number
@deleted = true
end
alias delete! delete #:nodoc: obsolete
# True if the mail has been deleted.
def deleted?
@deleted
end
# Returns the unique-id of the message.
# Normally the unique-id is a hash string of the message.
#
# This method raises a POPError if an error occurs.
def unique_id
return @uid if @uid
@pop.set_all_uids
@uid
end
alias uidl unique_id
def uid=( uid ) #:nodoc: internal use only (used from POP3#set_all_uids)
@uid = uid
end
end # class POPMail
class POP3Command #:nodoc: internal use only
def initialize( sock )
@socket = sock
@error_occured = false
res = check_response(critical { recv_response() })
@apop_stamp = res.slice(/<.+>/)
end
def inspect
"#<#{self.class} socket=#{@socket}>"
end
def auth( account, password )
check_response_auth(critical {
check_response_auth(get_response('USER ' + account))
get_response('PASS ' + password)
})
end
def apop( account, password )
raise POPAuthenticationError, 'not APOP server; cannot login' \
unless @apop_stamp
check_response_auth(critical {
get_response('APOP %s %s',
account,
Digest::MD5.hexdigest(@apop_stamp + password))
})
end
def list
critical {
getok 'LIST'
list = []
@socket.each_list_item do |line|
m = /\A(\d+)[ \t]+(\d+)/.match(line) or
raise POPBadResponse, "bad response: #{line}"
list.push [m[1].to_i, m[2].to_i]
end
return list
}
end
def stat
res = check_response(critical { get_response('STAT') })
m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
raise POPBadResponse, "wrong response format: #{res}"
[m[1].to_i, m[2].to_i]
end
def rset
check_response(critical { get_response 'RSET' })
end
def top( num, lines = 0, &block )
critical {
getok('TOP %d %d', num, lines)
@socket.each_message_chunk(&block)
}
end
def retr( num, &block )
critical {
getok('RETR %d', num)
@socket.each_message_chunk(&block)
}
end
def dele( num )
check_response(critical { get_response('DELE %d', num) })
end
def uidl( num = nil )
if num
res = check_response(critical { get_response('UIDL %d', num) })
return res.split(/ /)[1]
else
critical {
getok('UIDL')
table = {}
@socket.each_list_item do |line|
num, uid = line.split
table[num.to_i] = uid
end
return table
}
end
end
def quit
check_response(critical { get_response('QUIT') })
end
private
def getok( fmt, *fargs )
@socket.writeline sprintf(fmt, *fargs)
check_response(recv_response())
end
def get_response( fmt, *fargs )
@socket.writeline sprintf(fmt, *fargs)
recv_response()
end
def recv_response
@socket.readline
end
def check_response( res )
raise POPError, res unless /\A\+OK/i === res
res
end
def check_response_auth( res )
raise POPAuthenticationError, res unless /\A\+OK/i === res
res
end
def critical
return '+OK dummy ok response' if @error_occured
begin
return yield()
rescue Exception
@error_occured = true
raise
end
end
end # class POP3Command
end # module Net