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,217 +0,0 @@
1
- require "json"
2
-
3
- require "email/mboxrd/message"
4
- require "imap/backup/serializer/mbox_enumerator"
5
-
6
- module Imap::Backup
7
- class Serializer::MboxStore
8
- CURRENT_VERSION = 2
9
-
10
- attr_reader :folder
11
- attr_reader :path
12
- attr_reader :loaded
13
-
14
- def initialize(path, folder)
15
- @path = path
16
- @folder = folder
17
- @loaded = false
18
- @uids = nil
19
- @uid_validity = nil
20
- end
21
-
22
- def exist?
23
- mbox_exist? && imap_exist?
24
- end
25
-
26
- def uid_validity
27
- do_load if !loaded
28
- @uid_validity
29
- end
30
-
31
- def uid_validity=(value)
32
- do_load if !loaded
33
- @uid_validity = value
34
- @uids ||= []
35
- write_imap_file
36
- end
37
-
38
- def uids
39
- do_load if !loaded
40
- @uids || []
41
- end
42
-
43
- def add(uid, body)
44
- do_load if !loaded
45
- raise "Can't add messages without uid_validity" if !uid_validity
46
-
47
- uid = uid.to_i
48
- if uids.include?(uid)
49
- Imap::Backup::Logger.logger.debug(
50
- "[#{folder}] message #{uid} already downloaded - skipping"
51
- )
52
- return
53
- end
54
-
55
- mboxrd_message = Email::Mboxrd::Message.new(body)
56
- mbox = nil
57
- begin
58
- mbox = File.open(mbox_pathname, "ab")
59
- mbox.write mboxrd_message.to_serialized
60
- @uids << uid
61
- write_imap_file
62
- rescue StandardError => e
63
- message = <<-ERROR.gsub(/^\s*/m, "")
64
- [#{folder}] failed to save message #{uid}:
65
- #{body}. #{e}:
66
- #{e.backtrace.join("\n")}"
67
- ERROR
68
- Imap::Backup::Logger.logger.warn message
69
- ensure
70
- mbox&.close
71
- end
72
- end
73
-
74
- def load(uid_maybe_string)
75
- do_load if !loaded
76
- uid = uid_maybe_string.to_i
77
- message_index = uids.find_index(uid)
78
- return nil if message_index.nil?
79
-
80
- load_nth(message_index)
81
- end
82
-
83
- def each_message(required_uids)
84
- return enum_for(:each_message, required_uids) if !block_given?
85
-
86
- indexes = required_uids.each.with_object({}) do |uid_maybe_string, acc|
87
- uid = uid_maybe_string.to_i
88
- index = uids.find_index(uid)
89
- acc[index] = uid if index
90
- end
91
- enumerator = Serializer::MboxEnumerator.new(mbox_pathname)
92
- enumerator.each.with_index do |raw, i|
93
- uid = indexes[i]
94
- next if !uid
95
-
96
- yield uid, Email::Mboxrd::Message.from_serialized(raw)
97
- end
98
- end
99
-
100
- def update_uid(old, new)
101
- index = uids.find_index(old.to_i)
102
- return if index.nil?
103
-
104
- uids[index] = new.to_i
105
- write_imap_file
106
- end
107
-
108
- def reset
109
- @uids = nil
110
- @uid_validity = nil
111
- @loaded = false
112
- delete_files
113
- write_blank_mbox_file
114
- end
115
-
116
- def rename(new_name)
117
- new_mbox_pathname = absolute_path("#{new_name}.mbox")
118
- new_imap_pathname = absolute_path("#{new_name}.imap")
119
- File.rename(mbox_pathname, new_mbox_pathname)
120
- File.rename(imap_pathname, new_imap_pathname)
121
- @folder = new_name
122
- end
123
-
124
- def mbox_pathname
125
- absolute_path("#{folder}.mbox")
126
- end
127
-
128
- def imap_pathname
129
- absolute_path("#{folder}.imap")
130
- end
131
-
132
- private
133
-
134
- def do_load
135
- data = imap_data
136
- if data
137
- @uids = data[:uids].map(&:to_i)
138
- @uid_validity = data[:uid_validity]
139
- @loaded = true
140
- else
141
- reset
142
- end
143
- end
144
-
145
- def imap_data
146
- return nil if !imap_ok?
147
-
148
- imap_data = nil
149
-
150
- begin
151
- imap_data = JSON.parse(File.read(imap_pathname), symbolize_names: true)
152
- rescue JSON::ParserError
153
- return nil
154
- end
155
-
156
- return nil if !imap_data.key?(:uids)
157
- return nil if !imap_data[:uids].is_a?(Array)
158
-
159
- imap_data
160
- end
161
-
162
- def imap_ok?
163
- return false if !exist?
164
- return false if !imap_looks_like_json?
165
-
166
- true
167
- end
168
-
169
- def load_nth(index)
170
- enumerator = Serializer::MboxEnumerator.new(mbox_pathname)
171
- enumerator.each.with_index do |raw, i|
172
- next if i != index
173
-
174
- return Email::Mboxrd::Message.from_serialized(raw)
175
- end
176
- nil
177
- end
178
-
179
- def imap_looks_like_json?
180
- return false if !imap_exist?
181
-
182
- content = File.read(imap_pathname)
183
- content.start_with?("{")
184
- end
185
-
186
- def write_imap_file
187
- imap_data = {
188
- version: CURRENT_VERSION,
189
- uid_validity: @uid_validity,
190
- uids: @uids
191
- }
192
- content = imap_data.to_json
193
- File.open(imap_pathname, "w") { |f| f.write content }
194
- end
195
-
196
- def write_blank_mbox_file
197
- File.open(mbox_pathname, "w") { |f| f.write "" }
198
- end
199
-
200
- def delete_files
201
- File.unlink(imap_pathname) if imap_exist?
202
- File.unlink(mbox_pathname) if mbox_exist?
203
- end
204
-
205
- def mbox_exist?
206
- File.exist?(mbox_pathname)
207
- end
208
-
209
- def imap_exist?
210
- File.exist?(imap_pathname)
211
- end
212
-
213
- def absolute_path(relative_path)
214
- File.join(path, relative_path)
215
- end
216
- end
217
- end
@@ -1,329 +0,0 @@
1
- describe Imap::Backup::Serializer::MboxStore do
2
- subject { described_class.new(base_path, folder) }
3
-
4
- let(:base_path) { "/base/path" }
5
- let(:folder) { "the/folder" }
6
- let(:folder_path) { File.join(base_path, folder) }
7
- let(:imap_pathname) { "#{folder_path}.imap" }
8
- let(:imap_exists) { true }
9
- let(:imap_file) { instance_double(File, write: nil, close: nil) }
10
- let(:mbox_pathname) { "#{folder_path}.mbox" }
11
- let(:mbox_exists) { true }
12
- let(:mbox_file) { instance_double(File, write: nil, close: nil) }
13
- let(:uids) { [3, 2, 1] }
14
- let(:imap_content) do
15
- {
16
- version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
17
- uid_validity: uid_validity,
18
- uids: uids
19
- }.to_json
20
- end
21
- let(:uid_validity) { 123 }
22
-
23
- before do
24
- allow(File).to receive(:exist?).and_call_original
25
- allow(File).to receive(:exist?).with(imap_pathname) { imap_exists }
26
- allow(File).to receive(:exist?).with(mbox_pathname) { mbox_exists }
27
-
28
- allow(File).to receive(:open).and_call_original
29
- allow(File).
30
- to receive(:open).with("/base/path/my/folder.imap") { imap_content }
31
- allow(File).to receive(:open).with(imap_pathname, "w").and_yield(imap_file)
32
- allow(File).to receive(:open).with(mbox_pathname, "w").and_yield(mbox_file)
33
-
34
- allow(File).to receive(:read).and_call_original
35
- allow(File).to receive(:read).with(imap_pathname) { imap_content }
36
-
37
- allow(File).to receive(:unlink).and_call_original
38
- allow(File).to receive(:unlink).with(imap_pathname)
39
- allow(File).to receive(:unlink).with(mbox_pathname)
40
-
41
- allow(FileUtils).to receive(:chmod)
42
- end
43
-
44
- describe "#uid_validity=" do
45
- let(:new_uid_validity) { "13" }
46
- let(:updated_imap_content) do
47
- {
48
- version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
49
- uid_validity: new_uid_validity,
50
- uids: uids
51
- }.to_json
52
- end
53
-
54
- it "sets uid_validity" do
55
- subject.uid_validity = new_uid_validity
56
-
57
- expect(subject.uid_validity).to eq(new_uid_validity)
58
- end
59
-
60
- it "writes the imap file" do
61
- expect(imap_file).to receive(:write).with(updated_imap_content)
62
-
63
- subject.uid_validity = new_uid_validity
64
- end
65
- end
66
-
67
- describe "#uids" do
68
- it "returns the backed-up uids as integers" do
69
- expect(subject.uids).to eq(uids.map(&:to_i))
70
- end
71
-
72
- context "when the imap file does not exist" do
73
- let(:imap_exists) { false }
74
-
75
- it "returns an empty Array" do
76
- expect(subject.uids).to eq([])
77
- end
78
- end
79
-
80
- context "when the imap file is malformed" do
81
- before do
82
- allow(JSON).to receive(:parse).and_raise(JSON::ParserError)
83
- end
84
-
85
- it "returns an empty Array" do
86
- expect(subject.uids).to eq([])
87
- end
88
-
89
- it "deletes the imap file" do
90
- expect(File).to receive(:unlink).with(imap_pathname)
91
-
92
- subject.uids
93
- end
94
-
95
- it "deletes the mbox file" do
96
- expect(File).to receive(:unlink).with(mbox_pathname)
97
-
98
- subject.uids
99
- end
100
-
101
- it "writes a blank mbox file" do
102
- expect(mbox_file).to receive(:write).with("")
103
-
104
- subject.uids
105
- end
106
- end
107
-
108
- context "when the mbox does not exist" do
109
- let(:mbox_exists) { false }
110
-
111
- it "returns an empty Array" do
112
- expect(subject.uids).to eq([])
113
- end
114
- end
115
- end
116
-
117
- describe "#add" do
118
- let(:mbox_formatted_message) { "message in mbox format" }
119
- let(:message_uid) { "999" }
120
- let(:message) do
121
- instance_double(
122
- Email::Mboxrd::Message,
123
- to_serialized: mbox_formatted_message
124
- )
125
- end
126
- let(:updated_imap_content) do
127
- {
128
- version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
129
- uid_validity: uid_validity,
130
- uids: uids + [999]
131
- }.to_json
132
- end
133
-
134
- before do
135
- allow(Email::Mboxrd::Message).to receive(:new) { message }
136
- allow(File).to receive(:open).with(mbox_pathname, "ab") { mbox_file }
137
- end
138
-
139
- it "saves the message to the mbox" do
140
- expect(mbox_file).to receive(:write).with(mbox_formatted_message)
141
-
142
- subject.add(message_uid, "The\nemail\n")
143
- end
144
-
145
- it "saves the uid to the imap file" do
146
- expect(imap_file).to receive(:write).with(updated_imap_content)
147
-
148
- subject.add(message_uid, "The\nemail\n")
149
- end
150
-
151
- context "when the message is already downloaded" do
152
- let(:uids) { [999] }
153
-
154
- it "skips the message" do
155
- expect(mbox_file).to_not receive(:write)
156
-
157
- subject.add(message_uid, "The\nemail\n")
158
- end
159
- end
160
-
161
- context "when the message causes parsing errors" do
162
- before do
163
- allow(message).to receive(:to_serialized).and_raise(ArgumentError)
164
- end
165
-
166
- it "skips the message" do
167
- expect(mbox_file).to_not receive(:write)
168
-
169
- subject.add(message_uid, "The\nemail\n")
170
- end
171
-
172
- it "does not fail" do
173
- expect do
174
- subject.add(message_uid, "The\nemail\n")
175
- end.to_not raise_error
176
- end
177
- end
178
- end
179
-
180
- describe "#load" do
181
- let(:uid) { "1" }
182
- let(:enumerator) do
183
- instance_double(Imap::Backup::Serializer::MboxEnumerator)
184
- end
185
- let(:enumeration) { instance_double(Enumerator) }
186
-
187
- before do
188
- allow(Imap::Backup::Serializer::MboxEnumerator).
189
- to receive(:new) { enumerator }
190
- allow(enumerator).to receive(:each) { enumeration }
191
- allow(enumeration).
192
- to receive(:with_index).
193
- and_yield("", 0).
194
- and_yield("", 1).
195
- and_yield("ciao", 2)
196
- end
197
-
198
- it "returns the message" do
199
- expect(subject.load(uid).supplied_body).to eq("ciao")
200
- end
201
-
202
- context "when the UID is unknown" do
203
- let(:uid) { "99" }
204
-
205
- it "returns nil" do
206
- expect(subject.load(uid)).to be_nil
207
- end
208
- end
209
- end
210
-
211
- describe "#each_message" do
212
- let(:enumerator) do
213
- instance_double(Imap::Backup::Serializer::MboxEnumerator)
214
- end
215
- let(:enumeration) { instance_double(Enumerator) }
216
-
217
- before do
218
- allow(Imap::Backup::Serializer::MboxEnumerator).
219
- to receive(:new) { enumerator }
220
- allow(enumerator).to receive(:each) { enumeration }
221
- allow(enumeration).
222
- to receive(:with_index).
223
- and_yield("", 0).
224
- and_yield("", 1).
225
- and_yield("ciao", 2)
226
- end
227
-
228
- it "yields messages" do
229
- expect { |b| subject.each_message([1], &b) }.
230
- to yield_successive_args([1, instance_of(Email::Mboxrd::Message)])
231
- end
232
-
233
- it "yields the requested message uid" do
234
- subject.each_message([1]) do |uid, _message|
235
- expect(uid).to eq(1)
236
- end
237
- end
238
-
239
- it "yields the requested message" do
240
- subject.each_message([1]) do |_uid, message|
241
- expect(message.supplied_body).to eq("ciao")
242
- end
243
- end
244
-
245
- context "without a block" do
246
- it "returns an Enumerator" do
247
- expect(subject.each_message([1])).to be_a(Enumerator)
248
- end
249
- end
250
- end
251
-
252
- describe "#update_uid" do
253
- let(:old_uid) { "2" }
254
- let(:updated_imap_content) do
255
- {
256
- version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
257
- uid_validity: uid_validity,
258
- uids: [3, 999, 1]
259
- }.to_json
260
- end
261
-
262
- it "updates the stored UID" do
263
- expect(imap_file).to receive(:write).with(updated_imap_content)
264
-
265
- subject.update_uid(old_uid, "999")
266
- end
267
-
268
- context "when the UID is unknown" do
269
- let(:old_uid) { "42" }
270
-
271
- it "does nothing" do
272
- expect(imap_file).to_not receive(:write)
273
-
274
- subject.update_uid(old_uid, "999")
275
- end
276
- end
277
- end
278
-
279
- describe "#reset" do
280
- it "deletes the imap file" do
281
- expect(File).to receive(:unlink).with(imap_pathname)
282
-
283
- subject.reset
284
- end
285
-
286
- it "deletes the mbox file" do
287
- expect(File).to receive(:unlink).with(mbox_pathname)
288
-
289
- subject.reset
290
- end
291
-
292
- it "writes a blank mbox file" do
293
- expect(mbox_file).to receive(:write).with("")
294
-
295
- subject.reset
296
- end
297
- end
298
-
299
- describe "#rename" do
300
- let(:new_name) { "new_name" }
301
- let(:new_folder_path) { File.join(base_path, new_name) }
302
- let(:new_imap_name) { "#{new_folder_path}.imap" }
303
- let(:new_mbox_name) { "#{new_folder_path}.mbox" }
304
-
305
- before do
306
- allow(File).to receive(:rename).and_call_original
307
- allow(File).to receive(:rename).with(imap_pathname, new_imap_name)
308
- allow(File).to receive(:rename).with(mbox_pathname, new_mbox_name)
309
- end
310
-
311
- it "renames the imap file" do
312
- expect(File).to receive(:rename).with(imap_pathname, new_imap_name)
313
-
314
- subject.rename(new_name)
315
- end
316
-
317
- it "renames the mbox file" do
318
- expect(File).to receive(:rename).with(mbox_pathname, new_mbox_name)
319
-
320
- subject.rename(new_name)
321
- end
322
-
323
- it "updates the folder name" do
324
- subject.rename(new_name)
325
-
326
- expect(subject.folder).to eq(new_name)
327
- end
328
- end
329
- end