imap-backup 15.0.1 → 15.0.3.rc1
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/backup.rb +11 -5
- data/lib/imap/backup/account/folder.rb +7 -8
- data/lib/imap/backup/account/folder_backup.rb +8 -4
- data/lib/imap/backup/account/folder_mapper.rb +2 -2
- data/lib/imap/backup/cli/backup.rb +3 -0
- data/lib/imap/backup/cli/helpers.rb +4 -62
- data/lib/imap/backup/cli/options.rb +74 -0
- data/lib/imap/backup/client/default.rb +3 -0
- data/lib/imap/backup/downloader.rb +24 -4
- data/lib/imap/backup/flag_refresher.rb +7 -0
- data/lib/imap/backup/logger.rb +2 -2
- data/lib/imap/backup/retry_on_error.rb +2 -2
- data/lib/imap/backup/serializer/message_enumerator.rb +2 -2
- data/lib/imap/backup/version.rb +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef0c787a6f2a954caddfc9a5c329333781ad650bf1782feb28cc69e74aeba077
|
4
|
+
data.tar.gz: 23387da9df55557ab56dd46bdeef6608f7af4e80434a02c21767d080598583cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7432a214ec8dd20758786550785be876f208f1bc9b76f7f60f75b30abf800b2bc4c9fca655370576fff0dea4675d1a298c4c3e8743449774ae90d914aa635805
|
7
|
+
data.tar.gz: 461488fe00ea29917415845c2fd7a231ffc6f3646638c1ad64c23f74f96acc5c2581f7fb3adc04a3410d5dbec67eb36a417d4b02b31e827ab95870172ab52922
|
@@ -18,27 +18,33 @@ module Imap::Backup
|
|
18
18
|
# Runs the backup
|
19
19
|
# @return [void]
|
20
20
|
def run
|
21
|
-
Logger.logger.info "Running backup of account
|
21
|
+
Logger.logger.info "Running backup of account '#{account.username}'"
|
22
22
|
# start the connection so we get logging messages in the right order
|
23
23
|
account.client.login
|
24
24
|
|
25
|
-
|
26
|
-
Account::LocalOnlyFolderDeleter.new(account: account).run if account.mirror_mode
|
25
|
+
run_pre_backup_tasks
|
27
26
|
backup_folders = Account::BackupFolders.new(
|
28
27
|
client: account.client, account: account
|
29
|
-
)
|
28
|
+
).to_a
|
30
29
|
if backup_folders.none?
|
31
|
-
Logger.logger.warn "
|
30
|
+
Logger.logger.warn "No folders found to backup for account '#{account.username}'"
|
32
31
|
return
|
33
32
|
end
|
33
|
+
Logger.logger.debug "Starting backup of #{backup_folders.count} folders"
|
34
34
|
backup_folders.each do |folder|
|
35
35
|
Account::FolderBackup.new(account: account, folder: folder, refresh: refresh).run
|
36
36
|
end
|
37
|
+
Logger.logger.debug "Backup of account '#{account.username}' complete"
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
40
41
|
|
41
42
|
attr_reader :account
|
42
43
|
attr_reader :refresh
|
44
|
+
|
45
|
+
def run_pre_backup_tasks
|
46
|
+
Account::FolderEnsurer.new(account: account).run
|
47
|
+
Account::LocalOnlyFolderDeleter.new(account: account).run if account.mirror_mode
|
48
|
+
end
|
43
49
|
end
|
44
50
|
end
|
@@ -31,19 +31,15 @@ module Imap::Backup
|
|
31
31
|
|
32
32
|
# @raise any error that occurs more than 10 times
|
33
33
|
def exist?
|
34
|
-
|
35
|
-
previous_debug = Net::IMAP.debug
|
36
|
-
Imap::Backup::Logger.logger.level = ::Logger::Severity::UNKNOWN
|
37
|
-
Net::IMAP.debug = false
|
34
|
+
Logger.logger.debug "Checking whether folder '#{name}' exists"
|
38
35
|
retry_on_error(errors: EXAMINE_RETRY_CLASSES) do
|
39
36
|
examine
|
40
37
|
end
|
38
|
+
Logger.logger.debug "Folder '#{name}' exists"
|
41
39
|
true
|
42
40
|
rescue FolderNotFound
|
41
|
+
Logger.logger.debug "Folder '#{name}' does not exist"
|
43
42
|
false
|
44
|
-
ensure
|
45
|
-
Imap::Backup::Logger.logger.level = previous_level
|
46
|
-
Net::IMAP.debug = previous_debug
|
47
43
|
end
|
48
44
|
|
49
45
|
# Creates the folder on the server
|
@@ -69,8 +65,11 @@ module Imap::Backup
|
|
69
65
|
# @raise any error that occurs more than 10 times
|
70
66
|
# @return [Array<Integer>] the folders message UIDs
|
71
67
|
def uids
|
68
|
+
Logger.logger.debug "Fetching UIDs for folder '#{name}'"
|
72
69
|
examine
|
73
|
-
client.uid_search(["ALL"]).sort
|
70
|
+
result = client.uid_search(["ALL"]).sort
|
71
|
+
Logger.logger.debug "#{result.count} UIDs found for folder '#{name}'"
|
72
|
+
result
|
74
73
|
rescue FolderNotFound
|
75
74
|
[]
|
76
75
|
rescue NoMethodError
|
@@ -22,11 +22,11 @@ module Imap::Backup
|
|
22
22
|
# @raise [RuntimeError] if the configured download strategy is incorrect
|
23
23
|
# @return [void]
|
24
24
|
def run
|
25
|
+
Logger.logger.debug "Running backup for folder '#{folder.name}'"
|
26
|
+
|
25
27
|
folder_ok = folder_ok?
|
26
28
|
return if !folder_ok
|
27
29
|
|
28
|
-
Logger.logger.debug "[#{folder.name}] running backup"
|
29
|
-
|
30
30
|
serializer.apply_uid_validity(folder.uid_validity)
|
31
31
|
|
32
32
|
serializer.transaction do
|
@@ -36,6 +36,7 @@ module Imap::Backup
|
|
36
36
|
# After the transaction the serializer will have any appended messages
|
37
37
|
# so we can check differences between the server and the local backup
|
38
38
|
LocalOnlyMessageDeleter.new(folder, raw_serializer).run if account.mirror_mode
|
39
|
+
Logger.logger.debug "Backup for folder '#{folder.name}' complete"
|
39
40
|
end
|
40
41
|
|
41
42
|
private
|
@@ -46,10 +47,13 @@ module Imap::Backup
|
|
46
47
|
|
47
48
|
def folder_ok?
|
48
49
|
begin
|
49
|
-
|
50
|
+
if !folder.exist?
|
51
|
+
Logger.logger.info "Skipping backup for folder '#{folder.name}' as it does not exist"
|
52
|
+
return false
|
53
|
+
end
|
50
54
|
rescue Encoding::UndefinedConversionError
|
51
55
|
message = "Skipping backup for '#{folder.name}' " \
|
52
|
-
"as it is not UTF-7 encoded correctly"
|
56
|
+
"as it's name is not UTF-7 encoded correctly"
|
53
57
|
Logger.logger.info message
|
54
58
|
return false
|
55
59
|
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
|
-
|
43
|
+
block.call(serializer, folder)
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -24,6 +24,7 @@ module Imap::Backup
|
|
24
24
|
# @return [void]
|
25
25
|
no_commands do
|
26
26
|
def run
|
27
|
+
Logger.logger.debug "Loading configuration"
|
27
28
|
config = load_config(**options)
|
28
29
|
exit_code = nil
|
29
30
|
accounts = requested_accounts(config)
|
@@ -31,6 +32,7 @@ module Imap::Backup
|
|
31
32
|
Logger.logger.warn "No matching accounts found to backup"
|
32
33
|
return
|
33
34
|
end
|
35
|
+
Logger.logger.debug "Starting backup of #{accounts.count} accounts"
|
34
36
|
accounts.each do |account|
|
35
37
|
backup = Account::Backup.new(account: account, refresh: refresh)
|
36
38
|
backup.run
|
@@ -43,6 +45,7 @@ module Imap::Backup
|
|
43
45
|
Logger.logger.error message
|
44
46
|
next
|
45
47
|
end
|
48
|
+
Logger.logger.debug "Backup complete"
|
46
49
|
exit(exit_code) if exit_code
|
47
50
|
end
|
48
51
|
end
|
@@ -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
|
15
|
-
|
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
|
@@ -27,6 +27,7 @@ module Imap::Backup
|
|
27
27
|
# @return [Array<String>] the account folders
|
28
28
|
def list
|
29
29
|
root = provider_root
|
30
|
+
Logger.logger.debug "Listing all account folders"
|
30
31
|
mailbox_lists = imap.list(root, "*")
|
31
32
|
|
32
33
|
return [] if mailbox_lists.nil?
|
@@ -105,7 +106,9 @@ module Imap::Backup
|
|
105
106
|
# in the reference.
|
106
107
|
def provider_root
|
107
108
|
@provider_root ||= begin
|
109
|
+
Logger.logger.debug "Fetching provider root"
|
108
110
|
root_info = imap.list("", "")[0]
|
111
|
+
Logger.logger.debug "Provider root is '#{root_info.name}'"
|
109
112
|
root_info.name
|
110
113
|
end
|
111
114
|
end
|
@@ -19,7 +19,19 @@ module Imap::Backup
|
|
19
19
|
# Runs the downloader
|
20
20
|
# @return [void]
|
21
21
|
def run
|
22
|
-
|
22
|
+
debug("#{serializer_uids.count} already messages already downloaded")
|
23
|
+
debug("#{folder_uids.count} messages on server")
|
24
|
+
local_only_count = (serializer_uids - folder_uids).count
|
25
|
+
if local_only_count.positive?
|
26
|
+
debug("#{local_only_count} downloaded messages no longer on server")
|
27
|
+
end
|
28
|
+
|
29
|
+
if uids.none?
|
30
|
+
debug("no new messages on server — skipping")
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
info("#{uids.count} new messages")
|
23
35
|
|
24
36
|
uids.each_slice(multi_fetch_size).with_index do |block, i|
|
25
37
|
multifetch_failed = download_block(block, i)
|
@@ -62,8 +74,8 @@ module Imap::Backup
|
|
62
74
|
end
|
63
75
|
if uids_and_bodies.nil?
|
64
76
|
if multi_fetch_size > 1
|
65
|
-
|
66
|
-
debug("Multi fetch failed for UIDs #{
|
77
|
+
uid_list = block.join(", ")
|
78
|
+
debug("Multi fetch failed for UIDs #{uid_list}, switching to single fetches")
|
67
79
|
return true
|
68
80
|
else
|
69
81
|
debug("Fetch failed for UID #{block[0]} - skipping")
|
@@ -96,8 +108,16 @@ module Imap::Backup
|
|
96
108
|
error(e)
|
97
109
|
end
|
98
110
|
|
111
|
+
def folder_uids
|
112
|
+
@folder_uids ||= folder.uids
|
113
|
+
end
|
114
|
+
|
115
|
+
def serializer_uids
|
116
|
+
@serializer_uids ||= serializer.uids
|
117
|
+
end
|
118
|
+
|
99
119
|
def uids
|
100
|
-
@uids ||=
|
120
|
+
@uids ||= folder_uids - serializer_uids
|
101
121
|
end
|
102
122
|
|
103
123
|
def debug(message)
|
@@ -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]
|
data/lib/imap/backup/logger.rb
CHANGED
@@ -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
|
-
|
58
|
+
block.call
|
59
59
|
ensure
|
60
60
|
sanitizer.flush
|
61
61
|
$stderr = previous_stderr
|
@@ -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
|
-
|
18
|
+
block.call
|
19
19
|
rescue *errors => e
|
20
20
|
if tries < limit
|
21
21
|
message = "#{e}, attempt #{tries} of #{limit}"
|
@@ -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
|
-
|
24
|
+
block.call(message)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
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: 15.0.
|
4
|
+
version: 15.0.3.rc1
|
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-
|
11
|
+
date: 2024-11-15 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
|