imap-backup 14.6.1 → 15.0.2

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: 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
  - - ">="