Fix up Rubinius specific library specs.
[rbx.git] / lib / net / pop.rb
blob33597de702acc4f554802bacf90fa912bedf8b85
1 # = net/pop.rb
3 # Copyright (c) 1999-2003 Yukihiro Matsumoto.
5 # Copyright (c) 1999-2003 Minero Aoki.
6
7 # Written & maintained by Minero Aoki <aamine@loveruby.net>.
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 or GNU General Public License.
14
15 # NOTE: You can find Japanese version of this document in
16 # the doc/net directory of the standard ruby interpreter package.
17
18 #   $Id: pop.rb 11708 2007-02-12 23:01:19Z shyouhei $
20 # See Net::POP3 for documentation.
23 require 'net/protocol'
24 require 'digest/md5'
26 module Net
28   # Non-authentication POP3 protocol error
29   # (reply code "-ERR", except authentication).
30   class POPError < ProtocolError; end
32   # POP3 authentication error.
33   class POPAuthenticationError < ProtoAuthError; end
35   # Unexpected response from the server.
36   class POPBadResponse < POPError; end
38   #
39   # = Net::POP3
40   #
41   # == What is This Library?
42   # 
43   # This library provides functionality for retrieving 
44   # email via POP3, the Post Office Protocol version 3. For details
45   # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
46   # 
47   # == Examples
48   # 
49   # === Retrieving Messages 
50   # 
51   # This example retrieves messages from the server and deletes them 
52   # on the server.
53   #
54   # Messages are written to files named 'inbox/1', 'inbox/2', ....
55   # Replace 'pop.example.com' with your POP3 server address, and
56   # 'YourAccount' and 'YourPassword' with the appropriate account
57   # details.
58   # 
59   #     require 'net/pop'
60   # 
61   #     pop = Net::POP3.new('pop.example.com')
62   #     pop.start('YourAccount', 'YourPassword')             # (1)
63   #     if pop.mails.empty?
64   #       puts 'No mail.'
65   #     else
66   #       i = 0
67   #       pop.each_mail do |m|   # or "pop.mails.each ..."   # (2)
68   #         File.open("inbox/#{i}", 'w') do |f|
69   #           f.write m.pop
70   #         end
71   #         m.delete
72   #         i += 1
73   #       end
74   #       puts "#{pop.mails.size} mails popped."
75   #     end
76   #     pop.finish                                           # (3)
77   # 
78   # 1. Call Net::POP3#start and start POP session.
79   # 2. Access messages by using POP3#each_mail and/or POP3#mails.
80   # 3. Close POP session by calling POP3#finish or use the block form of #start.
81   # 
82   # === Shortened Code
83   # 
84   # The example above is very verbose. You can shorten the code by using
85   # some utility methods. First, the block form of Net::POP3.start can
86   # be used instead of POP3.new, POP3#start and POP3#finish.
87   # 
88   #     require 'net/pop'
89   # 
90   #     Net::POP3.start('pop.example.com', 110,
91   #                     'YourAccount', 'YourPassword') do |pop|
92   #       if pop.mails.empty?
93   #         puts 'No mail.'
94   #       else
95   #         i = 0
96   #         pop.each_mail do |m|   # or "pop.mails.each ..."
97   #           File.open("inbox/#{i}", 'w') do |f|
98   #             f.write m.pop
99   #           end
100   #           m.delete
101   #           i += 1
102   #         end
103   #         puts "#{pop.mails.size} mails popped."
104   #       end
105   #     end
106   # 
107   # POP3#delete_all is an alternative for #each_mail and #delete.
108   # 
109   #     require 'net/pop'
110   # 
111   #     Net::POP3.start('pop.example.com', 110,
112   #                     'YourAccount', 'YourPassword') do |pop|
113   #       if pop.mails.empty?
114   #         puts 'No mail.'
115   #       else
116   #         i = 1
117   #         pop.delete_all do |m|
118   #           File.open("inbox/#{i}", 'w') do |f|
119   #             f.write m.pop
120   #           end
121   #           i += 1
122   #         end
123   #       end
124   #     end
125   # 
126   # And here is an even shorter example.
127   # 
128   #     require 'net/pop'
129   # 
130   #     i = 0
131   #     Net::POP3.delete_all('pop.example.com', 110,
132   #                          'YourAccount', 'YourPassword') do |m|
133   #       File.open("inbox/#{i}", 'w') do |f|
134   #         f.write m.pop
135   #       end
136   #       i += 1
137   #     end
138   # 
139   # === Memory Space Issues
140   # 
141   # All the examples above get each message as one big string.
142   # This example avoids this.
143   # 
144   #     require 'net/pop'
145   # 
146   #     i = 1
147   #     Net::POP3.delete_all('pop.example.com', 110,
148   #                          'YourAccount', 'YourPassword') do |m|
149   #       File.open("inbox/#{i}", 'w') do |f|
150   #         m.pop do |chunk|    # get a message little by little.
151   #           f.write chunk
152   #         end
153   #         i += 1
154   #       end
155   #     end
156   # 
157   # === Using APOP
158   # 
159   # The net/pop library supports APOP authentication.
160   # To use APOP, use the Net::APOP class instead of the Net::POP3 class.
161   # You can use the utility method, Net::POP3.APOP(). For example:
162   # 
163   #     require 'net/pop'
164   # 
165   #     # Use APOP authentication if $isapop == true
166   #     pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
167   #     pop.start(YourAccount', 'YourPassword') do |pop|
168   #       # Rest of the code is the same.
169   #     end
170   # 
171   # === Fetch Only Selected Mail Using 'UIDL' POP Command
172   # 
173   # If your POP server provides UIDL functionality,
174   # you can grab only selected mails from the POP server.
175   # e.g.
176   # 
177   #     def need_pop?( id )
178   #       # determine if we need pop this mail...
179   #     end
180   # 
181   #     Net::POP3.start('pop.example.com', 110,
182   #                     'Your account', 'Your password') do |pop|
183   #       pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m|
184   #         do_something(m.pop)
185   #       end
186   #     end
187   # 
188   # The POPMail#unique_id() method returns the unique-id of the message as a
189   # String. Normally the unique-id is a hash of the message.
190   # 
191   class POP3 < Protocol
193     Revision = %q$Revision: 11708 $.split[1]
195     #
196     # Class Parameters
197     #
199     # The default port for POP3 connections, port 110
200     def POP3.default_port
201       110
202     end
204     def POP3.socket_type   #:nodoc: obsolete
205       Net::InternetMessageIO
206     end
208     #
209     # Utilities
210     #
212     # Returns the APOP class if +isapop+ is true; otherwise, returns
213     # the POP class.  For example:
214     #
215     #     # Example 1
216     #     pop = Net::POP3::APOP($is_apop).new(addr, port)
217     #
218     #     # Example 2
219     #     Net::POP3::APOP($is_apop).start(addr, port) do |pop|
220     #       ....
221     #     end
222     #
223     def POP3.APOP( isapop )
224       isapop ? APOP : POP3
225     end
227     # Starts a POP3 session and iterates over each POPMail object,
228     # yielding it to the +block+.
229     # This method is equivalent to:
230     #
231     #     Net::POP3.start(address, port, account, password) do |pop|
232     #       pop.each_mail do |m|
233     #         yield m
234     #       end
235     #     end
236     #
237     # This method raises a POPAuthenticationError if authentication fails.
238     #
239     # === Example
240     #
241     #     Net::POP3.foreach('pop.example.com', 110,
242     #                       'YourAccount', 'YourPassword') do |m|
243     #       file.write m.pop
244     #       m.delete if $DELETE
245     #     end
246     #
247     def POP3.foreach( address, port = nil,
248                       account = nil, password = nil,
249                       isapop = false, &block )  # :yields: message
250       start(address, port, account, password, isapop) {|pop|
251         pop.each_mail(&block)
252       }
253     end
255     # Starts a POP3 session and deletes all messages on the server.
256     # If a block is given, each POPMail object is yielded to it before
257     # being deleted.
258     #
259     # This method raises a POPAuthenticationError if authentication fails.
260     #
261     # === Example
262     #
263     #     Net::POP3.delete_all('pop.example.com', 110,
264     #                          'YourAccount', 'YourPassword') do |m|
265     #       file.write m.pop
266     #     end
267     #
268     def POP3.delete_all( address, port = nil,
269                          account = nil, password = nil,
270                          isapop = false, &block )
271       start(address, port, account, password, isapop) {|pop|
272         pop.delete_all(&block)
273       }
274     end
276     # Opens a POP3 session, attempts authentication, and quits.
277     #
278     # This method raises POPAuthenticationError if authentication fails.
279     #
280     # === Example: normal POP3
281     #
282     #     Net::POP3.auth_only('pop.example.com', 110,
283     #                         'YourAccount', 'YourPassword')
284     #
285     # === Example: APOP
286     #
287     #     Net::POP3.auth_only('pop.example.com', 110,
288     #                         'YourAccount', 'YourPassword', true)
289     #
290     def POP3.auth_only( address, port = nil,
291                         account = nil, password = nil,
292                         isapop = false )
293       new(address, port, isapop).auth_only account, password
294     end
296     # Starts a pop3 session, attempts authentication, and quits.
297     # This method must not be called while POP3 session is opened.
298     # This method raises POPAuthenticationError if authentication fails.
299     def auth_only( account, password )
300       raise IOError, 'opening previously opened POP session' if started?
301       start(account, password) {
302         ;
303       }
304     end
306     #
307     # Session management
308     #
310     # Creates a new POP3 object and open the connection.  Equivalent to 
311     #
312     #   Net::POP3.new(address, port, isapop).start(account, password)
313     #
314     # If +block+ is provided, yields the newly-opened POP3 object to it,
315     # and automatically closes it at the end of the session.
316     #
317     # === Example
318     #
319     #    Net::POP3.start(addr, port, account, password) do |pop|
320     #      pop.each_mail do |m|
321     #        file.write m.pop
322     #        m.delete
323     #      end
324     #    end
325     #
326     def POP3.start( address, port = nil,
327                     account = nil, password = nil,
328                     isapop = false, &block ) # :yield: pop
329       new(address, port, isapop).start(account, password, &block)
330     end
332     # Creates a new POP3 object.
333     #
334     # +address+ is the hostname or ip address of your POP3 server.
335     #
336     # The optional +port+ is the port to connect to; it defaults to 110.
337     #
338     # The optional +isapop+ specifies whether this connection is going
339     # to use APOP authentication; it defaults to +false+.
340     #
341     # This method does *not* open the TCP connection.
342     def initialize( addr, port = nil, isapop = false )
343       @address = addr
344       @port = port || self.class.default_port
345       @apop = isapop
347       @command = nil
348       @socket = nil
349       @started = false
350       @open_timeout = 30
351       @read_timeout = 60
352       @debug_output = nil
354       @mails = nil
355       @n_mails = nil
356       @n_bytes = nil
357     end
359     # Does this instance use APOP authentication?
360     def apop?
361       @apop
362     end
364     # Provide human-readable stringification of class state.
365     def inspect
366       "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
367     end
369     # *WARNING*: This method causes a serious security hole.
370     # Use this method only for debugging.
371     #
372     # Set an output stream for debugging.
373     #
374     # === Example
375     #
376     #   pop = Net::POP.new(addr, port)
377     #   pop.set_debug_output $stderr
378     #   pop.start(account, passwd) do |pop|
379     #     ....
380     #   end
381     #
382     def set_debug_output( arg )
383       @debug_output = arg
384     end
386     # The address to connect to.
387     attr_reader :address
389     # The port number to connect to.
390     attr_reader :port
392     # Seconds to wait until a connection is opened.
393     # If the POP3 object cannot open a connection within this time,
394     # it raises a TimeoutError exception.
395     attr_accessor :open_timeout
397     # Seconds to wait until reading one block (by one read(1) call).
398     # If the POP3 object cannot complete a read() within this time,
399     # it raises a TimeoutError exception.
400     attr_reader :read_timeout
402     # Set the read timeout.
403     def read_timeout=( sec )
404       @command.socket.read_timeout = sec if @command
405       @read_timeout = sec
406     end
408     # +true+ if the POP3 session has started.
409     def started?
410       @started
411     end
413     alias active? started?   #:nodoc: obsolete
415     # Starts a POP3 session.
416     #
417     # When called with block, gives a POP3 object to the block and
418     # closes the session after block call finishes.
419     #
420     # This method raises a POPAuthenticationError if authentication fails.
421     def start( account, password ) # :yield: pop
422       raise IOError, 'POP session already started' if @started
424       if block_given?
425         begin
426           do_start account, password
427           return yield(self)
428         ensure
429           do_finish
430         end
431       else
432         do_start account, password
433         return self
434       end
435     end
437     def do_start( account, password )
438       @socket = self.class.socket_type.old_open(@address, @port,
439                                    @open_timeout, @read_timeout, @debug_output)
440       on_connect
441       @command = POP3Command.new(@socket)
442       if apop?
443         @command.apop account, password
444       else
445         @command.auth account, password
446       end
447       @started = true
448     ensure
449       do_finish if not @started
450     end
451     private :do_start
453     def on_connect
454     end
455     private :on_connect
457     # Finishes a POP3 session and closes TCP connection.
458     def finish
459       raise IOError, 'POP session not yet started' unless started?
460       do_finish
461     end
463     def do_finish
464       @mails = nil
465       @command.quit if @command
466     ensure
467       @started = false
468       @command = nil
469       @socket.close if @socket and not @socket.closed?
470       @socket = nil
471     end
472     private :do_finish
474     def command
475       raise IOError, 'POP session not opened yet' \
476                                       if not @socket or @socket.closed?
477       @command
478     end
479     private :command
481     #
482     # POP protocol wrapper
483     #
485     # Returns the number of messages on the POP server.
486     def n_mails
487       return @n_mails if @n_mails
488       @n_mails, @n_bytes = command().stat
489       @n_mails
490     end
492     # Returns the total size in bytes of all the messages on the POP server.
493     def n_bytes
494       return @n_bytes if @n_bytes
495       @n_mails, @n_bytes = command().stat
496       @n_bytes
497     end
499     # Returns an array of Net::POPMail objects, representing all the
500     # messages on the server.  This array is renewed when the session
501     # restarts; otherwise, it is fetched from the server the first time
502     # this method is called (directly or indirectly) and cached.
503     #
504     # This method raises a POPError if an error occurs.
505     def mails
506       return @mails.dup if @mails
507       if n_mails() == 0
508         # some popd raises error for LIST on the empty mailbox.
509         @mails = []
510         return []
511       end
513       @mails = command().list.map {|num, size|
514         POPMail.new(num, size, self, command())
515       }
516       @mails.dup
517     end
519     # Yields each message to the passed-in block in turn.
520     # Equivalent to:
521     # 
522     #   pop3.mails.each do |popmail|
523     #     ....
524     #   end
525     #
526     # This method raises a POPError if an error occurs.
527     def each_mail( &block )  # :yield: message
528       mails().each(&block)
529     end
531     alias each each_mail
533     # Deletes all messages on the server.
534     #
535     # If called with a block, yields each message in turn before deleting it.
536     #
537     # === Example
538     #
539     #     n = 1
540     #     pop.delete_all do |m|
541     #       File.open("inbox/#{n}") do |f|
542     #         f.write m.pop
543     #       end
544     #       n += 1
545     #     end
546     #
547     # This method raises a POPError if an error occurs.
548     #
549     def delete_all # :yield: message
550       mails().each do |m|
551         yield m if block_given?
552         m.delete unless m.deleted?
553       end
554     end
556     # Resets the session.  This clears all "deleted" marks from messages.
557     #
558     # This method raises a POPError if an error occurs.
559     def reset
560       command().rset
561       mails().each do |m|
562         m.instance_eval {
563           @deleted = false
564         }
565       end
566     end
568     def set_all_uids   #:nodoc: internal use only (called from POPMail#uidl)
569       command().uidl.each do |num, uid|
570         @mails.find {|m| m.number == num }.uid = uid
571       end
572     end
574   end   # class POP3
576   # class aliases
577   POP = POP3
578   POPSession  = POP3
579   POP3Session = POP3
581   #
582   # This class is equivalent to POP3, except that it uses APOP authentication.
583   #
584   class APOP < POP3
585     # Always returns true.
586     def apop?
587       true
588     end
589   end
591   # class aliases
592   APOPSession = APOP
594   #
595   # This class represents a message which exists on the POP server.
596   # Instances of this class are created by the POP3 class; they should
597   # not be directly created by the user.
598   #
599   class POPMail
601     def initialize( num, len, pop, cmd )   #:nodoc:
602       @number = num
603       @length = len
604       @pop = pop
605       @command = cmd
606       @deleted = false
607       @uid = nil
608     end
610     # The sequence number of the message on the server.
611     attr_reader :number
613     # The length of the message in octets.
614     attr_reader :length
615     alias size length
617     # Provide human-readable stringification of class state.
618     def inspect
619       "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
620     end
622     #
623     # This method fetches the message.  If called with a block, the
624     # message is yielded to the block one chunk at a time.  If called
625     # without a block, the message is returned as a String.  The optional 
626     # +dest+ argument will be prepended to the returned String; this
627     # argument is essentially obsolete.
628     #
629     # === Example without block
630     #
631     #     POP3.start('pop.example.com', 110,
632     #                'YourAccount, 'YourPassword') do |pop|
633     #       n = 1
634     #       pop.mails.each do |popmail|
635     #         File.open("inbox/#{n}", 'w') do |f|
636     #           f.write popmail.pop              
637     #         end
638     #         popmail.delete
639     #         n += 1
640     #       end
641     #     end
642     #
643     # === Example with block
644     #
645     #     POP3.start('pop.example.com', 110,
646     #                'YourAccount, 'YourPassword') do |pop|
647     #       n = 1
648     #       pop.mails.each do |popmail|
649     #         File.open("inbox/#{n}", 'w') do |f|
650     #           popmail.pop do |chunk|            ####
651     #             f.write chunk
652     #           end
653     #         end
654     #         n += 1
655     #       end
656     #     end
657     #
658     # This method raises a POPError if an error occurs.
659     #
660     def pop( dest = '', &block ) # :yield: message_chunk
661       if block_given?
662         @command.retr(@number, &block)
663         nil
664       else
665         @command.retr(@number) do |chunk|
666           dest << chunk
667         end
668         dest
669       end
670     end
672     alias all pop    #:nodoc: obsolete
673     alias mail pop   #:nodoc: obsolete
675     # Fetches the message header and +lines+ lines of body. 
676     #
677     # The optional +dest+ argument is obsolete.
678     #
679     # This method raises a POPError if an error occurs.
680     def top( lines, dest = '' )
681       @command.top(@number, lines) do |chunk|
682         dest << chunk
683       end
684       dest
685     end
687     # Fetches the message header.     
688     #
689     # The optional +dest+ argument is obsolete.
690     #
691     # This method raises a POPError if an error occurs.
692     def header( dest = '' )
693       top(0, dest)
694     end
696     # Marks a message for deletion on the server.  Deletion does not
697     # actually occur until the end of the session; deletion may be
698     # cancelled for _all_ marked messages by calling POP3#reset().
699     #
700     # This method raises a POPError if an error occurs.
701     #
702     # === Example
703     #
704     #     POP3.start('pop.example.com', 110,
705     #                'YourAccount, 'YourPassword') do |pop|
706     #       n = 1
707     #       pop.mails.each do |popmail|
708     #         File.open("inbox/#{n}", 'w') do |f|
709     #           f.write popmail.pop
710     #         end
711     #         popmail.delete         ####
712     #         n += 1
713     #       end
714     #     end
715     #
716     def delete
717       @command.dele @number
718       @deleted = true
719     end
721     alias delete! delete    #:nodoc: obsolete
723     # True if the mail has been deleted.
724     def deleted?
725       @deleted
726     end
728     # Returns the unique-id of the message.
729     # Normally the unique-id is a hash string of the message.
730     #
731     # This method raises a POPError if an error occurs.
732     def unique_id
733       return @uid if @uid
734       @pop.set_all_uids
735       @uid
736     end
738     alias uidl unique_id
740     def uid=( uid )   #:nodoc: internal use only (used from POP3#set_all_uids)
741       @uid = uid
742     end
744   end   # class POPMail
747   class POP3Command   #:nodoc: internal use only
749     def initialize( sock )
750       @socket = sock
751       @error_occured = false
752       res = check_response(critical { recv_response() })
753       @apop_stamp = res.slice(/<.+>/)
754     end
756     def inspect
757       "#<#{self.class} socket=#{@socket}>"
758     end
760     def auth( account, password )
761       check_response_auth(critical {
762         check_response_auth(get_response('USER %s', account))
763         get_response('PASS %s', password)
764       })
765     end
767     def apop( account, password )
768       raise POPAuthenticationError, 'not APOP server; cannot login' \
769                                                       unless @apop_stamp
770       check_response_auth(critical {
771         get_response('APOP %s %s',
772                      account,
773                      Digest::MD5.hexdigest(@apop_stamp + password))
774       })
775     end
777     def list
778       critical {
779         getok 'LIST'
780         list = []
781         @socket.each_list_item do |line|
782           m = /\A(\d+)[ \t]+(\d+)/.match(line) or
783                   raise POPBadResponse, "bad response: #{line}"
784           list.push  [m[1].to_i, m[2].to_i]
785         end
786         return list
787       }
788     end
790     def stat
791       res = check_response(critical { get_response('STAT') })
792       m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
793               raise POPBadResponse, "wrong response format: #{res}"
794       [m[1].to_i, m[2].to_i]
795     end
797     def rset
798       check_response(critical { get_response 'RSET' })
799     end
801     def top( num, lines = 0, &block )
802       critical {
803         getok('TOP %d %d', num, lines)
804         @socket.each_message_chunk(&block)
805       }
806     end
808     def retr( num, &block )
809       critical {
810         getok('RETR %d', num)
811         @socket.each_message_chunk(&block)
812       }
813     end
814     
815     def dele( num )
816       check_response(critical { get_response('DELE %d', num) })
817     end
819     def uidl( num = nil )
820       if num
821         res = check_response(critical { get_response('UIDL %d', num) })
822         return res.split(/ /)[1]
823       else
824         critical {
825           getok('UIDL')
826           table = {}
827           @socket.each_list_item do |line|
828             num, uid = line.split
829             table[num.to_i] = uid
830           end
831           return table
832         }
833       end
834     end
836     def quit
837       check_response(critical { get_response('QUIT') })
838     end
840     private
842     def getok( fmt, *fargs )
843       @socket.writeline sprintf(fmt, *fargs)
844       check_response(recv_response())
845     end
847     def get_response( fmt, *fargs )
848       @socket.writeline sprintf(fmt, *fargs)
849       recv_response()
850     end
852     def recv_response
853       @socket.readline
854     end
856     def check_response( res )
857       raise POPError, res unless /\A\+OK/i === res
858       res
859     end
861     def check_response_auth( res )
862       raise POPAuthenticationError, res unless /\A\+OK/i === res
863       res
864     end
866     def critical
867       return '+OK dummy ok response' if @error_occured
868       begin
869         return yield()
870       rescue Exception
871         @error_occured = true
872         raise
873       end
874     end
876   end   # class POP3Command
878 end   # module Net