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