=begin = net/pop.rb Copyright (c) 1999-2003 Yukihiro Matsumoto Copyright (c) 1999-2003 Minero Aoki written & maintained by Minero Aoki 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 Module? This module provides your program the function to retrieve mails via POP3, Post Office Protocol version 3. For details of POP3, refer [RFC1939] (()). == Examples === Retrieving Mails This example retrieves mails from server and delete it (on server). Mails are written in file named 'inbox/1', 'inbox/2', .... Replace 'pop3.server.address' your POP3 server address. require 'net/pop' pop = Net::POP3.new('pop.example.com', 110) 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 mails by using POP3#each_mail and/or POP3#mails (3) close POP session by calling POP3#finish or use block form #start. This example is using block form #start to close the session. === Enshort Code The example above is very verbose. You can enshort code by using some utility methods. At first, block form of Net::POP3.start can alternates 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 more 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 Issue All examples above get mail as one big string. This example does not create such one. 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 Net::APOP class instead of Net::POP3 class. You can use utility method, Net::POP3.APOP(). 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 function, you can pop only selected mails from 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 } POPMail#unique_id method returns the unique-id of the message (String). Normally unique-id is a hash of the message. == class Net::POP3 === Class Methods : new( address, port = 110, isapop = false ) creates a new Net::POP3 object. This method does NOT open TCP connection yet. : start( address, port = 110, account, password, isapop = false ) : start( address, port = 110, account, password, isapop = false ) {|pop| .... } equals to Net::POP3.new(address, port, isapop).start(account, password). This method raises POPAuthenticationError if authentication is failed. # Typical usage Net::POP3.start(addr, port, account, password) {|pop| pop.each_mail do |m| file.write m.pop m.delete end } : APOP( is_apop ) returns Net::APOP class object if IS_APOP. Else, IS_APOP. Use this method like: # Example 1 pop = Net::POP3::APOP($is_apop).new(addr, port) # Example 2 Net::POP3::APOP($is_apop).start(addr, port) {|pop| .... } : foreach( address, port = 110, account, password, isapop = false ) {|popmail| .... } starts POP3 protocol and iterates for each POPMail object. This method equals to: Net::POP3.start(address, port, account, password) {|pop| pop.each_mail do |m| yield m end } This method raises POPAuthenticationError if authentication is failed. # Typical usage Net::POP3.foreach('pop.example.com', 110, 'YourAccount', 'YourPassword') do |m| file.write m.pop m.delete if $DELETE end : delete_all( address, port = 110, account, password, isapop = false ) : delete_all( address, port = 110, account, password, isapop = false ) {|popmail| .... } starts POP3 session and delete all mails. If block is given, iterates for each POPMail object before delete. This method raises POPAuthenticationError if authentication is failed. # Example Net::POP3.delete_all('pop.example.com', 110, 'YourAccount', 'YourPassword') do |m| file.write m.pop end : auth_only( address, port = 110, account, password, isapop = false ) (just for POP-before-SMTP) opens POP3 session and does autholize and quit. This method must not be called while POP3 session is opened. This method raises POPAuthenticationError if authentication is failed. # 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) === Instance Methods : start( account, password ) : start( account, password ) {|pop| .... } starts POP3 session. When called with block, gives a POP3 object to block and closes the session after block call finish. This method raises POPAuthenticationError if authentication is failed. : started? : active? OBSOLETE true if POP3 session is started. : address the address to connect : port the port number to connect : open_timeout : open_timeout=(n) seconds to wait until connection is opened. If POP3 object cannot open a conection in this seconds, it raises TimeoutError exception. : read_timeout : read_timeout=(n) seconds to wait until reading one block (by one read(1) call). If POP3 object cannot open a conection in this seconds, it raises TimeoutError exception. : finish finishes POP3 session. If POP3 session had not be started, raises an IOError. : n_mails returns the number of mails on the POP server. : n_bytes returns the bytes of all mails on the POP server. : mails an array of Net::POPMail objects. This array is renewed when session restarts. This method raises POPError if any problem happened. : each_mail {|popmail| .... } : each {|popmail| .... } is equals to: pop3.mails.each do |popmail| .... end This method raises POPError if any problem happened. : delete_all : delete_all {|popmail| .... } deletes all mails on server. If called with block, gives mails to the block before deleting. # Example n = 1 pop.delete_all do |m| File.open("inbox/#{n}") {|f| f.write m.pop } n += 1 end This method raises POPError if any problem happened. : reset reset the session. All "deleted mark" are removed. This method raises POPError if any problem happened. : set_debug_output( output ) WARNING: This method causes serious security hole. Use this method for only debugging. set output stream for debugging. # Example pop = Net::POP.new(addr, port) pop.set_debug_output $stderr pop.start(account, passwd) { .... } == class Net::APOP This class defines no new methods. Only difference from POP3 is using APOP authentification. === Super Class Net::POP3 == class Net::POPMail A class of mail which exists on POP server. === Instance Methods : pop This method fetches a message as a String. This method may raise POPError. # 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 } : pop {|chunk| .... } gives the block parts of the message. This method may raise POPError. # Example 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 } : header fetches the message header. This method may raise POPError. : top( lines ) fetches the message header and LINES lines of body. This method may raise POPError. : delete deletes the message on the POP server. This method may raise POPError. # 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 } : length : size the length of the message (in octets). : deleted? true if mail was deleted. : unique_id returns the unique-id of the message. Normally unique-id is a hash string of the message. This method may raise POPError. == POP3 Related Exception Classes : POPError POP3 protocol error (reply code "-ERR", except authentication). ancestors: ProtocolError (obsolete) : POPAuthenticationError POP3 authentication error. ancestors: POPError, ProtoAuthError (obsolete), ProtocolError (obsolete) : POPBadResponse Unexpected response got from server. ancestors: POPError =end require 'net/protocol' require 'digest/md5' module Net class POPError < ProtocolError; end class POPAuthenticationError < ProtoAuthError; end class POPBadResponse < POPError; end class POP3 < Protocol Revision = %q$Revision$.split[1] # # Class Parameters # def POP3.default_port 110 end # obsolete def POP3.socket_type Net::InternetMessageIO end # # Utilities # def POP3.APOP( isapop ) isapop ? APOP : POP3 end def POP3.foreach( address, port = nil, account = nil, password = nil, isapop = false, &block ) start(address, port, account, password, isapop) {|pop| pop.each_mail(&block) } 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 def POP3.auth_only( address, port = nil, account = nil, password = nil, isapop = false ) new(address, port, isapop).auth_only account, password end def auth_only( account, password ) raise IOError, 'opening already opened POP session' if started? start(account, password) { ; } end # # Session management # def POP3.start( address, port = nil, account = nil, password = nil, isapop = false, &block ) new(address, port, isapop).start(account, password, &block) end 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 def apop? @apop end def inspect "#<#{self.class} #{@address}:#{@port} open=#{@started}>" end def set_debug_output( arg ) @debug_output = arg end attr_reader :address attr_reader :port attr_accessor :open_timeout attr_reader :read_timeout def read_timeout=( sec ) @command.socket.read_timeout = sec if @command @read_timeout = sec end def started? @started end alias active? started? # obsolete def start( account, password ) raise IOError, 'POP session already started' if @started if block_given? begin do_start account, password return yield(self) ensure finish if @started 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 def finish raise IOError, 'already closed POP session' unless @started @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 # def n_mails return @n_mails if @n_mails @n_mails, @n_bytes = command().stat @n_mails end def n_bytes return @n_bytes if @n_bytes @n_mails, @n_bytes = command().stat @n_bytes end 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 def each_mail( &block ) mails().each(&block) end alias each each_mail def delete_all mails().each do |m| yield m if block_given? m.delete unless m.deleted? end end def reset command().rset mails().each do |m| m.instance_eval { @deleted = false } end end # internal use only (called from POPMail#uidl). def set_all_uids 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 class APOP < POP3 def apop? true end end APOPSession = APOP class POPMail def initialize( num, len, pop, cmd ) @number = num @length = len @pop = pop @command = cmd @deleted = false @uid = nil end attr_reader :number attr_reader :length alias size length def inspect "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" end def pop( dest = '', &block ) if block_given? @command.retr(@number, &block) nil else @command.retr(@number) do |chunk| dest << chunk end dest end end alias all pop # backward compatibility alias mail pop # backward compatibility # `dest' argument is obsolete def top( lines, dest = '' ) @command.top(@number, lines) do |chunk| dest << chunk end dest end # `dest' argument is obsolete def header( dest = '' ) top(0, dest) end def delete @command.dele @number @deleted = true end alias delete! delete # backward compatibility def deleted? @deleted end def unique_id return @uid if @uid @pop.set_all_uids @uid end alias uidl unique_id # internal use only (used from POP3#set_all_uids). def uid=( uid ) @uid = uid end end # class POPMail class POP3Command 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