1# frozen_string_literal: true
2
3require "net/imap"
4require "test/unit"
5
6class IMAPTest < Test::Unit::TestCase
7  CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__)
8  SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__)
9  SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__)
10
11  def setup
12    @do_not_reverse_lookup = Socket.do_not_reverse_lookup
13    Socket.do_not_reverse_lookup = true
14    @threads = []
15  end
16
17  def teardown
18    if !@threads.empty?
19      assert_join_threads(@threads)
20    end
21  ensure
22    Socket.do_not_reverse_lookup = @do_not_reverse_lookup
23  end
24
25  def test_encode_utf7
26    assert_equal("foo", Net::IMAP.encode_utf7("foo"))
27    assert_equal("&-", Net::IMAP.encode_utf7("&"))
28
29    utf8 = "\357\274\241\357\274\242\357\274\243".dup.force_encoding("UTF-8")
30    s = Net::IMAP.encode_utf7(utf8)
31    assert_equal("&,yH,Iv8j-", s)
32    s = Net::IMAP.encode_utf7("foo&#{utf8}-bar".encode("EUC-JP"))
33    assert_equal("foo&-&,yH,Iv8j--bar", s)
34
35    utf8 = "\343\201\202&".dup.force_encoding("UTF-8")
36    s = Net::IMAP.encode_utf7(utf8)
37    assert_equal("&MEI-&-", s)
38    s = Net::IMAP.encode_utf7(utf8.encode("EUC-JP"))
39    assert_equal("&MEI-&-", s)
40  end
41
42  def test_decode_utf7
43    assert_equal("&", Net::IMAP.decode_utf7("&-"))
44    assert_equal("&-", Net::IMAP.decode_utf7("&--"))
45
46    s = Net::IMAP.decode_utf7("&,yH,Iv8j-")
47    utf8 = "\357\274\241\357\274\242\357\274\243".dup.force_encoding("UTF-8")
48    assert_equal(utf8, s)
49  end
50
51  def test_format_date
52    time = Time.mktime(2009, 7, 24)
53    s = Net::IMAP.format_date(time)
54    assert_equal("24-Jul-2009", s)
55  end
56
57  def test_format_datetime
58    time = Time.mktime(2009, 7, 24, 1, 23, 45)
59    s = Net::IMAP.format_datetime(time)
60    assert_match(/\A24-Jul-2009 01:23 [+\-]\d{4}\z/, s)
61  end
62
63  if defined?(OpenSSL::SSL::SSLError)
64    def test_imaps_unknown_ca
65      assert_raise(OpenSSL::SSL::SSLError) do
66        imaps_test do |port|
67          begin
68            Net::IMAP.new("localhost",
69                          :port => port,
70                          :ssl => true)
71          rescue SystemCallError
72            skip $!
73          end
74        end
75      end
76    end
77
78    def test_imaps_with_ca_file
79      assert_nothing_raised do
80        imaps_test do |port|
81          begin
82            Net::IMAP.new("localhost",
83                          :port => port,
84                          :ssl => { :ca_file => CA_FILE })
85          rescue SystemCallError
86            skip $!
87          end
88        end
89      end
90    end
91
92    def test_imaps_verify_none
93      assert_nothing_raised do
94        imaps_test do |port|
95          Net::IMAP.new(server_addr,
96                        :port => port,
97                        :ssl => { :verify_mode => OpenSSL::SSL::VERIFY_NONE })
98        end
99      end
100    end
101
102    def test_imaps_post_connection_check
103      assert_raise(OpenSSL::SSL::SSLError) do
104        imaps_test do |port|
105          # server_addr is different from the hostname in the certificate,
106          # so the following code should raise a SSLError.
107          Net::IMAP.new(server_addr,
108                        :port => port,
109                        :ssl => { :ca_file => CA_FILE })
110        end
111      end
112    end
113  end
114
115  if defined?(OpenSSL::SSL)
116    def test_starttls
117      imap = nil
118      starttls_test do |port|
119        imap = Net::IMAP.new("localhost", :port => port)
120        imap.starttls(:ca_file => CA_FILE)
121        imap
122      end
123    rescue SystemCallError
124      skip $!
125    ensure
126      if imap && !imap.disconnected?
127        imap.disconnect
128      end
129    end
130
131    def test_starttls_stripping
132      starttls_stripping_test do |port|
133        imap = Net::IMAP.new("localhost", :port => port)
134        assert_raise(Net::IMAP::UnknownResponseError) do
135          imap.starttls(:ca_file => CA_FILE)
136        end
137        imap
138      end
139    end
140  end
141
142  def start_server
143    th = Thread.new do
144      yield
145    end
146    @threads << th
147    sleep 0.1 until th.stop?
148  end
149
150  def test_unexpected_eof
151    server = create_tcp_server
152    port = server.addr[1]
153    @threads << Thread.start do
154      sock = server.accept
155      begin
156        sock.print("* OK test server\r\n")
157        sock.gets
158#       sock.print("* BYE terminating connection\r\n")
159#       sock.print("RUBY0001 OK LOGOUT completed\r\n")
160      ensure
161        sock.close
162        server.close
163      end
164    end
165    begin
166      imap = Net::IMAP.new(server_addr, :port => port)
167      assert_raise(EOFError) do
168        imap.logout
169      end
170    ensure
171      imap.disconnect if imap
172    end
173  end
174
175  def test_idle
176    server = create_tcp_server
177    port = server.addr[1]
178    requests = []
179    @threads << Thread.start do
180      sock = server.accept
181      begin
182        sock.print("* OK test server\r\n")
183        requests.push(sock.gets)
184        sock.print("+ idling\r\n")
185        sock.print("* 3 EXISTS\r\n")
186        sock.print("* 2 EXPUNGE\r\n")
187        requests.push(sock.gets)
188        sock.print("RUBY0001 OK IDLE terminated\r\n")
189        sock.gets
190        sock.print("* BYE terminating connection\r\n")
191        sock.print("RUBY0002 OK LOGOUT completed\r\n")
192      ensure
193        sock.close
194        server.close
195      end
196    end
197
198    begin
199      imap = Net::IMAP.new(server_addr, :port => port)
200      responses = []
201      imap.idle do |res|
202        responses.push(res)
203        if res.name == "EXPUNGE"
204          imap.idle_done
205        end
206      end
207      assert_equal(3, responses.length)
208      assert_instance_of(Net::IMAP::ContinuationRequest, responses[0])
209      assert_equal("EXISTS", responses[1].name)
210      assert_equal(3, responses[1].data)
211      assert_equal("EXPUNGE", responses[2].name)
212      assert_equal(2, responses[2].data)
213      assert_equal(2, requests.length)
214      assert_equal("RUBY0001 IDLE\r\n", requests[0])
215      assert_equal("DONE\r\n", requests[1])
216      imap.logout
217    ensure
218      imap.disconnect if imap
219    end
220  end
221
222  def test_exception_during_idle
223    server = create_tcp_server
224    port = server.addr[1]
225    requests = []
226    @threads << Thread.start do
227      sock = server.accept
228      begin
229        sock.print("* OK test server\r\n")
230        requests.push(sock.gets)
231        sock.print("+ idling\r\n")
232        sock.print("* 3 EXISTS\r\n")
233        sock.print("* 2 EXPUNGE\r\n")
234        requests.push(sock.gets)
235        sock.print("RUBY0001 OK IDLE terminated\r\n")
236        sock.gets
237        sock.print("* BYE terminating connection\r\n")
238        sock.print("RUBY0002 OK LOGOUT completed\r\n")
239      ensure
240        sock.close
241        server.close
242      end
243    end
244    begin
245      imap = Net::IMAP.new(server_addr, :port => port)
246      begin
247        th = Thread.current
248        m = Monitor.new
249        in_idle = false
250        exception_raised = false
251        c = m.new_cond
252        raiser = Thread.start do
253          m.synchronize do
254            until in_idle
255              c.wait(0.1)
256            end
257          end
258          th.raise(Interrupt)
259          m.synchronize do
260            exception_raised = true
261            c.signal
262          end
263        end
264        @threads << raiser
265        imap.idle do |res|
266          m.synchronize do
267            in_idle = true
268            c.signal
269            until exception_raised
270              c.wait(0.1)
271            end
272          end
273        end
274      rescue Interrupt
275      end
276      assert_equal(2, requests.length)
277      assert_equal("RUBY0001 IDLE\r\n", requests[0])
278      assert_equal("DONE\r\n", requests[1])
279      imap.logout
280    ensure
281      imap.disconnect if imap
282      raiser.kill unless in_idle
283    end
284  end
285
286  def test_idle_done_not_during_idle
287    server = create_tcp_server
288    port = server.addr[1]
289    @threads << Thread.start do
290      sock = server.accept
291      begin
292        sock.print("* OK test server\r\n")
293      ensure
294        sock.close
295        server.close
296      end
297    end
298    begin
299      imap = Net::IMAP.new(server_addr, :port => port)
300      assert_raise(Net::IMAP::Error) do
301        imap.idle_done
302      end
303    ensure
304      imap.disconnect if imap
305    end
306  end
307
308  def test_idle_timeout
309    server = create_tcp_server
310    port = server.addr[1]
311    requests = []
312    @threads << Thread.start do
313      sock = server.accept
314      begin
315        sock.print("* OK test server\r\n")
316        requests.push(sock.gets)
317        sock.print("+ idling\r\n")
318        sock.print("* 3 EXISTS\r\n")
319        sock.print("* 2 EXPUNGE\r\n")
320        requests.push(sock.gets)
321        sock.print("RUBY0001 OK IDLE terminated\r\n")
322        sock.gets
323        sock.print("* BYE terminating connection\r\n")
324        sock.print("RUBY0002 OK LOGOUT completed\r\n")
325      ensure
326        sock.close
327        server.close
328      end
329    end
330
331    begin
332      imap = Net::IMAP.new(server_addr, :port => port)
333      responses = []
334      Thread.pass
335      imap.idle(0.2) do |res|
336        responses.push(res)
337      end
338      # There is no guarantee that this thread has received all the responses,
339      # so check the response length.
340      if responses.length > 0
341        assert_instance_of(Net::IMAP::ContinuationRequest, responses[0])
342        if responses.length > 1
343          assert_equal("EXISTS", responses[1].name)
344          assert_equal(3, responses[1].data)
345          if responses.length > 2
346            assert_equal("EXPUNGE", responses[2].name)
347            assert_equal(2, responses[2].data)
348          end
349        end
350      end
351      # Also, there is no guarantee that the server thread has stored
352      # all the requests into the array, so check the length.
353      if requests.length > 0
354        assert_equal("RUBY0001 IDLE\r\n", requests[0])
355        if requests.length > 1
356          assert_equal("DONE\r\n", requests[1])
357        end
358      end
359      imap.logout
360    ensure
361      imap.disconnect if imap
362    end
363  end
364
365  def test_unexpected_bye
366    server = create_tcp_server
367    port = server.addr[1]
368    @threads << Thread.start do
369      sock = server.accept
370      begin
371        sock.print("* OK Gimap ready for requests from 75.101.246.151 33if2752585qyk.26\r\n")
372        sock.gets
373        sock.print("* BYE System Error 33if2752585qyk.26\r\n")
374      ensure
375        sock.close
376        server.close
377      end
378    end
379    begin
380      imap = Net::IMAP.new(server_addr, :port => port)
381      assert_raise(Net::IMAP::ByeResponseError) do
382        imap.login("user", "password")
383      end
384    end
385  end
386
387  def test_exception_during_shutdown
388    server = create_tcp_server
389    port = server.addr[1]
390    @threads << Thread.start do
391      sock = server.accept
392      begin
393        sock.print("* OK test server\r\n")
394        sock.gets
395        sock.print("* BYE terminating connection\r\n")
396        sock.print("RUBY0001 OK LOGOUT completed\r\n")
397      ensure
398        sock.close
399        server.close
400      end
401    end
402    begin
403      imap = Net::IMAP.new(server_addr, :port => port)
404      imap.instance_eval do
405        def @sock.shutdown(*args)
406          super
407        ensure
408          raise "error"
409        end
410      end
411      imap.logout
412    ensure
413      assert_raise(RuntimeError) do
414        imap.disconnect
415      end
416    end
417  end
418
419  def test_connection_closed_during_idle
420    server = create_tcp_server
421    port = server.addr[1]
422    requests = []
423    sock = nil
424    threads = []
425    threads << Thread.start do
426      begin
427        sock = server.accept
428        sock.print("* OK test server\r\n")
429        requests.push(sock.gets)
430        sock.print("+ idling\r\n")
431      rescue IOError # sock is closed by another thread
432      ensure
433        server.close
434      end
435    end
436    threads << Thread.start do
437      imap = Net::IMAP.new(server_addr, :port => port)
438      begin
439        m = Monitor.new
440        in_idle = false
441        closed = false
442        c = m.new_cond
443        threads << Thread.start do
444          m.synchronize do
445            until in_idle
446              c.wait(0.1)
447            end
448          end
449          sock.close
450          m.synchronize do
451            closed = true
452            c.signal
453          end
454        end
455        assert_raise(EOFError) do
456          imap.idle do |res|
457            m.synchronize do
458              in_idle = true
459              c.signal
460              until closed
461                c.wait(0.1)
462              end
463            end
464          end
465        end
466        assert_equal(1, requests.length)
467        assert_equal("RUBY0001 IDLE\r\n", requests[0])
468      ensure
469        imap.disconnect if imap
470      end
471    end
472    assert_join_threads(threads)
473  ensure
474    if sock && !sock.closed?
475      sock.close
476    end
477  end
478
479  def test_connection_closed_without_greeting
480    server = create_tcp_server
481    port = server.addr[1]
482    @threads << Thread.start do
483      begin
484        sock = server.accept
485        sock.close
486      ensure
487        server.close
488      end
489    end
490    assert_raise(Net::IMAP::Error) do
491      Net::IMAP.new(server_addr, :port => port)
492    end
493  end
494
495  def test_default_port
496    assert_equal(143, Net::IMAP.default_port)
497    assert_equal(143, Net::IMAP.default_imap_port)
498    assert_equal(993, Net::IMAP.default_tls_port)
499    assert_equal(993, Net::IMAP.default_ssl_port)
500    assert_equal(993, Net::IMAP.default_imaps_port)
501  end
502
503  def test_send_invalid_number
504    server = create_tcp_server
505    port = server.addr[1]
506    @threads << Thread.start do
507      sock = server.accept
508      begin
509        sock.print("* OK test server\r\n")
510        sock.gets
511        sock.print("RUBY0001 OK TEST completed\r\n")
512        sock.gets
513        sock.print("RUBY0002 OK TEST completed\r\n")
514        sock.gets
515        sock.print("RUBY0003 OK TEST completed\r\n")
516        sock.gets
517        sock.print("RUBY0004 OK TEST completed\r\n")
518        sock.gets
519        sock.print("* BYE terminating connection\r\n")
520        sock.print("RUBY0005 OK LOGOUT completed\r\n")
521      ensure
522        sock.close
523        server.close
524      end
525    end
526    begin
527      imap = Net::IMAP.new(server_addr, :port => port)
528      assert_raise(Net::IMAP::DataFormatError) do
529        imap.send(:send_command, "TEST", -1)
530      end
531      imap.send(:send_command, "TEST", 0)
532      imap.send(:send_command, "TEST", 4294967295)
533      assert_raise(Net::IMAP::DataFormatError) do
534        imap.send(:send_command, "TEST", 4294967296)
535      end
536      assert_raise(Net::IMAP::DataFormatError) do
537        imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(-1))
538      end
539      assert_raise(Net::IMAP::DataFormatError) do
540        imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(0))
541      end
542      imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(1))
543      imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967295))
544      assert_raise(Net::IMAP::DataFormatError) do
545        imap.send(:send_command, "TEST", Net::IMAP::MessageSet.new(4294967296))
546      end
547      imap.logout
548    ensure
549      imap.disconnect
550    end
551  end
552
553  def test_send_literal
554    server = create_tcp_server
555    port = server.addr[1]
556    requests = []
557    literal = nil
558    @threads << Thread.start do
559      sock = server.accept
560      begin
561        sock.print("* OK test server\r\n")
562        line = sock.gets
563        requests.push(line)
564        size = line.slice(/{(\d+)}\r\n/, 1).to_i
565        sock.print("+ Ready for literal data\r\n")
566        literal = sock.read(size)
567        requests.push(sock.gets)
568        sock.print("RUBY0001 OK TEST completed\r\n")
569        sock.gets
570        sock.print("* BYE terminating connection\r\n")
571        sock.print("RUBY0002 OK LOGOUT completed\r\n")
572      ensure
573        sock.close
574        server.close
575      end
576    end
577    begin
578      imap = Net::IMAP.new(server_addr, :port => port)
579      imap.send(:send_command, "TEST", ["\xDE\xAD\xBE\xEF".b])
580      assert_equal(2, requests.length)
581      assert_equal("RUBY0001 TEST ({4}\r\n", requests[0])
582      assert_equal("\xDE\xAD\xBE\xEF".b, literal)
583      assert_equal(")\r\n", requests[1])
584      imap.logout
585    ensure
586      imap.disconnect
587    end
588  end
589
590  def test_disconnect
591    server = create_tcp_server
592    port = server.addr[1]
593    @threads << Thread.start do
594      sock = server.accept
595      begin
596        sock.print("* OK test server\r\n")
597        sock.gets
598        sock.print("* BYE terminating connection\r\n")
599        sock.print("RUBY0001 OK LOGOUT completed\r\n")
600      ensure
601        sock.close
602        server.close
603      end
604    end
605    begin
606      imap = Net::IMAP.new(server_addr, :port => port)
607      imap.logout
608      imap.disconnect
609      assert_equal(true, imap.disconnected?)
610      imap.disconnect
611      assert_equal(true, imap.disconnected?)
612    ensure
613      imap.disconnect if imap && !imap.disconnected?
614    end
615  end
616
617  def test_append
618    server = create_tcp_server
619    port = server.addr[1]
620    mail = <<EOF.gsub(/\n/, "\r\n")
621From: shugo@example.com
622To: matz@example.com
623Subject: hello
624
625hello world
626EOF
627    requests = []
628    received_mail = nil
629    @threads << Thread.start do
630      sock = server.accept
631      begin
632        sock.print("* OK test server\r\n")
633        line = sock.gets
634        requests.push(line)
635        size = line.slice(/{(\d+)}\r\n/, 1).to_i
636        sock.print("+ Ready for literal data\r\n")
637        received_mail = sock.read(size)
638        sock.gets
639        sock.print("RUBY0001 OK APPEND completed\r\n")
640        requests.push(sock.gets)
641        sock.print("* BYE terminating connection\r\n")
642        sock.print("RUBY0002 OK LOGOUT completed\r\n")
643      ensure
644        sock.close
645        server.close
646      end
647    end
648
649    begin
650      imap = Net::IMAP.new(server_addr, :port => port)
651      resp = imap.append("INBOX", mail)
652      assert_equal(1, requests.length)
653      assert_equal("RUBY0001 APPEND INBOX {#{mail.size}}\r\n", requests[0])
654      assert_equal(mail, received_mail)
655      imap.logout
656      assert_equal(2, requests.length)
657      assert_equal("RUBY0002 LOGOUT\r\n", requests[1])
658    ensure
659      imap.disconnect if imap
660    end
661  end
662
663  def test_append_fail
664    server = create_tcp_server
665    port = server.addr[1]
666    mail = <<EOF.gsub(/\n/, "\r\n")
667From: shugo@example.com
668To: matz@example.com
669Subject: hello
670
671hello world
672EOF
673    requests = []
674    received_mail = nil
675    @threads << Thread.start do
676      sock = server.accept
677      begin
678        sock.print("* OK test server\r\n")
679        requests.push(sock.gets)
680        sock.print("RUBY0001 NO Mailbox doesn't exist\r\n")
681        requests.push(sock.gets)
682        sock.print("* BYE terminating connection\r\n")
683        sock.print("RUBY0002 OK LOGOUT completed\r\n")
684      ensure
685        sock.close
686        server.close
687      end
688    end
689
690    begin
691      imap = Net::IMAP.new(server_addr, :port => port)
692      assert_raise(Net::IMAP::NoResponseError) do
693        imap.append("INBOX", mail)
694      end
695      assert_equal(1, requests.length)
696      assert_equal("RUBY0001 APPEND INBOX {#{mail.size}}\r\n", requests[0])
697      imap.logout
698      assert_equal(2, requests.length)
699      assert_equal("RUBY0002 LOGOUT\r\n", requests[1])
700    ensure
701      imap.disconnect if imap
702    end
703  end
704
705  private
706
707  def imaps_test
708    server = create_tcp_server
709    port = server.addr[1]
710    ctx = OpenSSL::SSL::SSLContext.new
711    ctx.ca_file = CA_FILE
712    ctx.key = File.open(SERVER_KEY) { |f|
713      OpenSSL::PKey::RSA.new(f)
714    }
715    ctx.cert = File.open(SERVER_CERT) { |f|
716      OpenSSL::X509::Certificate.new(f)
717    }
718    ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx)
719    ths = Thread.start do
720      Thread.current.report_on_exception = false # always join-ed
721      begin
722        sock = ssl_server.accept
723        begin
724          sock.print("* OK test server\r\n")
725          sock.gets
726          sock.print("* BYE terminating connection\r\n")
727          sock.print("RUBY0001 OK LOGOUT completed\r\n")
728        ensure
729          sock.close
730        end
731      rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
732      end
733    end
734    begin
735      begin
736        imap = yield(port)
737        imap.logout
738      ensure
739        imap.disconnect if imap
740      end
741    ensure
742      ssl_server.close
743      ths.join
744    end
745  end
746
747  def starttls_test
748    server = create_tcp_server
749    port = server.addr[1]
750    @threads << Thread.start do
751      sock = server.accept
752      begin
753        sock.print("* OK test server\r\n")
754        sock.gets
755        sock.print("RUBY0001 OK completed\r\n")
756        ctx = OpenSSL::SSL::SSLContext.new
757        ctx.ca_file = CA_FILE
758        ctx.key = File.open(SERVER_KEY) { |f|
759          OpenSSL::PKey::RSA.new(f)
760        }
761        ctx.cert = File.open(SERVER_CERT) { |f|
762          OpenSSL::X509::Certificate.new(f)
763        }
764        sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
765        sock.sync_close = true
766        sock.accept
767        sock.gets
768        sock.print("* BYE terminating connection\r\n")
769        sock.print("RUBY0002 OK LOGOUT completed\r\n")
770      ensure
771        sock.close
772        server.close
773      end
774    end
775    begin
776      imap = yield(port)
777      imap.logout if !imap.disconnected?
778    ensure
779      imap.disconnect if imap && !imap.disconnected?
780    end
781  end
782
783  def starttls_stripping_test
784    server = create_tcp_server
785    port = server.addr[1]
786    start_server do
787      sock = server.accept
788      begin
789        sock.print("* OK test server\r\n")
790        sock.gets
791        sock.print("RUBY0001 BUG unhandled command\r\n")
792      ensure
793        sock.close
794        server.close
795      end
796    end
797    begin
798      imap = yield(port)
799    ensure
800      imap.disconnect if imap && !imap.disconnected?
801    end
802  end
803
804  def create_tcp_server
805    return TCPServer.new(server_addr, 0)
806  end
807
808  def server_addr
809    Addrinfo.tcp("localhost", 0).ip_address
810  end
811end
812