imap-backup 14.6.1 → 15.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf35a029c76b09db247f5d45e6624fe6d0d1f1453ee29edd1e02e6621eb42710
4
- data.tar.gz: 2238c658e5306848de14ff2edffbf1275576ff1b27c0aa04c642a7c2de9d7fba
3
+ metadata.gz: de4f5bfc1260430435d380fd3fa6c315efad4ef503b0429bce041615d3ae2e10
4
+ data.tar.gz: 3c1541e30cb01d1ecd6da9372e79b8e8c71f119eee5a11770bef76b90e89e037
5
5
  SHA512:
6
- metadata.gz: d066629d29da0114e76d95b137caa7ba4ac26d91bda560a8a2fced551ad88f22477a67a0cfc9130365dafe7a4a60928b2ebedbc301ab6371d4aeade2de6131ab
7
- data.tar.gz: 75ac7182884ca791e30a7058940b40c4940f4ad515677f081de8f29e7c9da72061d3ee6eee9fce02efc734803f31ab2f2a66ae25356d284ce38f66d3af2ee8eb
6
+ metadata.gz: 25aa565a7860560958b59c5f88f67b470128405acc15fc67fe3683152d0ea9c63d8787287f006249a22b26a88d92425bed653254126ef92259ea85022b33efcb
7
+ data.tar.gz: 54d8df69cef34dcafd3ba97f63f202b0f00e2306f8e12c4ea7329dfae5028e42e9a4050630fae2d03131048aa3c70e90ee7bb8a49a1388084bd136ad3e0ce973
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
@@ -32,7 +32,7 @@ module Imap::Backup
32
32
  # @yieldparam serializer [Serializer] the folder's serializer
33
33
  # @yieldparam folder [Account::Folder] the online folder
34
34
  # @return [Enumerator, void]
35
- def each
35
+ def each(&block)
36
36
  return enum_for(:each) if !block_given?
37
37
 
38
38
  glob = File.join(source_local_path, "**", "*.imap")
@@ -40,7 +40,7 @@ module Imap::Backup
40
40
  name = source_folder_name(path)
41
41
  serializer = Serializer.new(source_local_path, name)
42
42
  folder = destination_folder_for_path(name)
43
- yield serializer, folder
43
+ block.call(serializer, folder)
44
44
  end
45
45
  end
46
46
 
@@ -1,5 +1,6 @@
1
1
  require "thor"
2
2
 
3
+ require "imap/backup/cli/options"
3
4
  require "imap/backup/configuration"
4
5
  require "imap/backup/configuration_not_found"
5
6
 
@@ -11,67 +12,8 @@ module Imap::Backup
11
12
  # Provides helper methods for CLI classes
12
13
  module CLI::Helpers
13
14
  def self.included(base)
14
- base.class_eval do
15
- def self.accounts_option
16
- method_option(
17
- "accounts",
18
- type: :string,
19
- desc: "a comma-separated list of accounts (defaults to all configured accounts)",
20
- aliases: ["-a"]
21
- )
22
- end
23
-
24
- def self.config_option
25
- method_option(
26
- "config",
27
- type: :string,
28
- desc: "supply the configuration file path (default: ~/.imap-backup/config.json)",
29
- aliases: ["-c"]
30
- )
31
- end
32
-
33
- def self.format_option
34
- method_option(
35
- "format",
36
- type: :string,
37
- desc: "the output type, 'text' for plain text or 'json'",
38
- aliases: ["-f"]
39
- )
40
- end
41
-
42
- def self.quiet_option
43
- method_option(
44
- "quiet",
45
- type: :boolean,
46
- desc: "silence all output",
47
- aliases: ["-q"]
48
- )
49
- end
50
-
51
- def self.refresh_option
52
- method_option(
53
- "refresh",
54
- type: :boolean,
55
- desc: "in the default 'keep all emails' mode, " \
56
- "updates flags for messages that are already downloaded",
57
- aliases: ["-r"]
58
- )
59
- end
60
-
61
- def self.verbose_option
62
- method_option(
63
- "verbose",
64
- type: :boolean,
65
- desc:
66
- "increase the amount of logging. " \
67
- "Without this option, the program gives minimal output. " \
68
- "Using this option once gives more detailed output. " \
69
- "Whereas, using this option twice also shows all IMAP network calls",
70
- aliases: ["-v"],
71
- repeatable: true
72
- )
73
- end
74
- end
15
+ options = CLI::Options.new(base: base)
16
+ options.define_options
75
17
  end
76
18
 
77
19
  # Processes command-line parameters
@@ -81,7 +23,7 @@ module Imap::Backup
81
23
  def options
82
24
  @symbolized_options ||= # rubocop:disable Naming/MemoizedInstanceVariableName
83
25
  begin
84
- options = super()
26
+ options = super
85
27
  options.each.with_object({}) do |(k, v), acc|
86
28
  key =
87
29
  if k.is_a?(String)
@@ -0,0 +1,74 @@
1
+ require "thor"
2
+
3
+ module Imap; end
4
+
5
+ module Imap::Backup
6
+ class CLI < Thor; end
7
+
8
+ # Defines option methods for CLI classes
9
+ class CLI::Options
10
+ attr_reader :base
11
+
12
+ # Options common to many commands
13
+ OPTIONS = [
14
+ {
15
+ name: "accounts",
16
+ parameters: {
17
+ type: :string, aliases: ["-a"],
18
+ desc: "a comma-separated list of accounts (defaults to all configured accounts)"
19
+ }
20
+ },
21
+ {
22
+ name: "config",
23
+ parameters: {
24
+ type: :string, aliases: ["-c"],
25
+ desc: "supply the configuration file path (default: ~/.imap-backup/config.json)"
26
+ }
27
+ },
28
+ {
29
+ name: "format",
30
+ parameters: {
31
+ type: :string, desc: "the output type, 'text' for plain text or 'json'", aliases: ["-f"]
32
+ }
33
+ },
34
+ {
35
+ name: "quiet",
36
+ parameters: {
37
+ type: :boolean, desc: "silence all output", aliases: ["-q"]
38
+ }
39
+ },
40
+ {
41
+ name: "refresh",
42
+ parameters: {
43
+ type: :boolean, aliases: ["-r"],
44
+ desc: "in the default 'keep all emails' mode, " \
45
+ "updates flags for messages that are already downloaded"
46
+ }
47
+ },
48
+ {
49
+ name: "verbose",
50
+ parameters: {
51
+ type: :boolean, aliases: ["-v"], repeatable: true,
52
+ desc: "increase the amount of logging. " \
53
+ "Without this option, the program gives minimal output. " \
54
+ "Using this option once gives more detailed output. " \
55
+ "Whereas, using this option twice also shows all IMAP network calls"
56
+ }
57
+ }
58
+ ].freeze
59
+
60
+ def initialize(base:)
61
+ @base = base
62
+ end
63
+
64
+ def define_options
65
+ OPTIONS.each do |option|
66
+ base.singleton_class.class_eval do
67
+ define_method("#{option[:name]}_option") do
68
+ method_option(option[:name], **option[:parameters])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -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
 
@@ -28,6 +28,13 @@ module Imap::Backup
28
28
 
29
29
  def refresh_block(uids)
30
30
  uids_and_flags = folder.fetch_multi(uids, ["FLAGS"])
31
+ if !uids_and_flags
32
+ Logger.logger.debug(
33
+ "[#{folder.name}] failed to fetch flags for #{uids} - " \
34
+ "cannot refresh flags"
35
+ )
36
+ return
37
+ end
31
38
  uids_and_flags.each do |uid_and_flags|
32
39
  uid = uid_and_flags[:uid]
33
40
  flags = uid_and_flags[:flags]
@@ -51,11 +51,11 @@ module Imap::Backup
51
51
  # Wraps a block, filtering output to standard error,
52
52
  # hidng passwords and outputs the results to standard out
53
53
  # @return [void]
54
- def self.sanitize_stderr
54
+ def self.sanitize_stderr(&block)
55
55
  sanitizer = Text::Sanitizer.new($stdout)
56
56
  previous_stderr = $stderr
57
57
  $stderr = sanitizer
58
- yield
58
+ block.call
59
59
  ensure
60
60
  sanitizer.flush
61
61
  $stderr = previous_stderr
@@ -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
@@ -13,9 +13,9 @@ module Imap::Backup
13
13
  # @param on_error [Proc] a block to call when an error occurs
14
14
  # @raise any error ocurring more than `limit` times
15
15
  # @return the result of any successful completion of the block
16
- def retry_on_error(errors:, limit: 10, on_error: nil)
16
+ def retry_on_error(errors:, limit: 10, on_error: nil, &block)
17
17
  tries ||= 1
18
- yield
18
+ block.call
19
19
  rescue *errors => e
20
20
  if tries < limit
21
21
  message = "#{e}, attempt #{tries} of #{limit}"
@@ -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
 
@@ -14,14 +14,14 @@ module Imap::Backup
14
14
  # @param uids [Array<Integer>] the message UIDs of the messages to iterate over
15
15
  # @yieldparam message [Serializer::Message]
16
16
  # @return [void]
17
- def run(uids:)
17
+ def run(uids:, &block)
18
18
  uids.each do |uid_maybe_string|
19
19
  uid = uid_maybe_string.to_i
20
20
  message = imap.get(uid)
21
21
 
22
22
  next if !message
23
23
 
24
- yield message
24
+ block.call(message)
25
25
  end
26
26
  end
27
27
 
@@ -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 = 1
9
+ REVISION = 2
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.1
4
+ version: 15.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-30 00:00:00.000000000 Z
11
+ date: 2024-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -153,6 +153,7 @@ files:
153
153
  - lib/imap/backup/cli/helpers.rb
154
154
  - lib/imap/backup/cli/local.rb
155
155
  - lib/imap/backup/cli/local/check.rb
156
+ - lib/imap/backup/cli/options.rb
156
157
  - lib/imap/backup/cli/remote.rb
157
158
  - lib/imap/backup/cli/restore.rb
158
159
  - lib/imap/backup/cli/setup.rb
@@ -226,7 +227,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
227
  requirements:
227
228
  - - ">="
228
229
  - !ruby/object:Gem::Version
229
- version: '2.7'
230
+ version: '3.0'
230
231
  required_rubygems_version: !ruby/object:Gem::Requirement
231
232
  requirements:
232
233
  - - ">="