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
@@ -1,222 +1,101 @@
1
- describe Imap::Backup::Serializer::Mbox do
2
- subject { described_class.new(base_path, imap_folder) }
3
-
4
- let(:base_path) { "/base/path" }
5
- let(:store) do
6
- instance_double(
7
- Imap::Backup::Serializer::MboxStore,
8
- add: nil,
9
- rename: nil,
10
- uids: nil,
11
- uid_validity: existing_uid_validity,
12
- "uid_validity=": nil,
13
- update_uid: nil
14
- )
15
- end
16
- let(:imap_folder) { "folder" }
17
- let(:permissions) { 0o700 }
18
- let(:dir_exists) { true }
19
- let(:existing_uid_validity) { nil }
20
-
21
- before do
22
- allow(Imap::Backup::Utils).to receive(:make_folder)
23
- allow(Imap::Backup::Utils).to receive(:mode) { permissions }
24
- allow(Imap::Backup::Utils).to receive(:check_permissions) { true }
25
- allow(File).to receive(:directory?) { dir_exists }
26
- allow(FileUtils).to receive(:chmod)
27
- allow(Imap::Backup::Serializer::MboxStore).to receive(:new) { store }
28
- end
29
-
30
- describe "folder path" do
31
- context "when it has multiple elements" do
32
- let(:imap_folder) { "folder/path" }
33
-
34
- context "when the containing directory is missing" do
35
- let(:dir_exists) { false }
36
-
37
- it "is created" do
38
- expect(Imap::Backup::Utils).to receive(:make_folder).
39
- with(base_path, File.dirname(imap_folder), 0o700)
40
-
41
- subject.uids
42
- end
43
- end
1
+ module Imap::Backup
2
+ describe Serializer::Mbox do
3
+ subject { described_class.new(folder_path) }
4
+
5
+ let(:folder_path) { "folder_path" }
6
+ let(:pathname) { "folder_path.mbox" }
7
+ let(:exists) { true }
8
+ let(:file) { instance_double(File, truncate: nil, write: nil) }
9
+
10
+ before do
11
+ allow(File).to receive(:exist?).and_call_original
12
+ allow(File).to receive(:exist?).with(pathname) { exists }
13
+ allow(File).to receive(:open).with(pathname, "ab").and_yield(file)
14
+ allow(File).to receive(:read).with(pathname) { existing.to_json }
44
15
  end
45
16
 
46
- context "when permissions are incorrect" do
47
- let(:permissions) { 0o777 }
48
-
49
- it "corrects them" do
50
- path = File.expand_path(File.join(base_path, File.dirname(imap_folder)))
51
- expect(FileUtils).to receive(:chmod).with(0o700, path)
17
+ describe "#append" do
18
+ it "appends the message" do
19
+ subject.append("message")
52
20
 
53
- subject.uids
21
+ expect(file).to have_received(:write).with("message")
54
22
  end
55
23
  end
56
24
 
57
- context "when permissons are correct" do
58
- it "does nothing" do
59
- expect(FileUtils).to_not receive(:chmod)
60
-
61
- subject.uids
25
+ describe "#exist?" do
26
+ context "when the mailbox exists" do
27
+ it "is true" do
28
+ expect(subject.exist?).to be true
29
+ end
62
30
  end
63
- end
64
31
 
65
- context "when it exists" do
66
- it "is not created" do
67
- expect(Imap::Backup::Utils).to_not receive(:make_folder).
68
- with(base_path, File.dirname(imap_folder), 0o700)
32
+ context "when the mailbox doesn't exist" do
33
+ let(:exists) { false }
69
34
 
70
- subject.uids
35
+ it "is false" do
36
+ expect(subject.exist?).to be false
37
+ end
71
38
  end
72
39
  end
73
- end
74
-
75
- describe "#apply_uid_validity" do
76
- context "when the existing uid validity is unset" do
77
- it "sets uid validity" do
78
- expect(store).to receive(:uid_validity=).with("aaa")
79
40
 
80
- subject.apply_uid_validity("aaa")
81
- end
41
+ describe "#length" do
42
+ let(:stat) { instance_double(File::Stat, size: 99) }
82
43
 
83
- it "does not rename the store" do
84
- expect(store).to_not receive(:rename)
44
+ before { allow(File).to receive(:stat) { stat } }
85
45
 
86
- subject.apply_uid_validity("aaa")
87
- end
88
-
89
- it "returns nil" do
90
- expect(subject.apply_uid_validity("aaa")).to be_nil
46
+ it "returns the length of the mailbox file" do
47
+ expect(subject.length).to eq(99)
91
48
  end
92
49
  end
93
50
 
94
- context "when the uid validity is unchanged" do
95
- let(:existing_uid_validity) { "aaa" }
96
-
97
- it "does not set uid validity" do
98
- expect(store).to_not receive(:uid_validity=)
99
-
100
- subject.apply_uid_validity("aaa")
101
- end
102
-
103
- it "does not rename the store" do
104
- expect(store).to_not receive(:rename)
105
-
106
- subject.apply_uid_validity("aaa")
107
- end
108
-
109
- it "returns nil" do
110
- expect(subject.apply_uid_validity("aaa")).to be_nil
51
+ describe "#pathname" do
52
+ it "is the folder_path plus .mbox" do
53
+ expect(subject.pathname).to eq("folder_path.mbox")
111
54
  end
112
55
  end
113
56
 
114
- context "when the uid validity is changed" do
115
- let(:existing_uid_validity) { "bbb" }
116
- let(:existing_store) do
117
- instance_double(Imap::Backup::Serializer::MboxStore)
118
- end
119
- let(:exists) { false }
120
-
121
- before do
122
- allow(Imap::Backup::Serializer::MboxStore).
123
- to receive(:new).with(anything, /bbb/) { existing_store }
124
- allow(existing_store).to receive(:exist?).and_return(exists, false)
125
- end
126
-
127
- it "sets uid validity" do
128
- expect(store).to receive(:uid_validity=).with("aaa")
57
+ describe "#rename" do
58
+ context "when the mailbox exists" do
59
+ let(:exists) { true }
129
60
 
130
- subject.apply_uid_validity("aaa")
131
- end
61
+ before do
62
+ allow(File).to receive(:rename)
132
63
 
133
- context "when adding the uid validity does not cause a name clash" do
134
- it "renames the store, adding the existing uid validity" do
135
- expect(store).to receive(:rename).with("folder-bbb")
64
+ subject.rename("new_name")
65
+ end
136
66
 
137
- subject.apply_uid_validity("aaa")
67
+ it "renames the mailbox" do
68
+ expect(File).to have_received(:rename)
138
69
  end
139
70
 
140
- it "returns the new name" do
141
- expect(subject.apply_uid_validity("aaa")).to eq("folder-bbb")
71
+ it "sets the folder_path" do
72
+ expect(subject.folder_path).to eq("new_name")
142
73
  end
143
74
  end
144
75
 
145
- context "when adding the uid validity causes a name clash" do
146
- let(:exists) { true }
147
-
148
- it "renames the store, adding the existing uid validity and a digit" do
149
- expect(store).to receive(:rename).with("folder-bbb-1")
76
+ context "when the mailbox doesn't exist" do
77
+ let(:exists) { false }
150
78
 
151
- subject.apply_uid_validity("aaa")
152
- end
79
+ it "sets the folder_path" do
80
+ subject.rename("new_name")
153
81
 
154
- it "returns the new name" do
155
- expect(subject.apply_uid_validity("aaa")).to eq("folder-bbb-1")
82
+ expect(subject.folder_path).to eq("new_name")
156
83
  end
157
84
  end
158
85
  end
159
- end
160
-
161
- describe "#force_uid_validity" do
162
- it "sets the uid_validity" do
163
- expect(store).to receive(:uid_validity=).with("66")
164
-
165
- subject.force_uid_validity("66")
166
- end
167
- end
168
-
169
- describe "#uids" do
170
- it "calls the store" do
171
- expect(store).to receive(:uids)
172
-
173
- subject.uids
174
- end
175
- end
176
-
177
- describe "#load" do
178
- before { allow(store).to receive(:load).with("66") { "xxx" } }
179
-
180
- it "returns the value loaded by the store" do
181
- expect(subject.load("66")).to eq("xxx")
182
- end
183
- end
184
-
185
- describe "#each_message" do
186
- it "calls the store" do
187
- expect(store).to receive(:each_message).with([1])
188
86
 
189
- subject.each_message([1])
190
- end
191
- end
192
-
193
- describe "#save" do
194
- it "calls the store" do
195
- expect(store).to receive(:add).with("foo", "bar")
196
-
197
- subject.save("foo", "bar")
198
- end
199
- end
200
-
201
- describe "#rename" do
202
- it "calls the store" do
203
- expect(store).to receive(:rename).with("foo")
204
-
205
- subject.rename("foo")
206
- end
207
-
208
- it "updates the folder name" do
209
- subject.rename("foo")
210
-
211
- expect(subject.folder).to eq("foo")
212
- end
213
- end
87
+ describe "#rewind" do
88
+ before do
89
+ allow(File).to receive(:open).
90
+ with(pathname, File::RDWR | File::CREAT, 0o644).
91
+ and_yield(file)
92
+ end
214
93
 
215
- describe "#update_uid" do
216
- it "calls the store" do
217
- expect(store).to receive(:update_uid).with("foo", "bar")
94
+ it "truncates the mailbox" do
95
+ subject.rewind(123)
218
96
 
219
- subject.update_uid("foo", "bar")
97
+ expect(file).to have_received(:truncate).with(123)
98
+ end
220
99
  end
221
100
  end
222
101
  end
@@ -0,0 +1,296 @@
1
+ module Imap::Backup
2
+ describe Serializer do
3
+ subject { described_class.new("path", "folder/sub") }
4
+
5
+ let(:directory) { instance_double(Serializer::Directory, ensure_exists: nil) }
6
+ let(:imap) do
7
+ instance_double(
8
+ Serializer::Imap,
9
+ exist?: true,
10
+ rename: nil,
11
+ uid_validity: existing_uid_validity,
12
+ "uid_validity=": nil
13
+ )
14
+ end
15
+ let(:mbox) do
16
+ instance_double(
17
+ Serializer::Mbox,
18
+ append: nil,
19
+ exist?: false,
20
+ length: 1,
21
+ pathname: "aaa",
22
+ rename: nil,
23
+ rewind: nil
24
+ )
25
+ end
26
+ let(:folder_path) { File.expand_path(File.join("path", "folder/sub")) }
27
+ let(:existing_uid_validity) { nil }
28
+ let(:enumerator) { instance_double(Serializer::MboxEnumerator) }
29
+
30
+ before do
31
+ allow(Serializer::Directory).to receive(:new) { directory }
32
+ allow(Serializer::Imap).to receive(:new).with(folder_path) { imap }
33
+ allow(Serializer::Mbox).to receive(:new) { mbox }
34
+ allow(Serializer::MboxEnumerator).to receive(:new) { enumerator }
35
+ end
36
+
37
+ describe "#apply_uid_validity" do
38
+ let(:imap_test) { instance_double(Serializer::Imap, exist?: imap_test_exists) }
39
+ let(:imap_test_exists) { false }
40
+ let(:test_folder_path) do
41
+ File.expand_path(File.join("path", "folder/sub-#{existing_uid_validity}"))
42
+ end
43
+ let(:result) { subject.apply_uid_validity("new") }
44
+
45
+ before do
46
+ allow(Serializer::Imap).to receive(:new).with(test_folder_path) { imap_test }
47
+ end
48
+
49
+ context "when there is no existing uid_validity" do
50
+ it "sets the metadata file's uid_validity" do
51
+ result
52
+
53
+ expect(imap).to have_received(:"uid_validity=").with("new")
54
+ end
55
+ end
56
+
57
+ context "when the new value is the same as the old value" do
58
+ let(:existing_uid_validity) { "new" }
59
+
60
+ it "does nothing" do
61
+ result
62
+
63
+ expect(imap).to_not have_received(:"uid_validity=")
64
+ end
65
+ end
66
+
67
+ context "when the new value is different from the old value" do
68
+ let(:existing_uid_validity) { "existing" }
69
+
70
+ it "renames the existing mailbox" do
71
+ result
72
+
73
+ expect(mbox).to have_received(:rename).with(test_folder_path)
74
+ end
75
+
76
+ it "renames the existing metadata file" do
77
+ result
78
+
79
+ expect(imap).to have_received(:rename).with(test_folder_path)
80
+ end
81
+
82
+ it "returns the new name for the old folder" do
83
+ expect(result).to eq("folder/sub-existing")
84
+ end
85
+
86
+ context "when the default rename is not possible" do
87
+ let(:imap_test_exists) { true }
88
+ let(:imap_test1) { instance_double(Serializer::Imap, exist?: false) }
89
+ let(:test_folder_path1) do
90
+ File.expand_path(File.join("path", "folder/sub-#{existing_uid_validity}-1"))
91
+ end
92
+
93
+ before do
94
+ allow(Serializer::Imap).to receive(:new).with(test_folder_path1) { imap_test1 }
95
+ end
96
+
97
+ it "renames the mailbox, appending a numeral" do
98
+ result
99
+
100
+ expect(mbox).to have_received(:rename).with(test_folder_path1)
101
+ end
102
+
103
+ it "renames the metadata file, appending a numeral" do
104
+ result
105
+
106
+ expect(imap).to have_received(:rename).with(test_folder_path1)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#force_uid_validity" do
113
+ it "sets the metadata file's uid_validity" do
114
+ subject.force_uid_validity("new")
115
+
116
+ expect(imap).to have_received(:"uid_validity=").with("new")
117
+ end
118
+ end
119
+
120
+ describe "#append" do
121
+ let(:existing_uid_validity) { "42" }
122
+ let(:mboxrd_message) do
123
+ instance_double(Email::Mboxrd::Message, to_serialized: "serialized")
124
+ end
125
+ let(:uid_found) { false }
126
+ let(:command) { subject.append(99, "Hi") }
127
+
128
+ before do
129
+ allow(imap).to receive(:include?) { uid_found }
130
+ allow(imap).to receive(:append)
131
+ allow(Email::Mboxrd::Message).to receive(:new) { mboxrd_message }
132
+ end
133
+
134
+ it "appends the message to the mailbox" do
135
+ command
136
+
137
+ expect(mbox).to have_received(:append).with("serialized")
138
+ end
139
+
140
+ it "appends the UID to the metadata" do
141
+ command
142
+
143
+ expect(imap).to have_received(:append).with(99)
144
+ end
145
+
146
+ context "when appending to the mailbox causes an error" do
147
+ before do
148
+ allow(mbox).to receive(:append).and_throw(RuntimeError, "Boom")
149
+ end
150
+
151
+ it "does not fail" do
152
+ command
153
+ end
154
+
155
+ it "leaves the metadata file unchanged" do
156
+ command
157
+
158
+ expect(imap).to_not have_received(:append)
159
+ end
160
+ end
161
+
162
+ context "when appending to the metadata file causes an error" do
163
+ before do
164
+ allow(imap).to receive(:append).and_throw(RuntimeError, "Boom")
165
+ end
166
+
167
+ it "does not fail" do
168
+ command
169
+ end
170
+
171
+ it "reset the mailbox to the previous position" do
172
+ command
173
+
174
+ expect(mbox).to have_received(:rewind)
175
+ end
176
+ end
177
+
178
+ context "when the metadata uid_validity has not been set" do
179
+ let(:existing_uid_validity) { nil }
180
+
181
+ it "fails" do
182
+ expect { command }.to raise_error(RuntimeError, /without uid_validity/)
183
+ end
184
+ end
185
+
186
+ context "when the message has already been backed up" do
187
+ let(:uid_found) { true }
188
+
189
+ it "doesn't append to the mailbox file" do
190
+ command
191
+
192
+ expect(mbox).to_not have_received(:append)
193
+ end
194
+
195
+ it "doesn't append to the metadata file" do
196
+ command
197
+
198
+ expect(imap).to_not have_received(:append)
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#load" do
204
+ let(:uid) { 999 }
205
+ let(:imap_index) { 0 }
206
+ let(:result) { subject.load(uid) }
207
+
208
+ before do
209
+ allow(imap).to receive(:index).with(999) { imap_index }
210
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
211
+ end
212
+
213
+ it "returns an Email::Mboxrd::Message" do
214
+ expect(result).to be_a(Email::Mboxrd::Message)
215
+ end
216
+
217
+ it "returns the message" do
218
+ expect(result.supplied_body).to eq("message")
219
+ end
220
+
221
+ context "when the message is not found" do
222
+ let(:imap_index) { nil }
223
+
224
+ it "returns nil" do
225
+ expect(result).to be nil
226
+ end
227
+ end
228
+
229
+ context "when the supplied UID is a string" do
230
+ let(:uid) { "999" }
231
+
232
+ it "works" do
233
+ expect(result).to be_a(Email::Mboxrd::Message)
234
+ end
235
+ end
236
+ end
237
+
238
+ describe "#load_nth" do
239
+ let(:imap_index) { 0 }
240
+ let(:result) { subject.load_nth(imap_index) }
241
+
242
+ before do
243
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
244
+ end
245
+
246
+ it "returns an Email::Mboxrd::Message" do
247
+ expect(result).to be_a(Email::Mboxrd::Message)
248
+ end
249
+
250
+ it "returns the message" do
251
+ expect(result.supplied_body).to eq("message")
252
+ end
253
+
254
+ context "when the message is not found" do
255
+ let(:imap_index) { 1 }
256
+
257
+ it "returns nil" do
258
+ expect(result).to be nil
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "#each_message" do
264
+ let(:good_uid) { 999 }
265
+
266
+ before do
267
+ allow(imap).to receive(:index) { nil }
268
+ allow(imap).to receive(:index).with(good_uid) { 0 }
269
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
270
+ end
271
+
272
+ it "yields matching UIDs" do
273
+ expect { |b| subject.each_message([good_uid], &b) }.
274
+ to yield_successive_args([good_uid, anything])
275
+ end
276
+
277
+ it "yields matching messages" do
278
+ messages = subject.each_message([good_uid]).map { |_uid, message| message }
279
+ expect(messages[0].supplied_body).to eq("message")
280
+ end
281
+
282
+ context "with UIDs that are not present" do
283
+ it "skips them" do
284
+ expect { |b| subject.each_message([good_uid, 1234], &b) }.
285
+ to yield_successive_args([good_uid, anything])
286
+ end
287
+ end
288
+
289
+ context "when called without a block" do
290
+ it "returns an Enumerator" do
291
+ expect(subject.each_message([])).to be_a(Enumerator)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end