imap-backup 5.2.0 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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