imap-backup 15.1.3 → 16.1.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/imap-backup.gemspec +2 -0
- data/lib/imap/backup/account/client_factory.rb +2 -26
- data/lib/imap/backup/account/folder.rb +8 -5
- data/lib/imap/backup/account.rb +1 -5
- data/lib/imap/backup/cli/helpers.rb +22 -0
- data/lib/imap/backup/cli/migrate.rb +79 -0
- data/lib/imap/backup/cli/mirror.rb +79 -0
- data/lib/imap/backup/cli/transfer.rb +7 -6
- data/lib/imap/backup/cli.rb +20 -94
- 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/mirror.rb +5 -3
- data/lib/imap/backup/version.rb +2 -2
- metadata +36 -4
- 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: bf6997d289ca5babbecd35e683ede921273300779f3f7dac95e247c0478f1814
|
4
|
+
data.tar.gz: '009892c47699a0cc94ff28c2d3ea7346915732b7dc86c7231b7e606302219888'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46bdbccf9be3efade25e500f6c67f37de4fc0a0aa8514b55f8eabf7e1171042b5be87d029f37d9cb449c23d1fcf0d5fd2a20c2e8595c6149eff0c20f89e6b3d3
|
7
|
+
data.tar.gz: d507a668897c325e2cc771d69b083f9929247b839b55d1fe86bfaeac3ee8be40df43b9f634d4f97c0188fe3bcbfba666456a9131236103455020fb45ecec9d61
|
data/imap-backup.gemspec
CHANGED
@@ -21,10 +21,12 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.required_ruby_version = ">= 3.0"
|
22
22
|
|
23
23
|
gem.add_runtime_dependency "highline"
|
24
|
+
gem.add_runtime_dependency "logger"
|
24
25
|
gem.add_runtime_dependency "mail", "2.7.1"
|
25
26
|
gem.add_runtime_dependency "net-imap", ">= 0.3.2"
|
26
27
|
gem.add_runtime_dependency "net-smtp"
|
27
28
|
gem.add_runtime_dependency "os"
|
29
|
+
gem.add_runtime_dependency "ostruct"
|
28
30
|
gem.add_runtime_dependency "rake"
|
29
31
|
gem.add_runtime_dependency "thor", "~> 1.1"
|
30
32
|
gem.add_runtime_dependency "thunderbird", "0.3.0"
|
@@ -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
|
|
@@ -67,7 +66,10 @@ module Imap::Backup
|
|
67
66
|
def uids
|
68
67
|
Logger.logger.debug "Fetching UIDs for folder '#{name}'"
|
69
68
|
examine
|
70
|
-
result =
|
69
|
+
result =
|
70
|
+
retry_on_error(errors: UID_SEARCH_RETRY_CLASSES) do
|
71
|
+
client.uid_search(["ALL"]).sort
|
72
|
+
end
|
71
73
|
Logger.logger.debug "#{result.count} UIDs found for folder '#{name}'"
|
72
74
|
result
|
73
75
|
rescue FolderNotFound
|
@@ -177,6 +179,7 @@ module Imap::Backup
|
|
177
179
|
|
178
180
|
BODY_ATTRIBUTE = "BODY[]".freeze
|
179
181
|
UID_FETCH_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::IOError].freeze
|
182
|
+
UID_SEARCH_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::IOError].freeze
|
180
183
|
APPEND_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
|
181
184
|
CREATE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
|
182
185
|
EXAMINE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
|
@@ -185,9 +188,9 @@ module Imap::Backup
|
|
185
188
|
def examine
|
186
189
|
client.examine(utf7_encoded_name)
|
187
190
|
rescue Net::IMAP::NoResponseError
|
188
|
-
|
189
|
-
Imap::Backup::Logger.logger.warn
|
190
|
-
raise FolderNotFound,
|
191
|
+
message = "Folder '#{name}' does not exist on server"
|
192
|
+
Imap::Backup::Logger.logger.warn message
|
193
|
+
raise FolderNotFound, message
|
191
194
|
end
|
192
195
|
|
193
196
|
def extract_uid(response)
|
data/lib/imap/backup/account.rb
CHANGED
@@ -211,11 +211,7 @@ module Imap::Backup
|
|
211
211
|
def multi_fetch_size
|
212
212
|
@multi_fetch_size ||= begin
|
213
213
|
int = @multi_fetch_size_orignal.to_i
|
214
|
-
|
215
|
-
int
|
216
|
-
else
|
217
|
-
DEFAULT_MULTI_FETCH_SIZE
|
218
|
-
end
|
214
|
+
int.positive? ? int : DEFAULT_MULTI_FETCH_SIZE
|
219
215
|
end
|
220
216
|
end
|
221
217
|
|
@@ -16,6 +16,28 @@ module Imap::Backup
|
|
16
16
|
options.define_options
|
17
17
|
end
|
18
18
|
|
19
|
+
# @return [String] a description of the namespace configuration
|
20
|
+
NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
|
21
|
+
Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
|
22
|
+
while others, while others concatenate the names of subfolders
|
23
|
+
with a charater ("delimiter") other than "/".
|
24
|
+
|
25
|
+
In these cases there are two choices.
|
26
|
+
|
27
|
+
You can use the `--automatic-namespaces` option.
|
28
|
+
This will query the source and detination servers for their
|
29
|
+
namespace configuration and will adapt paths accordingly.
|
30
|
+
This option requires that both the source and destination
|
31
|
+
servers are available and work with the provided parameters
|
32
|
+
and authentication.
|
33
|
+
|
34
|
+
If automatic configuration does not work as desired, there are the
|
35
|
+
`--source-prefix=`, `--source-delimiter=`,
|
36
|
+
`--destination-prefix=` and `--destination-delimiter=` parameters.
|
37
|
+
To check what values you should use, check the output of the
|
38
|
+
`imap-backup remote namespaces EMAIL` command.
|
39
|
+
DESC
|
40
|
+
|
19
41
|
# Processes command-line parameters
|
20
42
|
# @return [Hash] the supplied command-line parameters with
|
21
43
|
# with hyphens in keys replaced by underscores
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Imap; end
|
2
|
+
|
3
|
+
module Imap::Backup
|
4
|
+
# Processes parameters to run the `migrate` command via command-line parameters
|
5
|
+
module CLI::Migrate
|
6
|
+
include Thor::Actions
|
7
|
+
include CLI::Helpers
|
8
|
+
|
9
|
+
LONG_DESCRIPTION = <<~DESC.freeze
|
10
|
+
This command is deprecated and will be removed in a future version.
|
11
|
+
Use 'copy' instead.
|
12
|
+
|
13
|
+
All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
|
14
|
+
uploaded to the "destination account" (DESTINATION_EMAIL).
|
15
|
+
|
16
|
+
Some configuration may be necessary, as follows:
|
17
|
+
|
18
|
+
#{CLI::Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
|
19
|
+
|
20
|
+
Finally, if you want to delete existing emails in destination folders,
|
21
|
+
use the `--reset` option. In this case, all existing emails are
|
22
|
+
deleted before uploading the migrated emails.
|
23
|
+
DESC
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
base.class_eval do
|
27
|
+
desc(
|
28
|
+
"migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
|
29
|
+
"(Deprecated) Uploads backed-up emails from account SOURCE_EMAIL " \
|
30
|
+
"to account DESTINATION_EMAIL"
|
31
|
+
)
|
32
|
+
long_desc LONG_DESCRIPTION
|
33
|
+
config_option
|
34
|
+
quiet_option
|
35
|
+
verbose_option
|
36
|
+
method_option(
|
37
|
+
"automatic-namespaces",
|
38
|
+
type: :boolean,
|
39
|
+
desc: "automatically choose delimiters and prefixes"
|
40
|
+
)
|
41
|
+
method_option(
|
42
|
+
"destination-delimiter",
|
43
|
+
type: :string,
|
44
|
+
desc: "the delimiter for destination folder names"
|
45
|
+
)
|
46
|
+
method_option(
|
47
|
+
"destination-prefix",
|
48
|
+
type: :string,
|
49
|
+
desc: "the prefix (namespace) to add to destination folder names",
|
50
|
+
aliases: ["-d"]
|
51
|
+
)
|
52
|
+
method_option(
|
53
|
+
"reset",
|
54
|
+
type: :boolean,
|
55
|
+
desc: "DANGER! This option deletes all messages from destination " \
|
56
|
+
"folders before uploading",
|
57
|
+
aliases: ["-r"]
|
58
|
+
)
|
59
|
+
method_option(
|
60
|
+
"source-delimiter",
|
61
|
+
type: :string,
|
62
|
+
desc: "the delimiter for source folder names"
|
63
|
+
)
|
64
|
+
method_option(
|
65
|
+
"source-prefix",
|
66
|
+
type: :string,
|
67
|
+
desc: "the prefix (namespace) to strip from source folder names",
|
68
|
+
aliases: ["-s"]
|
69
|
+
)
|
70
|
+
# Migrates emails from one account to another
|
71
|
+
# @return [void]
|
72
|
+
def migrate(source_email, destination_email)
|
73
|
+
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
74
|
+
CLI::Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Imap; end
|
2
|
+
|
3
|
+
module Imap::Backup
|
4
|
+
# Processes parameters to run the `mirror` command via command-line parameters
|
5
|
+
module CLI::Mirror
|
6
|
+
include Thor::Actions
|
7
|
+
include CLI::Helpers
|
8
|
+
|
9
|
+
LONG_DESCRIPTION = <<~DESC.freeze
|
10
|
+
This command is deprecated and will be removed in a future version.
|
11
|
+
Use 'copy' instead.
|
12
|
+
|
13
|
+
This command updates the DESTINATION_EMAIL account's folders to have the same contents
|
14
|
+
as those on the SOURCE_EMAIL account.
|
15
|
+
|
16
|
+
If a folder list is configured for the SOURCE_EMAIL account,
|
17
|
+
only the folders indicated by the setting are copied.
|
18
|
+
|
19
|
+
First, it runs the download of the SOURCE_EMAIL account.
|
20
|
+
If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
|
21
|
+
a warning is printed.
|
22
|
+
|
23
|
+
When the mirror command is used, for each folder that is processed,
|
24
|
+
a new file is created alongside the normal backup files (.imap and .mbox)
|
25
|
+
This file has a '.mirror' extension. This file contains a mapping of
|
26
|
+
the known UIDs on the source account to those on the destination account.
|
27
|
+
|
28
|
+
Some configuration may be necessary, as follows:
|
29
|
+
|
30
|
+
#{CLI::Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
|
31
|
+
DESC
|
32
|
+
|
33
|
+
def self.included(base)
|
34
|
+
base.class_eval do
|
35
|
+
desc(
|
36
|
+
"mirror SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
|
37
|
+
"(Deprecated) Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
|
38
|
+
)
|
39
|
+
long_desc LONG_DESCRIPTION
|
40
|
+
config_option
|
41
|
+
quiet_option
|
42
|
+
verbose_option
|
43
|
+
method_option(
|
44
|
+
"automatic-namespaces",
|
45
|
+
type: :boolean,
|
46
|
+
desc: "automatically choose delimiters and prefixes"
|
47
|
+
)
|
48
|
+
method_option(
|
49
|
+
"destination-delimiter",
|
50
|
+
type: :string,
|
51
|
+
desc: "the delimiter for destination folder names"
|
52
|
+
)
|
53
|
+
method_option(
|
54
|
+
"destination-prefix",
|
55
|
+
type: :string,
|
56
|
+
desc: "the prefix (namespace) to add to destination folder names",
|
57
|
+
aliases: ["-d"]
|
58
|
+
)
|
59
|
+
method_option(
|
60
|
+
"source-delimiter",
|
61
|
+
type: :string,
|
62
|
+
desc: "the delimiter for source folder names"
|
63
|
+
)
|
64
|
+
method_option(
|
65
|
+
"source-prefix",
|
66
|
+
type: :string,
|
67
|
+
desc: "the prefix (namespace) to strip from source folder names",
|
68
|
+
aliases: ["-s"]
|
69
|
+
)
|
70
|
+
# Keeps one email account in line with another
|
71
|
+
# @return [void]
|
72
|
+
def mirror(source_email, destination_email)
|
73
|
+
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
74
|
+
CLI::Transfer.new(:mirror, source_email, destination_email, non_logging_options).run
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -13,7 +13,7 @@ module Imap::Backup
|
|
13
13
|
include CLI::Helpers
|
14
14
|
|
15
15
|
# The possible values for the action parameter
|
16
|
-
ACTIONS = %i(migrate mirror).freeze
|
16
|
+
ACTIONS = %i(copy migrate mirror).freeze
|
17
17
|
|
18
18
|
def initialize(action, source_email, destination_email, options)
|
19
19
|
@action = action
|
@@ -39,14 +39,17 @@ module Imap::Backup
|
|
39
39
|
raise "Unknown action '#{action}'" if !ACTIONS.include?(action)
|
40
40
|
|
41
41
|
process_options!
|
42
|
-
|
42
|
+
warn_if_source_account_is_not_in_mirror_mode if action == :mirror
|
43
|
+
run_backup if %i(copy mirror).include?(action)
|
43
44
|
|
44
45
|
folders.each do |serializer, folder|
|
45
46
|
case action
|
47
|
+
when :copy
|
48
|
+
Mirror.new(serializer, folder, reset: false).run
|
46
49
|
when :migrate
|
47
50
|
Migrator.new(serializer, folder, reset: reset).run
|
48
51
|
when :mirror
|
49
|
-
Mirror.new(serializer, folder).run
|
52
|
+
Mirror.new(serializer, folder, reset: true).run
|
50
53
|
end
|
51
54
|
end
|
52
55
|
end
|
@@ -123,9 +126,7 @@ module Imap::Backup
|
|
123
126
|
self.source_prefix ||= ""
|
124
127
|
end
|
125
128
|
|
126
|
-
def
|
127
|
-
warn_if_source_account_is_not_in_mirror_mode
|
128
|
-
|
129
|
+
def run_backup
|
129
130
|
CLI::Backup.new(config: config_path, accounts: source_email).run
|
130
131
|
end
|
131
132
|
|
data/lib/imap/backup/cli.rb
CHANGED
@@ -12,6 +12,8 @@ module Imap::Backup
|
|
12
12
|
|
13
13
|
autoload :Backup, "imap/backup/cli/backup"
|
14
14
|
autoload :Local, "imap/backup/cli/local"
|
15
|
+
autoload :Migrate, "imap/backup/cli/migrate"
|
16
|
+
autoload :Mirror, "imap/backup/cli/mirror"
|
15
17
|
autoload :Remote, "imap/backup/cli/remote"
|
16
18
|
autoload :Restore, "imap/backup/cli/restore"
|
17
19
|
autoload :Setup, "imap/backup/cli/setup"
|
@@ -22,28 +24,6 @@ module Imap::Backup
|
|
22
24
|
|
23
25
|
include Helpers
|
24
26
|
|
25
|
-
NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
|
26
|
-
Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
|
27
|
-
while others, while others concatenate the names of subfolders
|
28
|
-
with a charater ("delimiter") other than "/".
|
29
|
-
|
30
|
-
In these cases there are two choices.
|
31
|
-
|
32
|
-
You can use the `--automatic-namespaces` option.
|
33
|
-
This will query the source and detination servers for their
|
34
|
-
namespace configuration and will adapt paths accordingly.
|
35
|
-
This option requires that both the source and destination
|
36
|
-
servers are available and work with the provided parameters
|
37
|
-
and authentication.
|
38
|
-
|
39
|
-
If automatic configuration does not work as desired, there are the
|
40
|
-
`--source-prefix=`, `--source-delimiter=`,
|
41
|
-
`--destination-prefix=` and `--destination-delimiter=` parameters.
|
42
|
-
To check what values you should use, check the output of the
|
43
|
-
`imap-backup remote namespaces EMAIL` command.
|
44
|
-
DESC
|
45
|
-
private_constant :NAMESPACE_CONFIGURATION_DESCRIPTION
|
46
|
-
|
47
27
|
default_task :backup
|
48
28
|
|
49
29
|
# Overrides {https://www.rubydoc.info/gems/thor/Thor%2FBase%2FClassMethods:start Thor's method}
|
@@ -94,91 +74,31 @@ module Imap::Backup
|
|
94
74
|
Backup.new(non_logging_options).run
|
95
75
|
end
|
96
76
|
|
97
|
-
desc "local SUBCOMMAND [OPTIONS]", "View local info"
|
98
|
-
subcommand "local", Local
|
99
|
-
|
100
77
|
desc(
|
101
|
-
"
|
102
|
-
"
|
78
|
+
"copy SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
|
79
|
+
"Copies emails from the SOURCE account to the DESTINATION account, avoiding duplicates"
|
103
80
|
)
|
104
81
|
long_desc <<~DESC
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
Some configuration may be necessary, as follows:
|
109
|
-
|
110
|
-
#{NAMESPACE_CONFIGURATION_DESCRIPTION}
|
111
|
-
|
112
|
-
Finally, if you want to delete existing emails in destination folders,
|
113
|
-
use the `--reset` option. In this case, all existing emails are
|
114
|
-
deleted before uploading the migrated emails.
|
115
|
-
DESC
|
116
|
-
config_option
|
117
|
-
quiet_option
|
118
|
-
verbose_option
|
119
|
-
method_option(
|
120
|
-
"automatic-namespaces",
|
121
|
-
type: :boolean,
|
122
|
-
desc: "automatically choose delimiters and prefixes"
|
123
|
-
)
|
124
|
-
method_option(
|
125
|
-
"destination-delimiter",
|
126
|
-
type: :string,
|
127
|
-
desc: "the delimiter for destination folder names"
|
128
|
-
)
|
129
|
-
method_option(
|
130
|
-
"destination-prefix",
|
131
|
-
type: :string,
|
132
|
-
desc: "the prefix (namespace) to add to destination folder names",
|
133
|
-
aliases: ["-d"]
|
134
|
-
)
|
135
|
-
method_option(
|
136
|
-
"reset",
|
137
|
-
type: :boolean,
|
138
|
-
desc: "DANGER! This option deletes all messages from destination folders before uploading",
|
139
|
-
aliases: ["-r"]
|
140
|
-
)
|
141
|
-
method_option(
|
142
|
-
"source-delimiter",
|
143
|
-
type: :string,
|
144
|
-
desc: "the delimiter for source folder names"
|
145
|
-
)
|
146
|
-
method_option(
|
147
|
-
"source-prefix",
|
148
|
-
type: :string,
|
149
|
-
desc: "the prefix (namespace) to strip from source folder names",
|
150
|
-
aliases: ["-s"]
|
151
|
-
)
|
152
|
-
# Migrates emails from one account to another
|
153
|
-
# @return [void]
|
154
|
-
def migrate(source_email, destination_email)
|
155
|
-
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
156
|
-
Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
|
157
|
-
end
|
82
|
+
This command copies messages from the SOURCE_EMAIL account
|
83
|
+
to the DESTINATION_EMAIL account. It keeps track of copied
|
84
|
+
messages and avoids duplicate copies.
|
158
85
|
|
159
|
-
|
160
|
-
|
161
|
-
"Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
|
162
|
-
)
|
163
|
-
long_desc <<~DESC
|
164
|
-
This command updates the DESTINATION_EMAIL account's folders to have the same contents
|
165
|
-
as those on the SOURCE_EMAIL account.
|
86
|
+
Any other messages that are present on the DESTINATION_EMAIL account
|
87
|
+
are not affected.
|
166
88
|
|
167
89
|
If a folder list is configured for the SOURCE_EMAIL account,
|
168
90
|
only the folders indicated by the setting are copied.
|
169
91
|
|
170
92
|
First, it runs the download of the SOURCE_EMAIL account.
|
171
|
-
If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
|
172
|
-
a warning is printed.
|
173
93
|
|
174
|
-
When the
|
94
|
+
When the copy command is used, for each folder that is processed,
|
175
95
|
a new file is created alongside the normal backup files (.imap and .mbox)
|
176
96
|
This file has a '.mirror' extension. This file contains a mapping of
|
177
97
|
the known UIDs on the source account to those on the destination account.
|
178
98
|
|
179
99
|
Some configuration may be necessary, as follows:
|
180
100
|
|
181
|
-
#{NAMESPACE_CONFIGURATION_DESCRIPTION}
|
101
|
+
#{Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
|
182
102
|
DESC
|
183
103
|
config_option
|
184
104
|
quiet_option
|
@@ -210,13 +130,19 @@ module Imap::Backup
|
|
210
130
|
desc: "the prefix (namespace) to strip from source folder names",
|
211
131
|
aliases: ["-s"]
|
212
132
|
)
|
213
|
-
#
|
133
|
+
# Copies messages from one email account to another
|
214
134
|
# @return [void]
|
215
|
-
def
|
135
|
+
def copy(source_email, destination_email)
|
216
136
|
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
217
|
-
Transfer.new(:
|
137
|
+
Transfer.new(:copy, source_email, destination_email, non_logging_options).run
|
218
138
|
end
|
219
139
|
|
140
|
+
desc "local SUBCOMMAND [OPTIONS]", "View local info"
|
141
|
+
subcommand "local", Local
|
142
|
+
|
143
|
+
include Migrate
|
144
|
+
include Mirror
|
145
|
+
|
220
146
|
desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
|
221
147
|
subcommand "remote", Remote
|
222
148
|
|
@@ -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"
|
data/lib/imap/backup/mirror.rb
CHANGED
@@ -5,9 +5,10 @@ module Imap; end
|
|
5
5
|
module Imap::Backup
|
6
6
|
# Synchronises a folder between a source and destination
|
7
7
|
class Mirror
|
8
|
-
def initialize(serializer, folder)
|
8
|
+
def initialize(serializer, folder, reset: false)
|
9
9
|
@serializer = serializer
|
10
10
|
@folder = folder
|
11
|
+
@reset = reset
|
11
12
|
end
|
12
13
|
|
13
14
|
# If necessary, reates the destination folder,
|
@@ -19,7 +20,7 @@ module Imap::Backup
|
|
19
20
|
# @return [void]
|
20
21
|
def run
|
21
22
|
ensure_destination_folder
|
22
|
-
delete_destination_only_emails
|
23
|
+
delete_destination_only_emails if reset
|
23
24
|
update_flags
|
24
25
|
append_emails
|
25
26
|
map.save
|
@@ -31,6 +32,7 @@ module Imap::Backup
|
|
31
32
|
|
32
33
|
attr_reader :serializer
|
33
34
|
attr_reader :folder
|
35
|
+
attr_reader :reset
|
34
36
|
|
35
37
|
def ensure_destination_folder
|
36
38
|
return if folder.exist?
|
@@ -90,7 +92,7 @@ module Imap::Backup
|
|
90
92
|
destination: folder.uid_validity
|
91
93
|
)
|
92
94
|
if !map_ok
|
93
|
-
folder.clear
|
95
|
+
folder.clear if reset
|
94
96
|
map.reset(
|
95
97
|
source_uid_validity: serializer.uid_validity,
|
96
98
|
destination_uid_validity: folder.uid_validity
|
data/lib/imap/backup/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: imap-backup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 16.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Yates
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-08-02 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: highline
|
@@ -23,6 +24,20 @@ dependencies:
|
|
23
24
|
- - ">="
|
24
25
|
- !ruby/object:Gem::Version
|
25
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
26
41
|
- !ruby/object:Gem::Dependency
|
27
42
|
name: mail
|
28
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,6 +94,20 @@ dependencies:
|
|
79
94
|
- - ">="
|
80
95
|
- !ruby/object:Gem::Version
|
81
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: ostruct
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
82
111
|
- !ruby/object:Gem::Dependency
|
83
112
|
name: rake
|
84
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,6 +181,8 @@ files:
|
|
152
181
|
- lib/imap/backup/cli/helpers.rb
|
153
182
|
- lib/imap/backup/cli/local.rb
|
154
183
|
- lib/imap/backup/cli/local/check.rb
|
184
|
+
- lib/imap/backup/cli/migrate.rb
|
185
|
+
- lib/imap/backup/cli/mirror.rb
|
155
186
|
- lib/imap/backup/cli/options.rb
|
156
187
|
- lib/imap/backup/cli/remote.rb
|
157
188
|
- lib/imap/backup/cli/restore.rb
|
@@ -161,7 +192,6 @@ files:
|
|
161
192
|
- lib/imap/backup/cli/stats.rb
|
162
193
|
- lib/imap/backup/cli/transfer.rb
|
163
194
|
- lib/imap/backup/cli/utils.rb
|
164
|
-
- lib/imap/backup/client/apple_mail.rb
|
165
195
|
- lib/imap/backup/client/automatic_login_wrapper.rb
|
166
196
|
- lib/imap/backup/client/default.rb
|
167
197
|
- lib/imap/backup/configuration.rb
|
@@ -218,6 +248,7 @@ licenses:
|
|
218
248
|
- MIT
|
219
249
|
metadata:
|
220
250
|
rubygems_mfa_required: 'true'
|
251
|
+
post_install_message:
|
221
252
|
rdoc_options: []
|
222
253
|
require_paths:
|
223
254
|
- lib
|
@@ -232,7 +263,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
232
263
|
- !ruby/object:Gem::Version
|
233
264
|
version: '0'
|
234
265
|
requirements: []
|
235
|
-
rubygems_version: 3.
|
266
|
+
rubygems_version: 3.2.33
|
267
|
+
signing_key:
|
236
268
|
specification_version: 4
|
237
269
|
summary: Backup GMail (or other IMAP) accounts to disk
|
238
270
|
test_files: []
|
@@ -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
|