imap-backup 16.0.0 → 16.2.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 +4 -4
- data/lib/imap/backup/account/client_factory.rb +2 -26
- data/lib/imap/backup/account/folder.rb +3 -4
- data/lib/imap/backup/account.rb +55 -8
- data/lib/imap/backup/cli/backup.rb +2 -0
- data/lib/imap/backup/cli/transfer.rb +10 -0
- data/lib/imap/backup/client/default.rb +31 -12
- data/lib/imap/backup/downloader.rb +19 -10
- data/lib/imap/backup/email/provider/apple_mail.rb +5 -0
- data/lib/imap/backup/email/provider/base.rb +11 -0
- data/lib/imap/backup/email/provider/gmail.rb +5 -0
- data/lib/imap/backup/setup/account/header.rb +8 -1
- data/lib/imap/backup/setup/account.rb +14 -0
- data/lib/imap/backup/version.rb +1 -1
- metadata +2 -3
- data/lib/imap/backup/client/apple_mail.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 773e831319b208a930da663eae601d32236281104f4712960bfdb42dbc9d7794
|
4
|
+
data.tar.gz: 0da5b5f25c8e89022b387f08e5a5bee101284067fa6c5b9db7b35c95890e92da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccd49d19ec804c70c5aaaadcc40129ad8fcccf129c8de01c2c9e74cec0068bd2268fe6ea4e79f1d313539069e727891da9d0fc16e91a9e64c2301b00248204d8
|
7
|
+
data.tar.gz: 92553f44cf28d9c3a19b4cc3ff63fbd5a2dae59056663e9222ba0481ac6768d77f91c33e6d2aec652e451fbc1a5c9ae8f3933fcc2b9d601a5d3ce04865720af3
|
@@ -1,9 +1,7 @@
|
|
1
1
|
require "socket"
|
2
2
|
|
3
|
-
require "imap/backup/client/apple_mail"
|
4
3
|
require "imap/backup/client/automatic_login_wrapper"
|
5
4
|
require "imap/backup/client/default"
|
6
|
-
require "imap/backup/email/provider"
|
7
5
|
|
8
6
|
module Imap; end
|
9
7
|
|
@@ -14,39 +12,17 @@ module Imap::Backup
|
|
14
12
|
class Account::ClientFactory
|
15
13
|
def initialize(account:)
|
16
14
|
@account = account
|
17
|
-
@provider = nil
|
18
|
-
@server = nil
|
19
15
|
end
|
20
16
|
|
21
17
|
# @return [Client::AutomaticLoginWrapper] a client for the account
|
22
18
|
def run
|
23
|
-
|
24
|
-
|
25
|
-
"Creating IMAP instance: #{server}, options: #{options.inspect}"
|
26
|
-
)
|
27
|
-
client =
|
28
|
-
if provider.is_a?(Email::Provider::AppleMail)
|
29
|
-
Client::AppleMail.new(server, account, options)
|
30
|
-
else
|
31
|
-
Client::Default.new(server, account, options)
|
32
|
-
end
|
19
|
+
Logger.logger.debug("Creating IMAP instance")
|
20
|
+
client = Client::Default.new(account)
|
33
21
|
Client::AutomaticLoginWrapper.new(client: client)
|
34
22
|
end
|
35
23
|
|
36
24
|
private
|
37
25
|
|
38
26
|
attr_reader :account
|
39
|
-
|
40
|
-
def provider
|
41
|
-
@provider ||= Email::Provider.for_address(account.username)
|
42
|
-
end
|
43
|
-
|
44
|
-
def provider_options
|
45
|
-
provider.options.merge(account.connection_options || {})
|
46
|
-
end
|
47
|
-
|
48
|
-
def server
|
49
|
-
@server ||= account.server || provider.host
|
50
|
-
end
|
51
27
|
end
|
52
28
|
end
|
@@ -38,7 +38,6 @@ module Imap::Backup
|
|
38
38
|
Logger.logger.debug "Folder '#{name}' exists"
|
39
39
|
true
|
40
40
|
rescue FolderNotFound
|
41
|
-
Logger.logger.debug "Folder '#{name}' does not exist"
|
42
41
|
false
|
43
42
|
end
|
44
43
|
|
@@ -189,9 +188,9 @@ module Imap::Backup
|
|
189
188
|
def examine
|
190
189
|
client.examine(utf7_encoded_name)
|
191
190
|
rescue Net::IMAP::NoResponseError
|
192
|
-
|
193
|
-
Imap::Backup::Logger.logger.warn
|
194
|
-
raise FolderNotFound,
|
191
|
+
message = "Folder '#{name}' does not exist on server"
|
192
|
+
Imap::Backup::Logger.logger.warn message
|
193
|
+
raise FolderNotFound, message
|
195
194
|
end
|
196
195
|
|
197
196
|
def extract_uid(response)
|
data/lib/imap/backup/account.rb
CHANGED
@@ -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
|
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 behavior
|
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 = {
|
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
|
@@ -211,11 +222,7 @@ module Imap::Backup
|
|
211
222
|
def multi_fetch_size
|
212
223
|
@multi_fetch_size ||= begin
|
213
224
|
int = @multi_fetch_size_orignal.to_i
|
214
|
-
|
215
|
-
int
|
216
|
-
else
|
217
|
-
DEFAULT_MULTI_FETCH_SIZE
|
218
|
-
end
|
225
|
+
int.positive? ? int : DEFAULT_MULTI_FETCH_SIZE
|
219
226
|
end
|
220
227
|
end
|
221
228
|
|
@@ -234,6 +241,44 @@ module Imap::Backup
|
|
234
241
|
update(:reset_seen_flags_after_fetch, value)
|
235
242
|
end
|
236
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
|
+
|
237
282
|
private
|
238
283
|
|
239
284
|
attr_reader :changes
|
@@ -241,9 +286,11 @@ module Imap::Backup
|
|
241
286
|
REQUIRED_ATTRIBUTES = %i[password username].freeze
|
242
287
|
OPTIONAL_ATTRIBUTES = %i[
|
243
288
|
connection_options download_strategy folders folder_blacklist local_path mirror_mode
|
244
|
-
multi_fetch_size reset_seen_flags_after_fetch server
|
289
|
+
multi_fetch_size reset_seen_flags_after_fetch server status
|
245
290
|
].freeze
|
246
291
|
KNOWN_ATTRIBUTES = REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES
|
292
|
+
VALID_STATUSES = %w[active archived offline].freeze
|
293
|
+
DEFAULT_STATUS = "active".freeze
|
247
294
|
|
248
295
|
def check_options!(options)
|
249
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
|
@@ -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!
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "forwardable"
|
2
2
|
require "net/imap"
|
3
3
|
|
4
|
+
require "imap/backup/email/provider"
|
4
5
|
require "imap/backup/logger"
|
5
6
|
|
6
7
|
module Imap; end
|
@@ -17,10 +18,8 @@ module Imap::Backup
|
|
17
18
|
responses uid_fetch uid_search uid_store
|
18
19
|
)
|
19
20
|
|
20
|
-
def initialize(
|
21
|
+
def initialize(account)
|
21
22
|
@account = account
|
22
|
-
@options = options
|
23
|
-
@server = server
|
24
23
|
@state = nil
|
25
24
|
end
|
26
25
|
|
@@ -32,7 +31,10 @@ module Imap::Backup
|
|
32
31
|
|
33
32
|
return [] if mailbox_lists.nil?
|
34
33
|
|
35
|
-
|
34
|
+
ignored_tags = provider.folder_ignore_tags
|
35
|
+
mailbox_lists.
|
36
|
+
select { |ml| ml.attr & ignored_tags == [] }.
|
37
|
+
map { |ml| extract_name(ml) }
|
36
38
|
end
|
37
39
|
|
38
40
|
# Logs in to the account on the IMAP server
|
@@ -83,8 +85,6 @@ module Imap::Backup
|
|
83
85
|
private
|
84
86
|
|
85
87
|
attr_reader :account
|
86
|
-
attr_reader :options
|
87
|
-
attr_reader :server
|
88
88
|
attr_accessor :state
|
89
89
|
|
90
90
|
def imap
|
@@ -100,17 +100,36 @@ module Imap::Backup
|
|
100
100
|
account.password.gsub(/./, "x")
|
101
101
|
end
|
102
102
|
|
103
|
+
def provider
|
104
|
+
@provider ||= Email::Provider.for_address(account.username)
|
105
|
+
end
|
106
|
+
|
107
|
+
def options
|
108
|
+
@options ||= provider.options.merge(account.connection_options || {})
|
109
|
+
end
|
110
|
+
|
111
|
+
def server
|
112
|
+
@server ||= account.server || provider.host
|
113
|
+
end
|
114
|
+
|
103
115
|
# 6.3.8. LIST Command
|
104
116
|
# An empty ("" string) mailbox name argument is a special request to
|
105
117
|
# return the hierarchy delimiter and the root name of the name given
|
106
118
|
# in the reference.
|
107
119
|
def provider_root
|
108
|
-
@provider_root ||=
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
120
|
+
@provider_root ||=
|
121
|
+
if provider.root
|
122
|
+
Logger.logger.debug "Using fixed provider root '#{provider.root}'"
|
123
|
+
provider.root
|
124
|
+
else
|
125
|
+
Logger.logger.debug "Fetching provider root"
|
126
|
+
result = imap.list("", "")
|
127
|
+
raise "IMAP server did not return root folder for #{account.username}" if result.empty?
|
128
|
+
|
129
|
+
root_info = result[0]
|
130
|
+
Logger.logger.debug "Provider root is '#{root_info.name}'"
|
131
|
+
root_info.name
|
132
|
+
end
|
114
133
|
end
|
115
134
|
end
|
116
135
|
end
|
@@ -13,13 +13,15 @@ module Imap::Backup
|
|
13
13
|
@serializer = serializer
|
14
14
|
@multi_fetch_size = multi_fetch_size
|
15
15
|
@reset_seen_flags_after_fetch = reset_seen_flags_after_fetch
|
16
|
+
@folder_uids = nil
|
17
|
+
@serializer_uids = nil
|
16
18
|
@uids = nil
|
17
19
|
end
|
18
20
|
|
19
21
|
# Runs the downloader
|
20
22
|
# @return [void]
|
21
23
|
def run
|
22
|
-
debug("#{serializer_uids.count}
|
24
|
+
debug("#{serializer_uids.count} messages already downloaded")
|
23
25
|
debug("#{folder_uids.count} messages on server")
|
24
26
|
local_only_count = (serializer_uids - folder_uids).count
|
25
27
|
if local_only_count.positive?
|
@@ -33,15 +35,7 @@ module Imap::Backup
|
|
33
35
|
|
34
36
|
info("#{uids.count} new messages")
|
35
37
|
|
36
|
-
|
37
|
-
multifetch_failed = download_block(block, i)
|
38
|
-
raise MultiFetchFailedError if multifetch_failed
|
39
|
-
end
|
40
|
-
rescue MultiFetchFailedError
|
41
|
-
@count = nil
|
42
|
-
@multi_fetch_size = 1
|
43
|
-
@uids = nil
|
44
|
-
retry
|
38
|
+
download
|
45
39
|
rescue Net::IMAP::ByeResponseError
|
46
40
|
folder.client.reconnect
|
47
41
|
retry
|
@@ -54,6 +48,21 @@ module Imap::Backup
|
|
54
48
|
attr_reader :multi_fetch_size
|
55
49
|
attr_reader :reset_seen_flags_after_fetch
|
56
50
|
|
51
|
+
def download
|
52
|
+
block_count = (uids.count / multi_fetch_size.to_f).ceil
|
53
|
+
uids.each_slice(multi_fetch_size).with_index do |block, i|
|
54
|
+
debug("Downloading #{block.count} messages (block #{i + 1}/#{block_count})")
|
55
|
+
multifetch_failed = download_block(block, i)
|
56
|
+
raise MultiFetchFailedError if multifetch_failed
|
57
|
+
end
|
58
|
+
rescue MultiFetchFailedError
|
59
|
+
@multi_fetch_size = 1
|
60
|
+
@uids = nil
|
61
|
+
@folder_uids = nil
|
62
|
+
@serializer_uids = nil
|
63
|
+
retry
|
64
|
+
end
|
65
|
+
|
57
66
|
def download_block(block, index)
|
58
67
|
uids_and_bodies =
|
59
68
|
if reset_seen_flags_after_fetch
|
@@ -6,11 +6,22 @@ module Imap::Backup
|
|
6
6
|
|
7
7
|
# Supplies defaults for email provider behaviour
|
8
8
|
class Email::Provider::Base
|
9
|
+
# @return [Array<Symbol>] tags to ignore when listing folders
|
10
|
+
def folder_ignore_tags
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
|
9
14
|
# @return [Hash] defaults for the Net::IMAP connection
|
10
15
|
def options
|
11
16
|
{port: 993, ssl: {min_version: OpenSSL::SSL::TLS1_2_VERSION}}
|
12
17
|
end
|
13
18
|
|
19
|
+
# By default, we query the server for this value.
|
20
|
+
# It is only fixed for Apple Mail accounts.
|
21
|
+
# @return [String, nil] any fixed value to use when requesting the list of account folders
|
22
|
+
def root
|
23
|
+
end
|
24
|
+
|
14
25
|
def sets_seen_flags_on_fetch?
|
15
26
|
false
|
16
27
|
end
|
@@ -5,6 +5,11 @@ module Imap; end
|
|
5
5
|
module Imap::Backup
|
6
6
|
# Provides overrides for GMail accounts
|
7
7
|
class Email::Provider::GMail < Email::Provider::Base
|
8
|
+
# https://imap-use.u.washington.narkive.com/RYMsOHTN/imap-protocol-status-on-a-noselect-mailbox
|
9
|
+
def folder_ignore_tags
|
10
|
+
[:Noselect]
|
11
|
+
end
|
12
|
+
|
8
13
|
# @return [String] the GMail IMAP server host name
|
9
14
|
def host
|
10
15
|
"imap.gmail.com"
|
@@ -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
|
data/lib/imap/backup/version.rb
CHANGED
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: 16.
|
4
|
+
version: 16.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Yates
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -192,7 +192,6 @@ files:
|
|
192
192
|
- lib/imap/backup/cli/stats.rb
|
193
193
|
- lib/imap/backup/cli/transfer.rb
|
194
194
|
- lib/imap/backup/cli/utils.rb
|
195
|
-
- lib/imap/backup/client/apple_mail.rb
|
196
195
|
- lib/imap/backup/client/automatic_login_wrapper.rb
|
197
196
|
- lib/imap/backup/client/default.rb
|
198
197
|
- lib/imap/backup/configuration.rb
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require "imap/backup/client/default"
|
2
|
-
|
3
|
-
module Imap; end
|
4
|
-
|
5
|
-
module Imap::Backup
|
6
|
-
# Overrides default IMAP client behaviour for Apple Mail accounts
|
7
|
-
class Client::AppleMail < Client::Default
|
8
|
-
# With Apple Mails's IMAP, passing "/" to list
|
9
|
-
# results in an empty list
|
10
|
-
# @return [String] the value to use when requesting the list of account folders
|
11
|
-
def provider_root
|
12
|
-
""
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|