imap-backup 2.1.1 → 2.2.0

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/README.md +6 -2
  4. data/bin/imap-backup +1 -1
  5. data/lib/email/mboxrd/message.rb +0 -1
  6. data/lib/imap/backup/account/connection.rb +28 -21
  7. data/lib/imap/backup/account/folder.rb +0 -1
  8. data/lib/imap/backup/serializer/mbox_enumerator.rb +1 -1
  9. data/lib/imap/backup/uploader.rb +10 -1
  10. data/lib/imap/backup/version.rb +2 -2
  11. data/spec/features/restore_spec.rb +75 -27
  12. data/spec/features/support/email_server.rb +1 -3
  13. data/spec/features/support/shared/message_fixtures.rb +8 -0
  14. data/spec/unit/email/mboxrd/message_spec.rb +0 -6
  15. data/spec/unit/imap/backup/account/connection_spec.rb +58 -43
  16. data/spec/unit/imap/backup/account/folder_spec.rb +16 -20
  17. data/spec/unit/imap/backup/configuration/account_spec.rb +31 -25
  18. data/spec/unit/imap/backup/configuration/asker_spec.rb +20 -17
  19. data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +6 -10
  20. data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +16 -10
  21. data/spec/unit/imap/backup/configuration/list_spec.rb +6 -3
  22. data/spec/unit/imap/backup/configuration/setup_spec.rb +40 -25
  23. data/spec/unit/imap/backup/configuration/store_spec.rb +18 -16
  24. data/spec/unit/imap/backup/downloader_spec.rb +14 -14
  25. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +6 -1
  26. data/spec/unit/imap/backup/serializer/mbox_spec.rb +54 -40
  27. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +49 -31
  28. data/spec/unit/imap/backup/uploader_spec.rb +20 -7
  29. data/spec/unit/imap/backup/utils_spec.rb +8 -9
  30. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78c6ccd7ca7a6f60ae02c24c76c121470372ffbadf268eb6f52b8f6eac633e6d
4
- data.tar.gz: d5da356a60a6046301c2e66a990f24948c3dc63f6c7f757ca3da43f62772468d
3
+ metadata.gz: 119c39db4e00d91f68a482035050ec8c91606a120cc80f1c514f39d731a30e02
4
+ data.tar.gz: 451761b7d17bcc664ff6890e6c86bd33628929bfdf665a91b322bcfd92c7d7f3
5
5
  SHA512:
6
- metadata.gz: 82fc5680a656d79012690e7da5038f0b8e56b1c5b14168142c222cb2c336bf44c0ec5570fbed8c50b2b2c2ea445ec17bed286d452795aa0c692d61c71d497274
7
- data.tar.gz: 7eda772c5b9815dfbbe679981a0a281cecd7e3cc970c95345437d35ddc01b40f5fedb25eabef043271bdd7cfc7084bf7caad6bbd037f5926fabee0d23958252a
6
+ metadata.gz: 3021adbfe55940b7f0a5d306259c2ba755277105d0139e6b51c18a425cbbf49737bde1fc38c6db00f51e272b71790c2c037838fcfffac7197fd35f429636d4d6
7
+ data.tar.gz: 6ef608f3bc7655886b6926c746768741ca70fb3b604834fb3750dbb393ae4551c077fa30f47ee8edae24a4684853093697988e1b23f3f50c505e635496d792a8
@@ -12,6 +12,8 @@ AllCops:
12
12
  RSpec/ContextWording:
13
13
  Exclude:
14
14
  - "spec/features/**/*"
15
+ RSpec/MessageSpies:
16
+ Enabled: false
15
17
  RSpec/NestedGroups:
16
18
  Max: 4
17
19
  RSpec/ReturnFromStub:
data/README.md CHANGED
@@ -204,8 +204,8 @@ Integration tests (feature specs) are run against a Docker image
204
204
 
205
205
  In one shell, run the Docker image:
206
206
 
207
- ```
208
- docker run \
207
+ ```sh
208
+ $ docker run \
209
209
  --env MAIL_ADDRESS=address@example.org \
210
210
  --env MAIL_PASS=pass \
211
211
  --env MAILNAME=example.org \
@@ -213,6 +213,10 @@ docker run \
213
213
  antespi/docker-imap-devel:latest
214
214
  ```
215
215
 
216
+ ```sh
217
+ $ rake
218
+ ```
219
+
216
220
  To exclude Docker-based tests:
217
221
 
218
222
  ```sh
@@ -53,7 +53,7 @@ if KNOWN_COMMANDS.find { |c| c[:name] == options[:command] }.nil?
53
53
  end
54
54
 
55
55
  if options[:command] == "help"
56
- puts opts
56
+ puts parser
57
57
  exit
58
58
  end
59
59
 
@@ -18,7 +18,6 @@ module Email::Mboxrd
18
18
 
19
19
  def initialize(supplied_body)
20
20
  @supplied_body = supplied_body.clone
21
- @supplied_body.force_encoding("binary")
22
21
  end
23
22
 
24
23
  def to_serialized
@@ -57,27 +57,7 @@ module Imap::Backup
57
57
 
58
58
  def restore
59
59
  local_folders do |serializer, folder|
60
- exists = folder.exist?
61
- if exists
62
- new_name = serializer.apply_uid_validity(folder.uid_validity)
63
- old_name = serializer.folder
64
- if new_name
65
- Imap::Backup.logger.debug(
66
- "Backup '#{old_name}' renamed and restored to '#{new_name}'"
67
- )
68
- new_serializer = Serializer::Mbox.new(local_path, new_name)
69
- new_folder = Account::Folder.new(self, new_name)
70
- new_folder.create
71
- new_serializer.force_uid_validity(new_folder.uid_validity)
72
- Uploader.new(new_folder, new_serializer).run
73
- else
74
- Uploader.new(folder, serializer).run
75
- end
76
- else
77
- folder.create
78
- serializer.force_uid_validity(folder.uid_validity)
79
- Uploader.new(folder, serializer).run
80
- end
60
+ restore_folder serializer, folder
81
61
  end
82
62
  end
83
63
 
@@ -114,6 +94,33 @@ module Imap::Backup
114
94
  end
115
95
  end
116
96
 
97
+ def restore_folder(serializer, folder)
98
+ existing_uids = folder.uids
99
+ if existing_uids.any?
100
+ Imap::Backup.logger.debug(
101
+ "There's already a '#{folder.name}' folder with emails"
102
+ )
103
+ new_name = serializer.apply_uid_validity(folder.uid_validity)
104
+ old_name = serializer.folder
105
+ if new_name
106
+ Imap::Backup.logger.debug(
107
+ "Backup '#{old_name}' renamed and restored to '#{new_name}'"
108
+ )
109
+ new_serializer = Serializer::Mbox.new(local_path, new_name)
110
+ new_folder = Account::Folder.new(self, new_name)
111
+ new_folder.create
112
+ new_serializer.force_uid_validity(new_folder.uid_validity)
113
+ Uploader.new(new_folder, new_serializer).run
114
+ else
115
+ Uploader.new(folder, serializer).run
116
+ end
117
+ else
118
+ folder.create
119
+ serializer.force_uid_validity(folder.uid_validity)
120
+ Uploader.new(folder, serializer).run
121
+ end
122
+ end
123
+
117
124
  def create_account_folder
118
125
  Utils.make_folder(
119
126
  File.dirname(local_path),
@@ -63,7 +63,6 @@ module Imap::Backup
63
63
  attributes = fetch_data_item.attr
64
64
  return nil if !attributes.key?("RFC822")
65
65
 
66
- attributes["RFC822"].force_encoding("utf-8")
67
66
  attributes
68
67
  rescue FolderNotFound
69
68
  nil
@@ -9,7 +9,7 @@ module Imap::Backup
9
9
  def each
10
10
  return enum_for(:each) if !block_given?
11
11
 
12
- File.open(mbox_pathname) do |f|
12
+ File.open(mbox_pathname, "rb") do |f|
13
13
  lines = []
14
14
 
15
15
  loop do
@@ -9,10 +9,19 @@ module Imap::Backup
9
9
  end
10
10
 
11
11
  def run
12
- missing_uids.each do |uid|
12
+ count = missing_uids.count
13
+ return if count.zero?
14
+
15
+ Imap::Backup.logger.debug "[#{folder.name}] #{count} to restore"
16
+ missing_uids.each.with_index do |uid, i|
13
17
  message = serializer.load(uid)
14
18
  next if message.nil?
15
19
 
20
+ log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
21
+ Imap::Backup.logger.debug(
22
+ "#{log_prefix} #{message.supplied_body.size} bytes"
23
+ )
24
+
16
25
  new_uid = folder.append(message)
17
26
  serializer.update_uid(uid, new_uid)
18
27
  end
@@ -2,7 +2,7 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  MAJOR = 2
5
- MINOR = 1
6
- REVISION = 1
5
+ MINOR = 2
6
+ REVISION = 0
7
7
  VERSION = [MAJOR, MINOR, REVISION].compact.map(&:to_s).join(".")
8
8
  end
@@ -72,41 +72,89 @@ RSpec.describe "restore", type: :feature, docker: true do
72
72
  end
73
73
 
74
74
  context "when the uid_validity doesn't match" do
75
- let(:pre) do
76
- server_create_folder folder
77
- email3
78
- end
79
- let(:new_folder) { "#{folder}.#{uid_validity}" }
80
- let(:cleanup) do
81
- server_delete_folder new_folder
82
- super()
83
- end
75
+ context "when the folder is empty" do
76
+ let(:pre) do
77
+ server_create_folder folder
78
+ end
84
79
 
85
- it "sets the backup uid_validity to match the new folder" do
86
- updated_imap_content = imap_parsed(new_folder)
87
- expect(updated_imap_content[:uid_validity]).
88
- to eq(server_uid_validity(new_folder))
89
- end
80
+ it "sets the backup uid_validity to match the folder" do
81
+ updated_imap_content = imap_parsed(folder)
82
+ expect(updated_imap_content[:uid_validity]).
83
+ to eq(server_uid_validity(folder))
84
+ end
90
85
 
91
- it "renames the backup" do
92
- expect(mbox_content(new_folder)).to eq(messages_as_mbox)
86
+ it "uploads to the new folder" do
87
+ messages = server_messages(folder).map do |m|
88
+ server_message_to_body(m)
89
+ end
90
+ expect(messages).to eq(messages_as_server_messages)
91
+ end
93
92
  end
94
93
 
95
- it "leaves the existing folder as is" do
96
- messages = server_messages(folder).map { |m| server_message_to_body(m) }
97
- expect(messages).to eq([message_as_server_message(msg3)])
98
- end
94
+ context "when the folder has content" do
95
+ let(:new_folder) { "#{folder}.#{uid_validity}" }
96
+ let(:cleanup) do
97
+ server_delete_folder new_folder
98
+ super()
99
+ end
99
100
 
100
- it "creates the new folder" do
101
- expect(server_folders.map(&:name)).to include(new_folder)
102
- end
101
+ let(:pre) do
102
+ server_create_folder folder
103
+ email3
104
+ end
103
105
 
104
- it "uploads to the new folder" do
105
- messages = server_messages(new_folder).map do |m|
106
- server_message_to_body(m)
106
+ it "renames the backup" do
107
+ expect(mbox_content(new_folder)).to eq(messages_as_mbox)
108
+ end
109
+
110
+ it "leaves the existing folder as is" do
111
+ messages = server_messages(folder).map do |m|
112
+ server_message_to_body(m)
113
+ end
114
+ expect(messages).to eq([message_as_server_message(msg3)])
115
+ end
116
+
117
+ it "creates the new folder" do
118
+ expect(server_folders.map(&:name)).to include(new_folder)
119
+ end
120
+
121
+ it "sets the backup uid_validity to match the new folder" do
122
+ updated_imap_content = imap_parsed(new_folder)
123
+ expect(updated_imap_content[:uid_validity]).
124
+ to eq(server_uid_validity(new_folder))
125
+ end
126
+
127
+ it "uploads to the new folder" do
128
+ messages = server_messages(new_folder).map do |m|
129
+ server_message_to_body(m)
130
+ end
131
+ expect(messages).to eq(messages_as_server_messages)
107
132
  end
108
- expect(messages).to eq(messages_as_server_messages)
109
133
  end
110
134
  end
111
135
  end
136
+
137
+ context "when non-Unicode encodings are used" do
138
+ let(:server_message) do
139
+ message_as_server_message(msg_iso8859)
140
+ end
141
+ let(:messages_as_mbox) do
142
+ message_as_mbox_entry(msg_iso8859)
143
+ end
144
+ let(:message_uids) { [uid_iso8859] }
145
+ let(:uid_validity) { server_uid_validity(folder) }
146
+
147
+ let(:pre) do
148
+ server_create_folder folder
149
+ uid_validity
150
+ end
151
+
152
+ it "maintains encodings" do
153
+ message =
154
+ server_messages(folder).
155
+ first["RFC822"]
156
+
157
+ expect(message).to eq(server_message)
158
+ end
159
+ end
112
160
  end
@@ -38,9 +38,7 @@ module EmailServerHelpers
38
38
  return nil if fetch_data_items.nil?
39
39
 
40
40
  fetch_data_item = fetch_data_items[0]
41
- attributes = fetch_data_item.attr
42
- attributes["RFC822"].force_encoding("utf-8")
43
- attributes
41
+ fetch_data_item.attr
44
42
  end
45
43
 
46
44
  def delete_emails(folder)
@@ -2,7 +2,15 @@ shared_context "message-fixtures" do
2
2
  let(:uid1) { 123 }
3
3
  let(:uid2) { 345 }
4
4
  let(:uid3) { 567 }
5
+ let(:uid_iso8859) { 890 }
5
6
  let(:msg1) { {uid: uid1, subject: "Test 1", body: "body 1\nHi"} }
6
7
  let(:msg2) { {uid: uid2, subject: "Test 2", body: "body 2"} }
7
8
  let(:msg3) { {uid: uid3, subject: "Test 3", body: "body 3"} }
9
+ let(:msg_iso8859) do
10
+ {
11
+ uid: uid_iso8859,
12
+ subject: "iso8859 Body",
13
+ body: "Ma, perchè?".encode(Encoding::ISO_8859_1).force_encoding("binary")
14
+ }
15
+ end
8
16
  end
@@ -79,12 +79,6 @@ describe Email::Mboxrd::Message do
79
79
  allow(Mail).to receive(:new).with(cloned_message_body) { mail }
80
80
  end
81
81
 
82
- it "does not modify the message" do
83
- subject.to_serialized
84
-
85
- expect(message_body).to_not have_received(:force_encoding).with("binary")
86
- end
87
-
88
82
  it "adds a 'From ' line at the start" do
89
83
  expected = "From #{from} #{date.asctime}\n"
90
84
  expect(subject.to_serialized).to start_with(expected)
@@ -49,10 +49,6 @@ describe Imap::Backup::Account::Connection do
49
49
  end
50
50
 
51
51
  shared_examples "connects to IMAP" do
52
- it "sets up the IMAP connection" do
53
- expect(Net::IMAP).to have_received(:new)
54
- end
55
-
56
52
  it "logs in to the imap server" do
57
53
  expect(imap).to have_received(:login)
58
54
  end
@@ -70,8 +66,9 @@ describe Imap::Backup::Account::Connection do
70
66
  end
71
67
 
72
68
  it "creates the path" do
69
+ expect(Imap::Backup::Utils).to receive(:make_folder)
70
+
73
71
  subject.username
74
- expect(Imap::Backup::Utils).to have_received(:make_folder)
75
72
  end
76
73
  end
77
74
 
@@ -102,7 +99,7 @@ describe Imap::Backup::Account::Connection do
102
99
  let(:remote_uid) { "remote_uid" }
103
100
 
104
101
  before do
105
- allow(Imap::Backup::Account::Folder).to receive(:new).and_return(folder)
102
+ allow(Imap::Backup::Account::Folder).to receive(:new) { folder }
106
103
  allow(Imap::Backup::Serializer::Mbox).to receive(:new) { serializer }
107
104
  end
108
105
 
@@ -140,21 +137,24 @@ describe Imap::Backup::Account::Connection do
140
137
  context "with supplied backup_folders" do
141
138
  before do
142
139
  allow(Imap::Backup::Account::Folder).to receive(:new).
143
- with(subject, self.class.backup_folder).and_return(folder)
140
+ with(subject, self.class.backup_folder) { folder }
144
141
  allow(Imap::Backup::Serializer::Mbox).to receive(:new).
145
- with(local_path, self.class.backup_folder).and_return(serializer)
146
- subject.run_backup
142
+ with(local_path, self.class.backup_folder) { serializer }
147
143
  end
148
144
 
149
145
  it "runs the downloader" do
150
- expect(downloader).to have_received(:run)
146
+ expect(downloader).to receive(:run)
147
+
148
+ subject.run_backup
151
149
  end
152
150
 
153
151
  context "when a folder does not exist" do
154
152
  let(:exists) { false }
155
153
 
156
154
  it "does not run the downloader" do
157
- expect(downloader).to_not have_received(:run)
155
+ expect(downloader).to_not receive(:run)
156
+
157
+ subject.run_backup
158
158
  end
159
159
  end
160
160
  end
@@ -166,28 +166,28 @@ describe Imap::Backup::Account::Connection do
166
166
 
167
167
  before do
168
168
  allow(Imap::Backup::Account::Folder).to receive(:new).
169
- with(subject, "foo").and_return(folder)
169
+ with(subject, "foo") { folder }
170
170
  allow(Imap::Backup::Serializer::Mbox).to receive(:new).
171
- with(local_path, "foo").and_return(serializer)
171
+ with(local_path, "foo") { serializer }
172
172
  end
173
173
 
174
174
  context "when supplied backup_folders is nil" do
175
175
  let(:backup_folders) { nil }
176
176
 
177
- before { subject.run_backup }
178
-
179
177
  it "runs the downloader for each folder" do
180
- expect(downloader).to have_received(:run).exactly(:once)
178
+ expect(downloader).to receive(:run).exactly(:once)
179
+
180
+ subject.run_backup
181
181
  end
182
182
  end
183
183
 
184
184
  context "when supplied backup_folders is an empty list" do
185
185
  let(:backup_folders) { [] }
186
186
 
187
- before { subject.run_backup }
188
-
189
187
  it "runs the downloader for each folder" do
190
- expect(downloader).to have_received(:run).exactly(:once)
188
+ expect(downloader).to receive(:run).exactly(:once)
189
+
190
+ subject.run_backup
191
191
  end
192
192
  end
193
193
 
@@ -207,12 +207,12 @@ describe Imap::Backup::Account::Connection do
207
207
  instance_double(
208
208
  Imap::Backup::Account::Folder,
209
209
  create: nil,
210
- exist?: exists,
210
+ uids: uids,
211
211
  name: "my_folder",
212
212
  uid_validity: uid_validity
213
213
  )
214
214
  end
215
- let(:exists) { true }
215
+ let(:uids) { [99] }
216
216
  let(:uid_validity) { 123 }
217
217
  let(:serialized_folder) { "old name" }
218
218
  let(:uploader) do
@@ -249,74 +249,89 @@ describe Imap::Backup::Account::Connection do
249
249
  with(updated_folder, updated_serializer) { updated_uploader }
250
250
  allow(Pathname).to receive(:glob).
251
251
  and_yield(Pathname.new(File.join(local_path, "my_folder.imap")))
252
- subject.restore
253
252
  end
254
253
 
255
254
  it "sets local uid validity" do
256
- expect(serializer).
257
- to have_received(:apply_uid_validity).with(uid_validity)
255
+ expect(serializer).to receive(:apply_uid_validity).with(uid_validity)
256
+
257
+ subject.restore
258
258
  end
259
259
 
260
- context "when folders exist" do
260
+ context "when folders exist with contents" do
261
261
  context "when the local folder is renamed" do
262
262
  let(:new_uid_validity) { "new name" }
263
263
 
264
264
  it "creates the new folder" do
265
- expect(updated_folder).to have_received(:create)
265
+ expect(updated_folder).to receive(:create)
266
+
267
+ subject.restore
266
268
  end
267
269
 
268
270
  it "sets the renamed folder's uid validity" do
269
271
  expect(updated_serializer).
270
- to have_received(:force_uid_validity).with("new uid validity")
272
+ to receive(:force_uid_validity).with("new uid validity")
273
+
274
+ subject.restore
271
275
  end
272
276
 
273
277
  it "creates the uploader with updated folder and serializer" do
274
- expect(updated_uploader).to have_received(:run)
278
+ expect(updated_uploader).to receive(:run)
279
+
280
+ subject.restore
275
281
  end
276
282
  end
277
283
 
278
284
  context "when the local folder is not renamed" do
279
285
  it "runs the uploader" do
280
- expect(uploader).to have_received(:run)
286
+ expect(uploader).to receive(:run)
287
+
288
+ subject.restore
281
289
  end
282
290
  end
283
291
  end
284
292
 
285
- context "when folders don't exist" do
286
- let(:exists) { false }
293
+ context "when folders don't exist or are empty" do
294
+ let(:uids) { [] }
287
295
 
288
296
  it "creates the folder" do
289
- expect(folder).to have_received(:create)
297
+ expect(folder).to receive(:create)
298
+
299
+ subject.restore
290
300
  end
291
301
 
292
- it "sets local uid validity" do
302
+ it "forces local uid validity" do
303
+ expect(serializer).to receive(:force_uid_validity).with(uid_validity)
304
+
305
+ subject.restore
293
306
  end
294
307
 
295
308
  it "runs the uploader" do
296
- expect(uploader).to have_received(:run)
309
+ expect(uploader).to receive(:run)
310
+
311
+ subject.restore
297
312
  end
298
313
  end
299
314
  end
300
315
 
301
316
  describe "#reconnect" do
302
- before { subject.reconnect }
303
-
304
317
  it "disconnects from the server" do
305
- expect(imap).to have_received(:disconnect)
318
+ expect(imap).to receive(:disconnect)
319
+
320
+ subject.reconnect
306
321
  end
307
322
 
308
323
  it "causes reconnection on future access" do
309
- allow(Net::IMAP).to receive(:new) { imap }
310
- subject.imap
311
- expect(Net::IMAP).to have_received(:new).twice
324
+ expect(Net::IMAP).to receive(:new)
325
+
326
+ subject.reconnect
312
327
  end
313
328
  end
314
329
 
315
330
  describe "#disconnect" do
316
- before { subject.disconnect }
317
-
318
331
  it "disconnects from the server" do
319
- expect(imap).to have_received(:disconnect)
332
+ expect(imap).to receive(:disconnect)
333
+
334
+ subject.disconnect
320
335
  end
321
336
  end
322
337
  end