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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/README.md +6 -2
- data/bin/imap-backup +1 -1
- data/lib/email/mboxrd/message.rb +0 -1
- data/lib/imap/backup/account/connection.rb +28 -21
- data/lib/imap/backup/account/folder.rb +0 -1
- data/lib/imap/backup/serializer/mbox_enumerator.rb +1 -1
- data/lib/imap/backup/uploader.rb +10 -1
- data/lib/imap/backup/version.rb +2 -2
- data/spec/features/restore_spec.rb +75 -27
- data/spec/features/support/email_server.rb +1 -3
- data/spec/features/support/shared/message_fixtures.rb +8 -0
- data/spec/unit/email/mboxrd/message_spec.rb +0 -6
- data/spec/unit/imap/backup/account/connection_spec.rb +58 -43
- data/spec/unit/imap/backup/account/folder_spec.rb +16 -20
- data/spec/unit/imap/backup/configuration/account_spec.rb +31 -25
- data/spec/unit/imap/backup/configuration/asker_spec.rb +20 -17
- data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +6 -10
- data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +16 -10
- data/spec/unit/imap/backup/configuration/list_spec.rb +6 -3
- data/spec/unit/imap/backup/configuration/setup_spec.rb +40 -25
- data/spec/unit/imap/backup/configuration/store_spec.rb +18 -16
- data/spec/unit/imap/backup/downloader_spec.rb +14 -14
- data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +6 -1
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +54 -40
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +49 -31
- data/spec/unit/imap/backup/uploader_spec.rb +20 -7
- data/spec/unit/imap/backup/utils_spec.rb +8 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 119c39db4e00d91f68a482035050ec8c91606a120cc80f1c514f39d731a30e02
|
4
|
+
data.tar.gz: 451761b7d17bcc664ff6890e6c86bd33628929bfdf665a91b322bcfd92c7d7f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3021adbfe55940b7f0a5d306259c2ba755277105d0139e6b51c18a425cbbf49737bde1fc38c6db00f51e272b71790c2c037838fcfffac7197fd35f429636d4d6
|
7
|
+
data.tar.gz: 6ef608f3bc7655886b6926c746768741ca70fb3b604834fb3750dbb393ae4551c077fa30f47ee8edae24a4684853093697988e1b23f3f50c505e635496d792a8
|
data/.rubocop.yml
CHANGED
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
|
data/bin/imap-backup
CHANGED
data/lib/email/mboxrd/message.rb
CHANGED
@@ -57,27 +57,7 @@ module Imap::Backup
|
|
57
57
|
|
58
58
|
def restore
|
59
59
|
local_folders do |serializer, folder|
|
60
|
-
|
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),
|
data/lib/imap/backup/uploader.rb
CHANGED
@@ -9,10 +9,19 @@ module Imap::Backup
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def run
|
12
|
-
missing_uids.
|
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
|
data/lib/imap/backup/version.rb
CHANGED
@@ -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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
101
|
+
let(:pre) do
|
102
|
+
server_create_folder folder
|
103
|
+
email3
|
104
|
+
end
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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)
|
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)
|
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)
|
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
|
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
|
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")
|
169
|
+
with(subject, "foo") { folder }
|
170
170
|
allow(Imap::Backup::Serializer::Mbox).to receive(:new).
|
171
|
-
with(local_path, "foo")
|
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
|
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
|
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
|
-
|
210
|
+
uids: uids,
|
211
211
|
name: "my_folder",
|
212
212
|
uid_validity: uid_validity
|
213
213
|
)
|
214
214
|
end
|
215
|
-
let(:
|
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
|
-
|
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
|
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
|
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
|
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
|
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(:
|
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
|
297
|
+
expect(folder).to receive(:create)
|
298
|
+
|
299
|
+
subject.restore
|
290
300
|
end
|
291
301
|
|
292
|
-
it "
|
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
|
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
|
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
|
-
|
310
|
-
|
311
|
-
|
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
|
332
|
+
expect(imap).to receive(:disconnect)
|
333
|
+
|
334
|
+
subject.disconnect
|
320
335
|
end
|
321
336
|
end
|
322
337
|
end
|