imap-backup 16.1.0 → 16.3.0

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: bf6997d289ca5babbecd35e683ede921273300779f3f7dac95e247c0478f1814
4
- data.tar.gz: '009892c47699a0cc94ff28c2d3ea7346915732b7dc86c7231b7e606302219888'
3
+ metadata.gz: 1dbb91a882b2679944560fe5d0f16fd11b59dce626386626d151463a8354a981
4
+ data.tar.gz: 7b57013410b24ed4625a0405cc9b950f6cdf528e5d50d51b2919a81048f92947
5
5
  SHA512:
6
- metadata.gz: 46bdbccf9be3efade25e500f6c67f37de4fc0a0aa8514b55f8eabf7e1171042b5be87d029f37d9cb449c23d1fcf0d5fd2a20c2e8595c6149eff0c20f89e6b3d3
7
- data.tar.gz: d507a668897c325e2cc771d69b083f9929247b839b55d1fe86bfaeac3ee8be40df43b9f634d4f97c0188fe3bcbfba666456a9131236103455020fb45ecec9d61
6
+ metadata.gz: '04508eb2303ed44b9fdd8cd361958ed66eb5279a91dc310e10e404432a9e504094b4ff2d3972a645dc35ea2eff47f79f408466e3cf23cf6ac761ef1e5d3601fe'
7
+ data.tar.gz: a96050615e046863111e79a3350c38dc3d6e3e05f7be967fdd5003e295c1d896f07b48d0aad719c0d2571f0f7aa315833f343bb7b0432c0e3cef556f915b1cfc
data/docs/performance.md CHANGED
@@ -43,7 +43,7 @@ By default, during backup, each message is downloaded one-by-one.
43
43
  Using this setting, you can download chunks of emails at a time,
44
44
  potentially speeding up the process.
45
45
 
46
- Using multi-fetch mean that the backup process *will* use
46
+ Using multi-fetch means that the backup process *will* use
47
47
  more memory - equivalent to the size of the groups of messages
48
48
  that are downloaded.
49
49
 
@@ -196,7 +196,9 @@ module Imap::Backup
196
196
  def extract_uid(response)
197
197
  uid_data = response.data.code.data
198
198
  @uid_validity = uid_data.uidvalidity
199
- uid_data.assigned_uids.first
199
+ # With net-imap >= 0.6, assigned_uids is a `Net::IMAP::SequenceSet`
200
+ uids = uid_data.assigned_uids.to_a
201
+ uids.to_a.first
200
202
  end
201
203
 
202
204
  def utf7_encoded_name
@@ -44,7 +44,7 @@ module Imap::Backup
44
44
  # The name of the download strategy to adopt during backups
45
45
  # @return [String]
46
46
  attr_accessor :download_strategy
47
- # Should 'Seen' flags be cached before fetchiong emails and
47
+ # Should 'Seen' flags be cached before fetching emails and
48
48
  # rewritten to the server afterwards?
49
49
  #
50
50
  # Some IMAP providers, notably Apple Mail, set the '\Seen' flag
@@ -57,6 +57,12 @@ module Imap::Backup
57
57
  # mark messages as '\Seen' when accessed).
58
58
  # @return [Boolean]
59
59
  attr_reader :reset_seen_flags_after_fetch
60
+ # The status of the account - controls backup and migration behaviour
61
+ # "active" - the account is available for backup and migration,
62
+ # "archived" - the account is available for migration, but not backup,
63
+ # "offline" - the account is not available for backup or migration.
64
+ # @return [String] one of "active" (the default), "archived", or "offline"
65
+ attr_reader :status
60
66
 
61
67
  def initialize(options)
62
68
  check_options!(options)
@@ -72,6 +78,7 @@ module Imap::Backup
72
78
  @download_strategy = options[:download_strategy]
73
79
  @multi_fetch_size_orignal = options[:multi_fetch_size]
74
80
  @reset_seen_flags_after_fetch = options[:reset_seen_flags_after_fetch]
81
+ @status = options[:status] || DEFAULT_STATUS
75
82
  @client = nil
76
83
  @changes = {}
77
84
  @marked_for_deletion = false
@@ -122,7 +129,11 @@ module Imap::Backup
122
129
 
123
130
  # @return [Hash] all Account data for serialization
124
131
  def to_h
125
- h = {username: @username, password: @password}
132
+ h = {
133
+ username: @username,
134
+ password: @password,
135
+ status: status
136
+ }
126
137
  h[:local_path] = @local_path if @local_path
127
138
  h[:folders] = @folders if @folders
128
139
  h[:folder_blacklist] = true if @folder_blacklist
@@ -230,6 +241,44 @@ module Imap::Backup
230
241
  update(:reset_seen_flags_after_fetch, value)
231
242
  end
232
243
 
244
+ # Sets the status attribute and marks it as modified, storing the original value
245
+ #
246
+ # @param value [String] one of "active", "archived", or "offline"
247
+ # @raise [ArgumentError] if the value is not a valid status
248
+ # @return [void]
249
+ def status=(value)
250
+ if !VALID_STATUSES.include?(value)
251
+ raise ArgumentError, "status must be one of: #{VALID_STATUSES.join(', ')}"
252
+ end
253
+
254
+ update(:status, value)
255
+ end
256
+
257
+ # @return [Boolean] true if the account is active
258
+ def active?
259
+ @status == "active"
260
+ end
261
+
262
+ # @return [Boolean] true if the account is archived
263
+ def archived?
264
+ @status == "archived"
265
+ end
266
+
267
+ # @return [Boolean] true if the account is offline
268
+ def offline?
269
+ @status == "offline"
270
+ end
271
+
272
+ # @return [Boolean] true if the account is available for backup operations
273
+ def available_for_backup?
274
+ active?
275
+ end
276
+
277
+ # @return [Boolean] true if the account is available for migration operations
278
+ def available_for_migration?
279
+ active? || archived?
280
+ end
281
+
233
282
  private
234
283
 
235
284
  attr_reader :changes
@@ -237,9 +286,11 @@ module Imap::Backup
237
286
  REQUIRED_ATTRIBUTES = %i[password username].freeze
238
287
  OPTIONAL_ATTRIBUTES = %i[
239
288
  connection_options download_strategy folders folder_blacklist local_path mirror_mode
240
- multi_fetch_size reset_seen_flags_after_fetch server
289
+ multi_fetch_size reset_seen_flags_after_fetch server status
241
290
  ].freeze
242
291
  KNOWN_ATTRIBUTES = REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES
292
+ VALID_STATUSES = %w[active archived offline].freeze
293
+ DEFAULT_STATUS = "active".freeze
243
294
 
244
295
  def check_options!(options)
245
296
  missing_required = REQUIRED_ATTRIBUTES - options.keys
@@ -28,6 +28,8 @@ module Imap::Backup
28
28
  config = load_config(**options)
29
29
  exit_code = nil
30
30
  accounts = requested_accounts(config)
31
+ # Filter to only include accounts available for backup
32
+ accounts = accounts.select(&:available_for_backup?)
31
33
  if accounts.none?
32
34
  Logger.logger.warn "No matching accounts found to backup"
33
35
  return
@@ -1,3 +1,6 @@
1
+ require "erb"
2
+ require "json"
3
+ require "tempfile"
1
4
  require "thor"
2
5
 
3
6
  require "imap/backup/cli/options"
@@ -19,7 +22,7 @@ module Imap::Backup
19
22
  # @return [String] a description of the namespace configuration
20
23
  NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
21
24
  Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
22
- while others, while others concatenate the names of subfolders
25
+ while others concatenate the names of subfolders
23
26
  with a charater ("delimiter") other than "/".
24
27
 
25
28
  In these cases there are two choices.
@@ -60,9 +63,24 @@ module Imap::Backup
60
63
 
61
64
  # Loads the application configuration
62
65
  # @raise [ConfigurationNotFound] if the configuration file does not exist
66
+ # @raise [RuntimeError] if both config and erb_configuration are provided
67
+ # @raise [RuntimeError] if ERB template has syntax errors
68
+ # @raise [RuntimeError] if ERB template renders invalid JSON
63
69
  # @return [Configuration]
64
70
  def load_config(**options)
65
- path = options[:config]
71
+ config_path = options[:config]
72
+ erb_config_path = options[:erb_configuration]
73
+
74
+ # Check mutual exclusivity
75
+ if config_path && erb_config_path
76
+ raise "Cannot specify both --config and --erb-configuration options"
77
+ end
78
+
79
+ # Handle ERB configuration
80
+ return load_erb_config(erb_config_path, options) if erb_config_path
81
+
82
+ # Handle regular JSON configuration
83
+ path = config_path
66
84
  require_exists = options.key?(:require_exists) ? options[:require_exists] : true
67
85
  if require_exists
68
86
  exists = Configuration.exist?(path: path)
@@ -94,5 +112,54 @@ module Imap::Backup
94
112
  config.accounts
95
113
  end
96
114
  end
115
+
116
+ private
117
+
118
+ # Processes an ERB template and loads it as configuration
119
+ # @raise [ConfigurationNotFound] if the ERB file does not exist
120
+ # @raise [RuntimeError] if ERB processing fails
121
+ # @raise [RuntimeError] if rendered output is invalid JSON
122
+ # @return [Configuration]
123
+ def load_erb_config(erb_path, _options)
124
+ # Check if file exists
125
+ unless File.exist?(erb_path)
126
+ raise ConfigurationNotFound, "ERB configuration file '#{erb_path}' not found"
127
+ end
128
+
129
+ begin
130
+ # Read and process ERB template
131
+ erb_content = File.read(erb_path)
132
+ erb = ERB.new(erb_content)
133
+ rendered_json = erb.result
134
+ rescue SyntaxError => e
135
+ raise "ERB template has syntax error: #{e.message}"
136
+ rescue StandardError => e
137
+ raise "Error processing ERB template: #{e.message}"
138
+ end
139
+
140
+ # Validate rendered JSON
141
+ begin
142
+ JSON.parse(rendered_json)
143
+ rescue JSON::ParserError => e
144
+ raise "ERB template rendered invalid JSON: #{e.message}"
145
+ end
146
+
147
+ # Create temporary file with rendered JSON
148
+ temp_file = Tempfile.new(["config", ".json"])
149
+ begin
150
+ temp_file.write(rendered_json)
151
+ temp_file.flush
152
+ temp_file.close
153
+
154
+ # Load configuration from temporary file
155
+ config = Configuration.new(path: temp_file.path)
156
+ # Force loading of data before temp file is deleted
157
+ config.accounts
158
+ config
159
+ ensure
160
+ # Clean up temporary file
161
+ temp_file&.unlink
162
+ end
163
+ end
97
164
  end
98
165
  end
@@ -17,6 +17,7 @@ module Imap::Backup
17
17
 
18
18
  desc "accounts [OPTIONS]", "List locally backed-up accounts"
19
19
  config_option
20
+ erb_configuration_option
20
21
  format_option
21
22
  quiet_option
22
23
  verbose_option
@@ -44,6 +45,7 @@ module Imap::Backup
44
45
  )
45
46
  accounts_option
46
47
  config_option
48
+ erb_configuration_option
47
49
  format_option
48
50
  quiet_option
49
51
  verbose_option
@@ -56,6 +58,7 @@ module Imap::Backup
56
58
 
57
59
  desc "folders EMAIL [OPTIONS]", "List backed up folders"
58
60
  config_option
61
+ erb_configuration_option
59
62
  format_option
60
63
  quiet_option
61
64
  verbose_option
@@ -78,6 +81,7 @@ module Imap::Backup
78
81
 
79
82
  desc "list EMAIL FOLDER [OPTIONS]", "List emails in a folder"
80
83
  config_option
84
+ erb_configuration_option
81
85
  format_option
82
86
  quiet_option
83
87
  verbose_option
@@ -108,6 +112,7 @@ module Imap::Backup
108
112
  the UID.
109
113
  DESC
110
114
  config_option
115
+ erb_configuration_option
111
116
  format_option
112
117
  quiet_option
113
118
  verbose_option
@@ -31,6 +31,7 @@ module Imap::Backup
31
31
  )
32
32
  long_desc LONG_DESCRIPTION
33
33
  config_option
34
+ erb_configuration_option
34
35
  quiet_option
35
36
  verbose_option
36
37
  method_option(
@@ -38,6 +38,7 @@ module Imap::Backup
38
38
  )
39
39
  long_desc LONG_DESCRIPTION
40
40
  config_option
41
+ erb_configuration_option
41
42
  quiet_option
42
43
  verbose_option
43
44
  method_option(
@@ -25,6 +25,19 @@ module Imap::Backup
25
25
  desc: "supply the configuration file path (default: ~/.imap-backup/config.json)"
26
26
  }
27
27
  },
28
+ {
29
+ name: "erb_configuration",
30
+ parameters: {
31
+ type: :string,
32
+ desc:
33
+ "supply an ERB template file path for configuration. " \
34
+ "This is an alternative to the --config option. " \
35
+ "The file will be processed with ERB before being parsed as JSON. " \
36
+ "This allows use of environment variables and other dynamic content. " \
37
+ "Note that using this option is a potential security risk as it allows " \
38
+ "execution of arbitrary code."
39
+ }
40
+ },
28
41
  {
29
42
  name: "format",
30
43
  parameters: {
@@ -15,6 +15,7 @@ module Imap::Backup
15
15
 
16
16
  desc "folders EMAIL [OPTIONS]", "List account folders"
17
17
  config_option
18
+ erb_configuration_option
18
19
  format_option
19
20
  quiet_option
20
21
  verbose_option
@@ -36,6 +37,7 @@ module Imap::Backup
36
37
  Lists the IMAP capabilities supported by the IMAP server.
37
38
  DESC
38
39
  config_option
40
+ erb_configuration_option
39
41
  format_option
40
42
  quiet_option
41
43
  verbose_option
@@ -57,6 +59,7 @@ module Imap::Backup
57
59
  the `imap-backup migrate` and `imap-backup mirror` commands.
58
60
  DESC
59
61
  config_option
62
+ erb_configuration_option
60
63
  format_option
61
64
  quiet_option
62
65
  verbose_option
@@ -105,7 +105,7 @@ module Imap::Backup
105
105
  type: "string",
106
106
  desc: "the path of the directory where backups are to be saved. " \
107
107
  "If the directory does not exist, it will be created. " \
108
- "If not set, this is set to a diretory under the current path " \
108
+ "If not set, this is set to a directory under the current path " \
109
109
  "which is derived from the username, by replacing '@' with '_'." \
110
110
  "If this path parameter is not indicated, " \
111
111
  "the default is the current directory plus the email " \
@@ -88,6 +88,16 @@ module Imap::Backup
88
88
  raise "Account '#{destination_email}' does not exist" if !destination_account
89
89
 
90
90
  raise "Account '#{source_email}' does not exist" if !source_account
91
+
92
+ if !source_account.available_for_migration?
93
+ raise "Account '#{source_email}' is not available for migration " \
94
+ "(status: #{source_account.status})"
95
+ end
96
+
97
+ return if destination_account.available_for_migration?
98
+
99
+ raise "Account '#{destination_email}' is not available for migration " \
100
+ "(status: #{destination_account.status})"
91
101
  end
92
102
 
93
103
  def choose_prefixes_and_delimiters!
@@ -23,6 +23,7 @@ module Imap::Backup
23
23
  "Skip downloading emails up to today for all configured folders"
24
24
  )
25
25
  config_option
26
+ erb_configuration_option
26
27
  quiet_option
27
28
  verbose_option
28
29
  # Creates fake downloaded emails so that only the account's future emails
@@ -52,6 +53,7 @@ module Imap::Backup
52
53
  DOC
53
54
  )
54
55
  config_option
56
+ erb_configuration_option
55
57
  quiet_option
56
58
  verbose_option
57
59
  method_option(
@@ -64,6 +64,7 @@ module Imap::Backup
64
64
  DESC
65
65
  accounts_option
66
66
  config_option
67
+ erb_configuration_option
67
68
  quiet_option
68
69
  refresh_option
69
70
  verbose_option
@@ -101,6 +102,7 @@ module Imap::Backup
101
102
  #{Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
102
103
  DESC
103
104
  config_option
105
+ erb_configuration_option
104
106
  quiet_option
105
107
  verbose_option
106
108
  method_option(
@@ -153,6 +155,7 @@ module Imap::Backup
153
155
  DESC
154
156
  accounts_option
155
157
  config_option
158
+ erb_configuration_option
156
159
  quiet_option
157
160
  verbose_option
158
161
  method_option(
@@ -203,6 +206,7 @@ module Imap::Backup
203
206
  on the server).
204
207
  DESC
205
208
  config_option
209
+ erb_configuration_option
206
210
  format_option
207
211
  quiet_option
208
212
  verbose_option
@@ -30,7 +30,8 @@ module Imap::Backup
30
30
  path,
31
31
  folders,
32
32
  multi_fetch,
33
- reset_seen_flags_after_fetch
33
+ reset_seen_flags_after_fetch,
34
+ status_row
34
35
  ].compact
35
36
 
36
37
  menu.header = <<~HEADER.chomp
@@ -127,6 +128,12 @@ module Imap::Backup
127
128
  ["changes to unread flags will be reset during download"]
128
129
  end
129
130
 
131
+ def status_row
132
+ return nil if account.status == "active"
133
+
134
+ ["status", account.status]
135
+ end
136
+
130
137
  def format_rows(rows)
131
138
  largest_label, _value = rows.max_by do |(label, value)|
132
139
  if value
@@ -56,6 +56,7 @@ module Imap::Backup
56
56
  choose_folders menu
57
57
  modify_multi_fetch_size menu
58
58
  toggle_reset_seen_flags_after_fetch menu
59
+ rotate_status menu
59
60
  delete_account menu
60
61
  menu.choice("(q) return to main menu") { throw :done }
61
62
  menu.hidden("quit") { throw :done }
@@ -151,6 +152,19 @@ module Imap::Backup
151
152
  end
152
153
  end
153
154
 
155
+ def rotate_status(menu)
156
+ current_status = account.status
157
+ statuses = %w[active archived offline]
158
+ current_index = statuses.index(current_status) || 0
159
+ next_index = (current_index + 1) % statuses.length
160
+ next_status = statuses[next_index]
161
+
162
+ menu_item = "change status (currently: #{current_status} -> #{next_status})"
163
+ menu.choice(menu_item) do
164
+ account.status = next_status
165
+ end
166
+ end
167
+
154
168
  def test_connection(menu)
155
169
  menu.choice("test connection") do
156
170
  result = Setup::ConnectionTester.new(account).test
@@ -4,7 +4,7 @@ module Imap::Backup
4
4
  # @private
5
5
  MAJOR = 16
6
6
  # @private
7
- MINOR = 1
7
+ MINOR = 3
8
8
  # @private
9
9
  REVISION = 0
10
10
  # @private
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.1.0
4
+ version: 16.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-08-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: highline
@@ -248,7 +247,6 @@ licenses:
248
247
  - MIT
249
248
  metadata:
250
249
  rubygems_mfa_required: 'true'
251
- post_install_message:
252
250
  rdoc_options: []
253
251
  require_paths:
254
252
  - lib
@@ -263,8 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
263
261
  - !ruby/object:Gem::Version
264
262
  version: '0'
265
263
  requirements: []
266
- rubygems_version: 3.2.33
267
- signing_key:
264
+ rubygems_version: 3.6.7
268
265
  specification_version: 4
269
266
  summary: Backup GMail (or other IMAP) accounts to disk
270
267
  test_files: []