imap-backup 5.2.0 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -2
  3. data/docs/development.md +10 -4
  4. data/lib/cli_coverage.rb +1 -1
  5. data/lib/imap/backup/account/connection.rb +7 -11
  6. data/lib/imap/backup/account.rb +31 -11
  7. data/lib/imap/backup/cli/folders.rb +3 -3
  8. data/lib/imap/backup/cli/migrate.rb +3 -3
  9. data/lib/imap/backup/cli/utils.rb +2 -2
  10. data/lib/imap/backup/configuration.rb +1 -11
  11. data/lib/imap/backup/downloader.rb +13 -9
  12. data/lib/imap/backup/serializer/directory.rb +37 -0
  13. data/lib/imap/backup/serializer/imap.rb +120 -0
  14. data/lib/imap/backup/serializer/mbox.rb +23 -94
  15. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  16. data/lib/imap/backup/serializer.rb +180 -3
  17. data/lib/imap/backup/setup/account.rb +52 -29
  18. data/lib/imap/backup/setup/helpers.rb +1 -1
  19. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
  20. data/lib/imap/backup/version.rb +3 -3
  21. data/lib/imap/backup.rb +0 -1
  22. data/spec/features/backup_spec.rb +8 -16
  23. data/spec/features/support/aruba.rb +4 -3
  24. data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
  25. data/spec/unit/imap/backup/account/folder_spec.rb +10 -0
  26. data/spec/unit/imap/backup/account_spec.rb +246 -0
  27. data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
  28. data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
  29. data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
  30. data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
  31. data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
  32. data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
  33. data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
  34. data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
  35. data/spec/unit/imap/backup/cli_spec.rb +93 -0
  36. data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
  37. data/spec/unit/imap/backup/configuration_spec.rb +2 -2
  38. data/spec/unit/imap/backup/downloader_spec.rb +59 -7
  39. data/spec/unit/imap/backup/migrator_spec.rb +1 -1
  40. data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
  41. data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
  42. data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
  43. data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
  44. data/spec/unit/imap/backup/serializer_spec.rb +296 -0
  45. data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
  46. data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
  47. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
  48. data/spec/unit/imap/backup/uploader_spec.rb +1 -1
  49. data/spec/unit/retry_on_error_spec.rb +34 -0
  50. metadata +36 -7
  51. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  52. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
@@ -0,0 +1,93 @@
1
+ module Imap::Backup
2
+ RSpec.describe CLI do
3
+ describe ".exit_on_failure?" do
4
+ it "is true" do
5
+ expect(described_class.exit_on_failure?).to be true
6
+ end
7
+ end
8
+
9
+ describe "#backup" do
10
+ let(:backup) { instance_double(CLI::Backup, run: nil) }
11
+
12
+ before do
13
+ allow(CLI::Backup).to receive(:new) { backup }
14
+
15
+ subject.backup
16
+ end
17
+
18
+ it "runs Backup" do
19
+ expect(backup).to have_received(:run)
20
+ end
21
+ end
22
+
23
+ describe "#folders" do
24
+ let(:folders) { instance_double(CLI::Folders, run: nil) }
25
+
26
+ before do
27
+ allow(CLI::Folders).to receive(:new) { folders }
28
+
29
+ subject.folders
30
+ end
31
+
32
+ it "runs folders" do
33
+ expect(folders).to have_received(:run)
34
+ end
35
+ end
36
+
37
+ describe "#migrate" do
38
+ let(:migrate) { instance_double(CLI::Migrate, run: nil) }
39
+
40
+ before do
41
+ allow(CLI::Migrate).to receive(:new) { migrate }
42
+
43
+ subject.migrate("source", "destination")
44
+ end
45
+
46
+ it "runs migrate" do
47
+ expect(migrate).to have_received(:run)
48
+ end
49
+ end
50
+
51
+ describe "#restore" do
52
+ let(:restore) { instance_double(CLI::Restore, run: nil) }
53
+
54
+ before do
55
+ allow(CLI::Restore).to receive(:new) { restore }
56
+
57
+ subject.restore
58
+ end
59
+
60
+ it "runs restore" do
61
+ expect(restore).to have_received(:run)
62
+ end
63
+ end
64
+
65
+ describe "#setup" do
66
+ let(:setup) { instance_double(CLI::Setup, run: nil) }
67
+
68
+ before do
69
+ allow(CLI::Setup).to receive(:new) { setup }
70
+
71
+ subject.setup
72
+ end
73
+
74
+ it "runs setup" do
75
+ expect(setup).to have_received(:run)
76
+ end
77
+ end
78
+
79
+ describe "#status" do
80
+ let(:status) { instance_double(CLI::Status, run: nil) }
81
+
82
+ before do
83
+ allow(CLI::Status).to receive(:new) { status }
84
+
85
+ subject.status
86
+ end
87
+
88
+ it "runs status" do
89
+ expect(status).to have_received(:run)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ module Imap::Backup
2
+ RSpec.describe Client::AppleMail do
3
+ describe "#provider_root" do
4
+ it "is an empty string" do
5
+ expect(subject.provider_root).to eq("")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -59,7 +59,7 @@ describe Imap::Backup::Configuration do
59
59
  end
60
60
 
61
61
  context "with accounts flagged 'delete'" do
62
- before { subject.accounts[0].mark_for_deletion! }
62
+ before { subject.accounts[0].mark_for_deletion }
63
63
 
64
64
  it "is true" do
65
65
  expect(subject.modified?).to be_truthy
@@ -177,7 +177,7 @@ describe Imap::Backup::Configuration do
177
177
  before do
178
178
  allow(subject.accounts[0]).to receive(:to_h) { "Account1" }
179
179
  allow(subject.accounts[1]).to receive(:to_h) { "Account2" }
180
- subject.accounts[0].mark_for_deletion!
180
+ subject.accounts[0].mark_for_deletion
181
181
  end
182
182
 
183
183
  it "does not save them" do
@@ -1,6 +1,6 @@
1
1
  describe Imap::Backup::Downloader do
2
2
  describe "#run" do
3
- subject { described_class.new(folder, serializer) }
3
+ subject { described_class.new(folder, serializer, **options) }
4
4
 
5
5
  let(:body) { "blah" }
6
6
  let(:folder) do
@@ -8,17 +8,19 @@ describe Imap::Backup::Downloader do
8
8
  Imap::Backup::Account::Folder,
9
9
  fetch_multi: [{uid: "111", body: body}],
10
10
  name: "folder",
11
- uids: folder_uids
11
+ uids: remote_uids
12
12
  )
13
13
  end
14
- let(:folder_uids) { %w(111 222 333) }
14
+ let(:remote_uids) { %w(111 222 333) }
15
15
  let(:serializer) do
16
- instance_double(Imap::Backup::Serializer::Mbox, save: nil, uids: ["222"])
16
+ instance_double(Imap::Backup::Serializer, append: nil, uids: local_uids)
17
17
  end
18
+ let(:local_uids) { ["222"] }
19
+ let(:options) { {} }
18
20
 
19
21
  context "with fetched messages" do
20
22
  specify "are saved" do
21
- expect(serializer).to receive(:save).with("111", body)
23
+ expect(serializer).to receive(:append).with("111", body)
22
24
 
23
25
  subject.run
24
26
  end
@@ -26,7 +28,7 @@ describe Imap::Backup::Downloader do
26
28
 
27
29
  context "with messages which are already present" do
28
30
  specify "are skipped" do
29
- expect(serializer).to_not receive(:save).with("222", anything)
31
+ expect(serializer).to_not receive(:append).with("222", anything)
30
32
 
31
33
  subject.run
32
34
  end
@@ -35,10 +37,60 @@ describe Imap::Backup::Downloader do
35
37
  context "with failed fetches" do
36
38
  specify "are skipped" do
37
39
  allow(folder).to receive(:fetch_multi) { nil }
38
- expect(serializer).to_not receive(:save)
40
+ expect(serializer).to_not receive(:append)
39
41
 
40
42
  subject.run
41
43
  end
42
44
  end
45
+
46
+ context "when the block size is greater than one" do
47
+ let(:remote_uids) { %w(111 999) }
48
+ let(:local_uids) { [] }
49
+ let(:options) { {multi_fetch_size: 2} }
50
+
51
+ context "when the first fetch fails" do
52
+ before do
53
+ allow(folder).to receive(:fetch_multi).with(["111", "999"]) { nil }
54
+ allow(folder).to receive(:fetch_multi).with(["111"]).
55
+ and_return([{uid: "111", body: body}]).
56
+ and_return([{uid: "999", body: body}])
57
+
58
+ subject.run
59
+ end
60
+
61
+ it "retries fetching messages singly" do
62
+ expect(serializer).to have_received(:append).with("111", body)
63
+ expect(serializer).to have_received(:append).with("999", body)
64
+ end
65
+ end
66
+ end
67
+
68
+ context "when no body is returned by the fetch" do
69
+ let(:remote_uids) { %w(111) }
70
+
71
+ before do
72
+ allow(folder).to receive(:fetch_multi).with(["111"]) { [{uid: "111", body: nil}] }
73
+
74
+ subject.run
75
+ end
76
+
77
+ it "skips the append" do
78
+ expect(serializer).to_not have_received(:append)
79
+ end
80
+ end
81
+
82
+ context "when the UID is not returned by the fetch" do
83
+ let(:remote_uids) { %w(111) }
84
+
85
+ before do
86
+ allow(folder).to receive(:fetch_multi).with(["111"]) { [{uid: nil, body: body}] }
87
+
88
+ subject.run
89
+ end
90
+
91
+ it "skips the append" do
92
+ expect(serializer).to_not have_received(:append)
93
+ end
94
+ end
43
95
  end
44
96
  end
@@ -4,7 +4,7 @@ module Imap::Backup
4
4
  RSpec.describe Migrator do
5
5
  subject { described_class.new(serializer, folder, reset: reset) }
6
6
 
7
- let(:serializer) { instance_double(Serializer::MboxStore, uids: [1]) }
7
+ let(:serializer) { instance_double(Serializer, uids: [1]) }
8
8
  let(:folder) do
9
9
  instance_double(
10
10
  Account::Folder,
@@ -0,0 +1,42 @@
1
+ module Imap::Backup
2
+ describe Sanitizer do
3
+ require "stringio"
4
+
5
+ subject { described_class.new(output) }
6
+
7
+ let(:output) { StringIO.new }
8
+
9
+ describe "#puts" do
10
+ it "delegates to output" do
11
+ subject.puts("x")
12
+
13
+ expect(output.string).to eq("x\n")
14
+ end
15
+ end
16
+
17
+ describe "#write" do
18
+ it "delegates to output" do
19
+ subject.write("x")
20
+
21
+ expect(output.string).to eq("x")
22
+ end
23
+ end
24
+
25
+ describe "#print" do
26
+ it "removes passwords from complete lines of text" do
27
+ subject.print("C: RUBY99 LOGIN xx) secret!!!!\netc")
28
+
29
+ expect(output.string).to eq("C: RUBY99 LOGIN xx) [PASSWORD REDACTED]\n")
30
+ end
31
+ end
32
+
33
+ describe "#flush" do
34
+ it "sanitizes remaining text" do
35
+ subject.print("before\nC: RUBY99 LOGIN xx) secret!!!!")
36
+ subject.flush
37
+
38
+ expect(output.string).to eq("before\nC: RUBY99 LOGIN xx) [PASSWORD REDACTED]\n")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ module Imap::Backup
2
+ describe Serializer::Directory do
3
+ subject { described_class.new("path", "relative") }
4
+
5
+ let(:windows) { false }
6
+
7
+ before do
8
+ allow(File).to receive(:directory?) { false }
9
+ allow(Utils).to receive(:make_folder)
10
+ allow(OS).to receive(:windows?) { windows }
11
+ allow(Utils).to receive(:mode) { 0o600 }
12
+ allow(FileUtils).to receive(:chmod)
13
+
14
+ subject.ensure_exists
15
+ end
16
+
17
+ describe "#ensure_exists" do
18
+ context "when the directory doesn't exist" do
19
+ it "makes the directory" do
20
+ expect(Utils).to have_received(:make_folder)
21
+ end
22
+ end
23
+
24
+ it "sets permissions" do
25
+ expect(FileUtils).to have_received(:chmod)
26
+ end
27
+
28
+ context "when on Windows" do
29
+ let(:windows) { true }
30
+
31
+ it "doesn't set permissions" do
32
+ expect(FileUtils).to_not have_received(:chmod)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,218 @@
1
+ module Imap::Backup
2
+ describe Serializer::Imap do
3
+ subject { described_class.new(folder_path) }
4
+
5
+ let(:folder_path) { "folder_path" }
6
+ let(:pathname) { "folder_path.imap" }
7
+ let(:exists) { true }
8
+ let(:existing) { {uid_validity: 99, uids: [42]} }
9
+ let(:file) { instance_double(File, write: nil) }
10
+
11
+ before do
12
+ allow(File).to receive(:exist?).and_call_original
13
+ allow(File).to receive(:exist?).with(pathname) { exists }
14
+ allow(File).to receive(:open).and_call_original
15
+ allow(File).to receive(:open).with(pathname, "w").and_yield(file)
16
+ allow(File).to receive(:read).with(pathname) { existing.to_json }
17
+ end
18
+
19
+ describe "loading the metadata file" do
20
+ context "when it is malformed" do
21
+ before do
22
+ allow(File).to receive(:read).with(pathname).and_raise(JSON::ParserError)
23
+ end
24
+
25
+ it "ignores the file" do
26
+ subject.uid_validity
27
+
28
+ expect(subject.uids).to eq([])
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#append" do
34
+ context "when the metadata file exists" do
35
+ before { subject.append(123) }
36
+
37
+ it "loads the existing metadata" do
38
+ expect(subject.uids).to include(42)
39
+ end
40
+
41
+ it "appends the UID" do
42
+ expect(subject.uids).to include(123)
43
+ end
44
+
45
+ it "saves the file" do
46
+ expect(file).to have_received(:write).
47
+ with(/"uids":\[42,123\]/)
48
+ end
49
+ end
50
+
51
+ context "when the metadata file doesn't exist" do
52
+ let(:exists) { false }
53
+
54
+ context "when the uid_validity is set" do
55
+ before do
56
+ subject.uid_validity = 999
57
+ end
58
+
59
+ it "appends the UID" do
60
+ subject.append(123)
61
+
62
+ expect(subject.uids).to include(123)
63
+ end
64
+
65
+ it "saves the file" do
66
+ subject.append(123)
67
+
68
+ expect(file).to have_received(:write).
69
+ with(/"uids":\[123\]/)
70
+ end
71
+ end
72
+
73
+ context "when the uid_validity is not set" do
74
+ it "fails" do
75
+ expect do
76
+ subject.append(123)
77
+ end.to raise_error(RuntimeError, /without a uid_validity/)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "#exist?" do
84
+ context "when the metadata file exists" do
85
+ it "is true" do
86
+ expect(subject.exist?).to be true
87
+ end
88
+ end
89
+
90
+ context "when the metadata file doesn't exist" do
91
+ let(:exists) { false }
92
+
93
+ it "is false" do
94
+ expect(subject.exist?).to be false
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "#include?" do
100
+ it "loads the existing metadata" do
101
+ subject.include?(42)
102
+
103
+ expect(File).to have_received(:read).with(pathname)
104
+ end
105
+
106
+ context "when there is a matching UID" do
107
+ it "is true" do
108
+ expect(subject.include?(42)).to be true
109
+ end
110
+ end
111
+
112
+ context "when there isn't a matching UID" do
113
+ it "is false" do
114
+ expect(subject.include?(99)).to be false
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "#index" do
120
+ it "loads the existing metadata" do
121
+ subject.include?(42)
122
+
123
+ expect(File).to have_received(:read).with(pathname)
124
+ end
125
+
126
+ context "when there is a matching UID" do
127
+ it "returns the index of the matcing UID" do
128
+ expect(subject.index(42)).to eq(0)
129
+ end
130
+ end
131
+
132
+ context "when there isn't a matching UID" do
133
+ it "is nil" do
134
+ expect(subject.index(99)).to be nil
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "#rename" do
140
+ before do
141
+ allow(File).to receive(:rename)
142
+
143
+ subject.rename("new_path")
144
+ end
145
+
146
+ context "when the metadata file exists" do
147
+ it "sets the folder_path" do
148
+ expect(subject.folder_path).to eq("new_path")
149
+ end
150
+
151
+ it "renames the metadata file" do
152
+ expect(File).to have_received(:rename).with(pathname, "new_path.imap")
153
+ end
154
+ end
155
+
156
+ context "when the metadata file doesn't exist" do
157
+ let(:exists) { false }
158
+
159
+ it "sets the folder_path" do
160
+ expect(subject.folder_path).to eq("new_path")
161
+ end
162
+
163
+ it "doesn't try to rename the metadata file" do
164
+ expect(File).to_not have_received(:rename)
165
+ end
166
+ end
167
+ end
168
+
169
+ describe "#uid_validity" do
170
+ it "returns the uid_validity" do
171
+ expect(subject.uid_validity).to eq(99)
172
+ end
173
+ end
174
+
175
+ describe "#uid_validity=" do
176
+ before { subject.uid_validity = 567 }
177
+
178
+ it "updates the uid_validity" do
179
+ expect(subject.uid_validity).to eq(567)
180
+ end
181
+
182
+ it "saves the file" do
183
+ expect(file).to have_received(:write).
184
+ with(/"uid_validity":567/)
185
+ end
186
+
187
+ context "when no metadata file exists" do
188
+ let(:exists) { false }
189
+
190
+ it "saves an empty list of UIDs" do
191
+ expect(file).to have_received(:write).
192
+ with(/"uids":\[\]/)
193
+ end
194
+ end
195
+ end
196
+
197
+ describe "#update_uid" do
198
+ before { subject.update_uid(42, 57) }
199
+
200
+ it "sets the UID" do
201
+ expect(subject.uids).to eq([57])
202
+ end
203
+
204
+ it "saves the file" do
205
+ expect(file).to have_received(:write).
206
+ with(/"uids":\[57\]/)
207
+ end
208
+
209
+ context "when the UID is not present" do
210
+ let(:existing) { {uid_validity: 99, uids: [2]} }
211
+
212
+ it "doesn't save the file" do
213
+ expect(file).to_not have_received(:write)
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end