1# = net/pop.rb
2#
3# Copyright (c) 1999-2007 Yukihiro Matsumoto.
4#
5# Copyright (c) 1999-2007 Minero Aoki.
6#
7# Written & maintained by Minero Aoki <aamine@loveruby.net>.
8#
9# Documented by William Webber and Minero Aoki.
10#
11# This program is free software. You can re-distribute and/or
12# modify this program under the same terms as Ruby itself,
13# Ruby Distribute License.
14#
15# NOTE: You can find Japanese version of this document at:
16# http://www.ruby-lang.org/ja/man/html/net_pop.html
17#
18#   $Id: pop.rb 44391 2013-12-24 15:46:01Z nagachika $
19#
20# See Net::POP3 for documentation.
21#
22
23require 'net/protocol'
24require 'digest/md5'
25require 'timeout'
26
27begin
28  require "openssl"
29rescue LoadError
30end
31
32module Net
33
34  # Non-authentication POP3 protocol error
35  # (reply code "-ERR", except authentication).
36  class POPError < ProtocolError; end
37
38  # POP3 authentication error.
39  class POPAuthenticationError < ProtoAuthError; end
40
41  # Unexpected response from the server.
42  class POPBadResponse < POPError; end
43
44  #
45  # == What is This Library?
46  #
47  # This library provides functionality for retrieving
48  # email via POP3, the Post Office Protocol version 3. For details
49  # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
50  #
51  # == Examples
52  #
53  # === Retrieving Messages
54  #
55  # This example retrieves messages from the server and deletes them
56  # on the server.
57  #
58  # Messages are written to files named 'inbox/1', 'inbox/2', ....
59  # Replace 'pop.example.com' with your POP3 server address, and
60  # 'YourAccount' and 'YourPassword' with the appropriate account
61  # details.
62  #
63  #     require 'net/pop'
64  #
65  #     pop = Net::POP3.new('pop.example.com')
66  #     pop.start('YourAccount', 'YourPassword')             # (1)
67  #     if pop.mails.empty?
68  #       puts 'No mail.'
69  #     else
70  #       i = 0
71  #       pop.each_mail do |m|   # or "pop.mails.each ..."   # (2)
72  #         File.open("inbox/#{i}", 'w') do |f|
73  #           f.write m.pop
74  #         end
75  #         m.delete
76  #         i += 1
77  #       end
78  #       puts "#{pop.mails.size} mails popped."
79  #     end
80  #     pop.finish                                           # (3)
81  #
82  # 1. Call Net::POP3#start and start POP session.
83  # 2. Access messages by using POP3#each_mail and/or POP3#mails.
84  # 3. Close POP session by calling POP3#finish or use the block form of #start.
85  #
86  # === Shortened Code
87  #
88  # The example above is very verbose. You can shorten the code by using
89  # some utility methods. First, the block form of Net::POP3.start can
90  # be used instead of POP3.new, POP3#start and POP3#finish.
91  #
92  #     require 'net/pop'
93  #
94  #     Net::POP3.start('pop.example.com', 110,
95  #                     'YourAccount', 'YourPassword') do |pop|
96  #       if pop.mails.empty?
97  #         puts 'No mail.'
98  #       else
99  #         i = 0
100  #         pop.each_mail do |m|   # or "pop.mails.each ..."
101  #           File.open("inbox/#{i}", 'w') do |f|
102  #             f.write m.pop
103  #           end
104  #           m.delete
105  #           i += 1
106  #         end
107  #         puts "#{pop.mails.size} mails popped."
108  #       end
109  #     end
110  #
111  # POP3#delete_all is an alternative for #each_mail and #delete.
112  #
113  #     require 'net/pop'
114  #
115  #     Net::POP3.start('pop.example.com', 110,
116  #                     'YourAccount', 'YourPassword') do |pop|
117  #       if pop.mails.empty?
118  #         puts 'No mail.'
119  #       else
120  #         i = 1
121  #         pop.delete_all do |m|
122  #           File.open("inbox/#{i}", 'w') do |f|
123  #             f.write m.pop
124  #           end
125  #           i += 1
126  #         end
127  #       end
128  #     end
129  #
130  # And here is an even shorter example.
131  #
132  #     require 'net/pop'
133  #
134  #     i = 0
135  #     Net::POP3.delete_all('pop.example.com', 110,
136  #                          'YourAccount', 'YourPassword') do |m|
137  #       File.open("inbox/#{i}", 'w') do |f|
138  #         f.write m.pop
139  #       end
140  #       i += 1
141  #     end
142  #
143  # === Memory Space Issues
144  #
145  # All the examples above get each message as one big string.
146  # This example avoids this.
147  #
148  #     require 'net/pop'
149  #
150  #     i = 1
151  #     Net::POP3.delete_all('pop.example.com', 110,
152  #                          'YourAccount', 'YourPassword') do |m|
153  #       File.open("inbox/#{i}", 'w') do |f|
154  #         m.pop do |chunk|    # get a message little by little.
155  #           f.write chunk
156  #         end
157  #         i += 1
158  #       end
159  #     end
160  #
161  # === Using APOP
162  #
163  # The net/pop library supports APOP authentication.
164  # To use APOP, use the Net::APOP class instead of the Net::POP3 class.
165  # You can use the utility method, Net::POP3.APOP(). For example:
166  #
167  #     require 'net/pop'
168  #
169  #     # Use APOP authentication if $isapop == true
170  #     pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
171  #     pop.start(YourAccount', 'YourPassword') do |pop|
172  #       # Rest of the code is the same.
173  #     end
174  #
175  # === Fetch Only Selected Mail Using 'UIDL' POP Command
176  #
177  # If your POP server provides UIDL functionality,
178  # you can grab only selected mails from the POP server.
179  # e.g.
180  #
181  #     def need_pop?( id )
182  #       # determine if we need pop this mail...
183  #     end
184  #
185  #     Net::POP3.start('pop.example.com', 110,
186  #                     'Your account', 'Your password') do |pop|
187  #       pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m|
188  #         do_something(m.pop)
189  #       end
190  #     end
191  #
192  # The POPMail#unique_id() method returns the unique-id of the message as a
193  # String. Normally the unique-id is a hash of the message.
194  #
195  class POP3 < Protocol
196
197    # svn revision of this library
198    Revision = %q$Revision: 44391 $.split[1]
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.sync_close = true
552        s.connect
553        if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
554          s.post_connection_check(@address)
555        end
556      end
557      @socket = InternetMessageIO.new(s)
558      logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
559      @socket.read_timeout = @read_timeout
560      @socket.debug_output = @debug_output
561      on_connect
562      @command = POP3Command.new(@socket)
563      if apop?
564        @command.apop account, password
565      else
566        @command.auth account, password
567      end
568      @started = true
569    ensure
570      # Authentication failed, clean up connection.
571      unless @started
572        s.close if s and not s.closed?
573        @socket = nil
574        @command = nil
575      end
576    end
577    private :do_start
578
579    # Does nothing
580    def on_connect # :nodoc:
581    end
582    private :on_connect
583
584    # Finishes a POP3 session and closes TCP connection.
585    def finish
586      raise IOError, 'POP session not yet started' unless started?
587      do_finish
588    end
589
590    # nil's out the:
591    # - mails
592    # - number counter for mails
593    # - number counter for bytes
594    # - quits the current command, if any
595    def do_finish # :nodoc:
596      @mails = nil
597      @n_mails = nil
598      @n_bytes = nil
599      @command.quit if @command
600    ensure
601      @started = false
602      @command = nil
603      @socket.close if @socket and not @socket.closed?
604      @socket = nil
605    end
606    private :do_finish
607
608    # Returns the current command.
609    #
610    # Raises IOError if there is no active socket
611    def command # :nodoc:
612      raise IOError, 'POP session not opened yet' \
613                                      if not @socket or @socket.closed?
614      @command
615    end
616    private :command
617
618    #
619    # POP protocol wrapper
620    #
621
622    # Returns the number of messages on the POP server.
623    def n_mails
624      return @n_mails if @n_mails
625      @n_mails, @n_bytes = command().stat
626      @n_mails
627    end
628
629    # Returns the total size in bytes of all the messages on the POP server.
630    def n_bytes
631      return @n_bytes if @n_bytes
632      @n_mails, @n_bytes = command().stat
633      @n_bytes
634    end
635
636    # Returns an array of Net::POPMail objects, representing all the
637    # messages on the server.  This array is renewed when the session
638    # restarts; otherwise, it is fetched from the server the first time
639    # this method is called (directly or indirectly) and cached.
640    #
641    # This method raises a POPError if an error occurs.
642    def mails
643      return @mails.dup if @mails
644      if n_mails() == 0
645        # some popd raises error for LIST on the empty mailbox.
646        @mails = []
647        return []
648      end
649
650      @mails = command().list.map {|num, size|
651        POPMail.new(num, size, self, command())
652      }
653      @mails.dup
654    end
655
656    # Yields each message to the passed-in block in turn.
657    # Equivalent to:
658    #
659    #   pop3.mails.each do |popmail|
660    #     ....
661    #   end
662    #
663    # This method raises a POPError if an error occurs.
664    def each_mail(&block)  # :yield: message
665      mails().each(&block)
666    end
667
668    alias each each_mail
669
670    # Deletes all messages on the server.
671    #
672    # If called with a block, yields each message in turn before deleting it.
673    #
674    # === Example
675    #
676    #     n = 1
677    #     pop.delete_all do |m|
678    #       File.open("inbox/#{n}") do |f|
679    #         f.write m.pop
680    #       end
681    #       n += 1
682    #     end
683    #
684    # This method raises a POPError if an error occurs.
685    #
686    def delete_all # :yield: message
687      mails().each do |m|
688        yield m if block_given?
689        m.delete unless m.deleted?
690      end
691    end
692
693    # Resets the session.  This clears all "deleted" marks from messages.
694    #
695    # This method raises a POPError if an error occurs.
696    def reset
697      command().rset
698      mails().each do |m|
699        m.instance_eval {
700          @deleted = false
701        }
702      end
703    end
704
705    def set_all_uids   #:nodoc: internal use only (called from POPMail#uidl)
706      uidl = command().uidl
707      @mails.each {|m| m.uid = uidl[m.number] }
708    end
709
710    # deguging output for +msg+
711    def logging(msg)
712      @debug_output << msg + "\n" if @debug_output
713    end
714
715  end   # class POP3
716
717  # class aliases
718  POP = POP3 # :nodoc:
719  POPSession  = POP3 # :nodoc:
720  POP3Session = POP3 # :nodoc:
721
722  #
723  # This class is equivalent to POP3, except that it uses APOP authentication.
724  #
725  class APOP < POP3
726    # Always returns true.
727    def apop?
728      true
729    end
730  end
731
732  # class aliases
733  APOPSession = APOP
734
735  #
736  # This class represents a message which exists on the POP server.
737  # Instances of this class are created by the POP3 class; they should
738  # not be directly created by the user.
739  #
740  class POPMail
741
742    def initialize(num, len, pop, cmd)   #:nodoc:
743      @number = num
744      @length = len
745      @pop = pop
746      @command = cmd
747      @deleted = false
748      @uid = nil
749    end
750
751    # The sequence number of the message on the server.
752    attr_reader :number
753
754    # The length of the message in octets.
755    attr_reader :length
756    alias size length
757
758    # Provide human-readable stringification of class state.
759    def inspect
760      "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
761    end
762
763    #
764    # This method fetches the message.  If called with a block, the
765    # message is yielded to the block one chunk at a time.  If called
766    # without a block, the message is returned as a String.  The optional
767    # +dest+ argument will be prepended to the returned String; this
768    # argument is essentially obsolete.
769    #
770    # === Example without block
771    #
772    #     POP3.start('pop.example.com', 110,
773    #                'YourAccount, 'YourPassword') do |pop|
774    #       n = 1
775    #       pop.mails.each do |popmail|
776    #         File.open("inbox/#{n}", 'w') do |f|
777    #           f.write popmail.pop
778    #         end
779    #         popmail.delete
780    #         n += 1
781    #       end
782    #     end
783    #
784    # === Example with block
785    #
786    #     POP3.start('pop.example.com', 110,
787    #                'YourAccount, 'YourPassword') do |pop|
788    #       n = 1
789    #       pop.mails.each do |popmail|
790    #         File.open("inbox/#{n}", 'w') do |f|
791    #           popmail.pop do |chunk|            ####
792    #             f.write chunk
793    #           end
794    #         end
795    #         n += 1
796    #       end
797    #     end
798    #
799    # This method raises a POPError if an error occurs.
800    #
801    def pop( dest = '', &block ) # :yield: message_chunk
802      if block_given?
803        @command.retr(@number, &block)
804        nil
805      else
806        @command.retr(@number) do |chunk|
807          dest << chunk
808        end
809        dest
810      end
811    end
812
813    alias all pop    #:nodoc: obsolete
814    alias mail pop   #:nodoc: obsolete
815
816    # Fetches the message header and +lines+ lines of body.
817    #
818    # The optional +dest+ argument is obsolete.
819    #
820    # This method raises a POPError if an error occurs.
821    def top(lines, dest = '')
822      @command.top(@number, lines) do |chunk|
823        dest << chunk
824      end
825      dest
826    end
827
828    # Fetches the message header.
829    #
830    # The optional +dest+ argument is obsolete.
831    #
832    # This method raises a POPError if an error occurs.
833    def header(dest = '')
834      top(0, dest)
835    end
836
837    # Marks a message for deletion on the server.  Deletion does not
838    # actually occur until the end of the session; deletion may be
839    # cancelled for _all_ marked messages by calling POP3#reset().
840    #
841    # This method raises a POPError if an error occurs.
842    #
843    # === Example
844    #
845    #     POP3.start('pop.example.com', 110,
846    #                'YourAccount, 'YourPassword') do |pop|
847    #       n = 1
848    #       pop.mails.each do |popmail|
849    #         File.open("inbox/#{n}", 'w') do |f|
850    #           f.write popmail.pop
851    #         end
852    #         popmail.delete         ####
853    #         n += 1
854    #       end
855    #     end
856    #
857    def delete
858      @command.dele @number
859      @deleted = true
860    end
861
862    alias delete! delete    #:nodoc: obsolete
863
864    # True if the mail has been deleted.
865    def deleted?
866      @deleted
867    end
868
869    # Returns the unique-id of the message.
870    # Normally the unique-id is a hash string of the message.
871    #
872    # This method raises a POPError if an error occurs.
873    def unique_id
874      return @uid if @uid
875      @pop.set_all_uids
876      @uid
877    end
878
879    alias uidl unique_id
880
881    def uid=(uid)   #:nodoc: internal use only
882      @uid = uid
883    end
884
885  end   # class POPMail
886
887
888  class POP3Command   #:nodoc: internal use only
889
890    def initialize(sock)
891      @socket = sock
892      @error_occurred = false
893      res = check_response(critical { recv_response() })
894      @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/)
895    end
896
897    attr_reader :socket
898
899    def inspect
900      "#<#{self.class} socket=#{@socket}>"
901    end
902
903    def auth(account, password)
904      check_response_auth(critical {
905        check_response_auth(get_response('USER %s', account))
906        get_response('PASS %s', password)
907      })
908    end
909
910    def apop(account, password)
911      raise POPAuthenticationError, 'not APOP server; cannot login' \
912                                                      unless @apop_stamp
913      check_response_auth(critical {
914        get_response('APOP %s %s',
915                     account,
916                     Digest::MD5.hexdigest(@apop_stamp + password))
917      })
918    end
919
920    def list
921      critical {
922        getok 'LIST'
923        list = []
924        @socket.each_list_item do |line|
925          m = /\A(\d+)[ \t]+(\d+)/.match(line) or
926                  raise POPBadResponse, "bad response: #{line}"
927          list.push  [m[1].to_i, m[2].to_i]
928        end
929        return list
930      }
931    end
932
933    def stat
934      res = check_response(critical { get_response('STAT') })
935      m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
936              raise POPBadResponse, "wrong response format: #{res}"
937      [m[1].to_i, m[2].to_i]
938    end
939
940    def rset
941      check_response(critical { get_response('RSET') })
942    end
943
944    def top(num, lines = 0, &block)
945      critical {
946        getok('TOP %d %d', num, lines)
947        @socket.each_message_chunk(&block)
948      }
949    end
950
951    def retr(num, &block)
952      critical {
953        getok('RETR %d', num)
954        @socket.each_message_chunk(&block)
955      }
956    end
957
958    def dele(num)
959      check_response(critical { get_response('DELE %d', num) })
960    end
961
962    def uidl(num = nil)
963      if num
964        res = check_response(critical { get_response('UIDL %d', num) })
965        return res.split(/ /)[1]
966      else
967        critical {
968          getok('UIDL')
969          table = {}
970          @socket.each_list_item do |line|
971            num, uid = line.split
972            table[num.to_i] = uid
973          end
974          return table
975        }
976      end
977    end
978
979    def quit
980      check_response(critical { get_response('QUIT') })
981    end
982
983    private
984
985    def getok(fmt, *fargs)
986      @socket.writeline sprintf(fmt, *fargs)
987      check_response(recv_response())
988    end
989
990    def get_response(fmt, *fargs)
991      @socket.writeline sprintf(fmt, *fargs)
992      recv_response()
993    end
994
995    def recv_response
996      @socket.readline
997    end
998
999    def check_response(res)
1000      raise POPError, res unless /\A\+OK/i =~ res
1001      res
1002    end
1003
1004    def check_response_auth(res)
1005      raise POPAuthenticationError, res unless /\A\+OK/i =~ res
1006      res
1007    end
1008
1009    def critical
1010      return '+OK dummy ok response' if @error_occurred
1011      begin
1012        return yield()
1013      rescue Exception
1014        @error_occurred = true
1015        raise
1016      end
1017    end
1018
1019  end   # class POP3Command
1020
1021end   # module Net
1022