sup 0.19.0 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +12 -6
  5. data/CONTRIBUTORS +28 -14
  6. data/Gemfile +5 -0
  7. data/History.txt +92 -0
  8. data/README.md +26 -5
  9. data/Rakefile +41 -1
  10. data/ReleaseNotes +17 -0
  11. data/bin/sup +12 -23
  12. data/bin/sup-add +15 -16
  13. data/bin/sup-config +30 -45
  14. data/bin/sup-dump +2 -3
  15. data/bin/sup-import-dump +5 -6
  16. data/bin/sup-sync +3 -4
  17. data/bin/sup-sync-back-maildir +3 -4
  18. data/bin/sup-tweak-labels +6 -7
  19. data/contrib/colorpicker.rb +0 -2
  20. data/contrib/completion/_sup.bash +102 -0
  21. data/devel/profile.rb +0 -1
  22. data/ext/mkrf_conf_xapian.rb +47 -0
  23. data/lib/sup.rb +10 -8
  24. data/lib/sup/buffer.rb +12 -0
  25. data/lib/sup/colormap.rb +5 -2
  26. data/lib/sup/contact.rb +4 -2
  27. data/lib/sup/crypto.rb +58 -16
  28. data/lib/sup/draft.rb +8 -8
  29. data/lib/sup/hook.rb +9 -9
  30. data/lib/sup/index.rb +20 -7
  31. data/lib/sup/label.rb +1 -1
  32. data/lib/sup/logger.rb +1 -1
  33. data/lib/sup/maildir.rb +16 -5
  34. data/lib/sup/mbox.rb +13 -5
  35. data/lib/sup/message.rb +36 -12
  36. data/lib/sup/message_chunks.rb +13 -4
  37. data/lib/sup/mode.rb +34 -28
  38. data/lib/sup/modes/contact_list_mode.rb +1 -0
  39. data/lib/sup/modes/edit_message_mode.rb +3 -2
  40. data/lib/sup/modes/forward_mode.rb +22 -3
  41. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  42. data/lib/sup/modes/reply_mode.rb +3 -1
  43. data/lib/sup/modes/text_mode.rb +6 -1
  44. data/lib/sup/modes/thread_index_mode.rb +12 -2
  45. data/lib/sup/modes/thread_view_mode.rb +111 -14
  46. data/lib/sup/person.rb +68 -61
  47. data/lib/sup/search.rb +1 -1
  48. data/lib/sup/sent.rb +1 -1
  49. data/lib/sup/source.rb +1 -1
  50. data/lib/sup/util.rb +15 -94
  51. data/lib/sup/util/axe.rb +17 -0
  52. data/lib/sup/util/locale_fiddler.rb +24 -0
  53. data/lib/sup/util/ncurses.rb +3 -3
  54. data/lib/sup/version.rb +10 -1
  55. data/sup.gemspec +29 -11
  56. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  57. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  58. data/test/fixtures/blank-header-fields.eml +71 -0
  59. data/test/fixtures/contacts.txt +1 -0
  60. data/test/fixtures/mailing-list-header.eml +80 -0
  61. data/test/fixtures/malicious-attachment-names.eml +55 -0
  62. data/test/fixtures/missing-from-to.eml +18 -0
  63. data/test/{messages → fixtures}/missing-line.eml +0 -0
  64. data/test/fixtures/multi-part-2.eml +72 -0
  65. data/test/fixtures/multi-part.eml +61 -0
  66. data/test/fixtures/no-body.eml +18 -0
  67. data/test/fixtures/simple-message.eml +29 -0
  68. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  69. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  70. data/test/gnupg_test_home/gpg.conf +3 -1
  71. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  72. data/test/gnupg_test_home/pubring.gpg +0 -0
  73. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  74. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  75. data/test/gnupg_test_home/regen_keys.sh +89 -0
  76. data/test/gnupg_test_home/secring.gpg +0 -0
  77. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
  78. data/test/integration/test_maildir.rb +75 -0
  79. data/test/integration/test_mbox.rb +69 -0
  80. data/test/test_crypto.rb +14 -2
  81. data/test/test_header_parsing.rb +1 -1
  82. data/test/test_helper.rb +6 -3
  83. data/test/test_message.rb +115 -341
  84. data/test/test_messages_dir.rb +4 -28
  85. data/test/test_yaml_regressions.rb +1 -1
  86. data/test/unit/test_contact.rb +33 -0
  87. data/test/unit/test_locale_fiddler.rb +15 -0
  88. data/test/unit/test_person.rb +37 -0
  89. data/test/unit/util/test_query.rb +10 -4
  90. data/test/unit/util/test_string.rb +6 -0
  91. metadata +137 -53
  92. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  93. data/test/gnupg_test_home/trustdb.gpg +0 -0
@@ -1,20 +1,23 @@
1
1
  -----BEGIN PGP PUBLIC KEY BLOCK-----
2
- Version: GnuPG v2.0.20 (GNU/Linux)
3
2
 
4
- mI0EUgi0fAEEAOLAcQW96NEUSB7YE/la8X56jGW5BMX3aAixOF8LvOwMBbUK1T+U
5
- 0H2PGIrXVcYyHcPqWRpRahbsIAldBqzffPlzMa+aqJaB1xKkNruxSoIzwPdidZMe
6
- l0Dxz2FDsoXD0KPyWnAYhGmQyz2MFpZxu2tlYqvwWVW//XGnk/KHvIXbABEBAAG0
7
- PlN1cCBUZXN0IFJlY2VpdmVyIChUZXN0IHJlY2VpdmVyIGZvciBTdXApIDxzdXAt
8
- dGVzdC0yQGZvby5iYXI+iL8EEwECACkFAlIItHwCGwMFCQHhM4AHCwkIBwMCAQYV
9
- CAIJCgsEFgIDAQIeAQIXgAAKCRAsABl+cWpykMMVBADHkQPgTz0CqKKp3k+z3dbm
10
- ocmI4tYNn1dOkDQqyfoBTfs6L3g4j5OE2UrguntRYyg5oon+uO5d18CQ5dY0sCw/
11
- o5IwyzTrxI8IocbtZvBdSb+XjLndynGuIQoqaJq9i6n1V4klFHVOna8Q9JstLfRX
12
- H1d4xPhnvKcaDDx/NV3X/biNBFIItHwBBADBpb43MpkrUWlg7HWJ1ZfOlxnOxrJ3
13
- Gz9WFNV06UbcZEuFKA/vHRjM6gWzUn5903FLuCWu3eBrq5xQfWipbp187PmocpoG
14
- skJ6gosLs1fMYRBjv2VbG9xJVKdKJMjqZw5FUpXKAaHr8P9jN6g2STQrbeQ8CVUK
15
- h7zOWRXAXSKUgwARAQABiKUEGAECAA8FAlIItHwCGwwFCQHhM4AACgkQLAAZfnFq
16
- cpDV1QQAzcxFXznEX92DjWxWRC7gRHgIsQk9WJnDzjtnDjSWCp3H85qeTZGZrn9W
17
- NoneV/S5Y7K3Mkceh4rFaANQ3zx4b05y1LFt5N/lPwIe5VB0vcPumtZum2fSGfpK
18
- nTXvzelcWcm2aGyUSaWvOkntWKEEt1kB5Oq6EtZoRZLMzAxLd7s=
19
- =aKsV
3
+ mQGNBF7leTkBDAC3auy8xodH6jxoISylFZTpVqy/0L2ul879YUb/QbC58+F/H36S
4
+ CjLfPxFlq0FAOXHelOvktxaybg+BG5UpSvTgBLbcArq5nctee+04TMXCzQzrG2V1
5
+ zb9gIRT665fX3+WYncSIXdr4LAp7r8Jw3RT3tTOZqbaencumCWaJblnvfFwPrMKf
6
+ AXWa/NVndNMAXmJ5uBf1MRr45KXaQ2tczPIeHqSOKhKNnKZPRqPs0fg4i3d0Vb6G
7
+ yItgtJapfBo50FV+PvtodMHo3LDlz/BBjdEJHSvghqEjb1S7xGo+hdXs+lfCMfa0
8
+ 3PAWoj+OeHNorbK0YbVKOtS0E0xYvScbyC7bfwtA9yb3LZYmy7VHsKJmQfygCNQ6
9
+ wIKQGAVN1NcQcJsvWyAwk9+WMN5oqB5lb76u40beoWlUjSJRlph2VvWvkGuh/huU
10
+ sVGqcN7EO4SFkwi2YQLoWfQRGur3mids/PQTBywpGE1SyziPZK76pT6SqP8b+OpI
11
+ CG1QbcTZzYpbv6kAEQEAAbQSc3VwLXRlc3QtMkBmb28uYmFyiQHOBBMBCgA4FiEE
12
+ e0oXvVeqMzUcfd1s2bF8xbTizW8FAl7leTkCGw8FCwkIBwIGFQoJCAsCBBYCAwEC
13
+ HgECF4AACgkQ2bF8xbTizW92TAv/WGlYfDTKNEmJ0K+kxt33T2ldmZXaJKL04Mft
14
+ h5s5KlRZWDNpkCC/L55uyaeEg+Uy+BEEQKLAEeJrrLMV8UMJwMPDOizSTT9uLyiz
15
+ b8RjnQw4iMT8wt9TQboXGaTMslwdXvFPii7w44KgCimE7VuPetJuLMLMbnl147G8
16
+ +QhkNUsrB51TuPS8xZJ4qjbH+K/Y2NlvwLtJrxNE3SRQuy2ApYJxKPZIj1KpUL8M
17
+ 7Jy/2hI8DaRm/0Fpu8HwRIVsd6/dgdkqdj1uVyLj+wyhgdzqV5WrPLFCRVhd3icd
18
+ lPNRIDjg8YKCh353LVHjKwefOW4SnkOPn4uVMdCP9gUFd9zpMP9lMFpjk0o0tcYO
19
+ NiFrOclS4q5qZ5jrj1MnBF0NaGhuC83DDgRfKV+p5noVeJxg0nXYZSlsSMfAT/K7
20
+ FbdNEg0XUsrLgWVzhvWv/ebMetFPSfGHIveZ7lhiq1qpA5hLBNfSSBb1JJsFmtQt
21
+ cEUluymdNe5W7Y6UGs1CpvcIvbj+
22
+ =Cy9S
20
23
  -----END PGP PUBLIC KEY BLOCK-----
@@ -0,0 +1,75 @@
1
+ require "test_helper"
2
+
3
+ class TestMaildir < Minitest::Test
4
+
5
+ def setup
6
+ @path = Dir.mktmpdir
7
+
8
+ @test_message_1 = <<EOS
9
+ From: Bob <bob@bob.com>
10
+ To: a dear friend
11
+
12
+ Hello there friend. How are you? Blah is blah blah.
13
+ Wow. Maildir FTW, am I right?
14
+ EOS
15
+
16
+ end
17
+
18
+ def teardown
19
+ ObjectSpace.each_object(Class).select {|a| a < Redwood::Singleton}.each do |klass|
20
+ klass.deinstantiate! unless klass == Redwood::Logger
21
+ end
22
+ FileUtils.rm_r @path
23
+ end
24
+
25
+ def create_a_maildir(extra='')
26
+ maildir = File.join @path, "test_maildir#{extra}"
27
+ ['', 'cur', 'new', 'tmp'].each do |dir|
28
+ Dir.mkdir(File.join maildir, dir)
29
+ end
30
+ maildir
31
+ end
32
+
33
+ def create_a_maildir_email(folder, content)
34
+ File.write(File.join(folder, "#{Time.now.to_f}.hostname:2,S"), content)
35
+ end
36
+
37
+ def start_sup_and_add_source(source)
38
+ start
39
+ Index.init @path
40
+ Index.load
41
+ SourceManager.instance.instance_eval '@sources = {}'
42
+ SourceManager.instance.add_source source
43
+ PollManager.poll_from source
44
+ end
45
+
46
+ # and now, let the tests begin!
47
+
48
+ def test_can_index_a_maildir_directory
49
+
50
+ maildir = create_a_maildir
51
+ create_a_maildir_email(File.join(maildir, 'cur'), @test_message_1)
52
+ start_sup_and_add_source Maildir.new "maildir:#{maildir}"
53
+
54
+ messages_in_index = []
55
+ Index.instance.each_message {|a| messages_in_index << a}
56
+ refute_empty messages_in_index, 'There are no messages in the index'
57
+ assert_equal(messages_in_index.first.raw_message, @test_message_1)
58
+
59
+ end
60
+
61
+ def test_can_index_a_maildir_directory_with_special_characters
62
+
63
+ maildir = create_a_maildir URI_ENCODE_CHARS
64
+ create_a_maildir_email(File.join(maildir, 'cur'), @test_message_1)
65
+ start_sup_and_add_source Maildir.new "maildir:#{maildir}"
66
+
67
+ messages_in_index = []
68
+ Index.instance.each_message {|a| messages_in_index << a}
69
+ refute_empty messages_in_index, 'There are no messages in the index'
70
+ assert_equal(messages_in_index.first.raw_message, @test_message_1)
71
+
72
+ end
73
+
74
+ end
75
+
@@ -0,0 +1,69 @@
1
+ require "test_helper"
2
+
3
+ class TestMbox < MiniTest::Test
4
+
5
+ def setup
6
+ @path = Dir.mktmpdir
7
+
8
+ @test_message_1 = <<EOS
9
+ From sup-talk-bounces@rubyforge.org Mon Apr 27 12:56:18 2009
10
+ From: Bob <bob@bob.com>
11
+ To: Joe <joe@joe.com>
12
+
13
+ Hello there friend. How are you? Blah is blah blah.
14
+ I like mboxes, don't you?
15
+ EOS
16
+
17
+ end
18
+
19
+ def teardown
20
+ ObjectSpace.each_object(Class).select {|a| a < Redwood::Singleton}.each do |klass|
21
+ klass.deinstantiate! unless klass == Redwood::Logger
22
+ end
23
+ FileUtils.rm_r @path
24
+ end
25
+
26
+ def create_a_mbox(extra='')
27
+ mbox = File.join(@path, "test_mbox#{extra}.mbox")
28
+ File.write(mbox, @test_message_1)
29
+ mbox
30
+ end
31
+
32
+ def start_sup_and_add_source(source)
33
+ start
34
+ Index.init @path
35
+ Index.load
36
+ SourceManager.instance.instance_eval '@sources = {}'
37
+ SourceManager.instance.add_source source
38
+ PollManager.poll_from source
39
+ end
40
+
41
+ # and now, let the tests begin!
42
+
43
+ def test_can_index_a_mbox_directory
44
+
45
+ mbox = create_a_mbox
46
+ start_sup_and_add_source MBox.new "mbox:#{mbox}"
47
+
48
+ messages_in_index = []
49
+ Index.instance.each_message {|a| messages_in_index << a}
50
+ refute_empty messages_in_index, 'There are no messages in the index'
51
+ test_message_without_first_line = @test_message_1.sub(/^.*\n/,'')
52
+ assert_equal(messages_in_index.first.raw_message, test_message_without_first_line)
53
+
54
+ end
55
+
56
+ def test_can_index_a_mbox_directory_with_special_characters
57
+
58
+ mbox = create_a_mbox URI_ENCODE_CHARS
59
+ start_sup_and_add_source MBox.new "mbox:#{mbox}"
60
+
61
+ messages_in_index = []
62
+ Index.instance.each_message {|a| messages_in_index << a}
63
+ refute_empty messages_in_index, 'There are no messages in the index'
64
+ test_message_without_first_line = @test_message_1.sub(/^.*\n/,'')
65
+ assert_equal(messages_in_index.first.raw_message, test_message_without_first_line)
66
+
67
+ end
68
+
69
+ end
@@ -25,10 +25,11 @@ require 'tmpdir'
25
25
 
26
26
  module Redwood
27
27
 
28
- class TestCryptoManager < ::Minitest::Unit::TestCase
28
+ class TestCryptoManager < Minitest::Test
29
29
 
30
30
  def setup
31
31
  @from_email = 'sup-test-1@foo.bar'
32
+ @from_email_ecc = 'sup-fake-ecc@fake.fake'
32
33
  @to_email = 'sup-test-2@foo.bar'
33
34
  # Use test gnupg setup
34
35
  @orig_gnupghome = ENV['GNUPGHOME']
@@ -37,7 +38,7 @@ class TestCryptoManager < ::Minitest::Unit::TestCase
37
38
  @path = Dir.mktmpdir
38
39
  Redwood::HookManager.init File.join(@path, 'hooks')
39
40
 
40
- am = {:default=> {:name => "test", :email=> 'sup-test-1@foo.bar'}}
41
+ am = {:default=> {name: "test", email: @from_email, alternates: [@from_email_ecc]}}
41
42
  Redwood::AccountManager.init am
42
43
 
43
44
  Redwood::CryptoManager.init
@@ -60,6 +61,8 @@ class TestCryptoManager < ::Minitest::Unit::TestCase
60
61
  if CryptoManager.have_crypto? then
61
62
  signed = CryptoManager.sign @from_email,@to_email,"ABCDEFG"
62
63
  assert_instance_of RMail::Message, signed
64
+ assert_equal("multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256",
65
+ signed.header["Content-Type"])
63
66
  assert_equal "ABCDEFG", signed.body[0]
64
67
  assert signed.body[1].body.length > 0 , "signature length must be > 0"
65
68
  assert (signed.body[1].body.include? "-----BEGIN PGP SIGNATURE-----") , "Expecting PGP armored data"
@@ -104,6 +107,15 @@ class TestCryptoManager < ::Minitest::Unit::TestCase
104
107
  CryptoManager.verify signed.body[0], signed.body[1], true
105
108
  end
106
109
  end
110
+
111
+ def test_verify_unknown_keytype
112
+ if CryptoManager.have_crypto?
113
+ signed = CryptoManager.sign @from_email_ecc, @to_email, "ABCDEFG"
114
+ assert_instance_of RMail::Message, signed
115
+ assert_instance_of String, (signed.body[1].body)
116
+ CryptoManager.verify signed.body[0], signed.body[1], true
117
+ end
118
+ end
107
119
  end
108
120
 
109
121
  end
@@ -6,7 +6,7 @@ require 'stringio'
6
6
 
7
7
  include Redwood
8
8
 
9
- class TestMBoxParsing < Minitest::Unit::TestCase
9
+ class TestMBoxParsing < Minitest::Test
10
10
 
11
11
  def setup
12
12
  @path = Dir.mktmpdir
@@ -2,6 +2,9 @@ require "rubygems" rescue nil
2
2
  require 'minitest/autorun'
3
3
  require "rr"
4
4
 
5
- class Minitest::Unit::TestCase
6
- include ::RR::Adapters::MiniTest
7
- end
5
+ def fixture(filename)
6
+ file = ''
7
+ path = File.expand_path("../fixtures/#{filename}", __FILE__)
8
+ File.open(path) { |io| file = io.read }
9
+ file
10
+ end
@@ -6,27 +6,9 @@ require 'stringio'
6
6
 
7
7
  require 'dummy_source'
8
8
 
9
- # override File.exists? to make it work with StringIO for testing.
10
- # FIXME: do aliasing to avoid breaking this when sup moves from
11
- # File.exists? to File.exist?
12
-
13
- class File
14
-
15
- def File.exists? file
16
- # puts "fake File::exists?"
17
-
18
- if file.is_a?(StringIO)
19
- return false
20
- end
21
- # use the different function
22
- File.exist?(file)
23
- end
24
-
25
- end
26
-
27
9
  module Redwood
28
10
 
29
- class TestMessage < ::Minitest::Unit::TestCase
11
+ class TestMessage < Minitest::Test
30
12
 
31
13
  def setup
32
14
  @path = Dir.mktmpdir
@@ -39,38 +21,7 @@ class TestMessage < ::Minitest::Unit::TestCase
39
21
  end
40
22
 
41
23
  def test_simple_message
42
-
43
- message = <<EOS
44
- Return-path: <fake_sender@example.invalid>
45
- Envelope-to: fake_receiver@localhost
46
- Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
47
- Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
48
- (envelope-from <fake_sender@example.invalid>)
49
- id 1J1S8R-0006lA-MJ
50
- for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
51
- Date: Sun, 9 Dec 2007 21:48:19 +0200
52
- Mailing-List: contact example-help@example.invalid; run by ezmlm
53
- Precedence: bulk
54
- List-Id: <example.list-id.example.invalid>
55
- List-Post: <mailto:example@example.invalid>
56
- List-Help: <mailto:example-help@example.invalid>
57
- List-Unsubscribe: <mailto:example-unsubscribe@example.invalid>
58
- List-Subscribe: <mailto:example-subscribe@example.invalid>
59
- Delivered-To: mailing list example@example.invalid
60
- Delivered-To: moderator for example@example.invalid
61
- From: Fake Sender <fake_sender@example.invalid>
62
- To: Fake Receiver <fake_receiver@localhost>
63
- Subject: Re: Test message subject
64
- Message-ID: <20071209194819.GA25972@example.invalid>
65
- References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
66
- MIME-Version: 1.0
67
- Content-Type: text/plain; charset=us-ascii
68
- Content-Disposition: inline
69
- In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
70
- User-Agent: Sup/0.3
71
-
72
- Test message!
73
- EOS
24
+ message = fixture('simple-message.eml')
74
25
 
75
26
  source = DummySource.new("sup-test://test_simple_message")
76
27
  source.messages = [ message ]
@@ -80,11 +31,9 @@ EOS
80
31
  sup_message.load_from_source!
81
32
 
82
33
  # see how well parsing the header went
83
-
84
34
  to = sup_message.to
85
- # "to" is an Array containing person items
86
-
87
- # there should be only one item
35
+ assert(to.is_a? Array)
36
+ assert(to.first.is_a? Person)
88
37
  assert_equal(1, to.length)
89
38
 
90
39
  # sup doesn't do capitalized letters in email addresses
@@ -92,8 +41,7 @@ EOS
92
41
  assert_equal("Fake Receiver", to[0].name)
93
42
 
94
43
  from = sup_message.from
95
- # "from" is just a simple person item
96
-
44
+ assert(from.is_a? Person)
97
45
  assert_equal("fake_sender@example.invalid", from.email)
98
46
  assert_equal("Fake Sender", from.name)
99
47
 
@@ -124,13 +72,8 @@ EOS
124
72
  assert_equal(1, replytos.length)
125
73
  assert_equal("E1J1Rvb-0006k2-CE@localhost.localdomain", replytos[0])
126
74
 
127
- cc = sup_message.cc
128
- # there are no ccs
129
- assert_equal(0, cc.length)
130
-
131
- bcc = sup_message.bcc
132
- # there are no bccs
133
- assert_equal(0, bcc.length)
75
+ assert_empty(sup_message.cc)
76
+ assert_empty(sup_message.bcc)
134
77
 
135
78
  recipient_email = sup_message.recipient_email
136
79
  assert_equal("fake_receiver@localhost", recipient_email)
@@ -142,86 +85,22 @@ EOS
142
85
  assert_equal(message_source_info, source_info)
143
86
 
144
87
  # read the message body chunks
145
-
146
88
  chunks = sup_message.load_from_source!
147
89
 
148
90
  # there should be only one chunk
149
91
  assert_equal(1, chunks.length)
150
92
 
151
- lines = chunks[0].lines
93
+ lines = chunks.first.lines
152
94
 
153
95
  # there should be only one line
154
96
  assert_equal(1, lines.length)
155
97
 
156
- assert_equal("Test message!", lines[0])
157
-
98
+ assert_equal("Test message!", lines.first)
158
99
  end
159
100
 
160
101
  def test_multipart_message
102
+ message = fixture('multi-part.eml')
161
103
 
162
- message = <<EOS
163
- From fake_receiver@localhost Sun Dec 09 22:33:37 +0200 2007
164
- Subject: Re: Test message subject
165
- From: Fake Receiver <fake_receiver@localhost>
166
- To: Fake Sender <fake_sender@example.invalid>
167
- References: <E1J1Rvb-0006k2-CE@localhost.localdomain> <20071209194819.GA25972example.invalid>
168
- In-Reply-To: <20071209194819.GA25972example.invalid>
169
- Date: Sun, 09 Dec 2007 22:33:37 +0200
170
- Message-Id: <1197232243-sup-2663example.invalid>
171
- User-Agent: Sup/0.3
172
- Content-Type: multipart/mixed; boundary="=-1197232418-506707-26079-6122-2-="
173
- MIME-Version: 1.0
174
-
175
-
176
- --=-1197232418-506707-26079-6122-2-=
177
- Content-Type: text/plain; charset=utf-8
178
- Content-Disposition: inline
179
-
180
- Excerpts from Fake Sender's message of Sun Dec 09 21:48:19 +0200 2007:
181
- > Test message!
182
-
183
- Thanks for the message!
184
- --=-1197232418-506707-26079-6122-2-=
185
- Content-Disposition: attachment; filename="HACKING"
186
- Content-Type: application/octet-stream; name="HACKING"
187
- Content-Transfer-Encoding: base64
188
-
189
- UnVubmluZyBTdXAgbG9jYWxseQotLS0tLS0tLS0tLS0tLS0tLS0tCkludm9r
190
- ZSBpdCBsaWtlIHRoaXM6CgpydWJ5IC1JIGxpYiAtdyBiaW4vc3VwCgpZb3Un
191
- bGwgaGF2ZSB0byBpbnN0YWxsIGFsbCBnZW1zIG1lbnRpb25lZCBpbiB0aGUg
192
- UmFrZWZpbGUgKGxvb2sgZm9yIHRoZSBsaW5lCnNldHRpbmcgcC5leHRyYV9k
193
- ZXBzKS4gSWYgeW91J3JlIG9uIGEgRGViaWFuIG9yIERlYmlhbi1iYXNlZCBz
194
- eXN0ZW0gKGUuZy4KVWJ1bnR1KSwgeW91J2xsIGhhdmUgdG8gbWFrZSBzdXJl
195
- IHlvdSBoYXZlIGEgY29tcGxldGUgUnVieSBpbnN0YWxsYXRpb24sCmVzcGVj
196
- aWFsbHkgbGlic3NsLXJ1YnkuCgpDb2Rpbmcgc3RhbmRhcmRzCi0tLS0tLS0t
197
- LS0tLS0tLS0KCi0gRG9uJ3Qgd3JhcCBjb2RlIHVubGVzcyBpdCByZWFsbHkg
198
- YmVuZWZpdHMgZnJvbSBpdC4gVGhlIGRheXMgb2YKICA4MC1jb2x1bW4gZGlz
199
- cGxheXMgYXJlIGxvbmcgb3Zlci4gQnV0IGRvIHdyYXAgY29tbWVudHMgYW5k
200
- IG90aGVyCiAgdGV4dCBhdCB3aGF0ZXZlciBFbWFjcyBtZXRhLVEgZG9lcy4K
201
- LSBJIGxpa2UgcG9ldHJ5IG1vZGUuCi0gVXNlIHt9IGZvciBvbmUtbGluZXIg
202
- YmxvY2tzIGFuZCBkby9lbmQgZm9yIG11bHRpLWxpbmUgYmxvY2tzLgoK
203
-
204
- --=-1197232418-506707-26079-6122-2-=
205
- Content-Disposition: attachment; filename="Manifest.txt"
206
- Content-Type: text/plain; name="Manifest.txt"
207
- Content-Transfer-Encoding: quoted-printable
208
-
209
- HACKING
210
- History.txt
211
- LICENSE
212
- Manifest.txt
213
- README.txt
214
- Rakefile
215
- bin/sup
216
- bin/sup-add
217
- bin/sup-config
218
- bin/sup-dump
219
- bin/sup-recover-sources
220
- bin/sup-sync
221
- bin/sup-sync-back
222
-
223
- --=-1197232418-506707-26079-6122-2-=--
224
- EOS
225
104
  source = DummySource.new("sup-test://test_multipart_message")
226
105
  source.messages = [ message ]
227
106
  source_info = 0
@@ -230,17 +109,16 @@ EOS
230
109
  sup_message.load_from_source!
231
110
 
232
111
  # read the message body chunks
233
-
234
112
  chunks = sup_message.load_from_source!
235
113
 
236
114
  # this time there should be four chunks: first the quoted part of
237
115
  # the message, then the non-quoted part, then the two attachments
238
116
  assert_equal(4, chunks.length)
239
117
 
240
- assert_equal(chunks[0].class, Redwood::Chunk::Quote)
241
- assert_equal(chunks[1].class, Redwood::Chunk::Text)
242
- assert_equal(chunks[2].class, Redwood::Chunk::Attachment)
243
- assert_equal(chunks[3].class, Redwood::Chunk::Attachment)
118
+ assert(chunks[0].is_a? Redwood::Chunk::Quote)
119
+ assert(chunks[1].is_a? Redwood::Chunk::Text)
120
+ assert(chunks[2].is_a? Redwood::Chunk::Attachment)
121
+ assert(chunks[3].is_a? Redwood::Chunk::Attachment)
244
122
 
245
123
  # further testing of chunks will happen in test_message_chunks.rb
246
124
  # (possibly not yet implemented)
@@ -248,29 +126,7 @@ EOS
248
126
  end
249
127
 
250
128
  def test_broken_message_1
251
-
252
- # an example of a broken message, missing "to" and "from" fields
253
-
254
- message = <<EOS
255
- Return-path: <fake_sender@example.invalid>
256
- Envelope-to: fake_receiver@localhost
257
- Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
258
- Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
259
- (envelope-from <fake_sender@example.invalid>)
260
- id 1J1S8R-0006lA-MJ
261
- for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
262
- Date: Sun, 9 Dec 2007 21:48:19 +0200
263
- Subject: Re: Test message subject
264
- Message-ID: <20071209194819.GA25972@example.invalid>
265
- References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
266
- MIME-Version: 1.0
267
- Content-Type: text/plain; charset=us-ascii
268
- Content-Disposition: inline
269
- In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
270
- User-Agent: Sup/0.3
271
-
272
- Test message!
273
- EOS
129
+ message = fixture('missing-from-to.eml')
274
130
 
275
131
  source = DummySource.new("sup-test://test_broken_message_1")
276
132
  source.messages = [ message ]
@@ -281,9 +137,9 @@ EOS
281
137
 
282
138
  to = sup_message.to
283
139
 
284
- # there should no items, since the message doesn't have any
285
- # recipients -- still not nil
286
- assert_equal(0, to.length)
140
+ # there should no items, since the message doesn't have any recipients -- still not nil
141
+ assert(!to.nil?)
142
+ assert_empty(to)
287
143
 
288
144
  # from will have bogus values
289
145
  from = sup_message.from
@@ -294,29 +150,7 @@ EOS
294
150
  end
295
151
 
296
152
  def test_broken_message_2
297
-
298
- # an example of a broken message, no body at all
299
-
300
- message = <<EOS
301
- Return-path: <fake_sender@example.invalid>
302
- From: Fake Sender <fake_sender@example.invalid>
303
- To: Fake Receiver <fake_receiver@localhost>
304
- Envelope-to: fake_receiver@localhost
305
- Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
306
- Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
307
- (envelope-from <fake_sender@example.invalid>)
308
- id 1J1S8R-0006lA-MJ
309
- for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
310
- Date: Sun, 9 Dec 2007 21:48:19 +0200
311
- Subject: Re: Test message subject
312
- Message-ID: <20071209194819.GA25972@example.invalid>
313
- References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
314
- MIME-Version: 1.0
315
- Content-Type: text/plain; charset=us-ascii
316
- Content-Disposition: inline
317
- In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
318
- User-Agent: Sup/0.3
319
- EOS
153
+ message = fixture('no-body.eml')
320
154
 
321
155
  source = DummySource.new("sup-test://test_broken_message_1")
322
156
  source.messages = [ message ]
@@ -329,90 +163,12 @@ EOS
329
163
 
330
164
  chunks = sup_message.load_from_source!
331
165
 
332
- # the chunks list should be empty
333
-
334
- assert_equal(0, chunks.length)
335
-
166
+ assert_empty(chunks)
336
167
  end
337
168
 
338
169
  def test_multipart_message_2
170
+ message = fixture('multi-part-2.eml')
339
171
 
340
- message = <<EOS
341
- Return-path: <vim-mac-return-3938-fake_receiver=localhost@vim.org>
342
- Envelope-to: fake_receiver@localhost
343
- Delivery-date: Wed, 14 Jun 2006 19:22:54 +0300
344
- Received: from localhost ([127.0.0.1] helo=localhost.localdomain)
345
- by localhost.localdomain with esmtp (Exim 4.60)
346
- (envelope-from <vim-mac-return-3938-fake_receiver=localhost@vim.org>)
347
- id 1FqXk3-0006jM-48
348
- for fake_receiver@localhost; Wed, 14 Jun 2006 18:57:15 +0300
349
- Received: from pop.gmail.com
350
- by localhost.localdomain with POP3 (fetchmail-6.3.2)
351
- for <fake_receiver@localhost> (single-drop); Wed, 14 Jun 2006 18:57:15 +0300 (EEST)
352
- X-Gmail-Received: 8ee0fe5f895736974c042c8eaf176014b1ba7b88
353
- Delivered-To: fake_receiver@localhost
354
- Received: by 10.49.8.16 with SMTP id l16cs11327nfi;
355
- Sun, 26 Mar 2006 19:31:56 -0800 (PST)
356
- Received: by 10.66.224.8 with SMTP id w8mr2172862ugg;
357
- Sun, 26 Mar 2006 19:31:56 -0800 (PST)
358
- Received: from foobar.math.fu-berlin.de (foobar.math.fu-berlin.de [160.45.45.151])
359
- by mx.gmail.com with SMTP id j3si553645ugd.2006.03.26.19.31.56;
360
- Sun, 26 Mar 2006 19:31:56 -0800 (PST)
361
- Received-SPF: neutral (gmail.com: 160.45.45.151 is neither permitted nor denied by best guess record for domain of vim-mac-return-3938-fake_receiver=localhost@vim.org)
362
- Message-Id: <44275cac.74a494f1.315a.ffff825cSMTPIN_ADDED@mx.gmail.com>
363
- Received: (qmail 24265 invoked by uid 200); 27 Mar 2006 02:32:39 -0000
364
- Mailing-List: contact vim-mac-help@vim.org; run by ezmlm
365
- Precedence: bulk
366
- Delivered-To: mailing list vim-mac@vim.org
367
- Received: (qmail 7913 invoked from network); 26 Mar 2006 23:37:34 -0000
368
- Received: from cpe-138-217-96-243.vic.bigpond.net.au (HELO vim.org) (138.217.96.243)
369
- by foobar.math.fu-berlin.de with SMTP; 26 Mar 2006 23:37:34 -0000
370
- From: fake_sender@example.invalid
371
- To: vim-mac@vim.org
372
- Subject: Mail Delivery (failure vim-mac@vim.org)
373
- Date: Mon, 27 Mar 2006 10:29:39 +1000
374
- MIME-Version: 1.0
375
- Content-Type: multipart/related;
376
- type="multipart/alternative";
377
- boundary="----=_NextPart_000_001B_01C0CA80.6B015D10"
378
- X-Priority: 3
379
- X-MSMail-Priority: Normal
380
-
381
- ------=_NextPart_000_001B_01C0CA80.6B015D10
382
- Content-Type: multipart/alternative;
383
- boundary="----=_NextPart_001_001C_01C0CA80.6B015D10"
384
-
385
- ------=_NextPart_001_001C_01C0CA80.6B015D10
386
- Content-Type: text/plain;
387
- charset="iso-8859-1"
388
- Content-Transfer-Encoding: quoted-printable
389
-
390
- ------=_NextPart_001_001C_01C0CA80.6B015D10
391
- Content-Type: text/html;
392
- charset="iso-8859-1"
393
- Content-Transfer-Encoding: quoted-printable
394
-
395
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
396
- <HTML><HEAD>
397
- <META content=3D"text/html; charset=3Diso-8859-1" =
398
- http-equiv=3DContent-Type>
399
- <META content=3D"MSHTML 5.00.2920.0" name=3DGENERATOR>
400
- <STYLE></STYLE>
401
- </HEAD>
402
- <BODY bgColor=3D#ffffff>If the message will not displayed automatically,<br>
403
- follow the link to read the delivered message.<br><br>
404
- Received message is available at:<br>
405
- <a href=3Dcid:031401Mfdab4$3f3dL780$73387018@57W81fa70Re height=3D0 width=3D0>www.vim.org/inbox/vim-mac/read.php?sessionid-18559</a>
406
- <iframe
407
- src=3Dcid:031401Mfdab4$3f3dL780$73387018@57W81fa70Re height=3D0 width=3D0></iframe>
408
- <DIV>&nbsp;</DIV></BODY></HTML>
409
-
410
- ------=_NextPart_001_001C_01C0CA80.6B015D10--
411
-
412
- ------=_NextPart_000_001B_01C0CA80.6B015D10--
413
-
414
-
415
- EOS
416
172
  source = DummySource.new("sup-test://test_multipart_message_2")
417
173
  source.messages = [ message ]
418
174
  source_info = 0
@@ -420,86 +176,58 @@ EOS
420
176
  sup_message = Message.build_from_source(source, source_info)
421
177
  sup_message.load_from_source!
422
178
 
423
- # read the message body chunks
179
+ chunks = sup_message.load_from_source! # read the message body chunks
180
+
181
+ # TODO: Add more asserts
182
+ end
183
+
184
+ def test_text_attachment_decoding
185
+ message = fixture('text-attachments-with-charset.eml')
424
186
 
187
+ source = DummySource.new("sup-test://test_text_attachment_decoding")
188
+ source.messages = [ message ]
189
+ source_info = 0
190
+
191
+ sup_message = Message.build_from_source(source, source_info)
425
192
  sup_message.load_from_source!
193
+
194
+ chunks = sup_message.load_from_source!
195
+ assert_equal(5, chunks.length)
196
+ assert(chunks[0].is_a? Redwood::Chunk::Text)
197
+ ## The first attachment declares charset=us-ascii
198
+ assert(chunks[1].is_a? Redwood::Chunk::Attachment)
199
+ assert_equal(["This is ASCII"], chunks[1].lines)
200
+ ## The second attachment declares charset=koi8-r and has some Cyrillic
201
+ assert(chunks[2].is_a? Redwood::Chunk::Attachment)
202
+ assert_equal(["\u041f\u0440\u0438\u0432\u0435\u0442"], chunks[2].lines)
203
+ ## The third attachment declares charset=utf-8 and has an emoji
204
+ assert(chunks[3].is_a? Redwood::Chunk::Attachment)
205
+ assert_equal(["\u{1f602}"], chunks[3].lines)
206
+ ## The fourth attachment declares no charset and has a non-ASCII byte,
207
+ ## which will be replaced with U+FFFD REPLACEMENT CHARACTER
208
+ assert(chunks[4].is_a? Redwood::Chunk::Attachment)
209
+ assert_equal(["Embedded\ufffdgarbage"], chunks[4].lines)
426
210
  end
427
211
 
428
- def test_blank_header_lines
212
+ def test_mailing_list_header
213
+ message = fixture('mailing-list-header.eml')
429
214
 
430
- message = <<EOS
431
- Return-Path: <monitor-list-bounces@widget.com>
432
- X-Original-To: nobody@localhost
433
- Delivered-To: nobody@localhost.eng.widget.com
434
- Received: from localhost (localhost.localdomain [127.0.0.1])
435
- by soquel.eng.widget.com (Postfix) with ESMTP id 609BC13C0DB1
436
- for <nobody@localhost>; Thu, 19 Mar 2009 13:43:21 -0700 (PDT)
437
- MIME-Version: 1.0
438
- Received: from pa-excas-vip.widget.com [10.16.67.200]
439
- by localhost with IMAP (fetchmail-6.2.5)
440
- for nobody@localhost (single-drop); Thu, 19 Mar 2009 13:43:21 -0700 (PDT)
441
- Received: from pa-exht01.widget.com (10.113.81.167) by pa-excaht11.widget.com
442
- (10.113.81.197) with Microsoft SMTP Server (TLS) id 8.1.311.2; Thu, 19 Mar
443
- 2009 13:42:30 -0700
444
- Received: from mailman2.widget.com (10.16.64.159) by pa-exht01.widget.com
445
- (10.113.81.167) with Microsoft SMTP Server id 8.1.336.0; Thu, 19 Mar 2009
446
- 13:42:30 -0700
447
- Received: by mailman2.widget.com (Postfix) id 47095AE30856; Thu, 19 Mar 2009
448
- 13:42:29 -0700 (PDT)
449
- Received: from countchocula.widget.com (localhost.localdomain [127.0.0.1]) by
450
- mailman2.widget.com (Postfix) with ESMTP id 5F782ABC5948; Thu, 19 Mar 2009
451
- 13:42:28 -0700 (PDT)
452
- Received: from mailhost4.widget.com (mailhost4.widget.com [10.16.67.124]) by
453
- mailman2.widget.com (Postfix) with ESMTP id 6CDCCABC5948 for
454
- <monitor-list@mailman2.widget.com>; Thu, 19 Mar 2009 13:42:26 -0700 (PDT)
455
- Received: by mailhost4.widget.com (Postfix) id 2364AC9AC4; Thu, 19 Mar 2009
456
- 13:42:26 -0700 (PDT)
457
- Received: from pa-exht01.widget.com (pa-exht01.widget.com [10.113.81.167]) by
458
- mailhost4.widget.com (Postfix) with ESMTP id 17A68C9AC3 for
459
- <monitor-list@widget.com>; Thu, 19 Mar 2009 13:42:26 -0700 (PDT)
460
- Received: from PA-EXMBX04.widget.com ([10.113.81.142]) by pa-exht01.widget.com
461
- ([10.113.81.167]) with mapi; Thu, 19 Mar 2009 13:42:26 -0700
462
- From: Some User <someuser@widget.com>
463
- To: "monitor-list@widget.com" <monitor-list@widget.com>
464
- Sender: "monitor-list-bounces@widget.com" <monitor-list-bounces@widget.com>
465
- Date: Thu, 19 Mar 2009 13:42:25 -0700
466
- Subject: Looking for a mac
467
- Thread-Topic: Looking for a mac
468
- Thread-Index: AQHJqNM1xIqqjNRWuUCUBaxzPFK5eQ==
469
- Message-ID:
470
- <D3C12B2AD838B44DA9D6B2CA334246D011E72A73A4@PA-EXMBX04.widget.com>
471
- List-Help: <mailto:monitor-list-request@widget.com?subject=help>
472
- List-Subscribe: <http://mailman2.widget.com/mailman/listinfo/monitor-list>,
473
- <mailto:monitor-list-request@widget.com?subject=subscribe>
474
- List-Unsubscribe:
475
- <http://mailman2.widget.com/mailman/listinfo/monitor-list>,
476
- <mailto:monitor-list-request@widget.com?subject=unsubscribe>
477
- Accept-Language: en-US
478
- Content-Language: en-US
479
- X-MS-Exchange-Organization-AuthAs: Anonymous
480
- X-MS-Exchange-Organization-AuthSource: pa-exht01.widget.com
481
- X-MS-Has-Attach:
482
- X-Auto-Response-Suppress: All
483
- X-MS-TNEF-Correlator:
484
- acceptlanguage: en-US
485
- delivered-to: monitor-list@widget.com
486
- errors-to: monitor-list-bounces@widget.com
487
- list-id: engineering monitor related <monitor-list.widget.com>
488
- x-mailman-version: 2.1.8
489
- x-beenthere: monitor-list@widget.com
490
- x-original-to: monitor-list@mailman2.widget.com
491
- list-post: <mailto:monitor-list@widget.com>
492
- list-archive: <http://mailman2.widget.com/pipermail/monitor-list>
493
- Content-Type: text/plain; charset="us-ascii"
494
- Content-Transfer-Encoding: quoted-printable
495
-
496
- Hi all,
497
-
498
- Just wondering if anybody can lend me a mac to reproduce PR 384931 ?
499
- Thanks.
500
-
501
- Michael=
502
- EOS
215
+ source = DummySource.new("sup-test://test_mailing_list_header")
216
+ source.messages = [ message ]
217
+ source_info = 0
218
+
219
+ sup_message = Message.build_from_source(source, source_info)
220
+ sup_message.load_from_source!
221
+
222
+ assert(sup_message.list_subscribe.nil?)
223
+ assert_equal("<https://lists.openembedded.org/g/openembedded-devel/unsub>",
224
+ sup_message.list_unsubscribe)
225
+ assert_equal("openembedded-devel@lists.openembedded.org", sup_message.list_address.email)
226
+ assert_equal("openembedded-devel", sup_message.list_address.name)
227
+ end
228
+
229
+ def test_blank_header_lines
230
+ message = fixture('blank-header-fields.eml')
503
231
 
504
232
  source = DummySource.new("sup-test://test_blank_header_lines")
505
233
  source.messages = [ message ]
@@ -514,17 +242,63 @@ EOS
514
242
 
515
243
  # Look at another header field whose first line was blank.
516
244
  list_unsubscribe = sup_message.list_unsubscribe
517
- assert_equal("<http://mailman2.widget.com/mailman/listinfo/monitor-list>,\n \t" +
245
+ assert_equal("<http://mailman2.widget.com/mailman/listinfo/monitor-list>,\n\t" +
518
246
  "<mailto:monitor-list-request@widget.com?subject=unsubscribe>",
519
247
  list_unsubscribe)
520
248
 
521
249
  end
522
250
 
251
+ def test_malicious_attachment_names
252
+ message = fixture('malicious-attachment-names.eml')
253
+
254
+ source = DummySource.new("sup-test://test_blank_header_lines")
255
+ source.messages = [ message ]
256
+ source_info = 0
257
+
258
+ sup_message = Message.build_from_source(source, source_info)
259
+ chunks = sup_message.load_from_source!
260
+
261
+ # See if attachment filenames can be safely used for saving.
262
+ # We do that by verifying that any folder-related character (/ or \)
263
+ # are not interpreted: the filename must not be interpreted into a
264
+ # path.
265
+ fn = chunks[3].safe_filename
266
+ assert_equal(fn, File.basename(fn))
267
+ end
523
268
  # TODO: test different error cases, malformed messages etc.
524
269
 
525
270
  # TODO: test different quoting styles, see that they are all divided
526
271
  # to chunks properly
527
272
 
273
+ def test_zimbra_quote_with_bottom_post
274
+ # Zimbra does an Outlook-style "Original Message" delimiter and then *also*
275
+ # prefixes each quoted line with a > marker. That's okay until the sender
276
+ # tries to do the right thing and reply after the quote.
277
+ # In this case we want to just look at the > markers when determining where
278
+ # the quoted chunk ends.
279
+ message = fixture('zimbra-quote-with-bottom-post.eml')
280
+
281
+ source = DummySource.new("sup-test://test_zimbra_quote_with_bottom_post")
282
+ source.messages = [ message ]
283
+ source_info = 0
284
+
285
+ sup_message = Message.build_from_source(source, source_info)
286
+ chunks = sup_message.load_from_source!
287
+
288
+ assert_equal(3, chunks.length)
289
+
290
+ # TODO this chunk should ideally be part of the quote chunk after it.
291
+ assert(chunks[0].is_a? Redwood::Chunk::Text)
292
+ assert_equal(1, chunks[0].lines.length)
293
+ assert_equal("----- Original Message -----", chunks[0].lines.first)
294
+
295
+ assert(chunks[1].is_a? Redwood::Chunk::Quote)
296
+
297
+ assert(chunks[2].is_a? Redwood::Chunk::Text)
298
+ assert_equal(3, chunks[2].lines.length)
299
+ assert_equal("This is the reply from the Zimbra user.",
300
+ chunks[2].lines[2])
301
+ end
528
302
  end
529
303
 
530
304
  end