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 29903 2010-11-24 07:38:32Z shyouhei $
19 #
20 # See Net::POP3 for documentation.
21 #
22
23 require 'net/protocol'
24 require 'digest/md5'
25 require 'timeout'
26
27 begin
28 require "openssl/ssl"
29 rescue LoadError
30 end
31
32 module 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 # = Net::POP3
46 #
47 # == What is This Library?
48 #
49 # This library provides functionality for retrieving
50 # email via POP3, the Post Office Protocol version 3. For details
51 # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
52 #
53 # == Examples
54 #
55 # === Retrieving Messages
56 #
57 # This example retrieves messages from the server and deletes them
58 # on the server.
59 #
60 # Messages are written to files named 'inbox/1', 'inbox/2', ....
61 # Replace 'pop.example.com' with your POP3 server address, and
62 # 'YourAccount' and 'YourPassword' with the appropriate account
63 # details.
64 #
65 # require 'net/pop'
66 #
67 # pop = Net::POP3.new('pop.example.com')
68 # pop.start('YourAccount', 'YourPassword') # (1)
69 # if pop.mails.empty?
70 # puts 'No mail.'
71 # else
72 # i = 0
73 # pop.each_mail do |m| # or "pop.mails.each ..." # (2)
74 # File.open("inbox/#{i}", 'w') do |f|
75 # f.write m.pop
76 # end
77 # m.delete
78 # i += 1
79 # end
80 # puts "#{pop.mails.size} mails popped."
81 # end
82 # pop.finish # (3)
83 #
84 # 1. Call Net::POP3#start and start POP session.
85 # 2. Access messages by using POP3#each_mail and/or POP3#mails.
86 # 3. Close POP session by calling POP3#finish or use the block form of #start.
87 #
88 # === Shortened Code
89 #
90 # The example above is very verbose. You can shorten the code by using
91 # some utility methods. First, the block form of Net::POP3.start can
92 # be used instead of POP3.new, POP3#start and POP3#finish.
93 #
94 # require 'net/pop'
95 #
96 # Net::POP3.start('pop.example.com', 110,
97 # 'YourAccount', 'YourPassword') do |pop|
98 # if pop.mails.empty?
99 # puts 'No mail.'
100 # else
101 # i = 0
102 # pop.each_mail do |m| # or "pop.mails.each ..."
103 # File.open("inbox/#{i}", 'w') do |f|
104 # f.write m.pop
105 # end
106 # m.delete
107 # i += 1
108 # end
109 # puts "#{pop.mails.size} mails popped."
110 # end
111 # end
112 #
113 # POP3#delete_all is an alternative for #each_mail and #delete.
114 #
115 # require 'net/pop'
116 #
117 # Net::POP3.start('pop.example.com', 110,
118 # 'YourAccount', 'YourPassword') do |pop|
119 # if pop.mails.empty?
120 # puts 'No mail.'
121 # else
122 # i = 1
123 # pop.delete_all do |m|
124 # File.open("inbox/#{i}", 'w') do |f|
125 # f.write m.pop
126 # end
127 # i += 1
128 # end
129 # end
130 # end
131 #
132 # And here is an even shorter example.
133 #
134 # require 'net/pop'
135 #
136 # i = 0
137 # Net::POP3.delete_all('pop.example.com', 110,
138 # 'YourAccount', 'YourPassword') do |m|
139 # File.open("inbox/#{i}", 'w') do |f|
140 # f.write m.pop
141 # end
142 # i += 1
143 # end
144 #
145 # === Memory Space Issues
146 #
147 # All the examples above get each message as one big string.
148 # This example avoids this.
149 #
150 # require 'net/pop'
151 #
152 # i = 1
153 # Net::POP3.delete_all('pop.example.com', 110,
154 # 'YourAccount', 'YourPassword') do |m|
155 # File.open("inbox/#{i}", 'w') do |f|
156 # m.pop do |chunk| # get a message little by little.
157 # f.write chunk
158 # end
159 # i += 1
160 # end
161 # end
162 #
163 # === Using APOP
164 #
165 # The net/pop library supports APOP authentication.
166 # To use APOP, use the Net::APOP class instead of the Net::POP3 class.
167 # You can use the utility method, Net::POP3.APOP(). For example:
168 #
169 # require 'net/pop'
170 #
171 # # Use APOP authentication if $isapop == true
172 # pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
173 # pop.start(YourAccount', 'YourPassword') do |pop|
174 # # Rest of the code is the same.
175 # end
176 #
177 # === Fetch Only Selected Mail Using 'UIDL' POP Command
178 #
179 # If your POP server provides UIDL functionality,
180 # you can grab only selected mails from the POP server.
181 # e.g.
182 #
183 # def need_pop?( id )
184 # # determine if we need pop this mail...
185 # end
186 #
187 # Net::POP3.start('pop.example.com', 110,
188 # 'Your account', 'Your password') do |pop|
189 # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m|
190 # do_something(m.pop)
191 # end
192 # end
193 #
194 # The POPMail#unique_id() method returns the unique-id of the message as a
195 # String. Normally the unique-id is a hash of the message.
196 #
197 class POP3 < Protocol
198
199 Revision = %q$Revision: 29903 $.split[1]
200
201 #
202 # Class Parameters
203 #
204
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 def POP3.create_ssl_params(verify_or_params = {}, certs = nil)
337 begin
338 params = verify_or_params.to_hash
339 rescue NoMethodError
340 params = {}
341 params[:verify_mode] = verify_or_params
342 if certs
343 if File.file?(certs)
344 params[:ca_file] = certs
345 elsif File.directory?(certs)
346 params[:ca_path] = certs
347 end
348 end
349 end
350 return params
351 end
352
353 # Disable SSL for all new instances.
354 def POP3.disable_ssl
355 @ssl_params = nil
356 end
357
358 def POP3.ssl_params
359 return @ssl_params
360 end
361
362 def POP3.use_ssl?
363 return !@ssl_params.nil?
364 end
365
366 def POP3.verify
367 return @ssl_params[:verify_mode]
368 end
369
370 def POP3.certs
371 return @ssl_params[:ca_file] || @ssl_params[:ca_path]
372 end
373
374 #
375 # Session management
376 #
377
378 # Creates a new POP3 object and open the connection. Equivalent to
379 #
380 # Net::POP3.new(address, port, isapop).start(account, password)
381 #
382 # If +block+ is provided, yields the newly-opened POP3 object to it,
383 # and automatically closes it at the end of the session.
384 #
385 # === Example
386 #
387 # Net::POP3.start(addr, port, account, password) do |pop|
388 # pop.each_mail do |m|
389 # file.write m.pop
390 # m.delete
391 # end
392 # end
393 #
394 def POP3.start(address, port = nil,
395 account = nil, password = nil,
396 isapop = false, &block) # :yield: pop
397 new(address, port, isapop).start(account, password, &block)
398 end
399
400 # Creates a new POP3 object.
401 #
402 # +address+ is the hostname or ip address of your POP3 server.
403 #
404 # The optional +port+ is the port to connect to.
405 #
406 # The optional +isapop+ specifies whether this connection is going
407 # to use APOP authentication; it defaults to +false+.
408 #
409 # This method does *not* open the TCP connection.
410 def initialize(addr, port = nil, isapop = false)
411 @address = addr
412 @ssl_params = POP3.ssl_params
413 @port = port
414 @apop = isapop
415
416 @command = nil
417 @socket = nil
418 @started = false
419 @open_timeout = 30
420 @read_timeout = 60
421 @debug_output = nil
422
423 @mails = nil
424 @n_mails = nil
425 @n_bytes = nil
426 end
427
428 # Does this instance use APOP authentication?
429 def apop?
430 @apop
431 end
432
433 # does this instance use SSL?
434 def use_ssl?
435 return !@ssl_params.nil?
436 end
437
438 # call-seq:
439 # Net::POP#enable_ssl(params = {})
440 #
441 # Enables SSL for this instance. Must be called before the connection is
442 # established to have any effect.
443 # +params[:port]+ is port to establish the SSL connection on; Defaults to 995.
444 # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params.
445 def enable_ssl(verify_or_params = {}, certs = nil, port = nil)
446 begin
447 @ssl_params = verify_or_params.to_hash.dup
448 @port = @ssl_params.delete(:port) || @port
449 rescue NoMethodError
450 @ssl_params = POP3.create_ssl_params(verify_or_params, certs)
451 @port = port || @port
452 end
453 end
454
455 def disable_ssl
456 @ssl_params = nil
457 end
458
459 # Provide human-readable stringification of class state.
460 def inspect
461 "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
462 end
463
464 # *WARNING*: This method causes a serious security hole.
465 # Use this method only for debugging.
466 #
467 # Set an output stream for debugging.
468 #
469 # === Example
470 #
471 # pop = Net::POP.new(addr, port)
472 # pop.set_debug_output $stderr
473 # pop.start(account, passwd) do |pop|
474 # ....
475 # end
476 #
477 def set_debug_output(arg)
478 @debug_output = arg
479 end
480
481 # The address to connect to.
482 attr_reader :address
483
484 # The port number to connect to.
485 def port
486 return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port)
487 end
488
489 # Seconds to wait until a connection is opened.
490 # If the POP3 object cannot open a connection within this time,
491 # it raises a TimeoutError exception.
492 attr_accessor :open_timeout
493
494 # Seconds to wait until reading one block (by one read(1) call).
495 # If the POP3 object cannot complete a read() within this time,
496 # it raises a TimeoutError exception.
497 attr_reader :read_timeout
498
499 # Set the read timeout.
500 def read_timeout=(sec)
501 @command.socket.read_timeout = sec if @command
502 @read_timeout = sec
503 end
504
505 # +true+ if the POP3 session has started.
506 def started?
507 @started
508 end
509
510 alias active? started? #:nodoc: obsolete
511
512 # Starts a POP3 session.
513 #
514 # When called with block, gives a POP3 object to the block and
515 # closes the session after block call finishes.
516 #
517 # This method raises a POPAuthenticationError if authentication fails.
518 def start(account, password) # :yield: pop
519 raise IOError, 'POP session already started' if @started
520 if block_given?
521 begin
522 do_start account, password
523 return yield(self)
524 ensure
525 do_finish
526 end
527 else
528 do_start account, password
529 return self
530 end
531 end
532
533 def do_start(account, password)
534 s = timeout(@open_timeout) { TCPSocket.open(@address, port) }
535 if use_ssl?
536 raise 'openssl library not installed' unless defined?(OpenSSL)
537 context = OpenSSL::SSL::SSLContext.new
538 context.set_params(@ssl_params)
539 s = OpenSSL::SSL::SSLSocket.new(s, context)
540 s.sync_close = true
541 s.connect
542 if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
543 s.post_connection_check(@address)
544 end
545 end
546 @socket = InternetMessageIO.new(s)
547 logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
548 @socket.read_timeout = @read_timeout
549 @socket.debug_output = @debug_output
550 on_connect
551 @command = POP3Command.new(@socket)
552 if apop?
553 @command.apop account, password
554 else
555 @command.auth account, password
556 end
557 @started = true
558 ensure
559 # Authentication failed, clean up connection.
560 unless @started
561 s.close if s and not s.closed?
562 @socket = nil
563 @command = nil
564 end
565 end
566 private :do_start
567
568 def on_connect
569 end
570 private :on_connect
571
572 # Finishes a POP3 session and closes TCP connection.
573 def finish
574 raise IOError, 'POP session not yet started' unless started?
575 do_finish
576 end
577
578 def do_finish
579 @mails = nil
580 @n_mails = nil
581 @n_bytes = nil
582 @command.quit if @command
583 ensure
584 @started = false
585 @command = nil
586 @socket.close if @socket and not @socket.closed?
587 @socket = nil
588 end
589 private :do_finish
590
591 def command
592 raise IOError, 'POP session not opened yet' \
593 if not @socket or @socket.closed?
594 @command
595 end
596 private :command
597
598 #
599 # POP protocol wrapper
600 #
601
602 # Returns the number of messages on the POP server.
603 def n_mails
604 return @n_mails if @n_mails
605 @n_mails, @n_bytes = command().stat
606 @n_mails
607 end
608
609 # Returns the total size in bytes of all the messages on the POP server.
610 def n_bytes
611 return @n_bytes if @n_bytes
612 @n_mails, @n_bytes = command().stat
613 @n_bytes
614 end
615
616 # Returns an array of Net::POPMail objects, representing all the
617 # messages on the server. This array is renewed when the session
618 # restarts; otherwise, it is fetched from the server the first time
619 # this method is called (directly or indirectly) and cached.
620 #
621 # This method raises a POPError if an error occurs.
622 def mails
623 return @mails.dup if @mails
624 if n_mails() == 0
625 # some popd raises error for LIST on the empty mailbox.
626 @mails = []
627 return []
628 end
629
630 @mails = command().list.map {|num, size|
631 POPMail.new(num, size, self, command())
632 }
633 @mails.dup
634 end
635
636 # Yields each message to the passed-in block in turn.
637 # Equivalent to:
638 #
639 # pop3.mails.each do |popmail|
640 # ....
641 # end
642 #
643 # This method raises a POPError if an error occurs.
644 def each_mail(&block) # :yield: message
645 mails().each(&block)
646 end
647
648 alias each each_mail
649
650 # Deletes all messages on the server.
651 #
652 # If called with a block, yields each message in turn before deleting it.
653 #
654 # === Example
655 #
656 # n = 1
657 # pop.delete_all do |m|
658 # File.open("inbox/#{n}") do |f|
659 # f.write m.pop
660 # end
661 # n += 1
662 # end
663 #
664 # This method raises a POPError if an error occurs.
665 #
666 def delete_all # :yield: message
667 mails().each do |m|
668 yield m if block_given?
669 m.delete unless m.deleted?
670 end
671 end
672
673 # Resets the session. This clears all "deleted" marks from messages.
674 #
675 # This method raises a POPError if an error occurs.
676 def reset
677 command().rset
678 mails().each do |m|
679 m.instance_eval {
680 @deleted = false
681 }
682 end
683 end
684
685 def set_all_uids #:nodoc: internal use only (called from POPMail#uidl)
686 uidl = command().uidl
687 @mails.each {|m| m.uid = uidl[m.number] }
688 end
689
690 def logging(msg)
691 @debug_output << msg + "\n" if @debug_output
692 end
693
694 end # class POP3
695
696 # class aliases
697 POP = POP3
698 POPSession = POP3
699 POP3Session = POP3
700
701 #
702 # This class is equivalent to POP3, except that it uses APOP authentication.
703 #
704 class APOP < POP3
705 # Always returns true.
706 def apop?
707 true
708 end
709 end
710
711 # class aliases
712 APOPSession = APOP
713
714 #
715 # This class represents a message which exists on the POP server.
716 # Instances of this class are created by the POP3 class; they should
717 # not be directly created by the user.
718 #
719 class POPMail
720
721 def initialize(num, len, pop, cmd) #:nodoc:
722 @number = num
723 @length = len
724 @pop = pop
725 @command = cmd
726 @deleted = false
727 @uid = nil
728 end
729
730 # The sequence number of the message on the server.
731 attr_reader :number
732
733 # The length of the message in octets.
734 attr_reader :length
735 alias size length
736
737 # Provide human-readable stringification of class state.
738 def inspect
739 "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
740 end
741
742 #
743 # This method fetches the message. If called with a block, the
744 # message is yielded to the block one chunk at a time. If called
745 # without a block, the message is returned as a String. The optional
746 # +dest+ argument will be prepended to the returned String; this
747 # argument is essentially obsolete.
748 #
749 # === Example without block
750 #
751 # POP3.start('pop.example.com', 110,
752 # 'YourAccount, 'YourPassword') do |pop|
753 # n = 1
754 # pop.mails.each do |popmail|
755 # File.open("inbox/#{n}", 'w') do |f|
756 # f.write popmail.pop
757 # end
758 # popmail.delete
759 # n += 1
760 # end
761 # end
762 #
763 # === Example with block
764 #
765 # POP3.start('pop.example.com', 110,
766 # 'YourAccount, 'YourPassword') do |pop|
767 # n = 1
768 # pop.mails.each do |popmail|
769 # File.open("inbox/#{n}", 'w') do |f|
770 # popmail.pop do |chunk| ####
771 # f.write chunk
772 # end
773 # end
774 # n += 1
775 # end
776 # end
777 #
778 # This method raises a POPError if an error occurs.
779 #
780 def pop( dest = '', &block ) # :yield: message_chunk
781 if block_given?
782 @command.retr(@number, &block)
783 nil
784 else
785 @command.retr(@number) do |chunk|
786 dest << chunk
787 end
788 dest
789 end
790 end
791
792 alias all pop #:nodoc: obsolete
793 alias mail pop #:nodoc: obsolete
794
795 # Fetches the message header and +lines+ lines of body.
796 #
797 # The optional +dest+ argument is obsolete.
798 #
799 # This method raises a POPError if an error occurs.
800 def top(lines, dest = '')
801 @command.top(@number, lines) do |chunk|
802 dest << chunk
803 end
804 dest
805 end
806
807 # Fetches the message header.
808 #
809 # The optional +dest+ argument is obsolete.
810 #
811 # This method raises a POPError if an error occurs.
812 def header(dest = '')
813 top(0, dest)
814 end
815
816 # Marks a message for deletion on the server. Deletion does not
817 # actually occur until the end of the session; deletion may be
818 # cancelled for _all_ marked messages by calling POP3#reset().
819 #
820 # This method raises a POPError if an error occurs.
821 #
822 # === Example
823 #
824 # POP3.start('pop.example.com', 110,
825 # 'YourAccount, 'YourPassword') do |pop|
826 # n = 1
827 # pop.mails.each do |popmail|
828 # File.open("inbox/#{n}", 'w') do |f|
829 # f.write popmail.pop
830 # end
831 # popmail.delete ####
832 # n += 1
833 # end
834 # end
835 #
836 def delete
837 @command.dele @number
838 @deleted = true
839 end
840
841 alias delete! delete #:nodoc: obsolete
842
843 # True if the mail has been deleted.
844 def deleted?
845 @deleted
846 end
847
848 # Returns the unique-id of the message.
849 # Normally the unique-id is a hash string of the message.
850 #
851 # This method raises a POPError if an error occurs.
852 def unique_id
853 return @uid if @uid
854 @pop.set_all_uids
855 @uid
856 end
857
858 alias uidl unique_id
859
860 def uid=(uid) #:nodoc: internal use only
861 @uid = uid
862 end
863
864 end # class POPMail
865
866
867 class POP3Command #:nodoc: internal use only
868
869 def initialize(sock)
870 @socket = sock
871 @error_occured = false
872 res = check_response(critical { recv_response() })
873 @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/)
874 end
875
876 def inspect
877 "#<#{self.class} socket=#{@socket}>"
878 end
879
880 def auth(account, password)
881 check_response_auth(critical {
882 check_response_auth(get_response('USER %s', account))
883 get_response('PASS %s', password)
884 })
885 end
886
887 def apop(account, password)
888 raise POPAuthenticationError, 'not APOP server; cannot login' \
889 unless @apop_stamp
890 check_response_auth(critical {
891 get_response('APOP %s %s',
892 account,
893 Digest::MD5.hexdigest(@apop_stamp + password))
894 })
895 end
896
897 def list
898 critical {
899 getok 'LIST'
900 list = []
901 @socket.each_list_item do |line|
902 m = /\A(\d+)[ \t]+(\d+)/.match(line) or
903 raise POPBadResponse, "bad response: #{line}"
904 list.push [m[1].to_i, m[2].to_i]
905 end
906 return list
907 }
908 end
909
910 def stat
911 res = check_response(critical { get_response('STAT') })
912 m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
913 raise POPBadResponse, "wrong response format: #{res}"
914 [m[1].to_i, m[2].to_i]
915 end
916
917 def rset
918 check_response(critical { get_response('RSET') })
919 end
920
921 def top(num, lines = 0, &block)
922 critical {
923 getok('TOP %d %d', num, lines)
924 @socket.each_message_chunk(&block)
925 }
926 end
927
928 def retr(num, &block)
929 critical {
930 getok('RETR %d', num)
931 @socket.each_message_chunk(&block)
932 }
933 end
934
935 def dele(num)
936 check_response(critical { get_response('DELE %d', num) })
937 end
938
939 def uidl(num = nil)
940 if num
941 res = check_response(critical { get_response('UIDL %d', num) })
942 return res.split(/ /)[1]
943 else
944 critical {
945 getok('UIDL')
946 table = {}
947 @socket.each_list_item do |line|
948 num, uid = line.split
949 table[num.to_i] = uid
950 end
951 return table
952 }
953 end
954 end
955
956 def quit
957 check_response(critical { get_response('QUIT') })
958 end
959
960 private
961
962 def getok(fmt, *fargs)
963 @socket.writeline sprintf(fmt, *fargs)
964 check_response(recv_response())
965 end
966
967 def get_response(fmt, *fargs)
968 @socket.writeline sprintf(fmt, *fargs)
969 recv_response()
970 end
971
972 def recv_response
973 @socket.readline
974 end
975
976 def check_response(res)
977 raise POPError, res unless /\A\+OK/i =~ res
978 res
979 end
980
981 def check_response_auth(res)
982 raise POPAuthenticationError, res unless /\A\+OK/i =~ res
983 res
984 end
985
986 def critical
987 return '+OK dummy ok response' if @error_occured
988 begin
989 return yield()
990 rescue Exception
991 @error_occured = true
992 raise
993 end
994 end
995
996 end # class POP3Command
997
998 end # module Net