Skip to content

Commit e5da4d4

Browse files
committed
Refresh flags inside the transaction
1 parent d23611b commit e5da4d4

File tree

8 files changed

+105
-76
lines changed

8 files changed

+105
-76
lines changed

lib/imap/backup/account/folder_backup.rb

+12-15
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ def run
2929

3030
serializer.apply_uid_validity(folder.uid_validity)
3131

32-
download_serializer.transaction do
32+
serializer.transaction do
3333
downloader.run
34+
FlagRefresher.new(folder, serializer).run if account.mirror_mode || refresh
3435
end
35-
36-
clean_up
36+
# After the transaction the serializer will have any appended messages
37+
# so we can check differences between the server and the local backup
38+
LocalOnlyMessageDeleter.new(folder, raw_serializer).run if account.mirror_mode
3739
end
3840

3941
private
@@ -55,34 +57,29 @@ def folder_ok?
5557
true
5658
end
5759

58-
def clean_up
59-
LocalOnlyMessageDeleter.new(folder, serializer).run if account.mirror_mode
60-
FlagRefresher.new(folder, serializer).run if account.mirror_mode || refresh
61-
end
62-
6360
def downloader
6461
@downloader ||= Downloader.new(
6562
folder,
66-
download_serializer,
63+
serializer,
6764
multi_fetch_size: account.multi_fetch_size,
6865
reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
6966
)
7067
end
7168

72-
def download_serializer
73-
@download_serializer ||=
69+
def serializer
70+
@serializer ||=
7471
case account.download_strategy
7572
when "direct"
76-
serializer
73+
raw_serializer
7774
when "delay_metadata"
78-
Serializer::DelayedMetadataSerializer.new(serializer: serializer)
75+
Serializer::DelayedMetadataSerializer.new(serializer: raw_serializer)
7976
else
8077
raise "Unknown download strategy '#{account.download_strategy}'"
8178
end
8279
end
8380

84-
def serializer
85-
@serializer ||= Serializer.new(account.local_path, folder.name)
81+
def raw_serializer
82+
@raw_serializer ||= Serializer.new(account.local_path, folder.name)
8683
end
8784
end
8885
end

lib/imap/backup/serializer.rb

+2-13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def self.folder_path_for(path:, folder:)
2828
extend Forwardable
2929

3030
def_delegator :mbox, :pathname, :mbox_pathname
31+
def_delegator :imap, :update
3132

3233
# Get message metadata
3334
# @param uid [Integer] a message UID
@@ -77,7 +78,7 @@ def initialize(path, folder)
7778
@validated = nil
7879
end
7980

80-
# Calls the supplied block.
81+
# Calls the supplied block without implementing transactional behaviour.
8182
# This method is present so that this class implements the same
8283
# interface as {DelayedMetadataSerializer}
8384
# @param block [block] the block that is wrapped by the transaction
@@ -177,18 +178,6 @@ def append(uid, message, flags)
177178
appender.append(uid: uid, message: message, flags: flags)
178179
end
179180

180-
# Updates a messages flags
181-
# @param uid [Integer] the message's UID
182-
# @param flags [Array<Symbol>] the flags to set on the message
183-
# @return [void]
184-
def update(uid, flags: nil)
185-
message = imap.get(uid)
186-
return if !message
187-
188-
message.flags = flags if flags
189-
imap.save
190-
end
191-
192181
# Enumerates over a series of messages.
193182
# When called without a block, returns an Enumerator
194183
# @param required_uids [Array<Integer>] the UIDs of the message to enumerate over

lib/imap/backup/serializer/delayed_metadata_serializer.rb

+33-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Imap; end
1010
module Imap::Backup
1111
class Serializer; end
1212

13-
# Wraps the Serializer, delaying metadata appends
13+
# Wraps the Serializer, delaying metadata changes
1414
class Serializer::DelayedMetadataSerializer
1515
extend Forwardable
1616

@@ -31,7 +31,7 @@ def initialize(serializer:)
3131
def transaction(&block)
3232
tsx.fail_in_transaction!(:transaction, message: "nested transactions are not supported")
3333

34-
tsx.begin({metadata: []}) do
34+
tsx.begin({appends: [], updates: []}) do
3535
mbox.transaction do
3636
block.call
3737

@@ -42,7 +42,21 @@ def transaction(&block)
4242
end
4343
end
4444

45-
# Appends a message to the mbox file and adds the metadata
45+
# Sets the folder's UID validity via the serializer
46+
#
47+
# @param uid_validity [Integer] the UID validity to apply
48+
# @raise [RuntimeError] if called inside a transaction
49+
# @return [void]
50+
def apply_uid_validity(uid_validity)
51+
tsx.fail_in_transaction!(
52+
:transaction,
53+
message: "UID validity cannot be changed in a transaction"
54+
)
55+
56+
serializer.apply_uid_validity(uid_validity)
57+
end
58+
59+
# Appends a message to the mbox file and adds the appended message's metadata
4660
# to the transaction
4761
#
4862
# @param uid [Integer] the UID of the message
@@ -53,20 +67,34 @@ def append(uid, message, flags)
5367
tsx.fail_outside_transaction!(:append)
5468
mboxrd_message = Email::Mboxrd::Message.new(message)
5569
serialized = mboxrd_message.to_serialized
56-
tsx.data[:metadata] << {uid: uid, length: serialized.bytesize, flags: flags}
70+
tsx.data[:appends] << {uid: uid, length: serialized.bytesize, flags: flags}
5771
mbox.append(serialized)
5872
end
5973

74+
# Stores changes to a message's metadata for later update
75+
#
76+
# @param uid [Integer] the UID of the message
77+
# @param length [Integer] the length of the message
78+
# @param flags [Array<Symbol>] the flags for the message
79+
# @return [void]
80+
def update(uid, length: nil, flags: nil)
81+
tsx.fail_outside_transaction!(:update)
82+
tsx.data[:updates] << {uid: uid, length: length, flags: flags}
83+
end
84+
6085
private
6186

6287
attr_reader :serializer
6388

6489
def commit
6590
# rubocop:disable Lint/RescueException
6691
imap.transaction do
67-
tsx.data[:metadata].each do |m|
92+
tsx.data[:appends].each do |m|
6893
imap.append m[:uid], m[:length], flags: m[:flags]
6994
end
95+
tsx.data[:updates].each do |m|
96+
imap.update m[:uid], length: m[:length], flags: m[:flags]
97+
end
7098
rescue Exception => e
7199
Logger.logger.error "#{self.class} handling #{e.class}"
72100
imap.rollback

lib/imap/backup/serializer/imap.rb

+23-3
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,27 @@ def append(uid, length, flags: [])
9797
save
9898
end
9999

100-
# Get message metadata
100+
# Updates a message's length and/or flags
101+
# @param uid [Integer] the existing message's UID
102+
# @param length [Integer] the length of the message (as stored on disk)
103+
# @param flags [Array[Symbol]] the message's flags
104+
# @raise [RuntimeError] if the UID does not exist
105+
# @return [void]
106+
def update(uid, length: nil, flags: nil)
107+
index = messages.find_index { |m| m.uid == uid }
108+
raise "UID #{uid} not found" if !index
109+
110+
messages[index].length = length if length
111+
messages[index].flags = flags if flags
112+
save
113+
end
114+
115+
# Get a copy of message metadata
101116
# @param uid [Integer] a message UID
102117
# @return [Serializer::Message]
103118
def get(uid)
104-
messages.find { |m| m.uid == uid }
119+
message = messages.find { |m| m.uid == uid }
120+
message&.dup
105121
end
106122

107123
# Deletes the metadata file
@@ -158,11 +174,15 @@ def uids
158174
messages.map(&:uid)
159175
end
160176

161-
# Update a message's metadata, replacing its UID
177+
# Update a message's UID
162178
# @param old [Integer] the existing message UID
163179
# @param new [Integer] the new UID to apply to the message
180+
# @raise [RuntimeError] if the new UID already exists
164181
# @return [void]
165182
def update_uid(old, new)
183+
existing = messages.find_index { |m| m.uid == new }
184+
raise "UID #{new} already exists" if existing
185+
166186
index = messages.find_index { |m| m.uid == old }
167187
return if index.nil?
168188

spec/features/backup_spec.rb

+25-7
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
write_config
101101
backup.run
102102
test_server.set_flags folder, [1], [:Seen]
103-
super()
104103
end
105104

106105
it "updates flags" do
@@ -182,27 +181,46 @@
182181

183182
context "in mirror mode" do
184183
let(:account_config) { super().merge(mirror_mode: true) }
185-
let(:imap_path) { File.join(account_config[:local_path], "Foo.imap") }
186-
let(:mbox_path) { File.join(account_config[:local_path], "Foo.mbox") }
184+
let(:foo_imap_path) { File.join(account_config[:local_path], "Foo.imap") }
185+
let(:foo_mbox_path) { File.join(account_config[:local_path], "Foo.mbox") }
187186

188187
let!(:setup) do
189188
create_directory account_config[:local_path]
190-
File.write(imap_path, "existing imap")
191-
File.write(mbox_path, "existing mbox")
189+
File.write(foo_imap_path, "existing imap")
190+
File.write(foo_mbox_path, "existing mbox")
192191
super()
193192
end
194193

195194
context "with folders that are not being backed up" do
196195
it "deletes .imap files" do
197196
run_command_and_stop command
198197

199-
expect(File.exist?(imap_path)).to be false
198+
expect(File.exist?(foo_imap_path)).to be false
200199
end
201200

202201
it "deletes .mbox files" do
203202
run_command_and_stop command
204203

205-
expect(File.exist?(mbox_path)).to be false
204+
expect(File.exist?(foo_mbox_path)).to be false
205+
end
206+
end
207+
208+
context "with messages that have been deleted from the server" do
209+
let(:setup) do
210+
super()
211+
backup.run
212+
end
213+
let(:mbox_two) { to_mbox_entry(**message_two) }
214+
let(:mbox_three) { to_mbox_entry(**message_three) }
215+
216+
it "deletes messages from the local backup" do
217+
expect(mbox_content(email, folder)).to eq(messages_as_mbox)
218+
test_server.delete_email folder, 1
219+
test_server.send_email folder, **message_three
220+
221+
run_command_and_stop command
222+
223+
expect(mbox_content(email, folder)).to eq(mbox_two + mbox_three)
206224
end
207225
end
208226
end

spec/features/migrate_spec.rb

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
after do
2828
destination_server.delete_folder source_folder
2929
destination_server.disconnect
30+
test_server.disconnect
3031
end
3132

3233
context "when a config path is supplied" do

spec/features/support/20_test_email_server.rb

+5-7
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,12 @@ def reconnect
3434
def disconnect
3535
return if !@imap
3636

37-
if !imap.disconnected?
38-
begin
39-
imap.logout
40-
rescue EOFError
41-
# ignore occasional error when closing connection
42-
end
43-
imap.disconnect
37+
begin
38+
imap.logout
39+
rescue IOError
40+
# ignore occasional error when closing connection
4441
end
42+
imap.disconnect
4543

4644
@imap = nil
4745
end

spec/unit/serializer_spec.rb

+4-26
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ module Imap::Backup
107107
rename: nil,
108108
save: nil,
109109
uid_validity: existing_uid_validity,
110-
"uid_validity=": nil
110+
"uid_validity=": nil,
111+
update: nil
111112
)
112113
end
113114
let(:mbox) do
@@ -247,36 +248,13 @@ module Imap::Backup
247248

248249
describe "#update" do
249250
let(:flags) { [:Foo] }
250-
let(:message) { instance_double(Serializer::Message, "flags=": nil) }
251251

252252
before do
253-
allow(imap).to receive(:get) { message }
254-
255253
subject.update(33, flags: flags)
256254
end
257255

258-
it "updates the message flags" do
259-
expect(message).to have_received(:flags=).with(flags)
260-
end
261-
262-
it "saves the .imap file" do
263-
expect(imap).to have_received(:save)
264-
end
265-
266-
context "when no flags are supplied" do
267-
let(:flags) { nil }
268-
269-
it "does not update the message flags" do
270-
expect(message).to_not have_received(:flags=)
271-
end
272-
end
273-
274-
context "when the UID is not known" do
275-
let(:message) { nil }
276-
277-
it "does not save" do
278-
expect(imap).to_not have_received(:save)
279-
end
256+
it "updates the .imap file" do
257+
expect(imap).to have_received(:update).with(33, flags: flags)
280258
end
281259
end
282260

0 commit comments

Comments
 (0)