imap-backup 2.0.0 → 2.2.2
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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rspec-all +2 -0
- data/.rubocop.yml +15 -2
- data/.rubocop_todo.yml +58 -0
- data/.travis.yml +15 -2
- data/README.md +14 -22
- data/Rakefile +6 -3
- data/bin/imap-backup +5 -11
- data/imap-backup.gemspec +10 -6
- data/lib/email/mboxrd/message.rb +16 -16
- data/lib/imap/backup/account/connection.rb +38 -22
- data/lib/imap/backup/account/folder.rb +23 -7
- data/lib/imap/backup/configuration/account.rb +25 -21
- data/lib/imap/backup/configuration/asker.rb +3 -2
- data/lib/imap/backup/configuration/connection_tester.rb +1 -1
- data/lib/imap/backup/configuration/folder_chooser.rb +32 -5
- data/lib/imap/backup/configuration/list.rb +2 -0
- data/lib/imap/backup/configuration/setup.rb +2 -1
- data/lib/imap/backup/configuration/store.rb +3 -6
- data/lib/imap/backup/downloader.rb +8 -7
- data/lib/imap/backup/serializer/mbox.rb +44 -25
- data/lib/imap/backup/serializer/mbox_enumerator.rb +31 -0
- data/lib/imap/backup/serializer/mbox_store.rb +35 -32
- data/lib/imap/backup/uploader.rb +11 -2
- data/lib/imap/backup/utils.rb +11 -9
- data/lib/imap/backup/version.rb +2 -2
- data/spec/features/backup_spec.rb +6 -5
- data/spec/features/helper.rb +1 -1
- data/spec/features/restore_spec.rb +75 -27
- data/spec/features/support/backup_directory.rb +7 -7
- data/spec/features/support/email_server.rb +15 -11
- data/spec/features/support/shared/connection_context.rb +2 -2
- data/spec/features/support/shared/message_fixtures.rb +8 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/fixtures.rb +2 -2
- data/spec/support/higline_test_helpers.rb +1 -1
- data/spec/unit/email/mboxrd/message_spec.rb +73 -53
- data/spec/unit/email/provider_spec.rb +3 -5
- data/spec/unit/imap/backup/account/connection_spec.rb +82 -59
- data/spec/unit/imap/backup/account/folder_spec.rb +75 -37
- data/spec/unit/imap/backup/configuration/account_spec.rb +95 -61
- data/spec/unit/imap/backup/configuration/asker_spec.rb +43 -45
- data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +21 -22
- data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +66 -33
- data/spec/unit/imap/backup/configuration/list_spec.rb +32 -11
- data/spec/unit/imap/backup/configuration/setup_spec.rb +97 -56
- data/spec/unit/imap/backup/configuration/store_spec.rb +30 -25
- data/spec/unit/imap/backup/downloader_spec.rb +28 -26
- data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +45 -0
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +109 -51
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +232 -20
- data/spec/unit/imap/backup/uploader_spec.rb +23 -9
- data/spec/unit/imap/backup/utils_spec.rb +14 -15
- data/spec/unit/imap/backup_spec.rb +28 -0
- metadata +13 -7
@@ -0,0 +1,31 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
class Serializer::MboxEnumerator
|
3
|
+
attr_reader :mbox_pathname
|
4
|
+
|
5
|
+
def initialize(mbox_pathname)
|
6
|
+
@mbox_pathname = mbox_pathname
|
7
|
+
end
|
8
|
+
|
9
|
+
def each
|
10
|
+
return enum_for(:each) if !block_given?
|
11
|
+
|
12
|
+
File.open(mbox_pathname, "rb") do |f|
|
13
|
+
lines = []
|
14
|
+
|
15
|
+
loop do
|
16
|
+
line = f.gets
|
17
|
+
break if !line
|
18
|
+
|
19
|
+
if line.start_with?("From ")
|
20
|
+
yield lines.join if lines.count.positive?
|
21
|
+
lines = [line]
|
22
|
+
else
|
23
|
+
lines << line
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
yield lines.join if lines.count.positive?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "json"
|
2
2
|
|
3
3
|
require "email/mboxrd/message"
|
4
|
+
require "imap/backup/serializer/mbox_enumerator"
|
4
5
|
|
5
6
|
module Imap::Backup
|
6
7
|
class Serializer::MboxStore
|
@@ -42,6 +43,7 @@ module Imap::Backup
|
|
42
43
|
def add(uid, message)
|
43
44
|
do_load if !loaded
|
44
45
|
raise "Can't add messages without uid_validity" if !uid_validity
|
46
|
+
|
45
47
|
uid = uid.to_i
|
46
48
|
if uids.include?(uid)
|
47
49
|
Imap::Backup.logger.debug(
|
@@ -58,7 +60,7 @@ module Imap::Backup
|
|
58
60
|
mbox.write mboxrd_message.to_serialized
|
59
61
|
@uids << uid
|
60
62
|
write_imap_file
|
61
|
-
rescue => e
|
63
|
+
rescue StandardError => e
|
62
64
|
message = <<-ERROR.gsub(/^\s*/m, "")
|
63
65
|
[#{folder}] failed to save message #{uid}:
|
64
66
|
#{body}. #{e}:
|
@@ -66,20 +68,39 @@ module Imap::Backup
|
|
66
68
|
ERROR
|
67
69
|
Imap::Backup.logger.warn message
|
68
70
|
ensure
|
69
|
-
mbox
|
71
|
+
mbox&.close
|
70
72
|
end
|
71
73
|
end
|
72
74
|
|
73
|
-
def load(
|
75
|
+
def load(uid_maybe_string)
|
74
76
|
do_load if !loaded
|
77
|
+
uid = uid_maybe_string.to_i
|
75
78
|
message_index = uids.find_index(uid)
|
76
79
|
return nil if message_index.nil?
|
80
|
+
|
77
81
|
load_nth(message_index)
|
78
82
|
end
|
79
83
|
|
84
|
+
def each_message(required_uids)
|
85
|
+
return enum_for(:each_message, required_uids) if !block_given?
|
86
|
+
|
87
|
+
indexes = required_uids.each.with_object({}) do |uid, acc|
|
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
|
+
|
80
100
|
def update_uid(old, new)
|
81
101
|
index = uids.find_index(old.to_i)
|
82
102
|
return if index.nil?
|
103
|
+
|
83
104
|
uids[index] = new.to_i
|
84
105
|
write_imap_file
|
85
106
|
end
|
@@ -93,23 +114,19 @@ module Imap::Backup
|
|
93
114
|
end
|
94
115
|
|
95
116
|
def rename(new_name)
|
96
|
-
new_mbox_pathname = absolute_path(new_name
|
97
|
-
new_imap_pathname = absolute_path(new_name
|
117
|
+
new_mbox_pathname = absolute_path("#{new_name}.mbox")
|
118
|
+
new_imap_pathname = absolute_path("#{new_name}.imap")
|
98
119
|
File.rename(mbox_pathname, new_mbox_pathname)
|
99
120
|
File.rename(imap_pathname, new_imap_pathname)
|
100
121
|
@folder = new_name
|
101
122
|
end
|
102
123
|
|
103
|
-
def relative_path
|
104
|
-
File.dirname(folder)
|
105
|
-
end
|
106
|
-
|
107
124
|
private
|
108
125
|
|
109
126
|
def do_load
|
110
127
|
data = imap_data
|
111
128
|
if data
|
112
|
-
@uids = data[:uids].map(&:to_i)
|
129
|
+
@uids = data[:uids].map(&:to_i)
|
113
130
|
@uid_validity = data[:uid_validity]
|
114
131
|
@loaded = true
|
115
132
|
else
|
@@ -128,7 +145,7 @@ module Imap::Backup
|
|
128
145
|
return nil
|
129
146
|
end
|
130
147
|
|
131
|
-
return nil if !imap_data.
|
148
|
+
return nil if !imap_data.key?(:uids)
|
132
149
|
return nil if !imap_data[:uids].is_a?(Array)
|
133
150
|
|
134
151
|
imap_data
|
@@ -137,37 +154,23 @@ module Imap::Backup
|
|
137
154
|
def imap_ok?
|
138
155
|
return false if !exist?
|
139
156
|
return false if !imap_looks_like_json?
|
157
|
+
|
140
158
|
true
|
141
159
|
end
|
142
160
|
|
143
161
|
def load_nth(index)
|
144
|
-
|
162
|
+
enumerator = Serializer::MboxEnumerator.new(mbox_pathname)
|
163
|
+
enumerator.each.with_index do |raw, i|
|
145
164
|
next unless i == index
|
165
|
+
|
146
166
|
return Email::Mboxrd::Message.from_serialized(raw)
|
147
167
|
end
|
148
168
|
nil
|
149
169
|
end
|
150
170
|
|
151
|
-
def each_mbox_message
|
152
|
-
Enumerator.new do |e|
|
153
|
-
File.open(mbox_pathname) do |f|
|
154
|
-
lines = []
|
155
|
-
|
156
|
-
while line = f.gets
|
157
|
-
if line.start_with?("From ")
|
158
|
-
e.yield lines.join if lines.count > 0
|
159
|
-
lines = [line]
|
160
|
-
else
|
161
|
-
lines << line
|
162
|
-
end
|
163
|
-
end
|
164
|
-
e.yield lines.join if lines.count > 0
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
171
|
def imap_looks_like_json?
|
170
172
|
return false unless imap_exist?
|
173
|
+
|
171
174
|
content = File.read(imap_pathname)
|
172
175
|
content.start_with?("{")
|
173
176
|
end
|
@@ -204,11 +207,11 @@ module Imap::Backup
|
|
204
207
|
end
|
205
208
|
|
206
209
|
def mbox_pathname
|
207
|
-
absolute_path(folder
|
210
|
+
absolute_path("#{folder}.mbox")
|
208
211
|
end
|
209
212
|
|
210
213
|
def imap_pathname
|
211
|
-
absolute_path(folder
|
214
|
+
absolute_path("#{folder}.imap")
|
212
215
|
end
|
213
216
|
end
|
214
217
|
end
|
data/lib/imap/backup/uploader.rb
CHANGED
@@ -9,9 +9,18 @@ module Imap::Backup
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def run
|
12
|
-
missing_uids.
|
13
|
-
|
12
|
+
count = missing_uids.count
|
13
|
+
return if count.zero?
|
14
|
+
|
15
|
+
Imap::Backup.logger.debug "[#{folder.name}] #{count} to restore"
|
16
|
+
serializer.each_message(missing_uids).with_index do |(uid, message), i|
|
14
17
|
next if message.nil?
|
18
|
+
|
19
|
+
log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
|
20
|
+
Imap::Backup.logger.debug(
|
21
|
+
"#{log_prefix} #{message.supplied_body.size} bytes"
|
22
|
+
)
|
23
|
+
|
15
24
|
new_uid = folder.append(message)
|
16
25
|
serializer.update_uid(uid, new_uid)
|
17
26
|
end
|
data/lib/imap/backup/utils.rb
CHANGED
@@ -5,15 +5,16 @@ module Imap::Backup
|
|
5
5
|
def self.check_permissions(filename, limit)
|
6
6
|
actual = mode(filename)
|
7
7
|
return nil if actual.nil?
|
8
|
+
|
8
9
|
mask = ~limit & 0o777
|
9
|
-
if actual & mask
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
return if (actual & mask).zero?
|
11
|
+
|
12
|
+
message = format(
|
13
|
+
"Permissions on '%<filename>s' " \
|
14
|
+
"should be 0%<limit>o, not 0%<actual>o",
|
15
|
+
filename: filename, limit: limit, actual: actual
|
16
|
+
)
|
17
|
+
raise message
|
17
18
|
end
|
18
19
|
|
19
20
|
def self.mode(filename)
|
@@ -25,7 +26,8 @@ module Imap::Backup
|
|
25
26
|
|
26
27
|
def self.make_folder(base_path, path, permissions)
|
27
28
|
parts = path.split("/")
|
28
|
-
return if parts.
|
29
|
+
return if parts.empty?
|
30
|
+
|
29
31
|
full_path = File.join(base_path, path)
|
30
32
|
FileUtils.mkdir_p full_path
|
31
33
|
path = base_path
|
data/lib/imap/backup/version.rb
CHANGED
@@ -10,7 +10,7 @@ RSpec.describe "backup", type: :feature, docker: true do
|
|
10
10
|
let(:folder) { "my-stuff" }
|
11
11
|
let(:email1) { send_email folder, msg1 }
|
12
12
|
let(:email2) { send_email folder, msg2 }
|
13
|
-
let!(:pre) {
|
13
|
+
let!(:pre) {}
|
14
14
|
let!(:setup) do
|
15
15
|
server_create_folder folder
|
16
16
|
email1
|
@@ -29,7 +29,7 @@ RSpec.describe "backup", type: :feature, docker: true do
|
|
29
29
|
expect(mbox_content(folder)).to eq(messages_as_mbox)
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
describe "IMAP metadata" do
|
33
33
|
let(:imap_metadata) { imap_parsed(folder) }
|
34
34
|
let(:folder_uids) { server_uids(folder) }
|
35
35
|
|
@@ -52,14 +52,15 @@ RSpec.describe "backup", type: :feature, docker: true do
|
|
52
52
|
context "when uid_validity does not match" do
|
53
53
|
let(:new_name) { "NEWNAME" }
|
54
54
|
let(:email3) { send_email folder, msg3 }
|
55
|
+
let(:original_folder_uid_validity) { server_uid_validity(folder) }
|
55
56
|
let!(:pre) do
|
56
57
|
server_create_folder folder
|
57
58
|
email3
|
58
|
-
|
59
|
+
original_folder_uid_validity
|
59
60
|
connection.run_backup
|
60
61
|
server_rename_folder folder, new_name
|
61
62
|
end
|
62
|
-
let(:renamed_folder) { folder
|
63
|
+
let(:renamed_folder) { "#{folder}.#{original_folder_uid_validity}" }
|
63
64
|
|
64
65
|
after do
|
65
66
|
server_delete_folder new_name
|
@@ -81,7 +82,7 @@ RSpec.describe "backup", type: :feature, docker: true do
|
|
81
82
|
end
|
82
83
|
|
83
84
|
it "moves the old backup to a uniquely named directory" do
|
84
|
-
renamed = folder
|
85
|
+
renamed = "#{folder}.#{original_folder_uid_validity}.1"
|
85
86
|
expect(mbox_content(renamed)).to eq(message_as_mbox_entry(msg3))
|
86
87
|
end
|
87
88
|
end
|
data/spec/features/helper.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
support_glob = File.expand_path("support/**/*.rb", __dir__)
|
2
|
-
Dir[support_glob].each { |f| require f }
|
2
|
+
Dir[support_glob].sort.each { |f| require f }
|
@@ -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
|
@@ -3,12 +3,12 @@ module BackupDirectoryHelpers
|
|
3
3
|
from = fixture("connection")[:username]
|
4
4
|
subject = options[:subject]
|
5
5
|
body = options[:body]
|
6
|
-
body_and_headers =
|
7
|
-
From: #{from}
|
8
|
-
Subject: #{subject}
|
6
|
+
body_and_headers = <<~BODY
|
7
|
+
From: #{from}
|
8
|
+
Subject: #{subject}
|
9
9
|
|
10
|
-
#{body}
|
11
|
-
|
10
|
+
#{body}
|
11
|
+
BODY
|
12
12
|
|
13
13
|
"From #{from}\n#{body_and_headers}\n"
|
14
14
|
end
|
@@ -26,11 +26,11 @@ Subject: #{subject}
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def mbox_path(name)
|
29
|
-
File.join(local_backup_path, name
|
29
|
+
File.join(local_backup_path, "#{name}.mbox")
|
30
30
|
end
|
31
31
|
|
32
32
|
def imap_path(name)
|
33
|
-
File.join(local_backup_path, name
|
33
|
+
File.join(local_backup_path, "#{name}.imap")
|
34
34
|
end
|
35
35
|
|
36
36
|
def imap_content(name)
|