imap-backup 5.0.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/bin/imap-backup +4 -0
  4. data/docs/development.md +10 -4
  5. data/imap-backup.gemspec +2 -7
  6. data/lib/cli_coverage.rb +18 -0
  7. data/lib/imap/backup/account/connection.rb +7 -11
  8. data/lib/imap/backup/account/folder.rb +0 -16
  9. data/lib/imap/backup/account.rb +31 -11
  10. data/lib/imap/backup/cli/folders.rb +3 -3
  11. data/lib/imap/backup/cli/migrate.rb +3 -3
  12. data/lib/imap/backup/cli/restore.rb +20 -4
  13. data/lib/imap/backup/cli/utils.rb +2 -2
  14. data/lib/imap/backup/cli.rb +6 -7
  15. data/lib/imap/backup/configuration.rb +1 -11
  16. data/lib/imap/backup/downloader.rb +13 -9
  17. data/lib/imap/backup/serializer/directory.rb +37 -0
  18. data/lib/imap/backup/serializer/imap.rb +120 -0
  19. data/lib/imap/backup/serializer/mbox.rb +23 -94
  20. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  21. data/lib/imap/backup/serializer.rb +180 -3
  22. data/lib/imap/backup/setup/account.rb +52 -29
  23. data/lib/imap/backup/setup/helpers.rb +1 -1
  24. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
  25. data/lib/imap/backup/version.rb +2 -2
  26. data/lib/imap/backup.rb +0 -1
  27. data/spec/features/backup_spec.rb +22 -29
  28. data/spec/features/restore_spec.rb +8 -6
  29. data/spec/features/support/aruba.rb +12 -3
  30. data/spec/features/support/backup_directory.rb +0 -4
  31. data/spec/features/support/email_server.rb +0 -1
  32. data/spec/spec_helper.rb +4 -9
  33. data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
  34. data/spec/unit/imap/backup/account/folder_spec.rb +18 -16
  35. data/spec/unit/imap/backup/account_spec.rb +246 -0
  36. data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
  37. data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
  38. data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
  39. data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
  40. data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
  41. data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
  42. data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
  43. data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
  44. data/spec/unit/imap/backup/cli_spec.rb +93 -0
  45. data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
  46. data/spec/unit/imap/backup/configuration_spec.rb +2 -2
  47. data/spec/unit/imap/backup/downloader_spec.rb +60 -8
  48. data/spec/unit/imap/backup/logger_spec.rb +1 -1
  49. data/spec/unit/imap/backup/migrator_spec.rb +1 -1
  50. data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
  51. data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
  52. data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
  53. data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
  54. data/spec/unit/imap/backup/serializer_spec.rb +296 -0
  55. data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
  56. data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
  57. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
  58. data/spec/unit/imap/backup/uploader_spec.rb +1 -1
  59. data/spec/unit/retry_on_error_spec.rb +34 -0
  60. metadata +44 -37
  61. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  62. data/lib/thunderbird/install.rb +0 -16
  63. data/lib/thunderbird/local_folder.rb +0 -65
  64. data/lib/thunderbird/profile.rb +0 -30
  65. data/lib/thunderbird/profiles.rb +0 -71
  66. data/lib/thunderbird/subdirectory.rb +0 -93
  67. data/lib/thunderbird/subdirectory_placeholder.rb +0 -21
  68. data/lib/thunderbird.rb +0 -14
  69. data/spec/gather_rspec_coverage.rb +0 -1
  70. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
@@ -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
@@ -11,10 +11,11 @@ describe Imap::Backup::Setup::Account do
11
11
  Imap::Backup::Account,
12
12
  username: existing_email,
13
13
  password: existing_password,
14
- server: current_server,
15
- connection_options: nil,
16
14
  local_path: "/backup/path",
17
15
  folders: [{name: "my_folder"}],
16
+ multi_fetch_size: multi_fetch_size,
17
+ server: current_server,
18
+ connection_options: connection_options,
18
19
  modified?: false
19
20
  )
20
21
  end
@@ -28,10 +29,12 @@ describe Imap::Backup::Setup::Account do
28
29
  let(:accounts) { [account, account1] }
29
30
  let(:existing_email) { "user@example.com" }
30
31
  let(:new_email) { "foo@example.com" }
31
- let(:current_server) { "imap.example.com" }
32
32
  let(:existing_password) { "password" }
33
33
  let(:other_email) { "other@example.com" }
34
34
  let(:other_existing_path) { "/other/existing/path" }
35
+ let(:multi_fetch_size) { 1 }
36
+ let(:current_server) { "imap.example.com" }
37
+ let(:connection_options) { nil }
35
38
 
36
39
  let(:highline) { HIGHLINE }
37
40
  let(:config) { CONFIG }
@@ -99,9 +102,11 @@ describe Imap::Backup::Setup::Account do
99
102
  [
100
103
  "modify email",
101
104
  "modify password",
102
- "modify server",
103
105
  "modify backup path",
104
106
  "choose backup folders",
107
+ "modify multi-fetch size (number of emails to fetch at a time)",
108
+ "modify server",
109
+ "modify connection options",
105
110
  "test connection",
106
111
  "delete",
107
112
  "(q) return to main menu",
@@ -136,7 +141,23 @@ describe Imap::Backup::Setup::Account do
136
141
  before { subject.run }
137
142
 
138
143
  it "indicates that a password is not set" do
139
- expect(menu.header).to include("password (unset)")
144
+ expect(menu.header).to match(/^password\s+\(unset\)/)
145
+ end
146
+ end
147
+
148
+ context "with multi_fetch_size" do
149
+ let(:multi_fetch_size) { 4 }
150
+
151
+ it "shows the size" do
152
+ expect(menu.header).to match(/^multi-fetch\s+4/)
153
+ end
154
+ end
155
+
156
+ context "with connection_options" do
157
+ let(:connection_options) { {some: "option"} }
158
+
159
+ it "shows the options" do
160
+ expect(menu.header).to match(/^connection options\s+'{"some":"option"}'/)
140
161
  end
141
162
  end
142
163
  end
@@ -240,23 +261,6 @@ describe Imap::Backup::Setup::Account do
240
261
  end
241
262
  end
242
263
 
243
- describe "choosing 'modify server'" do
244
- let(:server) { "server" }
245
-
246
- before do
247
- allow(account).to receive(:"server=")
248
- allow(highline).to receive(:ask).with("server: ") { server }
249
-
250
- subject.run
251
-
252
- menu.choices["modify server"].call
253
- end
254
-
255
- it "updates the server" do
256
- expect(account).to have_received(:"server=").with(server)
257
- end
258
- end
259
-
260
264
  describe "choosing 'modify backup path'" do
261
265
  let(:new_backup_path) { "/new/path" }
262
266
 
@@ -311,6 +315,97 @@ describe Imap::Backup::Setup::Account do
311
315
  end
312
316
  end
313
317
 
318
+ describe "choosing 'modify multi-fetch size'" do
319
+ let(:supplied) { "10" }
320
+
321
+ before do
322
+ allow(account).to receive(:multi_fetch_size=)
323
+ allow(highline).to receive(:ask).with("size: ") { supplied }
324
+
325
+ subject.run
326
+ menu.choices[
327
+ "modify multi-fetch size (number of emails to fetch at a time)"
328
+ ].call
329
+ end
330
+
331
+ it "sets the multi-fetch size" do
332
+ expect(account).to have_received(:multi_fetch_size=).with(10)
333
+ end
334
+
335
+ context "when the supplied value is not a number" do
336
+ let(:supplied) { "wrong!" }
337
+
338
+ it "does nothing" do
339
+ expect(account).to_not have_received(:multi_fetch_size=)
340
+ end
341
+ end
342
+
343
+ context "when the supplied value is not a positive number" do
344
+ let(:supplied) { "0" }
345
+
346
+ it "does nothing" do
347
+ expect(account).to_not have_received(:multi_fetch_size=)
348
+ end
349
+ end
350
+ end
351
+
352
+ describe "choosing 'modify server'" do
353
+ let(:server) { "server" }
354
+
355
+ before do
356
+ allow(account).to receive(:"server=")
357
+ allow(highline).to receive(:ask).with("server: ") { server }
358
+
359
+ subject.run
360
+
361
+ menu.choices["modify server"].call
362
+ end
363
+
364
+ it "updates the server" do
365
+ expect(account).to have_received(:"server=").with(server)
366
+ end
367
+ end
368
+
369
+ describe "choosing 'modify connection options'" do
370
+ context "when the JSON is well formed" do
371
+ let(:json) { "{}" }
372
+
373
+ before do
374
+ allow(highline).to receive(:ask).with("connections options (as JSON): ") { json }
375
+ allow(account).to receive(:"connection_options=")
376
+
377
+ subject.run
378
+
379
+ menu.choices["modify connection options"].call
380
+ end
381
+
382
+ it "updates the connection options" do
383
+ expect(account).to have_received(:"connection_options=").with(json)
384
+ end
385
+ end
386
+
387
+ context "when the JSON is malformed" do
388
+ before do
389
+ allow(highline).to receive(:ask).with("connections options (as JSON): ") { "xx" }
390
+ allow(account).to receive(:"connection_options=").and_raise(JSON::ParserError)
391
+ allow(highline).to receive(:ask).with("Press a key ")
392
+
393
+ subject.run
394
+
395
+ menu.choices["modify connection options"].call
396
+ end
397
+
398
+ it "does not fail" do
399
+ subject.run
400
+ end
401
+
402
+ it "reports the problem" do
403
+ expect(Kernel).to have_received(:puts).
404
+ with(/Malformed/)
405
+ end
406
+ end
407
+ end
408
+
314
409
  describe "choosing 'test connection'" do
315
410
  let(:connection_tester) do
316
411
  instance_double(
@@ -336,7 +431,7 @@ describe Imap::Backup::Setup::Account do
336
431
  let(:confirmed) { true }
337
432
 
338
433
  before do
339
- allow(account).to receive(:mark_for_deletion!)
434
+ allow(account).to receive(:mark_for_deletion)
340
435
  allow(highline).to receive(:agree) { confirmed }
341
436
  subject.run
342
437
  catch :done do
@@ -350,7 +445,7 @@ describe Imap::Backup::Setup::Account do
350
445
 
351
446
  context "when the user confirms deletion" do
352
447
  it "flags the account to be deleted" do
353
- expect(account).to have_received(:mark_for_deletion!)
448
+ expect(account).to have_received(:mark_for_deletion)
354
449
  end
355
450
  end
356
451
 
@@ -358,7 +453,7 @@ describe Imap::Backup::Setup::Account do
358
453
  let(:confirmed) { false }
359
454
 
360
455
  it "doesn't flag the account to be deleted" do
361
- expect(account).to_not have_received(:mark_for_deletion!)
456
+ expect(account).to_not have_received(:mark_for_deletion)
362
457
  end
363
458
  end
364
459
  end
@@ -0,0 +1,15 @@
1
+ module Imap::Backup
2
+ RSpec.describe Setup::Helpers do
3
+ describe "#title_prefix" do
4
+ it "is a string" do
5
+ expect(subject.title_prefix).to eq("imap-backup –")
6
+ end
7
+ end
8
+
9
+ describe "#version" do
10
+ it "is a version string" do
11
+ expect(subject.version).to match(/\A\d+\.\d+\.\d+(\.\w+)?\z/)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,116 @@
1
+ module Imap::Backup
2
+ describe Thunderbird::MailboxExporter do
3
+ subject { described_class.new("email", serializer, "profile", **args) }
4
+
5
+ let(:args) { {} }
6
+ let(:serializer) do
7
+ instance_double(
8
+ Serializer,
9
+ folder: "folder",
10
+ mbox_pathname: "mbox_pathname"
11
+ )
12
+ end
13
+ let(:local_folder) do
14
+ instance_double(
15
+ Thunderbird::LocalFolder,
16
+ exists?: local_folder_exists,
17
+ full_path: "full_path",
18
+ msf_exists?: msf_exists,
19
+ msf_path: "msf_path",
20
+ path: "path",
21
+ set_up: set_up_result
22
+ )
23
+ end
24
+ let(:local_folder_exists) { false }
25
+ let(:msf_exists) { false }
26
+ let(:set_up_result) { true }
27
+ let(:file) { instance_double(File, write: nil) }
28
+ let(:enumerator) { instance_double(Serializer::MboxEnumerator) }
29
+
30
+ before do
31
+ allow(File).to receive(:open).with("full_path", "w").and_yield(file)
32
+ allow(File).to receive(:unlink)
33
+ allow(Thunderbird::LocalFolder).to receive(:new) { local_folder }
34
+ allow(Serializer::MboxEnumerator).to receive(:new) { enumerator }
35
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
36
+ allow(Email::Mboxrd::Message).to receive(:clean_serialized) { "cleaned" }
37
+ allow(Kernel).to receive(:puts)
38
+ end
39
+
40
+ describe "#run" do
41
+ let!(:result) { subject.run }
42
+
43
+ context "when the destination folder cannot be set up" do
44
+ let(:set_up_result) { false }
45
+
46
+ it "doesn't copy the mailbox" do
47
+ expect(file).to_not have_received(:write)
48
+ end
49
+
50
+ it "returns false" do
51
+ expect(result).to be false
52
+ end
53
+ end
54
+
55
+ context "when the .msf file exists" do
56
+ let(:msf_exists) { true }
57
+
58
+ context "when 'force' is set" do
59
+ let(:args) { {force: true} }
60
+
61
+ it "deletes the file" do
62
+ expect(File).to have_received(:unlink).with("msf_path")
63
+ end
64
+ end
65
+
66
+ context "when 'force' isn't set" do
67
+ it "doesn't copy the mailbox" do
68
+ expect(file).to_not have_received(:write)
69
+ end
70
+
71
+ it "returns false" do
72
+ expect(result).to be false
73
+ end
74
+ end
75
+ end
76
+
77
+ context "when the destination mailbox exists" do
78
+ let(:local_folder_exists) { true }
79
+
80
+ context "when 'force' is set" do
81
+ let(:args) { {force: true} }
82
+
83
+ it "writes the message" do
84
+ expect(file).to have_received(:write)
85
+ end
86
+
87
+ it "returns true" do
88
+ expect(result).to be true
89
+ end
90
+ end
91
+
92
+ context "when 'force' isn't set" do
93
+ it "doesn't copy the mailbox" do
94
+ expect(file).to_not have_received(:write)
95
+ end
96
+
97
+ it "returns false" do
98
+ expect(result).to be false
99
+ end
100
+ end
101
+ end
102
+
103
+ it "adds a 'From' line" do
104
+ expect(file).to have_received(:write).with(/From - \w+ \w+ \d+ \d+:\d+:\d+/)
105
+ end
106
+
107
+ it "writes the cleaned message" do
108
+ expect(file).to have_received(:write).with(/cleaned/)
109
+ end
110
+
111
+ it "returns true" do
112
+ expect(result).to be true
113
+ end
114
+ end
115
+ end
116
+ end
@@ -8,7 +8,7 @@ describe Imap::Backup::Uploader do
8
8
  end
9
9
  let(:serializer) do
10
10
  instance_double(
11
- Imap::Backup::Serializer::Mbox,
11
+ Imap::Backup::Serializer,
12
12
  uids: [1, 2],
13
13
  update_uid: nil
14
14
  )
@@ -0,0 +1,34 @@
1
+ class Retrier
2
+ class FooError < StandardError; end
3
+
4
+ include RetryOnError
5
+
6
+ def do_stuff(errors:, limit:)
7
+ calls = 0
8
+
9
+ retry_on_error(errors: errors, limit: limit) do
10
+ calls += 1
11
+ raise FooError if calls < 3
12
+
13
+ 42
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.describe RetryOnError do
19
+ describe "#retry_on_error" do
20
+ subject { Retrier.new }
21
+
22
+ it "retries" do
23
+ expect(subject.do_stuff(errors: [Retrier::FooError], limit: 3)).to eq(42)
24
+ end
25
+
26
+ context "when the block fails more than the limit" do
27
+ it "fails" do
28
+ expect do
29
+ subject.do_stuff(errors: [Retrier::FooError], limit: 1)
30
+ end.to raise_error(Retrier::FooError)
31
+ end
32
+ end
33
+ end
34
+ end