imap-backup 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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