sup 1.0 → 1.2

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 +4 -4
  2. data/.github/workflows/checks.yml +58 -0
  3. data/.rubocop.yml +5 -0
  4. data/CONTRIBUTORS +5 -2
  5. data/Gemfile +5 -1
  6. data/History.txt +33 -0
  7. data/Manifest.txt +171 -0
  8. data/README.md +9 -4
  9. data/Rakefile +40 -1
  10. data/bin/sup-add +4 -8
  11. data/bin/sup-sync-back-maildir +1 -1
  12. data/contrib/nix/Gemfile +22 -0
  13. data/contrib/nix/Gemfile.lock +80 -0
  14. data/contrib/nix/README +7 -0
  15. data/contrib/nix/gem-install-shell.nix +12 -0
  16. data/contrib/nix/gemset.nix +339 -0
  17. data/contrib/nix/ruby2.4-Gemfile.lock +81 -0
  18. data/contrib/nix/ruby2.4-gemset.nix +309 -0
  19. data/contrib/nix/ruby2.4-shell.nix +30 -0
  20. data/contrib/nix/ruby2.5-Gemfile.lock +81 -0
  21. data/contrib/nix/ruby2.5-gemset.nix +309 -0
  22. data/contrib/nix/ruby2.5-shell.nix +30 -0
  23. data/contrib/nix/ruby2.6-Gemfile.lock +83 -0
  24. data/contrib/nix/ruby2.6-gemset.nix +319 -0
  25. data/contrib/nix/ruby2.6-shell.nix +30 -0
  26. data/contrib/nix/ruby2.7-shell.nix +23 -0
  27. data/contrib/nix/ruby3.0-shell.nix +23 -0
  28. data/contrib/nix/ruby3.1-shell.nix +23 -0
  29. data/contrib/nix/ruby3.2-shell.nix +23 -0
  30. data/contrib/nix/ruby3.3-shell.nix +23 -0
  31. data/contrib/nix/test-all-rubies.sh +6 -0
  32. data/doc/Hooks.txt +1 -1
  33. data/ext/mkrf_conf_xapian.rb +12 -6
  34. data/lib/sup/colormap.rb +1 -1
  35. data/lib/sup/crypto.rb +1 -1
  36. data/lib/sup/hook.rb +1 -1
  37. data/lib/sup/index.rb +4 -4
  38. data/lib/sup/keymap.rb +1 -1
  39. data/lib/sup/maildir.rb +5 -5
  40. data/lib/sup/mbox.rb +5 -5
  41. data/lib/sup/message.rb +8 -7
  42. data/lib/sup/message_chunks.rb +27 -19
  43. data/lib/sup/modes/completion_mode.rb +0 -1
  44. data/lib/sup/modes/console_mode.rb +1 -1
  45. data/lib/sup/modes/file_browser_mode.rb +2 -2
  46. data/lib/sup/modes/label_list_mode.rb +1 -1
  47. data/lib/sup/modes/search_list_mode.rb +2 -2
  48. data/lib/sup/modes/thread_view_mode.rb +1 -2
  49. data/lib/sup/rfc2047.rb +21 -6
  50. data/lib/sup/source.rb +8 -2
  51. data/lib/sup/textfield.rb +0 -1
  52. data/lib/sup/thread.rb +20 -21
  53. data/lib/sup/util.rb +31 -53
  54. data/lib/sup/version.rb +1 -1
  55. data/lib/sup.rb +12 -8
  56. data/man/sup-add.1 +39 -39
  57. data/man/sup-config.1 +31 -27
  58. data/man/sup-dump.1 +34 -35
  59. data/man/sup-import-dump.1 +36 -32
  60. data/man/sup-psych-ify-config-files.1 +29 -25
  61. data/man/sup-recover-sources.1 +32 -28
  62. data/man/sup-sync-back-maildir.1 +34 -30
  63. data/man/sup-sync.1 +40 -36
  64. data/man/sup-tweak-labels.1 +36 -32
  65. data/man/sup.1 +41 -37
  66. data/shell.nix +1 -0
  67. data/sup.gemspec +6 -4
  68. data/test/dummy_source.rb +21 -15
  69. data/test/fixtures/embedded-message.eml +34 -0
  70. data/test/fixtures/non-ascii-header-in-nested-message.eml +36 -0
  71. data/test/fixtures/non-ascii-header.eml +8 -0
  72. data/test/fixtures/rfc2047-header-encoding.eml +15 -0
  73. data/test/fixtures/text-attachments-with-charset.eml +15 -1
  74. data/test/fixtures/utf8-header.eml +17 -0
  75. data/test/integration/test_maildir.rb +3 -0
  76. data/test/integration/test_mbox.rb +4 -1
  77. data/test/integration/test_sup-add.rb +83 -0
  78. data/test/integration/test_sup-sync-back-maildir.rb +40 -0
  79. data/test/test_crypto.rb +44 -0
  80. data/test/test_header_parsing.rb +11 -3
  81. data/test/test_helper.rb +7 -4
  82. data/test/test_message.rb +124 -32
  83. data/test/test_messages_dir.rb +13 -15
  84. data/test/unit/test_horizontal_selector.rb +4 -4
  85. data/test/unit/test_locale_fiddler.rb +1 -1
  86. data/test/unit/util/test_query.rb +1 -1
  87. data/test/unit/util/test_string.rb +3 -3
  88. data/test/unit/util/test_uri.rb +2 -2
  89. metadata +69 -18
  90. data/.travis.yml +0 -18
  91. data/bin/sup-psych-ify-config-files +0 -21
  92. data/test/integration/test_label_service.rb +0 -18
  93. data/test/test_yaml_migration.rb +0 -85
@@ -0,0 +1,36 @@
1
+ Return-Path: <spammer@example.com>
2
+ From: SPAM � <spammer@example.com>
3
+ To: <recipient@example.invalid>
4
+ Subject: spam � spam
5
+ MIME-Version: 1.0
6
+ Content-Type: multipart/mixed; boundary="----------=_4F506AC2.EE281DC4"
7
+ Message-Id: <20120302063755.0FE2122017@a.a.a.a>
8
+ Date: Fri, 2 Mar 2012 07:37:55 +0100 (CET)
9
+
10
+ This is a multi-part message in MIME format.
11
+
12
+ ------------=_4F506AC2.EE281DC4
13
+ Content-Type: text/plain; charset=iso-8859-1
14
+ Content-Disposition: inline
15
+ Content-Transfer-Encoding: 8bit
16
+
17
+ Spam detection software, running on the system "a.a.a.a.a.", has
18
+ identified this incoming email as possible spam. The original message
19
+ has been attached to this so you can view it (if it isn't spam) or label
20
+ similar future email.
21
+
22
+
23
+ ------------=_4F506AC2.EE281DC4
24
+ Content-Type: message/rfc822; x-spam-type=original
25
+ Content-Description: original message before SpamAssassin
26
+ Content-Disposition: attachment
27
+ Content-Transfer-Encoding: 8bit
28
+
29
+ From: SPAM � <spammer@example.com>
30
+ To: <enclosed@example.invalid>
31
+ Subject: spam � spam
32
+
33
+ This is a spam.
34
+
35
+ ------------=_4F506AC2.EE281DC4--
36
+
@@ -0,0 +1,8 @@
1
+ Return-Path: <spammer@example.com>
2
+ From: SPAM � <spammer@example.com>
3
+ To: <a@b.c>
4
+ Subject: spam � spam
5
+ Message-Id: <20120302063755.0FE2122017@a.a.a.a>
6
+ Date: Fri, 2 Mar 2012 07:37:55 +0100 (CET)
7
+
8
+ https://github.com/sup-heliotrope/sup/issues/205
@@ -0,0 +1,15 @@
1
+ From: test@example.invalid
2
+ To: test@example.invalid
3
+ Date: Sun, 19 Jul 2020 17:03:56 +1000
4
+ Subject:
5
+ =?US-ASCII?q?Hans Martin Djupvik?= =?ISO-8859-1?q?,_Ingrid_B=F8?=
6
+ =?KOI8-R?b?LCDp0snOwSDzycTP0s/XwQ?=
7
+ =?UTF-16?b?//4sACAASgBlAHMAcABlAHIAIABCAGUAcgBnAA?=
8
+ =?UTF-7?b?LCBGcmlkYSBFbmcrQVBnLQ?=
9
+ bad: =?UTF16?q?badcharsetname?= =?US-ASCII?b?/w?=
10
+ =?UTF-7?Q?=41=6D=65=72=69=63=61=E2=80=99=73?=
11
+
12
+ The subject header contains various RFC2047 encoded words.
13
+ For completeness we test both base64 and quoted-printable, and some
14
+ ASCII-incompatible encodings.
15
+ We also include some bogus words which cannot be decoded.
@@ -42,5 +42,19 @@ Content-Disposition: attachment; filename="bad.txt"
42
42
  MIME-Version: 1.0
43
43
 
44
44
  Embedded=F0garbage
45
- --===============2385509127900810307==--
45
+
46
+ --===============2385509127900810307==
47
+ Content-Type: text/plain; charset="invalid-test"; name="invalid-charset.txt"
48
+ Content-Transfer-Encoding: quoted-printable
49
+ Content-Disposition: attachment; filename="invalid-charset.txt"
50
+
51
+ Example invalid charset
52
+ --===============2385509127900810307==
53
+ Content-Type: text/plain; charset="utf-7"
54
+ Content-Transfer-Encoding: 7bit
55
+ MIME-Version: 1.0
56
+ Content-Disposition: attachment; filename="ascii.txt"
57
+
58
+ This is +Jyg-UTF-7+Jyg-
59
+ --===============2385509127900810307==
46
60
 
@@ -0,0 +1,17 @@
1
+ Delivered-To: djc@djc.id.au
2
+ Received: from orpheus.librarything.com (orpheus.librarything.com [74.201.105.9])
3
+ by djc.id.au (Postfix) with ESMTP id A0CCB20AAB99
4
+ for <djc@djc.id.au>; Sat, 23 Jan 2021 02:52:15 +1000 (AEST)
5
+ Received: by orpheus.librarything.com (Postfix, from userid 0)
6
+ id 21B172C20F9; Fri, 22 Jan 2021 11:52:08 -0500 (EST)
7
+ To: djc@djc.id.au
8
+ Subject: LibraryThing: State of the Thing — January
9
+ MIME-Version: 1.0
10
+ Content-type: text/html; charset=iso-8859-1
11
+ From: tim@librarything.com
12
+ Reply-To: tim@librarything.com
13
+ Message-Id: <20210122165208.21B172C20F9@orpheus.librarything.com>
14
+ Date: Fri, 22 Jan 2021 11:52:08 -0500 (EST)
15
+ Return-Path: <root@orpheus.librarything.com>
16
+
17
+ <p>Some stuff</p>
@@ -1,7 +1,10 @@
1
+ require "sup"
1
2
  require "test_helper"
2
3
 
3
4
  class TestMaildir < Minitest::Test
4
5
 
6
+ include Redwood
7
+
5
8
  def setup
6
9
  @path = Dir.mktmpdir
7
10
 
@@ -1,6 +1,9 @@
1
+ require "sup"
1
2
  require "test_helper"
2
3
 
3
- class TestMbox < MiniTest::Test
4
+ class TestMbox < Minitest::Test
5
+
6
+ include Redwood
4
7
 
5
8
  def setup
6
9
  @path = Dir.mktmpdir
@@ -0,0 +1,83 @@
1
+ class TestSupAdd < Minitest::Test
2
+
3
+ def setup
4
+ @path = Dir.mktmpdir
5
+ end
6
+
7
+ def teardown
8
+ FileUtils.rm_r @path
9
+ end
10
+
11
+ def test_can_add_maildir_source
12
+ _out, _err = capture_subprocess_io do
13
+ assert system({"SUP_BASE" => @path}, "bin/sup-add", "maildir:///some/path")
14
+ end
15
+
16
+ generated_sources_yaml = File.read "#{@path}/sources.yaml"
17
+ assert_equal <<EOS, generated_sources_yaml
18
+ ---
19
+ - !<tag:supmua.org,2006-10-01/Redwood/Maildir>
20
+ uri: maildir:///some/path
21
+ usual: true
22
+ archived: false
23
+ sync_back: true
24
+ id: 1
25
+ labels: []
26
+ EOS
27
+ end
28
+
29
+ def test_fixes_old_tag_uri_syntax
30
+ File.write "#{@path}/sources.yaml", <<EOS
31
+ ---
32
+ - !supmua.org,2006-10-01/Redwood/Maildir
33
+ uri: maildir:/some/path
34
+ usual: true
35
+ archived: false
36
+ sync_back: true
37
+ id: 1
38
+ labels: []
39
+ EOS
40
+ _out, _err = capture_subprocess_io do
41
+ assert system({"SUP_BASE" => @path}, "bin/sup-add", "maildir:///other/path")
42
+ end
43
+
44
+ generated_sources_yaml = File.read "#{@path}/sources.yaml"
45
+ assert_equal <<EOS, generated_sources_yaml
46
+ ---
47
+ - !<tag:supmua.org,2006-10-01/Redwood/Maildir>
48
+ uri: maildir:/some/path
49
+ usual: true
50
+ archived: false
51
+ sync_back: true
52
+ id: 1
53
+ labels: []
54
+ - !<tag:supmua.org,2006-10-01/Redwood/Maildir>
55
+ uri: maildir:///other/path
56
+ usual: true
57
+ archived: false
58
+ sync_back: true
59
+ id: 2
60
+ labels: []
61
+ EOS
62
+ end
63
+
64
+ ## https://github.com/sup-heliotrope/sup/issues/545
65
+ def test_source_with_invalid_uri_chars_in_path
66
+ _out, _err = capture_subprocess_io do
67
+ assert system({"SUP_BASE" => @path}, "bin/sup-add", "maildir:~/.mail/gmail/[Gmail]/.All Mail/")
68
+ end
69
+
70
+ generated_sources_yaml = File.read "#{@path}/sources.yaml"
71
+ assert_equal <<EOS, generated_sources_yaml
72
+ ---
73
+ - !<tag:supmua.org,2006-10-01/Redwood/Maildir>
74
+ uri: maildir:~/.mail/gmail/[Gmail]/.All Mail/
75
+ usual: true
76
+ archived: false
77
+ sync_back: true
78
+ id: 1
79
+ labels: []
80
+ EOS
81
+ end
82
+
83
+ end
@@ -0,0 +1,40 @@
1
+ require "test_helper"
2
+
3
+ class TestSupSyncBackMaildir < Minitest::Test
4
+
5
+ def setup
6
+ @path = Dir.mktmpdir
7
+
8
+ @maildir = File.join @path, "test_maildir"
9
+ Dir.mkdir @maildir
10
+ %w[cur new tmp].each do |subdir|
11
+ Dir.mkdir (File.join @maildir, subdir)
12
+ end
13
+ msg_path = File.join @maildir, "new", "123.hostname"
14
+ FileUtils.copy_file fixture_path("simple-message.eml"), msg_path
15
+
16
+ _out, _err = capture_subprocess_io do
17
+ assert system({"SUP_BASE" => @path}, "bin/sup-add", "maildir://#{@maildir}")
18
+ assert system({"SUP_BASE" => @path}, "bin/sup-sync")
19
+ end
20
+ end
21
+
22
+ def teardown
23
+ FileUtils.rm_r @path
24
+ end
25
+
26
+ def test_it_syncs_seen_unread_flags
27
+ _out, _err = capture_subprocess_io do
28
+ assert system({"SUP_BASE" => @path},
29
+ "bin/sup-tweak-labels",
30
+ "--all-sources",
31
+ "--add=replied",
32
+ "--remove=unread")
33
+ assert system({"SUP_BASE" => @path}, "bin/sup-sync-back-maildir", "--no-confirm")
34
+ end
35
+
36
+ refute File.exist? (File.join @maildir, "new", "123.hostname")
37
+ assert File.exist? (File.join @maildir, "cur", "123.hostname:2,RS")
38
+ end
39
+
40
+ end
data/test/test_crypto.rb CHANGED
@@ -69,6 +69,24 @@ class TestCryptoManager < Minitest::Test
69
69
  end
70
70
  end
71
71
 
72
+ def test_sign_nested_parts
73
+ if CryptoManager.have_crypto? then
74
+ body = RMail::Message.new
75
+ body.header["Content-Disposition"] = "inline"
76
+ body.body = "ABCDEFG"
77
+ payload = RMail::Message.new
78
+ payload.header["MIME-Version"] = "1.0"
79
+ payload.add_part body
80
+ payload.add_part RMail::Message.make_attachment "attachment", "text/plain", nil, "attachment.txt"
81
+ signed = CryptoManager.sign @from_email, @to_email, payload
82
+ ## The result is a multipart/signed containing a multipart/mixed.
83
+ ## There should be a MIME-Version header on the top-level
84
+ ## multipart/signed message, but *not* on the enclosed
85
+ ## multipart/mixed part.
86
+ assert_equal 1, signed.to_s.scan(/MIME-Version:/).size
87
+ end
88
+ end
89
+
72
90
  def test_encrypt
73
91
  if CryptoManager.have_crypto? then
74
92
  encrypted = CryptoManager.encrypt @from_email, [@to_email], "ABCDEFG"
@@ -116,6 +134,32 @@ class TestCryptoManager < Minitest::Test
116
134
  CryptoManager.verify signed.body[0], signed.body[1], true
117
135
  end
118
136
  end
137
+
138
+ def test_verify_nested_parts
139
+ if CryptoManager.have_crypto?
140
+ ## Generate a multipart/signed containing a multipart/mixed.
141
+ ## We will test verifying the generated signature below.
142
+ ## Importantly, the inner multipart/mixed does *not* have a
143
+ ## MIME-Version header because it is not a top-level message.
144
+ payload = RMail::Parser.read <<EOS
145
+ Content-Type: multipart/mixed; boundary="=-1652088224-7794-561531-1825-1-="
146
+
147
+
148
+ --=-1652088224-7794-561531-1825-1-=
149
+ Content-Disposition: inline
150
+
151
+ ABCDEFG
152
+ --=-1652088224-7794-561531-1825-1-=
153
+ Content-Disposition: attachment; filename="attachment.txt"
154
+ Content-Type: text/plain; name="attachment.txt"
155
+
156
+ attachment
157
+ --=-1652088224-7794-561531-1825-1-=--
158
+ EOS
159
+ signed = CryptoManager.sign @from_email_ecc, @to_email, payload
160
+ CryptoManager.verify payload, signed.body[1], true
161
+ end
162
+ end
119
163
  end
120
164
 
121
165
  end
@@ -4,16 +4,22 @@ require 'test_helper'
4
4
  require 'sup'
5
5
  require 'stringio'
6
6
 
7
- include Redwood
8
-
9
7
  class TestMBoxParsing < Minitest::Test
10
8
 
9
+ include Redwood
10
+
11
11
  def setup
12
12
  @path = Dir.mktmpdir
13
13
  @mbox = File.join(@path, 'test_mbox')
14
+ @log = StringIO.new
15
+ Redwood::Logger.add_sink @log
16
+ Redwood::Logger.remove_sink $stderr
14
17
  end
15
18
 
16
19
  def teardown
20
+ Redwood::Logger.clear!
21
+ Redwood::Logger.remove_sink @log
22
+ Redwood::Logger.add_sink $stderr
17
23
  FileUtils.rm_r @path
18
24
  end
19
25
 
@@ -69,7 +75,7 @@ EOS
69
75
 
70
76
  def test_blank_lines
71
77
  h = Source.parse_raw_email_header StringIO.new("")
72
- assert_equal nil, h["message-id"]
78
+ assert_nil h["message-id"]
73
79
  end
74
80
 
75
81
  def test_empty_headers
@@ -133,6 +139,8 @@ EOS
133
139
  assert_equal 61, offset
134
140
  offset = l.next_offset 61
135
141
  assert_nil offset
142
+ assert_match(/WARNING: found invalid date in potential mbox split line, not splitting/,
143
+ @log.string)
136
144
  end
137
145
 
138
146
  def test_more_from_line_splitting
data/test/test_helper.rb CHANGED
@@ -2,9 +2,12 @@ require "rubygems" rescue nil
2
2
  require 'minitest/autorun'
3
3
  require "rr"
4
4
 
5
- def fixture(filename)
5
+ def fixture_path(filename)
6
+ File.expand_path("../fixtures/#{filename}", __FILE__)
7
+ end
8
+
9
+ def fixture_contents(filename)
6
10
  file = ''
7
- path = File.expand_path("../fixtures/#{filename}", __FILE__)
8
- File.open(path) { |io| file = io.read }
11
+ File.open(fixture_path(filename)) { |io| file = io.read }
9
12
  file
10
- end
13
+ end
data/test/test_message.rb CHANGED
@@ -21,10 +21,8 @@ class TestMessage < Minitest::Test
21
21
  end
22
22
 
23
23
  def test_simple_message
24
- message = fixture('simple-message.eml')
25
-
26
24
  source = DummySource.new("sup-test://test_simple_message")
27
- source.messages = [ message ]
25
+ source.messages = [ fixture_path('simple-message.eml') ]
28
26
  source_info = 0
29
27
 
30
28
  sup_message = Message.build_from_source(source, source_info)
@@ -99,10 +97,8 @@ class TestMessage < Minitest::Test
99
97
  end
100
98
 
101
99
  def test_multipart_message
102
- message = fixture('multi-part.eml')
103
-
104
100
  source = DummySource.new("sup-test://test_multipart_message")
105
- source.messages = [ message ]
101
+ source.messages = [ fixture_path('multi-part.eml') ]
106
102
  source_info = 0
107
103
 
108
104
  sup_message = Message.build_from_source(source, source_info)
@@ -126,10 +122,8 @@ class TestMessage < Minitest::Test
126
122
  end
127
123
 
128
124
  def test_broken_message_1
129
- message = fixture('missing-from-to.eml')
130
-
131
125
  source = DummySource.new("sup-test://test_broken_message_1")
132
- source.messages = [ message ]
126
+ source.messages = [ fixture_path('missing-from-to.eml') ]
133
127
  source_info = 0
134
128
 
135
129
  sup_message = Message.build_from_source(source, source_info)
@@ -150,10 +144,8 @@ class TestMessage < Minitest::Test
150
144
  end
151
145
 
152
146
  def test_broken_message_2
153
- message = fixture('no-body.eml')
154
-
155
147
  source = DummySource.new("sup-test://test_broken_message_1")
156
- source.messages = [ message ]
148
+ source.messages = [ fixture_path('no-body.eml') ]
157
149
  source_info = 0
158
150
 
159
151
  sup_message = Message.build_from_source(source, source_info)
@@ -167,10 +159,8 @@ class TestMessage < Minitest::Test
167
159
  end
168
160
 
169
161
  def test_multipart_message_2
170
- message = fixture('multi-part-2.eml')
171
-
172
162
  source = DummySource.new("sup-test://test_multipart_message_2")
173
- source.messages = [ message ]
163
+ source.messages = [ fixture_path('multi-part-2.eml') ]
174
164
  source_info = 0
175
165
 
176
166
  sup_message = Message.build_from_source(source, source_info)
@@ -178,21 +168,20 @@ class TestMessage < Minitest::Test
178
168
 
179
169
  chunks = sup_message.load_from_source! # read the message body chunks
180
170
 
181
- # TODO: Add more asserts
171
+ assert_equal(1, chunks.length)
172
+ assert(chunks[0].is_a? Redwood::Chunk::Attachment)
182
173
  end
183
174
 
184
175
  def test_text_attachment_decoding
185
- message = fixture('text-attachments-with-charset.eml')
186
-
187
176
  source = DummySource.new("sup-test://test_text_attachment_decoding")
188
- source.messages = [ message ]
177
+ source.messages = [ fixture_path('text-attachments-with-charset.eml') ]
189
178
  source_info = 0
190
179
 
191
180
  sup_message = Message.build_from_source(source, source_info)
192
181
  sup_message.load_from_source!
193
182
 
194
183
  chunks = sup_message.load_from_source!
195
- assert_equal(5, chunks.length)
184
+ assert_equal(7, chunks.length)
196
185
  assert(chunks[0].is_a? Redwood::Chunk::Text)
197
186
  ## The first attachment declares charset=us-ascii
198
187
  assert(chunks[1].is_a? Redwood::Chunk::Attachment)
@@ -207,13 +196,18 @@ class TestMessage < Minitest::Test
207
196
  ## which will be replaced with U+FFFD REPLACEMENT CHARACTER
208
197
  assert(chunks[4].is_a? Redwood::Chunk::Attachment)
209
198
  assert_equal(["Embedded\ufffdgarbage"], chunks[4].lines)
199
+ ## The fifth attachment has an invalid charset, which should still
200
+ ## be handled gracefully
201
+ assert(chunks[5].is_a? Redwood::Chunk::Attachment)
202
+ assert_equal(["Example invalid charset"], chunks[5].lines)
203
+ ## The sixth attachment is UTF-7 encoded
204
+ assert(chunks[6].is_a? Redwood::Chunk::Attachment)
205
+ assert_equal(["This is ✨UTF-7✨"], chunks[6].lines)
210
206
  end
211
207
 
212
208
  def test_mailing_list_header
213
- message = fixture('mailing-list-header.eml')
214
-
215
209
  source = DummySource.new("sup-test://test_mailing_list_header")
216
- source.messages = [ message ]
210
+ source.messages = [ fixture_path('mailing-list-header.eml') ]
217
211
  source_info = 0
218
212
 
219
213
  sup_message = Message.build_from_source(source, source_info)
@@ -227,10 +221,8 @@ class TestMessage < Minitest::Test
227
221
  end
228
222
 
229
223
  def test_blank_header_lines
230
- message = fixture('blank-header-fields.eml')
231
-
232
224
  source = DummySource.new("sup-test://test_blank_header_lines")
233
- source.messages = [ message ]
225
+ source.messages = [ fixture_path('blank-header-fields.eml') ]
234
226
  source_info = 0
235
227
 
236
228
  sup_message = Message.build_from_source(source, source_info)
@@ -248,11 +240,113 @@ class TestMessage < Minitest::Test
248
240
 
249
241
  end
250
242
 
251
- def test_malicious_attachment_names
252
- message = fixture('malicious-attachment-names.eml')
243
+ def test_rfc2047_header_encoding
244
+ source = DummySource.new("sup-test://test_rfc2047_header_encoding")
245
+ source.messages = [ fixture_path("rfc2047-header-encoding.eml") ]
246
+ source_info = 0
247
+
248
+ sup_message = Message.build_from_source(source, source_info)
249
+ sup_message.load_from_source!
250
+
251
+ assert_equal("Hans Martin Djupvik, Ingrid Bø, Ирина Сидорова, " +
252
+ "Jesper Berg, Frida Engø " +
253
+ "bad: =?UTF16?q?badcharsetname?==?US-ASCII?b?/w?=" +
254
+ "=?UTF-7?Q?=41=6D=65=72=69=63=61=E2=80=99=73?=",
255
+ sup_message.subj)
256
+ end
257
+
258
+ def test_nonascii_header
259
+ ## Spammers sometimes send invalid high bytes in the headers.
260
+ ## They will be replaced with U+FFFD REPLACEMENT CHARACTER.
261
+ source = DummySource.new("sup-test://test_nonascii_header")
262
+ source.messages = [ fixture_path("non-ascii-header.eml") ]
263
+ source_info = 0
264
+
265
+ sup_message = Message.build_from_source(source, source_info)
266
+ sup_message.load_from_source!
267
+
268
+ assert_equal("SPAM \ufffd", sup_message.from.name)
269
+ assert_equal("spammer@example.com", sup_message.from.email)
270
+ assert_equal("spam \ufffd spam", sup_message.subj)
271
+ end
272
+
273
+ def test_utf8_header
274
+ ## UTF-8 is allowed in header values according to RFC6532.
275
+ source = DummySource.new("sup-test://test_utf8_header")
276
+ source.messages = [ fixture_path("utf8-header.eml") ]
277
+ source_info = 0
278
+
279
+ sup_message = Message.build_from_source(source, source_info)
280
+ sup_message.load_from_source!
281
+
282
+ assert_equal(Encoding::UTF_8, sup_message.subj.encoding)
283
+ assert_equal("LibraryThing: State of the Thing — January", sup_message.subj)
284
+ end
285
+
286
+ def test_nonascii_header_in_nested_message
287
+ source = DummySource.new("sup-test://test_nonascii_header_in_nested_message")
288
+ source.messages = [ fixture_path("non-ascii-header-in-nested-message.eml") ]
289
+ source_info = 0
290
+
291
+ sup_message = Message.build_from_source(source, source_info)
292
+ chunks = sup_message.load_from_source!
293
+
294
+ assert_equal(3, chunks.length)
253
295
 
296
+ assert(chunks[0].is_a? Redwood::Chunk::Text)
297
+
298
+ assert(chunks[1].is_a? Redwood::Chunk::EnclosedMessage)
299
+ assert_equal(4, chunks[1].lines.length)
300
+ assert_equal("From: SPAM \ufffd <spammer@example.com>", chunks[1].lines[0])
301
+ assert_equal("To: enclosed <enclosed@example.invalid>", chunks[1].lines[1])
302
+ assert_equal("Subject: spam \ufffd spam", chunks[1].lines[3])
303
+
304
+ assert(chunks[2].is_a? Redwood::Chunk::Text)
305
+ assert_equal(1, chunks[2].lines.length)
306
+ assert_equal("This is a spam.", chunks[2].lines[0])
307
+ end
308
+
309
+ def test_embedded_message
310
+ source = DummySource.new("sup-test://test_embedded_message")
311
+ source.messages = [ fixture_path("embedded-message.eml") ]
312
+ source_info = 0
313
+
314
+ sup_message = Message.build_from_source(source, source_info)
315
+
316
+ chunks = sup_message.load_from_source!
317
+ assert_equal(3, chunks.length)
318
+
319
+ assert_equal("sender@example.com", sup_message.from.email)
320
+ assert_equal("Sender", sup_message.from.name)
321
+ assert_equal(1, sup_message.to.length)
322
+ assert_equal("recipient@example.invalid", sup_message.to[0].email)
323
+ assert_equal("recipient", sup_message.to[0].name)
324
+ assert_equal("Email with embedded message", sup_message.subj)
325
+
326
+ assert(chunks[0].is_a? Redwood::Chunk::Text)
327
+ assert_equal("Example outer message.", chunks[0].lines[0])
328
+ assert_equal("Example second line.", chunks[0].lines[1])
329
+
330
+ assert(chunks[1].is_a? Redwood::Chunk::EnclosedMessage)
331
+ assert_equal(4, chunks[1].lines.length)
332
+ assert_equal("From: Embed sender <embed@example.com>", chunks[1].lines[0])
333
+ assert_equal("To: rcpt2 <rcpt2@example.invalid>", chunks[1].lines[1])
334
+ assert_equal("Date: ", chunks[1].lines[2][0..5])
335
+ assert_equal(
336
+ Time.rfc2822("Wed, 15 Jul 2020 12:34:56 +0000"),
337
+ Time.rfc2822(chunks[1].lines[2][6..-1])
338
+ )
339
+ assert_equal("Subject: Embedded subject line", chunks[1].lines[3])
340
+
341
+ assert(chunks[2].is_a? Redwood::Chunk::Text)
342
+ assert_equal(2, chunks[2].lines.length)
343
+ assert_equal("Example embedded message.", chunks[2].lines[0])
344
+ assert_equal("Second line.", chunks[2].lines[1])
345
+ end
346
+
347
+ def test_malicious_attachment_names
254
348
  source = DummySource.new("sup-test://test_blank_header_lines")
255
- source.messages = [ message ]
349
+ source.messages = [ fixture_path('malicious-attachment-names.eml') ]
256
350
  source_info = 0
257
351
 
258
352
  sup_message = Message.build_from_source(source, source_info)
@@ -276,10 +370,8 @@ class TestMessage < Minitest::Test
276
370
  # tries to do the right thing and reply after the quote.
277
371
  # In this case we want to just look at the > markers when determining where
278
372
  # the quoted chunk ends.
279
- message = fixture('zimbra-quote-with-bottom-post.eml')
280
-
281
373
  source = DummySource.new("sup-test://test_zimbra_quote_with_bottom_post")
282
- source.messages = [ message ]
374
+ source.messages = [ fixture_path('zimbra-quote-with-bottom-post.eml') ]
283
375
  source_info = 0
284
376
 
285
377
  sup_message = Message.build_from_source(source, source_info)