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
@@ -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