imap-backup 14.6.0 → 15.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80b99aa3b4c713e6a88c9dbe504186fd64065a1d03d50d2d47a79de6ab14902f
4
- data.tar.gz: c7a7d6f52b36819de9f660c488a8b4dfcf75794affa56547e197aab79c56cf7b
3
+ metadata.gz: 3504dad0e3f788bcfa95b38065e0ff4035239886461689c478a2e991f5e06cf9
4
+ data.tar.gz: 7d1151b5fb25f2d84ae2013f8f9f74fee7f998ca36b29333bf44151e52726376
5
5
  SHA512:
6
- metadata.gz: 75e98d37c675dab0d44fe98f2689f3dd36cf30eb2968f3c6260268e733122fccf212b0e91024b4031bc2b8243011e240a8370c03681dca4b21e8a155262292f5
7
- data.tar.gz: 46ca660ed4f2f87a3285db448b6e77e052df7628e512c57d7600876abca390b5e872529c76ef1f01b4ae35035d08a4941e24dd298d3db5850f44968ccbda25c2
6
+ metadata.gz: 3ec40ec413bd6bedce02d4c823ae1654c06585223532d1260bf01fe4ef0e8ba6e0b8c25c7adf0d2b39f8ff29c5199251d07fb9234a75a3ac50856075c91ccc52
7
+ data.tar.gz: d31020db8b37295d8d7209758161ce4fe5d16869a6976f3538e12b2d7a74f3cb363328d0dcd79ce665a2b113cf7f6289794ee328ded6e8a73cb544c4d0897428
data/imap-backup.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20
20
  gem.require_paths = ["lib"]
21
- gem.required_ruby_version = ">= 2.7"
21
+ gem.required_ruby_version = ">= 3.0"
22
22
 
23
23
  gem.add_runtime_dependency "highline"
24
24
  gem.add_runtime_dependency "mail", "2.7.1"
@@ -29,11 +29,13 @@ module Imap::Backup
29
29
 
30
30
  serializer.apply_uid_validity(folder.uid_validity)
31
31
 
32
- download_serializer.transaction do
32
+ serializer.transaction do
33
33
  downloader.run
34
+ FlagRefresher.new(folder, serializer).run if account.mirror_mode || refresh
34
35
  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
37
39
  end
38
40
 
39
41
  private
@@ -55,34 +57,29 @@ module Imap::Backup
55
57
  true
56
58
  end
57
59
 
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
-
63
60
  def downloader
64
61
  @downloader ||= Downloader.new(
65
62
  folder,
66
- download_serializer,
63
+ serializer,
67
64
  multi_fetch_size: account.multi_fetch_size,
68
65
  reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
69
66
  )
70
67
  end
71
68
 
72
- def download_serializer
73
- @download_serializer ||=
69
+ def serializer
70
+ @serializer ||=
74
71
  case account.download_strategy
75
72
  when "direct"
76
- serializer
73
+ raw_serializer
77
74
  when "delay_metadata"
78
- Serializer::DelayedMetadataSerializer.new(serializer: serializer)
75
+ Serializer::DelayedMetadataSerializer.new(serializer: raw_serializer)
79
76
  else
80
77
  raise "Unknown download strategy '#{account.download_strategy}'"
81
78
  end
82
79
  end
83
80
 
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)
86
83
  end
87
84
  end
88
85
  end
@@ -59,6 +59,7 @@ module Imap::Backup
59
59
  attr_reader :reset_seen_flags_after_fetch
60
60
 
61
61
  def initialize(options)
62
+ check_options!(options)
62
63
  @username = options[:username]
63
64
  @password = options[:password]
64
65
  @local_path = options[:local_path]
@@ -97,14 +98,6 @@ module Imap::Backup
97
98
  client.capability
98
99
  end
99
100
 
100
- # Indicates whether the account has been configured, and is ready
101
- # to be used
102
- #
103
- # @return [Boolean]
104
- def valid?
105
- username && password ? true : false
106
- end
107
-
108
101
  def modified?
109
102
  changes.any?
110
103
  end
@@ -245,6 +238,23 @@ module Imap::Backup
245
238
 
246
239
  attr_reader :changes
247
240
 
241
+ REQUIRED_ATTRIBUTES = %i[password username].freeze
242
+ OPTIONAL_ATTRIBUTES = %i[
243
+ connection_options download_strategy folders folder_blacklist local_path mirror_mode
244
+ multi_fetch_size reset_seen_flags_after_fetch server
245
+ ].freeze
246
+ KNOWN_ATTRIBUTES = REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES
247
+
248
+ def check_options!(options)
249
+ missing_required = REQUIRED_ATTRIBUTES - options.keys
250
+ if missing_required.any?
251
+ raise ArgumentError, "Missing required options: #{missing_required.join(', ')}"
252
+ end
253
+
254
+ unknown = options.keys - KNOWN_ATTRIBUTES
255
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}" if unknown.any?
256
+ end
257
+
248
258
  def update(field, value)
249
259
  key = :"@#{field}"
250
260
  if changes[field]
@@ -28,7 +28,7 @@ module Imap::Backup
28
28
  download_strategy: download_strategy,
29
29
  folder_blacklist: folder_blacklist,
30
30
  local_path: local_path,
31
- mirror: mirror,
31
+ mirror_mode: mirror,
32
32
  reset_seen_flags_after_fetch: reset_seen_flags_after_fetch
33
33
  )
34
34
  account.connection_options = connection_options if connection_options
@@ -23,12 +23,12 @@ module Imap::Backup
23
23
  # Proxies calls to the client.
24
24
  # Before the first call does login
25
25
  # @return the return value of the client method called
26
- def method_missing(method_name, *arguments, &block)
26
+ def method_missing(method_name, ...)
27
27
  if login_called
28
- client.send(method_name, *arguments, &block)
28
+ client.send(method_name, ...)
29
29
  else
30
30
  do_first_login
31
- client.send(method_name, *arguments, &block) if method_name != :login
31
+ client.send(method_name, ...) if method_name != :login
32
32
  end
33
33
  end
34
34
 
@@ -7,7 +7,7 @@ module Imap::Backup
7
7
  # The characters that cannot be used in file names
8
8
  INVALID_FILENAME_CHARACTERS = ":%;".freeze
9
9
  # A regular expression that captures each disallowed character
10
- INVALID_FILENAME_CHARACTER_MATCH = /([#{INVALID_FILENAME_CHARACTERS}])/.freeze
10
+ INVALID_FILENAME_CHARACTER_MATCH = /([#{INVALID_FILENAME_CHARACTERS}])/
11
11
 
12
12
  # @param name [String] a folder name
13
13
  # @return [String] the supplied string iwth disallowed characters replaced
@@ -10,7 +10,7 @@ module Imap; end
10
10
  module Imap::Backup
11
11
  class Serializer; end
12
12
 
13
- # Wraps the Serializer, delaying metadata appends
13
+ # Wraps the Serializer, delaying metadata changes
14
14
  class Serializer::DelayedMetadataSerializer
15
15
  extend Forwardable
16
16
 
@@ -31,7 +31,7 @@ module Imap::Backup
31
31
  def transaction(&block)
32
32
  tsx.fail_in_transaction!(:transaction, message: "nested transactions are not supported")
33
33
 
34
- tsx.begin({metadata: []}) do
34
+ tsx.begin({appends: [], updates: []}) do
35
35
  mbox.transaction do
36
36
  block.call
37
37
 
@@ -42,7 +42,21 @@ module Imap::Backup
42
42
  end
43
43
  end
44
44
 
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
46
60
  # to the transaction
47
61
  #
48
62
  # @param uid [Integer] the UID of the message
@@ -53,10 +67,21 @@ module Imap::Backup
53
67
  tsx.fail_outside_transaction!(:append)
54
68
  mboxrd_message = Email::Mboxrd::Message.new(message)
55
69
  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}
57
71
  mbox.append(serialized)
58
72
  end
59
73
 
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
+
60
85
  private
61
86
 
62
87
  attr_reader :serializer
@@ -64,9 +89,12 @@ module Imap::Backup
64
89
  def commit
65
90
  # rubocop:disable Lint/RescueException
66
91
  imap.transaction do
67
- tsx.data[:metadata].each do |m|
92
+ tsx.data[:appends].each do |m|
68
93
  imap.append m[:uid], m[:length], flags: m[:flags]
69
94
  end
95
+ tsx.data[:updates].each do |m|
96
+ imap.update m[:uid], length: m[:length], flags: m[:flags]
97
+ end
70
98
  rescue Exception => e
71
99
  Logger.logger.error "#{self.class} handling #{e.class}"
72
100
  imap.rollback
@@ -97,11 +97,27 @@ module Imap::Backup
97
97
  save
98
98
  end
99
99
 
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
101
116
  # @param uid [Integer] a message UID
102
117
  # @return [Serializer::Message]
103
118
  def get(uid)
104
- messages.find { |m| m.uid == uid }
119
+ message = messages.find { |m| m.uid == uid }
120
+ message&.dup
105
121
  end
106
122
 
107
123
  # Deletes the metadata file
@@ -158,11 +174,15 @@ module Imap::Backup
158
174
  messages.map(&:uid)
159
175
  end
160
176
 
161
- # Update a message's metadata, replacing its UID
177
+ # Update a message's UID
162
178
  # @param old [Integer] the existing message UID
163
179
  # @param new [Integer] the new UID to apply to the message
180
+ # @raise [RuntimeError] if the new UID already exists
164
181
  # @return [void]
165
182
  def update_uid(old, new)
183
+ existing = messages.find_index { |m| m.uid == new }
184
+ raise "UID #{new} already exists" if existing
185
+
166
186
  index = messages.find_index { |m| m.uid == old }
167
187
  return if index.nil?
168
188
 
@@ -28,6 +28,7 @@ module Imap::Backup
28
28
  extend Forwardable
29
29
 
30
30
  def_delegator :mbox, :pathname, :mbox_pathname
31
+ def_delegator :imap, :update
31
32
 
32
33
  # Get message metadata
33
34
  # @param uid [Integer] a message UID
@@ -77,7 +78,7 @@ module Imap::Backup
77
78
  @validated = nil
78
79
  end
79
80
 
80
- # Calls the supplied block.
81
+ # Calls the supplied block without implementing transactional behaviour.
81
82
  # This method is present so that this class implements the same
82
83
  # interface as {DelayedMetadataSerializer}
83
84
  # @param block [block] the block that is wrapped by the transaction
@@ -177,18 +178,6 @@ module Imap::Backup
177
178
  appender.append(uid: uid, message: message, flags: flags)
178
179
  end
179
180
 
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
-
192
181
  # Enumerates over a series of messages.
193
182
  # When called without a block, returns an Enumerator
194
183
  # @param required_uids [Array<Integer>] the UIDs of the message to enumerate over
@@ -55,7 +55,7 @@ module Imap::Backup
55
55
 
56
56
  private
57
57
 
58
- EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i.freeze
58
+ EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i
59
59
 
60
60
  attr_reader :highline
61
61
  end
@@ -2,11 +2,11 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  # @private
5
- MAJOR = 14
5
+ MAJOR = 15
6
6
  # @private
7
- MINOR = 6
7
+ MINOR = 0
8
8
  # @private
9
- REVISION = 0
9
+ REVISION = 1
10
10
  # @private
11
11
  PRE = nil
12
12
  # The application version
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 14.6.0
4
+ version: 15.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-31 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -218,7 +218,7 @@ licenses:
218
218
  - MIT
219
219
  metadata:
220
220
  rubygems_mfa_required: 'true'
221
- post_install_message:
221
+ post_install_message:
222
222
  rdoc_options: []
223
223
  require_paths:
224
224
  - lib
@@ -226,7 +226,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
226
  requirements:
227
227
  - - ">="
228
228
  - !ruby/object:Gem::Version
229
- version: '2.7'
229
+ version: '3.0'
230
230
  required_rubygems_version: !ruby/object:Gem::Requirement
231
231
  requirements:
232
232
  - - ">="
@@ -234,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
234
  version: '0'
235
235
  requirements: []
236
236
  rubygems_version: 3.5.3
237
- signing_key:
237
+ signing_key:
238
238
  specification_version: 4
239
239
  summary: Backup GMail (or other IMAP) accounts to disk
240
240
  test_files: []