rubysl-net-imap 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 841f3c85cbbc3507e5e11e282dd6198c4a244533
4
- data.tar.gz: c3ad2c88d26bb75102d40e430c4d8aa71e8a34a9
3
+ metadata.gz: 3308e84d0e8024e4ece77eec112a7334dd2dbb2a
4
+ data.tar.gz: 8b643041df74e584d02daff83af3890bd5c3b9cc
5
5
  SHA512:
6
- metadata.gz: c8071c4a15639b9388e9d1eb9425b5b4fe088955e65a7c14499a56bd886bc34c9fb75c7d4c98a39cc22b9f496443ffc2dcc61439f7495a33b6ab4ec978f5a5db
7
- data.tar.gz: 67d571ea592de7a3640dd61e10b85ea6f5850f32060493c18edb0e21a625f0c079cf74a9787368502c50d4ff52e4160ee9554ff3be2eabcfefc24b9850813347
6
+ metadata.gz: 4e67f4cb84f0d12d8b957dde9604e0c5baea70d9da4249dfc29230b2e3deff38b77c38d1fd7366ed5d01c80835359ca14c6a0d3f7f8dc830c85ade0531b37873
7
+ data.tar.gz: efac146ad3c2c8b579f2454f39b342aaa7130c83ef6da4e72473b1cd8dfb5bd3e483900eed0c5f549e0812dff90d86e47f1ef113cb98eb17f90e5a0314f3f267
@@ -1,8 +1,9 @@
1
1
  language: ruby
2
2
  before_install:
3
+ - rvm use $RVM --install --binary --fuzzy
3
4
  - gem update --system
4
5
  - gem --version
5
6
  - gem install rubysl-bundler
7
+ env:
8
+ - RVM=rbx-nightly-d21 RUBYLIB=lib
6
9
  script: bundle exec mspec spec
7
- rvm:
8
- - rbx-nightly-18mode
@@ -9,13 +9,14 @@
9
9
  # Documentation: Shugo Maeda, with RDoc conversion and overview by William
10
10
  # Webber.
11
11
  #
12
- # See Net::IMAP for documentation.
12
+ # See Net::IMAP for documentation.
13
13
  #
14
14
 
15
15
 
16
16
  require "socket"
17
17
  require "monitor"
18
18
  require "digest/md5"
19
+ require "strscan"
19
20
  begin
20
21
  require "openssl"
21
22
  rescue LoadError
@@ -44,12 +45,12 @@ module Net
44
45
  # read-only access) #examine(). Once the client has successfully
45
46
  # selected a mailbox, they enter _selected_ state, and that
46
47
  # mailbox becomes the _current_ mailbox, on which mail-item
47
- # related commands implicitly operate.
48
+ # related commands implicitly operate.
48
49
  #
49
50
  # Messages have two sorts of identifiers: message sequence
50
- # numbers, and UIDs.
51
+ # numbers, and UIDs.
51
52
  #
52
- # Message sequence numbers number messages within a mail box
53
+ # Message sequence numbers number messages within a mail box
53
54
  # from 1 up to the number of items in the mail box. If new
54
55
  # message arrives during a session, it receives a sequence
55
56
  # number equal to the new size of the mail box. If messages
@@ -57,7 +58,7 @@ module Net
57
58
  # sequence numbers "shuffled down" to fill the gaps.
58
59
  #
59
60
  # UIDs, on the other hand, are permanently guaranteed not to
60
- # identify another message within the same mailbox, even if
61
+ # identify another message within the same mailbox, even if
61
62
  # the existing message is deleted. UIDs are required to
62
63
  # be assigned in ascending (but not necessarily sequential)
63
64
  # order within a mailbox; this means that if a non-IMAP client
@@ -90,11 +91,11 @@ module Net
90
91
  # imap.store(message_id, "+FLAGS", [:Deleted])
91
92
  # end
92
93
  # imap.expunge
93
- #
94
+ #
94
95
  # == Thread Safety
95
96
  #
96
97
  # Net::IMAP supports concurrent threads. For example,
97
- #
98
+ #
98
99
  # imap = Net::IMAP.new("imap.foo.net", "imap2")
99
100
  # imap.authenticate("cram-md5", "bar", "password")
100
101
  # imap.select("inbox")
@@ -102,7 +103,7 @@ module Net
102
103
  # search_result = imap.search(["BODY", "hello"])
103
104
  # fetch_result = fetch_thread.value
104
105
  # imap.disconnect
105
- #
106
+ #
106
107
  # This script invokes the FETCH command and the SEARCH command concurrently.
107
108
  #
108
109
  # == Errors
@@ -112,9 +113,9 @@ module Net
112
113
  #
113
114
  # NO:: the attempted command could not be successfully completed. For
114
115
  # instance, the username/password used for logging in are incorrect;
115
- # the selected mailbox does not exists; etc.
116
+ # the selected mailbox does not exists; etc.
116
117
  #
117
- # BAD:: the request from the client does not follow the server's
118
+ # BAD:: the request from the client does not follow the server's
118
119
  # understanding of the IMAP protocol. This includes attempting
119
120
  # commands from the wrong client state; for instance, attempting
120
121
  # to perform a SEARCH command without having SELECTed a current
@@ -146,8 +147,8 @@ module Net
146
147
  #
147
148
  # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
148
149
  # is found to be in an incorrect format (for instance, when converting
149
- # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
150
- # thrown if a server response is non-parseable.
150
+ # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
151
+ # thrown if a server response is non-parseable.
151
152
  #
152
153
  #
153
154
  # == References
@@ -199,7 +200,7 @@ module Net
199
200
  #
200
201
  class IMAP
201
202
  include MonitorMixin
202
- if defined?(OpenSSL)
203
+ if defined?(OpenSSL::SSL)
203
204
  include OpenSSL
204
205
  include SSL
205
206
  end
@@ -269,12 +270,24 @@ module Net
269
270
  return @@debug = val
270
271
  end
271
272
 
273
+ # Returns the max number of flags interned to symbols.
274
+ def self.max_flag_count
275
+ return @@max_flag_count
276
+ end
277
+
278
+ # Sets the max number of flags interned to symbols.
279
+ def self.max_flag_count=(count)
280
+ @@max_flag_count = count
281
+ end
282
+
272
283
  # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
273
284
  # is the type of authentication this authenticator supports
274
285
  # (for instance, "LOGIN"). The +authenticator+ is an object
275
286
  # which defines a process() method to handle authentication with
276
- # the server. See Net::IMAP::LoginAuthenticator and
277
- # Net::IMAP::CramMD5Authenticator for examples.
287
+ # the server. See Net::IMAP::LoginAuthenticator,
288
+ # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
289
+ # for examples.
290
+ #
278
291
  #
279
292
  # If +auth_type+ refers to an existing authenticator, it will be
280
293
  # replaced by the new one.
@@ -282,17 +295,44 @@ module Net
282
295
  @@authenticators[auth_type] = authenticator
283
296
  end
284
297
 
298
+ # The default port for IMAP connections, port 143
299
+ def self.default_port
300
+ return PORT
301
+ end
302
+
303
+ # The default port for IMAPS connections, port 993
304
+ def self.default_tls_port
305
+ return SSL_PORT
306
+ end
307
+
308
+ class << self
309
+ alias default_imap_port default_port
310
+ alias default_imaps_port default_tls_port
311
+ alias default_ssl_port default_tls_port
312
+ end
313
+
285
314
  # Disconnects from the server.
286
315
  def disconnect
287
316
  begin
288
- # try to call SSL::SSLSocket#io.
289
- @sock.io.shutdown
290
- rescue NoMethodError
291
- # @sock is not an SSL::SSLSocket.
292
- @sock.shutdown
317
+ begin
318
+ # try to call SSL::SSLSocket#io.
319
+ @sock.io.shutdown
320
+ rescue NoMethodError
321
+ # @sock is not an SSL::SSLSocket.
322
+ @sock.shutdown
323
+ end
324
+ rescue Errno::ENOTCONN
325
+ # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
326
+ rescue Exception => e
327
+ @receiver_thread.raise(e)
293
328
  end
294
329
  @receiver_thread.join
295
- @sock.close
330
+ synchronize do
331
+ unless @sock.closed?
332
+ @sock.close
333
+ end
334
+ end
335
+ raise e if e
296
336
  end
297
337
 
298
338
  # Returns true if disconnected from the server.
@@ -307,7 +347,7 @@ module Net
307
347
  #
308
348
  # Note that the Net::IMAP class does not modify its
309
349
  # behaviour according to the capabilities of the server;
310
- # it is up to the user of the class to ensure that
350
+ # it is up to the user of the class to ensure that
311
351
  # a certain capability is supported by a server before
312
352
  # using it.
313
353
  def capability
@@ -328,12 +368,27 @@ module Net
328
368
  send_command("LOGOUT")
329
369
  end
330
370
 
371
+ # Sends a STARTTLS command to start TLS session.
372
+ def starttls(options = {}, verify = true)
373
+ send_command("STARTTLS") do |resp|
374
+ if resp.kind_of?(TaggedResponse) && resp.name == "OK"
375
+ begin
376
+ # for backward compatibility
377
+ certs = options.to_str
378
+ options = create_ssl_params(certs, verify)
379
+ rescue NoMethodError
380
+ end
381
+ start_tls_session(options)
382
+ end
383
+ end
384
+ end
385
+
331
386
  # Sends an AUTHENTICATE command to authenticate the client.
332
387
  # The +auth_type+ parameter is a string that represents
333
388
  # the authentication mechanism to be used. Currently Net::IMAP
334
389
  # supports authentication mechanisms:
335
390
  #
336
- # LOGIN:: login using cleartext user and password.
391
+ # LOGIN:: login using cleartext user and password.
337
392
  # CRAM-MD5:: login with cleartext user and encrypted password
338
393
  # (see [RFC-2195] for a full description). This
339
394
  # mechanism requires that the server have the user's
@@ -381,7 +436,7 @@ module Net
381
436
  end
382
437
 
383
438
  # Sends a SELECT command to select a +mailbox+ so that messages
384
- # in the +mailbox+ can be accessed.
439
+ # in the +mailbox+ can be accessed.
385
440
  #
386
441
  # After you have selected a mailbox, you may retrieve the
387
442
  # number of items in that mailbox from @responses["EXISTS"][-1],
@@ -432,7 +487,7 @@ module Net
432
487
  # Sends a RENAME command to change the name of the +mailbox+ to
433
488
  # +newname+.
434
489
  #
435
- # A Net::IMAP::NoResponseError is raised if a mailbox with the
490
+ # A Net::IMAP::NoResponseError is raised if a mailbox with the
436
491
  # name +mailbox+ cannot be renamed to +newname+ for whatever
437
492
  # reason; for instance, because +mailbox+ does not exist, or
438
493
  # because there is already a mailbox with the name +newname+.
@@ -479,8 +534,8 @@ module Net
479
534
  # imap.create("foo/bar")
480
535
  # imap.create("foo/baz")
481
536
  # p imap.list("", "foo/%")
482
- # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
483
- # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
537
+ # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
538
+ # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
484
539
  # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
485
540
  def list(refname, mailbox)
486
541
  synchronize do
@@ -489,6 +544,38 @@ module Net
489
544
  end
490
545
  end
491
546
 
547
+ # Sends a XLIST command, and returns a subset of names from
548
+ # the complete set of all names available to the client.
549
+ # +refname+ provides a context (for instance, a base directory
550
+ # in a directory-based mailbox hierarchy). +mailbox+ specifies
551
+ # a mailbox or (via wildcards) mailboxes under that context.
552
+ # Two wildcards may be used in +mailbox+: '*', which matches
553
+ # all characters *including* the hierarchy delimiter (for instance,
554
+ # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
555
+ # which matches all characters *except* the hierarchy delimiter.
556
+ #
557
+ # If +refname+ is empty, +mailbox+ is used directly to determine
558
+ # which mailboxes to match. If +mailbox+ is empty, the root
559
+ # name of +refname+ and the hierarchy delimiter are returned.
560
+ #
561
+ # The XLIST command is like the LIST command except that the flags
562
+ # returned refer to the function of the folder/mailbox, e.g. :Sent
563
+ #
564
+ # The return value is an array of +Net::IMAP::MailboxList+. For example:
565
+ #
566
+ # imap.create("foo/bar")
567
+ # imap.create("foo/baz")
568
+ # p imap.xlist("", "foo/%")
569
+ # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
570
+ # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
571
+ # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
572
+ def xlist(refname, mailbox)
573
+ synchronize do
574
+ send_command("XLIST", refname, mailbox)
575
+ return @responses.delete("XLIST")
576
+ end
577
+ end
578
+
492
579
  # Sends the GETQUOTAROOT command along with specified +mailbox+.
493
580
  # This command is generally available to both admin and user.
494
581
  # If mailbox exists, returns an array containing objects of
@@ -533,7 +620,7 @@ module Net
533
620
  # then that user will be stripped of any rights to that mailbox.
534
621
  # The IMAP ACL commands are described in [RFC-2086].
535
622
  def setacl(mailbox, user, rights)
536
- if rights.nil?
623
+ if rights.nil?
537
624
  send_command("SETACL", mailbox, user, "")
538
625
  else
539
626
  send_command("SETACL", mailbox, user, rights)
@@ -552,7 +639,7 @@ module Net
552
639
 
553
640
  # Sends a LSUB command, and returns a subset of names from the set
554
641
  # of names that the user has declared as being "active" or
555
- # "subscribed". +refname+ and +mailbox+ are interpreted as
642
+ # "subscribed". +refname+ and +mailbox+ are interpreted as
556
643
  # for #list().
557
644
  # The return value is an array of +Net::IMAP::MailboxList+.
558
645
  def lsub(refname, mailbox)
@@ -575,7 +662,7 @@ module Net
575
662
  # p imap.status("inbox", ["MESSAGES", "RECENT"])
576
663
  # #=> {"RECENT"=>0, "MESSAGES"=>44}
577
664
  #
578
- # A Net::IMAP::NoResponseError is raised if status values
665
+ # A Net::IMAP::NoResponseError is raised if status values
579
666
  # for +mailbox+ cannot be returned, for instance because it
580
667
  # does not exist.
581
668
  def status(mailbox, attr)
@@ -586,9 +673,9 @@ module Net
586
673
  end
587
674
 
588
675
  # Sends a APPEND command to append the +message+ to the end of
589
- # the +mailbox+. The optional +flags+ argument is an array of
676
+ # the +mailbox+. The optional +flags+ argument is an array of
590
677
  # flags to initially passing to the new message. The optional
591
- # +date_time+ argument specifies the creation time to assign to the
678
+ # +date_time+ argument specifies the creation time to assign to the
592
679
  # new message; it defaults to the current time.
593
680
  # For example:
594
681
  #
@@ -596,7 +683,7 @@ module Net
596
683
  # Subject: hello
597
684
  # From: shugo@ruby-lang.org
598
685
  # To: shugo@ruby-lang.org
599
- #
686
+ #
600
687
  # hello world
601
688
  # EOF
602
689
  #
@@ -615,7 +702,7 @@ module Net
615
702
 
616
703
  # Sends a CHECK command to request a checkpoint of the currently
617
704
  # selected mailbox. This performs implementation-specific
618
- # housekeeping, for instance, reconciling the mailbox's
705
+ # housekeeping, for instance, reconciling the mailbox's
619
706
  # in-memory and on-disk state.
620
707
  def check
621
708
  send_command("CHECK")
@@ -639,8 +726,8 @@ module Net
639
726
 
640
727
  # Sends a SEARCH command to search the mailbox for messages that
641
728
  # match the given searching criteria, and returns message sequence
642
- # numbers. +keys+ can either be a string holding the entire
643
- # search string, or a single-dimension array of search keywords and
729
+ # numbers. +keys+ can either be a string holding the entire
730
+ # search string, or a single-dimension array of search keywords and
644
731
  # arguments. The following are some common search criteria;
645
732
  # see [IMAP] section 6.4.4 for a full list.
646
733
  #
@@ -664,7 +751,7 @@ module Net
664
751
  #
665
752
  # OR <search-key> <search-key>:: "or" two search keys together.
666
753
  #
667
- # ON <date>:: messages with an internal date exactly equal to <date>,
754
+ # ON <date>:: messages with an internal date exactly equal to <date>,
668
755
  # which has a format similar to 8-Aug-2002.
669
756
  #
670
757
  # SINCE <date>:: messages with an internal date on or after <date>.
@@ -672,7 +759,7 @@ module Net
672
759
  # SUBJECT <string>:: messages with <string> in their subject.
673
760
  #
674
761
  # TO <string>:: messages with <string> in their TO field.
675
- #
762
+ #
676
763
  # For example:
677
764
  #
678
765
  # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
@@ -695,8 +782,8 @@ module Net
695
782
  # The return value is an array of Net::IMAP::FetchData. For example:
696
783
  #
697
784
  # p imap.fetch(6..8, "UID")
698
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
699
- # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
785
+ # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
786
+ # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
700
787
  # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
701
788
  # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
702
789
  # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
@@ -719,9 +806,9 @@ module Net
719
806
  end
720
807
 
721
808
  # Sends a STORE command to alter data associated with messages
722
- # in the mailbox, in particular their flags. The +set+ parameter
723
- # is a number or an array of numbers or a Range object. Each number
724
- # is a message sequence number. +attr+ is the name of a data item
809
+ # in the mailbox, in particular their flags. The +set+ parameter
810
+ # is a number or an array of numbers or a Range object. Each number
811
+ # is a message sequence number. +attr+ is the name of a data item
725
812
  # to store: 'FLAGS' means to replace the message's flag list
726
813
  # with the provided one; '+FLAGS' means to add the provided flags;
727
814
  # and '-FLAGS' means to remove them. +flags+ is a list of flags.
@@ -729,8 +816,8 @@ module Net
729
816
  # The return value is an array of Net::IMAP::FetchData. For example:
730
817
  #
731
818
  # p imap.store(6..8, "+FLAGS", [:Deleted])
732
- # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
733
- # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
819
+ # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
820
+ # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
734
821
  # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
735
822
  def store(set, attr, flags)
736
823
  return store_internal("STORE", set, attr, flags)
@@ -772,9 +859,9 @@ module Net
772
859
  return sort_internal("UID SORT", sort_keys, search_keys, charset)
773
860
  end
774
861
 
775
- # Adds a response handler. For example, to detect when
862
+ # Adds a response handler. For example, to detect when
776
863
  # the server sends us a new EXISTS response (which normally
777
- # indicates new messages being added to the mail box),
864
+ # indicates new messages being added to the mail box),
778
865
  # you could add the following handler after selecting the
779
866
  # mailbox.
780
867
  #
@@ -810,12 +897,55 @@ module Net
810
897
  return thread_internal("THREAD", algorithm, search_keys, charset)
811
898
  end
812
899
 
813
- # As for #thread(), but returns unique identifiers instead of
900
+ # As for #thread(), but returns unique identifiers instead of
814
901
  # message sequence numbers.
815
902
  def uid_thread(algorithm, search_keys, charset)
816
903
  return thread_internal("UID THREAD", algorithm, search_keys, charset)
817
904
  end
818
905
 
906
+ # Sends an IDLE command that waits for notifications of new or expunged
907
+ # messages. Yields responses from the server during the IDLE.
908
+ #
909
+ # Use #idle_done() to leave IDLE.
910
+ def idle(&response_handler)
911
+ raise LocalJumpError, "no block given" unless response_handler
912
+
913
+ response = nil
914
+
915
+ synchronize do
916
+ tag = Thread.current[:net_imap_tag] = generate_tag
917
+ put_string("#{tag} IDLE#{CRLF}")
918
+
919
+ begin
920
+ add_response_handler(response_handler)
921
+ @idle_done_cond = new_cond
922
+ @idle_done_cond.wait
923
+ @idle_done_cond = nil
924
+ if @receiver_thread_terminating
925
+ raise Net::IMAP::Error, "connection closed"
926
+ end
927
+ ensure
928
+ unless @receiver_thread_terminating
929
+ remove_response_handler(response_handler)
930
+ put_string("DONE#{CRLF}")
931
+ response = get_tagged_response(tag, "IDLE")
932
+ end
933
+ end
934
+ end
935
+
936
+ return response
937
+ end
938
+
939
+ # Leaves IDLE.
940
+ def idle_done
941
+ synchronize do
942
+ if @idle_done_cond.nil?
943
+ raise Net::IMAP::Error, "not during IDLE"
944
+ end
945
+ @idle_done_cond.signal
946
+ end
947
+ end
948
+
819
949
  # Decode a string from modified UTF-7 format to UTF-8.
820
950
  #
821
951
  # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
@@ -825,48 +955,64 @@ module Net
825
955
  # Net::IMAP does _not_ automatically encode and decode
826
956
  # mailbox names to and from utf7.
827
957
  def self.decode_utf7(s)
828
- return s.gsub(/&(.*?)-/n) {
829
- if $1.empty?
830
- "&"
958
+ return s.gsub(/&([^-]+)?-/n) {
959
+ if $1
960
+ ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
831
961
  else
832
- base64 = $1.tr(",", "/")
833
- x = base64.length % 4
834
- if x > 0
835
- base64.concat("=" * (4 - x))
836
- end
837
- u16tou8(base64.unpack("m")[0])
962
+ "&"
838
963
  end
839
964
  }
840
965
  end
841
966
 
842
967
  # Encode a string from UTF-8 format to modified UTF-7.
843
968
  def self.encode_utf7(s)
844
- return s.gsub(/(&)|([^\x20-\x7e]+)/u) { |x|
969
+ return s.gsub(/(&)|[^\x20-\x7e]+/) {
845
970
  if $1
846
971
  "&-"
847
972
  else
848
- base64 = [u8tou16(x)].pack("m")
973
+ base64 = [$&.encode(Encoding::UTF_16BE)].pack("m")
849
974
  "&" + base64.delete("=\n").tr("/", ",") + "-"
850
975
  end
851
- }
976
+ }.force_encoding("ASCII-8BIT")
977
+ end
978
+
979
+ # Formats +time+ as an IMAP-style date.
980
+ def self.format_date(time)
981
+ return time.strftime('%d-%b-%Y')
982
+ end
983
+
984
+ # Formats +time+ as an IMAP-style date-time.
985
+ def self.format_datetime(time)
986
+ return time.strftime('%d-%b-%Y %H:%M %z')
852
987
  end
853
988
 
854
989
  private
855
990
 
856
991
  CRLF = "\r\n" # :nodoc:
857
992
  PORT = 143 # :nodoc:
993
+ SSL_PORT = 993 # :nodoc:
858
994
 
859
995
  @@debug = false
860
996
  @@authenticators = {}
997
+ @@max_flag_count = 10000
861
998
 
999
+ # :call-seq:
1000
+ # Net::IMAP.new(host, options = {})
1001
+ #
862
1002
  # Creates a new Net::IMAP object and connects it to the specified
863
- # +port+ (143 by default) on the named +host+. If +usessl+ is true,
864
- # then an attempt will
865
- # be made to use SSL (now TLS) to connect to the server. For this
866
- # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
867
- # extensions need to be installed. The +certs+ parameter indicates
868
- # the path or file containing the CA cert of the server, and the
869
- # +verify+ parameter is for the OpenSSL verification callback.
1003
+ # +host+.
1004
+ #
1005
+ # +options+ is an option hash, each key of which is a symbol.
1006
+ #
1007
+ # The available options are:
1008
+ #
1009
+ # port:: port number (default value is 143 for imap, or 993 for imaps)
1010
+ # ssl:: if options[:ssl] is true, then an attempt will be made
1011
+ # to use SSL (now TLS) to connect to the server. For this to work
1012
+ # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
1013
+ # be installed.
1014
+ # if options[:ssl] is a hash, it's passed to
1015
+ # OpenSSL::SSL::SSLContext#set_params as parameters.
870
1016
  #
871
1017
  # The most common errors are:
872
1018
  #
@@ -876,60 +1022,66 @@ module Net
876
1022
  # being dropped by an intervening firewall).
877
1023
  # Errno::ENETUNREACH:: there is no route to that network.
878
1024
  # SocketError:: hostname not known or other socket error.
879
- # Net::IMAP::ByeResponseError:: we connected to the host, but they
1025
+ # Net::IMAP::ByeResponseError:: we connected to the host, but they
880
1026
  # immediately said goodbye to us.
881
- def initialize(host, port = PORT, usessl = false, certs = nil, verify = false)
1027
+ def initialize(host, port_or_options = {},
1028
+ usessl = false, certs = nil, verify = true)
882
1029
  super()
883
1030
  @host = host
884
- @port = port
1031
+ begin
1032
+ options = port_or_options.to_hash
1033
+ rescue NoMethodError
1034
+ # for backward compatibility
1035
+ options = {}
1036
+ options[:port] = port_or_options
1037
+ if usessl
1038
+ options[:ssl] = create_ssl_params(certs, verify)
1039
+ end
1040
+ end
1041
+ @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
885
1042
  @tag_prefix = "RUBY"
886
1043
  @tagno = 0
887
1044
  @parser = ResponseParser.new
888
- @sock = TCPSocket.open(host, port)
889
- if usessl
890
- unless defined?(OpenSSL)
891
- raise "SSL extension not installed"
892
- end
1045
+ @sock = TCPSocket.open(@host, @port)
1046
+ if options[:ssl]
1047
+ start_tls_session(options[:ssl])
893
1048
  @usessl = true
894
-
895
- # verify the server.
896
- context = SSLContext::new()
897
- context.ca_file = certs if certs && FileTest::file?(certs)
898
- context.ca_path = certs if certs && FileTest::directory?(certs)
899
- context.verify_mode = VERIFY_PEER if verify
900
- if defined?(VerifyCallbackProc)
901
- context.verify_callback = VerifyCallbackProc
902
- end
903
- @sock = SSLSocket.new(@sock, context)
904
- @sock.sync_close = true
905
- @sock.connect # start ssl session.
906
- @sock.post_connection_check(@host) if verify
907
1049
  else
908
1050
  @usessl = false
909
1051
  end
910
1052
  @responses = Hash.new([].freeze)
911
1053
  @tagged_responses = {}
912
1054
  @response_handlers = []
913
- @response_arrival = new_cond
914
- @continuation_request = nil
1055
+ @tagged_response_arrival = new_cond
1056
+ @continuation_request_arrival = new_cond
1057
+ @idle_done_cond = nil
915
1058
  @logout_command_tag = nil
916
1059
  @debug_output_bol = true
917
1060
  @exception = nil
918
1061
 
919
1062
  @greeting = get_response
1063
+ if @greeting.nil?
1064
+ @sock.close
1065
+ raise Error, "connection closed"
1066
+ end
920
1067
  if @greeting.name == "BYE"
921
1068
  @sock.close
922
- raise ByeResponseError, @greeting.raw_data
1069
+ raise ByeResponseError, @greeting
923
1070
  end
924
1071
 
925
1072
  @client_thread = Thread.current
926
1073
  @receiver_thread = Thread.start {
927
- receive_responses
1074
+ begin
1075
+ receive_responses
1076
+ rescue Exception
1077
+ end
928
1078
  }
1079
+ @receiver_thread_terminating = false
929
1080
  end
930
1081
 
931
1082
  def receive_responses
932
- while true
1083
+ connection_closed = false
1084
+ until connection_closed
933
1085
  synchronize do
934
1086
  @exception = nil
935
1087
  end
@@ -937,7 +1089,7 @@ module Net
937
1089
  resp = get_response
938
1090
  rescue Exception => e
939
1091
  synchronize do
940
- @sock.close unless @sock.closed?
1092
+ @sock.close
941
1093
  @exception = e
942
1094
  end
943
1095
  break
@@ -953,7 +1105,7 @@ module Net
953
1105
  case resp
954
1106
  when TaggedResponse
955
1107
  @tagged_responses[resp.tag] = resp
956
- @response_arrival.broadcast
1108
+ @tagged_response_arrival.broadcast
957
1109
  if resp.tag == @logout_command_tag
958
1110
  return
959
1111
  end
@@ -965,13 +1117,11 @@ module Net
965
1117
  end
966
1118
  if resp.name == "BYE" && @logout_command_tag.nil?
967
1119
  @sock.close
968
- @exception = ByeResponseError.new(resp.raw_data)
969
- @response_arrival.broadcast
970
- return
1120
+ @exception = ByeResponseError.new(resp)
1121
+ connection_closed = true
971
1122
  end
972
1123
  when ContinuationRequest
973
- @continuation_request = resp
974
- @response_arrival.broadcast
1124
+ @continuation_request_arrival.signal
975
1125
  end
976
1126
  @response_handlers.each do |handler|
977
1127
  handler.call(resp)
@@ -980,30 +1130,32 @@ module Net
980
1130
  rescue Exception => e
981
1131
  @exception = e
982
1132
  synchronize do
983
- @response_arrival.broadcast
1133
+ @tagged_response_arrival.broadcast
1134
+ @continuation_request_arrival.broadcast
984
1135
  end
985
1136
  end
986
1137
  end
987
1138
  synchronize do
988
- @response_arrival.broadcast
1139
+ @receiver_thread_terminating = true
1140
+ @tagged_response_arrival.broadcast
1141
+ @continuation_request_arrival.broadcast
1142
+ if @idle_done_cond
1143
+ @idle_done_cond.signal
1144
+ end
989
1145
  end
990
1146
  end
991
1147
 
992
- def get_tagged_response(tag)
1148
+ def get_tagged_response(tag, cmd)
993
1149
  until @tagged_responses.key?(tag)
994
1150
  raise @exception if @exception
995
- @response_arrival.wait
1151
+ @tagged_response_arrival.wait
996
1152
  end
997
- return pick_up_tagged_response(tag)
998
- end
999
-
1000
- def pick_up_tagged_response(tag)
1001
1153
  resp = @tagged_responses.delete(tag)
1002
1154
  case resp.name
1003
1155
  when /\A(?:NO)\z/ni
1004
- raise NoResponseError, resp.data.text
1156
+ raise NoResponseError, resp
1005
1157
  when /\A(?:BAD)\z/ni
1006
- raise BadResponseError, resp.data.text
1158
+ raise BadResponseError, resp
1007
1159
  else
1008
1160
  return resp
1009
1161
  end
@@ -1038,7 +1190,10 @@ module Net
1038
1190
 
1039
1191
  def send_command(cmd, *args, &block)
1040
1192
  synchronize do
1041
- tag = Thread.current[:net_imap_tag] = generate_tag
1193
+ args.each do |i|
1194
+ validate_data(i)
1195
+ end
1196
+ tag = generate_tag
1042
1197
  put_string(tag + " " + cmd)
1043
1198
  args.each do |i|
1044
1199
  put_string(" ")
@@ -1052,7 +1207,7 @@ module Net
1052
1207
  add_response_handler(block)
1053
1208
  end
1054
1209
  begin
1055
- return get_tagged_response(tag)
1210
+ return get_tagged_response(tag, cmd)
1056
1211
  ensure
1057
1212
  if block
1058
1213
  remove_response_handler(block)
@@ -1065,7 +1220,7 @@ module Net
1065
1220
  @tagno += 1
1066
1221
  return format("%s%04d", @tag_prefix, @tagno)
1067
1222
  end
1068
-
1223
+
1069
1224
  def put_string(str)
1070
1225
  @sock.print(str)
1071
1226
  if @@debug
@@ -1081,6 +1236,25 @@ module Net
1081
1236
  end
1082
1237
  end
1083
1238
 
1239
+ def validate_data(data)
1240
+ case data
1241
+ when nil
1242
+ when String
1243
+ when Integer
1244
+ if data < 0 || data >= 4294967296
1245
+ raise DataFormatError, num.to_s
1246
+ end
1247
+ when Array
1248
+ data.each do |i|
1249
+ validate_data(i)
1250
+ end
1251
+ when Time
1252
+ when Symbol
1253
+ else
1254
+ data.validate
1255
+ end
1256
+ end
1257
+
1084
1258
  def send_data(data)
1085
1259
  case data
1086
1260
  when nil
@@ -1114,30 +1288,19 @@ module Net
1114
1288
  put_string(str)
1115
1289
  end
1116
1290
  end
1117
-
1291
+
1118
1292
  def send_quoted_string(str)
1119
1293
  put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
1120
1294
  end
1121
1295
 
1122
1296
  def send_literal(str)
1123
- put_string("{" + str.length.to_s + "}" + CRLF)
1124
- while @continuation_request.nil? &&
1125
- !@tagged_responses.key?(Thread.current[:net_imap_tag])
1126
- @response_arrival.wait
1127
- raise @exception if @exception
1128
- end
1129
- if @continuation_request.nil?
1130
- pick_up_tagged_response(Thread.current[:net_imap_tag])
1131
- raise ResponseError.new("expected continuation request")
1132
- end
1133
- @continuation_request = nil
1297
+ put_string("{" + str.bytesize.to_s + "}" + CRLF)
1298
+ @continuation_request_arrival.wait
1299
+ raise @exception if @exception
1134
1300
  put_string(str)
1135
1301
  end
1136
1302
 
1137
1303
  def send_number_data(num)
1138
- if num < 0 || num >= 4294967296
1139
- raise DataFormatError, num.to_s
1140
- end
1141
1304
  put_string(num.to_s)
1142
1305
  end
1143
1306
 
@@ -1252,130 +1415,56 @@ module Net
1252
1415
  end
1253
1416
  end
1254
1417
 
1255
- def self.u16tou8(s)
1256
- len = s.length
1257
- if len < 2
1258
- return ""
1259
- end
1260
- buf = ""
1261
- i = 0
1262
- while i < len
1263
- c = s[i] << 8 | s[i + 1]
1264
- i += 2
1265
- if c == 0xfeff
1266
- next
1267
- elsif c < 0x0080
1268
- buf.concat(c)
1269
- elsif c < 0x0800
1270
- b2 = c & 0x003f
1271
- b1 = c >> 6
1272
- buf.concat(b1 | 0xc0)
1273
- buf.concat(b2 | 0x80)
1274
- elsif c >= 0xdc00 && c < 0xe000
1275
- raise DataFormatError, "invalid surrogate detected"
1276
- elsif c >= 0xd800 && c < 0xdc00
1277
- if i + 2 > len
1278
- raise DataFormatError, "invalid surrogate detected"
1279
- end
1280
- low = s[i] << 8 | s[i + 1]
1281
- i += 2
1282
- if low < 0xdc00 || low > 0xdfff
1283
- raise DataFormatError, "invalid surrogate detected"
1284
- end
1285
- c = (((c & 0x03ff)) << 10 | (low & 0x03ff)) + 0x10000
1286
- b4 = c & 0x003f
1287
- b3 = (c >> 6) & 0x003f
1288
- b2 = (c >> 12) & 0x003f
1289
- b1 = c >> 18;
1290
- buf.concat(b1 | 0xf0)
1291
- buf.concat(b2 | 0x80)
1292
- buf.concat(b3 | 0x80)
1293
- buf.concat(b4 | 0x80)
1294
- else # 0x0800-0xffff
1295
- b3 = c & 0x003f
1296
- b2 = (c >> 6) & 0x003f
1297
- b1 = c >> 12
1298
- buf.concat(b1 | 0xe0)
1299
- buf.concat(b2 | 0x80)
1300
- buf.concat(b3 | 0x80)
1301
- end
1302
- end
1303
- return buf
1304
- end
1305
- private_class_method :u16tou8
1306
-
1307
- def self.u8tou16(s)
1308
- len = s.length
1309
- buf = ""
1310
- i = 0
1311
- while i < len
1312
- c = s[i]
1313
- if (c & 0x80) == 0
1314
- buf.concat(0x00)
1315
- buf.concat(c)
1316
- i += 1
1317
- elsif (c & 0xe0) == 0xc0 &&
1318
- len >= 2 &&
1319
- (s[i + 1] & 0xc0) == 0x80
1320
- if c == 0xc0 || c == 0xc1
1321
- raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1322
- end
1323
- u = ((c & 0x1f) << 6) | (s[i + 1] & 0x3f)
1324
- buf.concat(u >> 8)
1325
- buf.concat(u & 0x00ff)
1326
- i += 2
1327
- elsif (c & 0xf0) == 0xe0 &&
1328
- i + 2 < len &&
1329
- (s[i + 1] & 0xc0) == 0x80 &&
1330
- (s[i + 2] & 0xc0) == 0x80
1331
- if c == 0xe0 && s[i + 1] < 0xa0
1332
- raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1333
- end
1334
- u = ((c & 0x0f) << 12) | ((s[i + 1] & 0x3f) << 6) | (s[i + 2] & 0x3f)
1335
- # surrogate chars
1336
- if u >= 0xd800 && u <= 0xdfff
1337
- raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1338
- end
1339
- buf.concat(u >> 8)
1340
- buf.concat(u & 0x00ff)
1341
- i += 3
1342
- elsif (c & 0xf8) == 0xf0 &&
1343
- i + 3 < len &&
1344
- (s[i + 1] & 0xc0) == 0x80 &&
1345
- (s[i + 2] & 0xc0) == 0x80 &&
1346
- (s[i + 3] & 0xc0) == 0x80
1347
- if c == 0xf0 && s[i + 1] < 0x90
1348
- raise DataFormatError, format("non-shortest UTF-8 sequence (%02x)", c)
1349
- end
1350
- u = ((c & 0x07) << 18) | ((s[i + 1] & 0x3f) << 12) |
1351
- ((s[i + 2] & 0x3f) << 6) | (s[i + 3] & 0x3f)
1352
- if u < 0x10000
1353
- buf.concat(u >> 8)
1354
- buf.concat(u & 0x00ff)
1355
- elsif u < 0x110000
1356
- high = ((u - 0x10000) >> 10) | 0xd800
1357
- low = (u & 0x03ff) | 0xdc00
1358
- buf.concat(high >> 8)
1359
- buf.concat(high & 0x00ff)
1360
- buf.concat(low >> 8)
1361
- buf.concat(low & 0x00ff)
1362
- else
1363
- raise DataFormatError, format("none-UTF-16 char detected (%04x)", u)
1364
- end
1365
- i += 4
1366
- else
1367
- raise DataFormatError, format("illegal UTF-8 sequence (%02x)", c)
1418
+ def create_ssl_params(certs = nil, verify = true)
1419
+ params = {}
1420
+ if certs
1421
+ if File.file?(certs)
1422
+ params[:ca_file] = certs
1423
+ elsif File.directory?(certs)
1424
+ params[:ca_path] = certs
1368
1425
  end
1369
1426
  end
1370
- return buf
1427
+ if verify
1428
+ params[:verify_mode] = VERIFY_PEER
1429
+ else
1430
+ params[:verify_mode] = VERIFY_NONE
1431
+ end
1432
+ return params
1433
+ end
1434
+
1435
+ def start_tls_session(params = {})
1436
+ unless defined?(OpenSSL::SSL)
1437
+ raise "SSL extension not installed"
1438
+ end
1439
+ if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
1440
+ raise RuntimeError, "already using SSL"
1441
+ end
1442
+ begin
1443
+ params = params.to_hash
1444
+ rescue NoMethodError
1445
+ params = {}
1446
+ end
1447
+ context = SSLContext.new
1448
+ context.set_params(params)
1449
+ if defined?(VerifyCallbackProc)
1450
+ context.verify_callback = VerifyCallbackProc
1451
+ end
1452
+ @sock = SSLSocket.new(@sock, context)
1453
+ @sock.sync_close = true
1454
+ @sock.connect
1455
+ if context.verify_mode != VERIFY_NONE
1456
+ @sock.post_connection_check(@host)
1457
+ end
1371
1458
  end
1372
- private_class_method :u8tou16
1373
1459
 
1374
1460
  class RawData # :nodoc:
1375
1461
  def send_data(imap)
1376
1462
  imap.send(:put_string, @data)
1377
1463
  end
1378
1464
 
1465
+ def validate
1466
+ end
1467
+
1379
1468
  private
1380
1469
 
1381
1470
  def initialize(data)
@@ -1388,6 +1477,9 @@ module Net
1388
1477
  imap.send(:put_string, @data)
1389
1478
  end
1390
1479
 
1480
+ def validate
1481
+ end
1482
+
1391
1483
  private
1392
1484
 
1393
1485
  def initialize(data)
@@ -1400,6 +1492,9 @@ module Net
1400
1492
  imap.send(:send_quoted_string, @data)
1401
1493
  end
1402
1494
 
1495
+ def validate
1496
+ end
1497
+
1403
1498
  private
1404
1499
 
1405
1500
  def initialize(data)
@@ -1412,6 +1507,9 @@ module Net
1412
1507
  imap.send(:send_literal, @data)
1413
1508
  end
1414
1509
 
1510
+ def validate
1511
+ end
1512
+
1415
1513
  private
1416
1514
 
1417
1515
  def initialize(data)
@@ -1424,6 +1522,10 @@ module Net
1424
1522
  imap.send(:put_string, format_internal(@data))
1425
1523
  end
1426
1524
 
1525
+ def validate
1526
+ validate_internal(@data)
1527
+ end
1528
+
1427
1529
  private
1428
1530
 
1429
1531
  def initialize(data)
@@ -1435,7 +1537,6 @@ module Net
1435
1537
  when "*"
1436
1538
  return data
1437
1539
  when Integer
1438
- ensure_nz_number(data)
1439
1540
  if data == -1
1440
1541
  return "*"
1441
1542
  else
@@ -1449,6 +1550,23 @@ module Net
1449
1550
  when ThreadMember
1450
1551
  return data.seqno.to_s +
1451
1552
  ":" + data.children.collect {|i| format_internal(i).join(",")}
1553
+ end
1554
+ end
1555
+
1556
+ def validate_internal(data)
1557
+ case data
1558
+ when "*"
1559
+ when Integer
1560
+ ensure_nz_number(data)
1561
+ when Range
1562
+ when Array
1563
+ data.each do |i|
1564
+ validate_internal(i)
1565
+ end
1566
+ when ThreadMember
1567
+ data.children.each do |i|
1568
+ validate_internal(i)
1569
+ end
1452
1570
  else
1453
1571
  raise DataFormatError, data.inspect
1454
1572
  end
@@ -1464,109 +1582,109 @@ module Net
1464
1582
  end
1465
1583
 
1466
1584
  # Net::IMAP::ContinuationRequest represents command continuation requests.
1467
- #
1585
+ #
1468
1586
  # The command continuation request response is indicated by a "+" token
1469
1587
  # instead of a tag. This form of response indicates that the server is
1470
1588
  # ready to accept the continuation of a command from the client. The
1471
1589
  # remainder of this response is a line of text.
1472
- #
1590
+ #
1473
1591
  # continue_req ::= "+" SPACE (resp_text / base64)
1474
- #
1592
+ #
1475
1593
  # ==== Fields:
1476
- #
1594
+ #
1477
1595
  # data:: Returns the data (Net::IMAP::ResponseText).
1478
- #
1596
+ #
1479
1597
  # raw_data:: Returns the raw data string.
1480
1598
  ContinuationRequest = Struct.new(:data, :raw_data)
1481
1599
 
1482
1600
  # Net::IMAP::UntaggedResponse represents untagged responses.
1483
- #
1601
+ #
1484
1602
  # Data transmitted by the server to the client and status responses
1485
1603
  # that do not indicate command completion are prefixed with the token
1486
1604
  # "*", and are called untagged responses.
1487
- #
1605
+ #
1488
1606
  # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1489
1607
  # mailbox_data / message_data / capability_data)
1490
- #
1608
+ #
1491
1609
  # ==== Fields:
1492
- #
1610
+ #
1493
1611
  # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1494
- #
1612
+ #
1495
1613
  # data:: Returns the data such as an array of flag symbols,
1496
1614
  # a ((<Net::IMAP::MailboxList>)) object....
1497
- #
1615
+ #
1498
1616
  # raw_data:: Returns the raw data string.
1499
1617
  UntaggedResponse = Struct.new(:name, :data, :raw_data)
1500
-
1618
+
1501
1619
  # Net::IMAP::TaggedResponse represents tagged responses.
1502
- #
1620
+ #
1503
1621
  # The server completion result response indicates the success or
1504
1622
  # failure of the operation. It is tagged with the same tag as the
1505
1623
  # client command which began the operation.
1506
- #
1624
+ #
1507
1625
  # response_tagged ::= tag SPACE resp_cond_state CRLF
1508
- #
1626
+ #
1509
1627
  # tag ::= 1*<any ATOM_CHAR except "+">
1510
- #
1628
+ #
1511
1629
  # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1512
- #
1630
+ #
1513
1631
  # ==== Fields:
1514
- #
1632
+ #
1515
1633
  # tag:: Returns the tag.
1516
- #
1634
+ #
1517
1635
  # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1518
- #
1636
+ #
1519
1637
  # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1520
- #
1638
+ #
1521
1639
  # raw_data:: Returns the raw data string.
1522
1640
  #
1523
1641
  TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1524
-
1642
+
1525
1643
  # Net::IMAP::ResponseText represents texts of responses.
1526
1644
  # The text may be prefixed by the response code.
1527
- #
1645
+ #
1528
1646
  # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1529
1647
  # ;; text SHOULD NOT begin with "[" or "="
1530
- #
1648
+ #
1531
1649
  # ==== Fields:
1532
- #
1650
+ #
1533
1651
  # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1534
- #
1652
+ #
1535
1653
  # text:: Returns the text.
1536
- #
1654
+ #
1537
1655
  ResponseText = Struct.new(:code, :text)
1538
1656
 
1539
- #
1657
+ #
1540
1658
  # Net::IMAP::ResponseCode represents response codes.
1541
- #
1659
+ #
1542
1660
  # resp_text_code ::= "ALERT" / "PARSE" /
1543
1661
  # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1544
1662
  # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1545
1663
  # "UIDVALIDITY" SPACE nz_number /
1546
1664
  # "UNSEEN" SPACE nz_number /
1547
1665
  # atom [SPACE 1*<any TEXT_CHAR except "]">]
1548
- #
1666
+ #
1549
1667
  # ==== Fields:
1550
- #
1668
+ #
1551
1669
  # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1552
- #
1670
+ #
1553
1671
  # data:: Returns the data if it exists.
1554
1672
  #
1555
1673
  ResponseCode = Struct.new(:name, :data)
1556
1674
 
1557
1675
  # Net::IMAP::MailboxList represents contents of the LIST response.
1558
- #
1676
+ #
1559
1677
  # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
1560
1678
  # "\Noselect" / "\Unmarked" / flag_extension) ")"
1561
1679
  # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1562
- #
1680
+ #
1563
1681
  # ==== Fields:
1564
- #
1682
+ #
1565
1683
  # attr:: Returns the name attributes. Each name attribute is a symbol
1566
1684
  # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1567
- #
1685
+ #
1568
1686
  # delim:: Returns the hierarchy delimiter
1569
- #
1687
+ #
1570
1688
  # name:: Returns the mailbox name.
1571
1689
  #
1572
1690
  MailboxList = Struct.new(:attr, :delim, :name)
@@ -1575,78 +1693,78 @@ module Net
1575
1693
  # This object can also be a response to GETQUOTAROOT. In the syntax
1576
1694
  # specification below, the delimiter used with the "#" construct is a
1577
1695
  # single space (SPACE).
1578
- #
1696
+ #
1579
1697
  # quota_list ::= "(" #quota_resource ")"
1580
- #
1698
+ #
1581
1699
  # quota_resource ::= atom SPACE number SPACE number
1582
- #
1700
+ #
1583
1701
  # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
1584
- #
1702
+ #
1585
1703
  # ==== Fields:
1586
- #
1704
+ #
1587
1705
  # mailbox:: The mailbox with the associated quota.
1588
- #
1706
+ #
1589
1707
  # usage:: Current storage usage of mailbox.
1590
- #
1708
+ #
1591
1709
  # quota:: Quota limit imposed on mailbox.
1592
1710
  #
1593
1711
  MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1594
1712
 
1595
1713
  # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
1596
1714
  # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
1597
- #
1715
+ #
1598
1716
  # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1599
- #
1717
+ #
1600
1718
  # ==== Fields:
1601
- #
1719
+ #
1602
1720
  # mailbox:: The mailbox with the associated quota.
1603
- #
1721
+ #
1604
1722
  # quotaroots:: Zero or more quotaroots that effect the quota on the
1605
1723
  # specified mailbox.
1606
1724
  #
1607
1725
  MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1608
1726
 
1609
1727
  # Net::IMAP::MailboxACLItem represents response from GETACL.
1610
- #
1728
+ #
1611
1729
  # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1612
- #
1730
+ #
1613
1731
  # identifier ::= astring
1614
- #
1732
+ #
1615
1733
  # rights ::= astring
1616
- #
1734
+ #
1617
1735
  # ==== Fields:
1618
- #
1736
+ #
1619
1737
  # user:: Login name that has certain rights to the mailbox
1620
1738
  # that was specified with the getacl command.
1621
- #
1739
+ #
1622
1740
  # rights:: The access rights the indicated user has to the
1623
1741
  # mailbox.
1624
1742
  #
1625
- MailboxACLItem = Struct.new(:user, :rights)
1743
+ MailboxACLItem = Struct.new(:user, :rights, :mailbox)
1626
1744
 
1627
1745
  # Net::IMAP::StatusData represents contents of the STATUS response.
1628
- #
1746
+ #
1629
1747
  # ==== Fields:
1630
- #
1748
+ #
1631
1749
  # mailbox:: Returns the mailbox name.
1632
- #
1750
+ #
1633
1751
  # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1634
1752
  # "UIDVALIDITY", "UNSEEN". Each value is a number.
1635
- #
1753
+ #
1636
1754
  StatusData = Struct.new(:mailbox, :attr)
1637
1755
 
1638
1756
  # Net::IMAP::FetchData represents contents of the FETCH response.
1639
- #
1757
+ #
1640
1758
  # ==== Fields:
1641
- #
1759
+ #
1642
1760
  # seqno:: Returns the message sequence number.
1643
1761
  # (Note: not the unique identifier, even for the UID command response.)
1644
- #
1762
+ #
1645
1763
  # attr:: Returns a hash. Each key is a data item name, and each value is
1646
1764
  # its value.
1647
- #
1765
+ #
1648
1766
  # The current data items are:
1649
- #
1767
+ #
1650
1768
  # [BODY]
1651
1769
  # A form of BODYSTRUCTURE without extension data.
1652
1770
  # [BODY[<section>]<<origin_octet>>]
@@ -1673,67 +1791,67 @@ module Net
1673
1791
  # Equivalent to BODY[TEXT].
1674
1792
  # [UID]
1675
1793
  # A number expressing the unique identifier of the message.
1676
- #
1794
+ #
1677
1795
  FetchData = Struct.new(:seqno, :attr)
1678
1796
 
1679
1797
  # Net::IMAP::Envelope represents envelope structures of messages.
1680
- #
1798
+ #
1681
1799
  # ==== Fields:
1682
- #
1800
+ #
1683
1801
  # date:: Returns a string that represents the date.
1684
- #
1802
+ #
1685
1803
  # subject:: Returns a string that represents the subject.
1686
- #
1804
+ #
1687
1805
  # from:: Returns an array of Net::IMAP::Address that represents the from.
1688
- #
1806
+ #
1689
1807
  # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1690
- #
1808
+ #
1691
1809
  # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1692
- #
1810
+ #
1693
1811
  # to:: Returns an array of Net::IMAP::Address that represents the to.
1694
- #
1812
+ #
1695
1813
  # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1696
- #
1814
+ #
1697
1815
  # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1698
- #
1816
+ #
1699
1817
  # in_reply_to:: Returns a string that represents the in-reply-to.
1700
- #
1818
+ #
1701
1819
  # message_id:: Returns a string that represents the message-id.
1702
- #
1820
+ #
1703
1821
  Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1704
1822
  :to, :cc, :bcc, :in_reply_to, :message_id)
1705
1823
 
1706
- #
1824
+ #
1707
1825
  # Net::IMAP::Address represents electronic mail addresses.
1708
- #
1826
+ #
1709
1827
  # ==== Fields:
1710
- #
1828
+ #
1711
1829
  # name:: Returns the phrase from [RFC-822] mailbox.
1712
- #
1830
+ #
1713
1831
  # route:: Returns the route from [RFC-822] route-addr.
1714
- #
1832
+ #
1715
1833
  # mailbox:: nil indicates end of [RFC-822] group.
1716
1834
  # If non-nil and host is nil, returns [RFC-822] group name.
1717
1835
  # Otherwise, returns [RFC-822] local-part
1718
- #
1836
+ #
1719
1837
  # host:: nil indicates [RFC-822] group syntax.
1720
1838
  # Otherwise, returns [RFC-822] domain name.
1721
1839
  #
1722
1840
  Address = Struct.new(:name, :route, :mailbox, :host)
1723
1841
 
1724
- #
1842
+ #
1725
1843
  # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1726
- #
1844
+ #
1727
1845
  # ==== Fields:
1728
- #
1846
+ #
1729
1847
  # dsp_type:: Returns the disposition type.
1730
- #
1848
+ #
1731
1849
  # param:: Returns a hash that represents parameters of the Content-Disposition
1732
1850
  # field.
1733
- #
1851
+ #
1734
1852
  ContentDisposition = Struct.new(:dsp_type, :param)
1735
1853
 
1736
- # Net::IMAP::ThreadMember represents a thread-node returned
1854
+ # Net::IMAP::ThreadMember represents a thread-node returned
1737
1855
  # by Net::IMAP#thread
1738
1856
  #
1739
1857
  # ==== Fields:
@@ -1746,37 +1864,37 @@ module Net
1746
1864
  ThreadMember = Struct.new(:seqno, :children)
1747
1865
 
1748
1866
  # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1749
- #
1867
+ #
1750
1868
  # ==== Fields:
1751
- #
1869
+ #
1752
1870
  # media_type:: Returns the content media type name as defined in [MIME-IMB].
1753
- #
1871
+ #
1754
1872
  # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1755
- #
1873
+ #
1756
1874
  # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1757
- #
1875
+ #
1758
1876
  # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1759
- #
1877
+ #
1760
1878
  # description:: Returns a string giving the content description as defined in
1761
1879
  # [MIME-IMB].
1762
- #
1880
+ #
1763
1881
  # encoding:: Returns a string giving the content transfer encoding as defined in
1764
1882
  # [MIME-IMB].
1765
- #
1883
+ #
1766
1884
  # size:: Returns a number giving the size of the body in octets.
1767
- #
1885
+ #
1768
1886
  # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1769
- #
1887
+ #
1770
1888
  # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1771
1889
  # the content disposition.
1772
- #
1890
+ #
1773
1891
  # language:: Returns a string or an array of strings giving the body
1774
1892
  # language value as defined in [LANGUAGE-TAGS].
1775
- #
1893
+ #
1776
1894
  # extension:: Returns extension data.
1777
- #
1895
+ #
1778
1896
  # multipart?:: Returns false.
1779
- #
1897
+ #
1780
1898
  class BodyTypeBasic < Struct.new(:media_type, :subtype,
1781
1899
  :param, :content_id,
1782
1900
  :description, :encoding, :size,
@@ -1787,7 +1905,7 @@ module Net
1787
1905
  end
1788
1906
 
1789
1907
  # Obsolete: use +subtype+ instead. Calling this will
1790
- # generate a warning message to +stderr+, then return
1908
+ # generate a warning message to +stderr+, then return
1791
1909
  # the value of +subtype+.
1792
1910
  def media_subtype
1793
1911
  $stderr.printf("warning: media_subtype is obsolete.\n")
@@ -1797,13 +1915,13 @@ module Net
1797
1915
  end
1798
1916
 
1799
1917
  # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1800
- #
1918
+ #
1801
1919
  # ==== Fields:
1802
- #
1920
+ #
1803
1921
  # lines:: Returns the size of the body in text lines.
1804
- #
1922
+ #
1805
1923
  # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1806
- #
1924
+ #
1807
1925
  class BodyTypeText < Struct.new(:media_type, :subtype,
1808
1926
  :param, :content_id,
1809
1927
  :description, :encoding, :size,
@@ -1815,7 +1933,7 @@ module Net
1815
1933
  end
1816
1934
 
1817
1935
  # Obsolete: use +subtype+ instead. Calling this will
1818
- # generate a warning message to +stderr+, then return
1936
+ # generate a warning message to +stderr+, then return
1819
1937
  # the value of +subtype+.
1820
1938
  def media_subtype
1821
1939
  $stderr.printf("warning: media_subtype is obsolete.\n")
@@ -1825,13 +1943,13 @@ module Net
1825
1943
  end
1826
1944
 
1827
1945
  # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1828
- #
1946
+ #
1829
1947
  # ==== Fields:
1830
- #
1948
+ #
1831
1949
  # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1832
- #
1950
+ #
1833
1951
  # body:: Returns an object giving the body structure.
1834
- #
1952
+ #
1835
1953
  # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1836
1954
  #
1837
1955
  class BodyTypeMessage < Struct.new(:media_type, :subtype,
@@ -1845,7 +1963,7 @@ module Net
1845
1963
  end
1846
1964
 
1847
1965
  # Obsolete: use +subtype+ instead. Calling this will
1848
- # generate a warning message to +stderr+, then return
1966
+ # generate a warning message to +stderr+, then return
1849
1967
  # the value of +subtype+.
1850
1968
  def media_subtype
1851
1969
  $stderr.printf("warning: media_subtype is obsolete.\n")
@@ -1854,29 +1972,49 @@ module Net
1854
1972
  end
1855
1973
  end
1856
1974
 
1857
- # Net::IMAP::BodyTypeMultipart represents multipart body structures
1975
+ # Net::IMAP::BodyTypeAttachment represents attachment body structures
1976
+ # of messages.
1977
+ #
1978
+ # ==== Fields:
1979
+ #
1980
+ # media_type:: Returns the content media type name.
1981
+ #
1982
+ # subtype:: Returns +nil+.
1983
+ #
1984
+ # param:: Returns a hash that represents parameters.
1985
+ #
1986
+ # multipart?:: Returns false.
1987
+ #
1988
+ class BodyTypeAttachment < Struct.new(:media_type, :subtype,
1989
+ :param)
1990
+ def multipart?
1991
+ return false
1992
+ end
1993
+ end
1994
+
1995
+ # Net::IMAP::BodyTypeMultipart represents multipart body structures
1858
1996
  # of messages.
1859
- #
1997
+ #
1860
1998
  # ==== Fields:
1861
- #
1999
+ #
1862
2000
  # media_type:: Returns the content media type name as defined in [MIME-IMB].
1863
- #
2001
+ #
1864
2002
  # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1865
- #
2003
+ #
1866
2004
  # parts:: Returns multiple parts.
1867
- #
2005
+ #
1868
2006
  # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1869
- #
2007
+ #
1870
2008
  # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1871
2009
  # the content disposition.
1872
- #
2010
+ #
1873
2011
  # language:: Returns a string or an array of strings giving the body
1874
2012
  # language value as defined in [LANGUAGE-TAGS].
1875
- #
2013
+ #
1876
2014
  # extension:: Returns extension data.
1877
- #
2015
+ #
1878
2016
  # multipart?:: Returns true.
1879
- #
2017
+ #
1880
2018
  class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1881
2019
  :parts,
1882
2020
  :param, :disposition, :language,
@@ -1886,7 +2024,7 @@ module Net
1886
2024
  end
1887
2025
 
1888
2026
  # Obsolete: use +subtype+ instead. Calling this will
1889
- # generate a warning message to +stderr+, then return
2027
+ # generate a warning message to +stderr+, then return
1890
2028
  # the value of +subtype+.
1891
2029
  def media_subtype
1892
2030
  $stderr.printf("warning: media_subtype is obsolete.\n")
@@ -1895,7 +2033,23 @@ module Net
1895
2033
  end
1896
2034
  end
1897
2035
 
2036
+ class BodyTypeExtension < Struct.new(:media_type, :subtype,
2037
+ :params, :content_id,
2038
+ :description, :encoding, :size)
2039
+ def multipart?
2040
+ return false
2041
+ end
2042
+ end
2043
+
1898
2044
  class ResponseParser # :nodoc:
2045
+ def initialize
2046
+ @str = nil
2047
+ @pos = nil
2048
+ @lex_state = nil
2049
+ @token = nil
2050
+ @flag_symbols = {}
2051
+ end
2052
+
1899
2053
  def parse(str)
1900
2054
  @str = str
1901
2055
  @pos = 0
@@ -1932,9 +2086,9 @@ module Net
1932
2086
 
1933
2087
  BEG_REGEXP = /\G(?:\
1934
2088
  (?# 1: SPACE )( +)|\
1935
- (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1936
- (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1937
- (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
2089
+ (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+])|\
2090
+ (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+])|\
2091
+ (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*#{'"'}\\\[\]+]+)|\
1938
2092
  (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1939
2093
  (?# 6: LPAR )(\()|\
1940
2094
  (?# 7: RPAR )(\))|\
@@ -2002,7 +2156,7 @@ module Net
2002
2156
  return response_cond
2003
2157
  when /\A(?:FLAGS)\z/ni
2004
2158
  return flags_response
2005
- when /\A(?:LIST|LSUB)\z/ni
2159
+ when /\A(?:LIST|LSUB|XLIST)\z/ni
2006
2160
  return list_response
2007
2161
  when /\A(?:QUOTA)\z/ni
2008
2162
  return getquota_response
@@ -2053,12 +2207,12 @@ module Net
2053
2207
  when "FETCH"
2054
2208
  shift_token
2055
2209
  match(T_SPACE)
2056
- data = FetchData.new(n, msg_att)
2210
+ data = FetchData.new(n, msg_att(n))
2057
2211
  return UntaggedResponse.new(name, data, @str)
2058
2212
  end
2059
2213
  end
2060
2214
 
2061
- def msg_att
2215
+ def msg_att(n)
2062
2216
  match(T_LPAR)
2063
2217
  attr = {}
2064
2218
  while true
@@ -2069,7 +2223,7 @@ module Net
2069
2223
  break
2070
2224
  when T_SPACE
2071
2225
  shift_token
2072
- token = lookahead
2226
+ next
2073
2227
  end
2074
2228
  case token.value
2075
2229
  when /\A(?:ENVELOPE)\z/ni
@@ -2087,7 +2241,7 @@ module Net
2087
2241
  when /\A(?:UID)\z/ni
2088
2242
  name, val = uid_data
2089
2243
  else
2090
- parse_error("unknown attribute `%s'", token.value)
2244
+ parse_error("unknown attribute `%s' for {%d}", token.value, n)
2091
2245
  end
2092
2246
  attr[name] = val
2093
2247
  end
@@ -2154,6 +2308,11 @@ module Net
2154
2308
  def rfc822_text
2155
2309
  token = match(T_ATOM)
2156
2310
  name = token.value.upcase
2311
+ token = lookahead
2312
+ if token.symbol == T_LBRA
2313
+ shift_token
2314
+ match(T_RBRA)
2315
+ end
2157
2316
  match(T_SPACE)
2158
2317
  return name, nstring
2159
2318
  end
@@ -2211,6 +2370,8 @@ module Net
2211
2370
  return body_type_text
2212
2371
  when /\A(?:MESSAGE)\z/ni
2213
2372
  return body_type_msg
2373
+ when /\A(?:ATTACHMENT)\z/ni
2374
+ return body_type_attachment
2214
2375
  else
2215
2376
  return body_type_basic
2216
2377
  end
@@ -2249,6 +2410,29 @@ module Net
2249
2410
  mtype, msubtype = media_type
2250
2411
  match(T_SPACE)
2251
2412
  param, content_id, desc, enc, size = body_fields
2413
+
2414
+ token = lookahead
2415
+ if token.symbol == T_RPAR
2416
+ # If this is not message/rfc822, we shouldn't apply the RFC822
2417
+ # spec to it. We should handle anything other than
2418
+ # message/rfc822 using multipart extension data [rfc3501] (i.e.
2419
+ # the data itself won't be returned, we would have to retrieve it
2420
+ # with BODYSTRUCTURE instead of with BODY
2421
+
2422
+ # Also, sometimes a message/rfc822 is included as a large
2423
+ # attachment instead of having all of the other details
2424
+ # (e.g. attaching a .eml file to an email)
2425
+ if msubtype == "RFC822"
2426
+ return BodyTypeMessage.new(mtype, msubtype, param, content_id,
2427
+ desc, enc, size, nil, nil, nil, nil,
2428
+ nil, nil, nil)
2429
+ else
2430
+ return BodyTypeExtension.new(mtype, msubtype,
2431
+ param, content_id,
2432
+ desc, enc, size)
2433
+ end
2434
+ end
2435
+
2252
2436
  match(T_SPACE)
2253
2437
  env = envelope
2254
2438
  match(T_SPACE)
@@ -2263,6 +2447,13 @@ module Net
2263
2447
  md5, disposition, language, extension)
2264
2448
  end
2265
2449
 
2450
+ def body_type_attachment
2451
+ mtype = case_insensitive_string
2452
+ match(T_SPACE)
2453
+ param = body_fld_param
2454
+ return BodyTypeAttachment.new(mtype, nil, param)
2455
+ end
2456
+
2266
2457
  def body_type_mpart
2267
2458
  parts = []
2268
2459
  while true
@@ -2283,6 +2474,10 @@ module Net
2283
2474
 
2284
2475
  def media_type
2285
2476
  mtype = case_insensitive_string
2477
+ token = lookahead
2478
+ if token.symbol != T_SPACE
2479
+ return mtype, nil
2480
+ end
2286
2481
  match(T_SPACE)
2287
2482
  msubtype = case_insensitive_string
2288
2483
  return mtype, msubtype
@@ -2502,7 +2697,7 @@ module Net
2502
2697
  return '""'
2503
2698
  when /[\x80-\xff\r\n]/n
2504
2699
  # literal
2505
- return "{" + str.length.to_s + "}" + CRLF + str
2700
+ return "{" + str.bytesize.to_s + "}" + CRLF + str
2506
2701
  when /[(){ \x00-\x1f\x7f%*"\\]/n
2507
2702
  # quoted string
2508
2703
  return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
@@ -2627,8 +2822,7 @@ module Net
2627
2822
  user = astring
2628
2823
  match(T_SPACE)
2629
2824
  rights = astring
2630
- ##XXX data.push([user, rights])
2631
- data.push(MailboxACLItem.new(user, rights))
2825
+ data.push(MailboxACLItem.new(user, rights, mailbox))
2632
2826
  end
2633
2827
  end
2634
2828
  return UntaggedResponse.new(name, data, @str)
@@ -2648,8 +2842,9 @@ module Net
2648
2842
  break
2649
2843
  when T_SPACE
2650
2844
  shift_token
2845
+ else
2846
+ data.push(number)
2651
2847
  end
2652
- data.push(number)
2653
2848
  end
2654
2849
  else
2655
2850
  data = []
@@ -2687,35 +2882,35 @@ module Net
2687
2882
  def thread_branch(token)
2688
2883
  rootmember = nil
2689
2884
  lastmember = nil
2690
-
2885
+
2691
2886
  while true
2692
2887
  shift_token # ignore first T_LPAR
2693
2888
  token = lookahead
2694
-
2889
+
2695
2890
  case token.symbol
2696
2891
  when T_NUMBER
2697
2892
  # new member
2698
2893
  newmember = ThreadMember.new(number, [])
2699
2894
  if rootmember.nil?
2700
2895
  rootmember = newmember
2701
- else
2896
+ else
2702
2897
  lastmember.children << newmember
2703
- end
2898
+ end
2704
2899
  lastmember = newmember
2705
- when T_SPACE
2706
- # do nothing
2900
+ when T_SPACE
2901
+ # do nothing
2707
2902
  when T_LPAR
2708
2903
  if rootmember.nil?
2709
2904
  # dummy member
2710
2905
  lastmember = rootmember = ThreadMember.new(nil, [])
2711
- end
2712
-
2906
+ end
2907
+
2713
2908
  lastmember.children << thread_branch(token)
2714
2909
  when T_RPAR
2715
- break
2716
- end
2910
+ break
2911
+ end
2717
2912
  end
2718
-
2913
+
2719
2914
  return rootmember
2720
2915
  end
2721
2916
 
@@ -2758,6 +2953,7 @@ module Net
2758
2953
  break
2759
2954
  when T_SPACE
2760
2955
  shift_token
2956
+ next
2761
2957
  end
2762
2958
  data.push(atom.upcase)
2763
2959
  end
@@ -2906,7 +3102,16 @@ module Net
2906
3102
  if @str.index(/\(([^)]*)\)/ni, @pos)
2907
3103
  @pos = $~.end(0)
2908
3104
  return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2909
- atom || flag.capitalize.intern
3105
+ if atom
3106
+ atom
3107
+ else
3108
+ symbol = flag.capitalize.untaint.intern
3109
+ @flag_symbols[symbol] = true
3110
+ if @flag_symbols.length > IMAP.max_flag_count
3111
+ raise FlagCountError, "number of flag symbols exceeded"
3112
+ end
3113
+ symbol
3114
+ end
2910
3115
  }
2911
3116
  else
2912
3117
  parse_error("invalid flag list")
@@ -3140,7 +3345,7 @@ module Net
3140
3345
  parse_error("unknown token - %s", $&.dump)
3141
3346
  end
3142
3347
  else
3143
- parse_error("illegal @lex_state - %s", @lex_state.inspect)
3348
+ parse_error("invalid @lex_state - %s", @lex_state.inspect)
3144
3349
  end
3145
3350
  end
3146
3351
 
@@ -3184,6 +3389,22 @@ module Net
3184
3389
  end
3185
3390
  add_authenticator "LOGIN", LoginAuthenticator
3186
3391
 
3392
+ # Authenticator for the "PLAIN" authentication type. See
3393
+ # #authenticate().
3394
+ class PlainAuthenticator
3395
+ def process(data)
3396
+ return "\0#{@user}\0#{@password}"
3397
+ end
3398
+
3399
+ private
3400
+
3401
+ def initialize(user, password)
3402
+ @user = user
3403
+ @password = password
3404
+ end
3405
+ end
3406
+ add_authenticator "PLAIN", PlainAuthenticator
3407
+
3187
3408
  # Authenticator for the "CRAM-MD5" authentication type. See
3188
3409
  # #authenticate().
3189
3410
  class CramMD5Authenticator
@@ -3207,8 +3428,8 @@ module Net
3207
3428
  k_ipad = key + "\0" * (64 - key.length)
3208
3429
  k_opad = key + "\0" * (64 - key.length)
3209
3430
  for i in 0..63
3210
- k_ipad[i] ^= 0x36
3211
- k_opad[i] ^= 0x5c
3431
+ k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
3432
+ k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
3212
3433
  end
3213
3434
 
3214
3435
  digest = Digest::MD5.digest(k_ipad + text)
@@ -3218,6 +3439,106 @@ module Net
3218
3439
  end
3219
3440
  add_authenticator "CRAM-MD5", CramMD5Authenticator
3220
3441
 
3442
+ # Authenticator for the "DIGEST-MD5" authentication type. See
3443
+ # #authenticate().
3444
+ class DigestMD5Authenticator
3445
+ def process(challenge)
3446
+ case @stage
3447
+ when STAGE_ONE
3448
+ @stage = STAGE_TWO
3449
+ sparams = {}
3450
+ c = StringScanner.new(challenge)
3451
+ while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
3452
+ k, v = c[1], c[2]
3453
+ if v =~ /^"(.*)"$/
3454
+ v = $1
3455
+ if v =~ /,/
3456
+ v = v.split(',')
3457
+ end
3458
+ end
3459
+ sparams[k] = v
3460
+ end
3461
+
3462
+ raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
3463
+ raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
3464
+
3465
+ response = {
3466
+ :nonce => sparams['nonce'],
3467
+ :username => @user,
3468
+ :realm => sparams['realm'],
3469
+ :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
3470
+ :'digest-uri' => 'imap/' + sparams['realm'],
3471
+ :qop => 'auth',
3472
+ :maxbuf => 65535,
3473
+ :nc => "%08d" % nc(sparams['nonce']),
3474
+ :charset => sparams['charset'],
3475
+ }
3476
+
3477
+ response[:authzid] = @authname unless @authname.nil?
3478
+
3479
+ # now, the real thing
3480
+ a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
3481
+
3482
+ a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
3483
+ a1 << ':' + response[:authzid] unless response[:authzid].nil?
3484
+
3485
+ a2 = "AUTHENTICATE:" + response[:'digest-uri']
3486
+ a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
3487
+
3488
+ response[:response] = Digest::MD5.hexdigest(
3489
+ [
3490
+ Digest::MD5.hexdigest(a1),
3491
+ response.values_at(:nonce, :nc, :cnonce, :qop),
3492
+ Digest::MD5.hexdigest(a2)
3493
+ ].join(':')
3494
+ )
3495
+
3496
+ return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
3497
+ when STAGE_TWO
3498
+ @stage = nil
3499
+ # if at the second stage, return an empty string
3500
+ if challenge =~ /rspauth=/
3501
+ return ''
3502
+ else
3503
+ raise ResponseParseError, challenge
3504
+ end
3505
+ else
3506
+ raise ResponseParseError, challenge
3507
+ end
3508
+ end
3509
+
3510
+ def initialize(user, password, authname = nil)
3511
+ @user, @password, @authname = user, password, authname
3512
+ @nc, @stage = {}, STAGE_ONE
3513
+ end
3514
+
3515
+ private
3516
+
3517
+ STAGE_ONE = :stage_one
3518
+ STAGE_TWO = :stage_two
3519
+
3520
+ def nc(nonce)
3521
+ if @nc.has_key? nonce
3522
+ @nc[nonce] = @nc[nonce] + 1
3523
+ else
3524
+ @nc[nonce] = 1
3525
+ end
3526
+ return @nc[nonce]
3527
+ end
3528
+
3529
+ # some responses need quoting
3530
+ def qdval(k, v)
3531
+ return if k.nil? or v.nil?
3532
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
3533
+ v.gsub!(/([\\"])/, "\\\1")
3534
+ return '%s="%s"' % [k, v]
3535
+ else
3536
+ return '%s=%s' % [k, v]
3537
+ end
3538
+ end
3539
+ end
3540
+ add_authenticator "DIGEST-MD5", DigestMD5Authenticator
3541
+
3221
3542
  # Superclass of IMAP errors.
3222
3543
  class Error < StandardError
3223
3544
  end
@@ -3233,6 +3554,16 @@ module Net
3233
3554
  # Superclass of all errors used to encapsulate "fail" responses
3234
3555
  # from the server.
3235
3556
  class ResponseError < Error
3557
+
3558
+ # The response that caused this error
3559
+ attr_accessor :response
3560
+
3561
+ def initialize(response)
3562
+ @response = response
3563
+
3564
+ super @response.data.text
3565
+ end
3566
+
3236
3567
  end
3237
3568
 
3238
3569
  # Error raised upon a "NO" response from the server, indicating
@@ -3246,11 +3577,15 @@ module Net
3246
3577
  class BadResponseError < ResponseError
3247
3578
  end
3248
3579
 
3249
- # Error raised upon a "BYE" response from the server, indicating
3580
+ # Error raised upon a "BYE" response from the server, indicating
3250
3581
  # that the client is not being allowed to login, or has been timed
3251
3582
  # out due to inactivity.
3252
3583
  class ByeResponseError < ResponseError
3253
3584
  end
3585
+
3586
+ # Error raised when too many flags are interned to symbols.
3587
+ class FlagCountError < Error
3588
+ end
3254
3589
  end
3255
3590
  end
3256
3591
 
@@ -3263,27 +3598,44 @@ if __FILE__ == $0
3263
3598
  $user = ENV["USER"] || ENV["LOGNAME"]
3264
3599
  $auth = "login"
3265
3600
  $ssl = false
3601
+ $starttls = false
3266
3602
 
3267
3603
  def usage
3268
- $stderr.print <<EOF
3604
+ <<EOF
3269
3605
  usage: #{$0} [options] <host>
3270
3606
 
3271
3607
  --help print this message
3272
3608
  --port=PORT specifies port
3273
3609
  --user=USER specifies user
3274
3610
  --auth=AUTH specifies auth type
3611
+ --starttls use starttls
3275
3612
  --ssl use ssl
3276
3613
  EOF
3277
3614
  end
3278
3615
 
3616
+ begin
3617
+ require 'io/console'
3618
+ rescue LoadError
3619
+ def _noecho(&block)
3620
+ system("stty", "-echo")
3621
+ begin
3622
+ yield STDIN
3623
+ ensure
3624
+ system("stty", "echo")
3625
+ end
3626
+ end
3627
+ else
3628
+ def _noecho(&block)
3629
+ STDIN.noecho(&block)
3630
+ end
3631
+ end
3632
+
3279
3633
  def get_password
3280
3634
  print "password: "
3281
- system("stty", "-echo")
3282
3635
  begin
3283
- return gets.chop
3636
+ return _noecho(&:gets).chomp
3284
3637
  ensure
3285
- system("stty", "echo")
3286
- print "\n"
3638
+ puts
3287
3639
  end
3288
3640
  end
3289
3641
 
@@ -3302,6 +3654,7 @@ EOF
3302
3654
  ['--port', GetoptLong::REQUIRED_ARGUMENT],
3303
3655
  ['--user', GetoptLong::REQUIRED_ARGUMENT],
3304
3656
  ['--auth', GetoptLong::REQUIRED_ARGUMENT],
3657
+ ['--starttls', GetoptLong::NO_ARGUMENT],
3305
3658
  ['--ssl', GetoptLong::NO_ARGUMENT])
3306
3659
  begin
3307
3660
  parser.each_option do |name, arg|
@@ -3314,28 +3667,30 @@ EOF
3314
3667
  $auth = arg
3315
3668
  when "--ssl"
3316
3669
  $ssl = true
3670
+ when "--starttls"
3671
+ $starttls = true
3317
3672
  when "--debug"
3318
3673
  Net::IMAP.debug = true
3319
3674
  when "--help"
3320
3675
  usage
3321
- exit(1)
3676
+ exit
3322
3677
  end
3323
3678
  end
3324
3679
  rescue
3325
- usage
3326
- exit(1)
3680
+ abort usage
3327
3681
  end
3328
3682
 
3329
3683
  $host = ARGV.shift
3330
3684
  unless $host
3331
- usage
3332
- exit(1)
3685
+ abort usage
3333
3686
  end
3334
- $port ||= $ssl ? 993 : 143
3335
-
3336
- imap = Net::IMAP.new($host, $port, $ssl)
3687
+
3688
+ imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
3337
3689
  begin
3338
- password = get_password
3690
+ imap.starttls if $starttls
3691
+ class << password = method(:get_password)
3692
+ alias to_str call
3693
+ end
3339
3694
  imap.authenticate($auth, $user, password)
3340
3695
  while true
3341
3696
  cmd, *args = get_command