imap-backup 11.1.0 → 12.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/README.md +14 -14
- data/docs/development.md +19 -19
- data/docs/migrate-server-keep-address.md +38 -20
- data/lib/imap/backup/account/backup.rb +1 -1
- data/lib/imap/backup/account/client_factory.rb +12 -19
- data/lib/imap/backup/cli/folder_enumerator.rb +3 -3
- data/lib/imap/backup/cli/stats.rb +2 -0
- data/lib/imap/backup/cli/transfer.rb +163 -0
- data/lib/imap/backup/cli.rb +46 -13
- data/lib/imap/backup/client/automatic_login_wrapper.rb +43 -0
- data/lib/imap/backup/serializer/appender.rb +1 -1
- data/lib/imap/backup/serializer/delayed_metadata_serializer.rb +1 -1
- data/lib/imap/backup/serializer/integrity_checker.rb +14 -2
- data/lib/imap/backup/setup/global_options/download_strategy_chooser.rb +5 -3
- data/lib/imap/backup/setup/global_options.rb +6 -4
- data/lib/imap/backup/setup.rb +1 -1
- data/lib/imap/backup/version.rb +1 -1
- data/lib/retry_on_error.rb +2 -1
- metadata +4 -4
- data/lib/imap/backup/cli/migrate.rb +0 -87
- data/lib/imap/backup/cli/mirror.rb +0 -97
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed7a9fd869f7b1ea66f5dc0816a4ce35d77c150bcf8170e2d778e893fda43e9d
|
4
|
+
data.tar.gz: e75c535f849eb326906c1965a1e86507676209cf41cae5b98ad62e894912d9b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7ced0838fae828c771e831441fc3caf71b0465f4da630c3580e1ff842c687e51ad92d7e1913e7f8d48e9bb2c2db2eeee60eea3e2dcb464fda2b51ffc9252d8ea
|
7
|
+
data.tar.gz: d751a6fb0e2eebc107f539cb80609d353bb0c07126fb09e5001bea69476ca9eaa5f1ffce7e6bab27f7efdc149be213997d20c741cc2b60bf3ccf6a3fd0bd810c
|
data/README.md
CHANGED
@@ -113,7 +113,7 @@ These are activated via two settings:
|
|
113
113
|
As with all performance tweaks, there are trade-offs.
|
114
114
|
If you are using a small virtual server or Raspberry Pi
|
115
115
|
to run your backups, you will probably want to leave
|
116
|
-
the
|
116
|
+
the default settings.
|
117
117
|
If, on the other hand, you are using a computer with a
|
118
118
|
fair bit of RAM, and you are dealing with a *lot* of email,
|
119
119
|
then changing these settings may be worthwhile.
|
@@ -122,16 +122,17 @@ then changing these settings may be worthwhile.
|
|
122
122
|
|
123
123
|
This setting affects all account backups.
|
124
124
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
from scratch.
|
125
|
+
By default, `imap-backup` uses the "delay metadata" strategy.
|
126
|
+
As messages are being backed-up, the message *text*
|
127
|
+
is written to disk, while the related metadata is stored in memory.
|
129
128
|
|
130
|
-
|
131
|
-
|
129
|
+
While this uses a little more memory, it avoids rewiting a growing JSON
|
130
|
+
file for every message, speeding things up and reducing disk wear.
|
132
131
|
|
133
|
-
|
134
|
-
|
132
|
+
The alternative strategy, called "direct", writes everything to disk
|
133
|
+
as it is received. This method is slower, but has the advantage
|
134
|
+
of using slightly less memory, which may be important on very
|
135
|
+
resource-limited systems, like Raspberry Pis.
|
135
136
|
|
136
137
|
## Multi-fetch Size
|
137
138
|
|
@@ -140,12 +141,11 @@ By default, during backup, each message is downloaded one-by-one.
|
|
140
141
|
Using this setting, you can download chunks of emails at a time,
|
141
142
|
potentially speeding up the process.
|
142
143
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
of messages downloaded at a time.
|
144
|
+
Using multi-fetch *will* mean that the backup process will use
|
145
|
+
more memory - equivalent to the size of the groups of messages
|
146
|
+
that are downloaded.
|
147
147
|
|
148
|
-
This behaviour may also exceed limits on your email provider,
|
148
|
+
This behaviour may also exceed the rate limits on your email provider,
|
149
149
|
so it's best to check before cranking it up!
|
150
150
|
|
151
151
|
# Troubleshooting
|
data/docs/development.md
CHANGED
@@ -6,25 +6,26 @@
|
|
6
6
|
|
7
7
|
# Development
|
8
8
|
|
9
|
-
A
|
10
|
-
|
9
|
+
A Dockerfile is available to allow testing with all available Ruby versions,
|
10
|
+
see the `dev/container` directory.
|
11
11
|
|
12
12
|
# Testing
|
13
13
|
|
14
14
|
## Feature Specs
|
15
15
|
|
16
|
-
Specs under `specs/features` are integration specs run against
|
17
|
-
controlled by Docker Compose.
|
18
|
-
|
16
|
+
Specs under `specs/features` are integration specs run against
|
17
|
+
two local IMAP servers controlled by Docker Compose.
|
18
|
+
|
19
|
+
Start them before running the test suite
|
19
20
|
|
20
21
|
```sh
|
21
|
-
$ docker-compose up -d
|
22
|
+
$ docker-compose -f dev/docker-compose.yml up -d
|
22
23
|
```
|
23
24
|
|
24
25
|
or, with Podman
|
25
26
|
|
26
27
|
```sh
|
27
|
-
$ podman-compose -f docker-compose.yml up -d
|
28
|
+
$ podman-compose -f dev/docker-compose.yml up -d
|
28
29
|
```
|
29
30
|
|
30
31
|
```sh
|
@@ -62,24 +63,23 @@ use `last_command_started.output`.
|
|
62
63
|
|
63
64
|
```ruby
|
64
65
|
require "net/imap"
|
66
|
+
require_relative "spec/features/support/30_email_server_helpers"
|
65
67
|
|
66
|
-
|
67
|
-
username = "address@example.com"
|
68
|
-
imap.login(username, "pass")
|
68
|
+
include EmailServerHelpers
|
69
69
|
|
70
|
-
|
71
|
-
response = imap.append("INBOX", message, nil, nil)
|
70
|
+
test_connection = test_server_connection_parameters
|
72
71
|
|
73
|
-
|
74
|
-
|
72
|
+
test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
|
73
|
+
test_imap.login(test_connection[:username], test_connection[:password])
|
75
74
|
|
76
|
-
|
77
|
-
|
75
|
+
message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
|
76
|
+
response = test_imap.append("INBOX", message, nil, nil)
|
78
77
|
|
79
|
-
|
78
|
+
test_imap.examine("INBOX")
|
79
|
+
uids = test_imap.uid_search(["ALL"]).sort
|
80
80
|
|
81
|
-
|
82
|
-
|
81
|
+
fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
|
82
|
+
```
|
83
83
|
|
84
84
|
# Contributing
|
85
85
|
|
@@ -1,29 +1,47 @@
|
|
1
1
|
# Migrate to a new e-mail server while keeping your existing address
|
2
2
|
|
3
|
-
While switching e-mail provider (from provider `A` to `B`),
|
3
|
+
While switching e-mail provider (from provider `A` to `B`),
|
4
|
+
you might want to keep the same address (`mymail@domain.com`),
|
5
|
+
and copy all your existing e-mails to your new server `B`.
|
6
|
+
`imap-backup` can do that too!
|
4
7
|
|
5
|
-
|
8
|
+
It is best to use [`imap-backup migrate`](/docs/commands/migrate.md)
|
9
|
+
and not [`imap-backup restore`](/docs/commands/restore.md) here because
|
10
|
+
`migrate` simply copies emails to folders with the same name as the ones
|
11
|
+
they were downloaded from, while `restore` changes the names of restored
|
12
|
+
folders if folders with the same name already exist on the destination server.
|
13
|
+
|
14
|
+
1. Backup your e-mails: use [`imap-backup setup`](/docs/commands/setup.md)
|
15
|
+
to setup connection to your old provider `A`,
|
16
|
+
then launch [`imap-backup backup`](/docs/commands/backup.md).
|
6
17
|
1. Actually switch your e-mail service provider (update your DNS MX and all that...).
|
7
|
-
1.
|
18
|
+
1. As both the source and the destination have the same address,
|
19
|
+
you need to manually rename your old account first:
|
8
20
|
|
9
|
-
|
21
|
+
1. Modify your configuration file manually
|
22
|
+
(i.e. not via `imap-backup setup`) and
|
23
|
+
rename your account to `mymail-old@domain.com`:
|
10
24
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
```diff
|
26
|
+
"accounts": [
|
27
|
+
{
|
28
|
+
- "username": "mymail@domain.com",
|
29
|
+
+ "username": "mymail-old@domain.com",
|
30
|
+
"password": "...",
|
31
|
+
- "local_path": "/some/path/.imap-backup/mymail_domain.com",
|
32
|
+
+ "local_path": "/some/path/.imap-backup/mymail-old_domain.com",
|
33
|
+
"folders": [...],
|
34
|
+
"server": "..."
|
35
|
+
}
|
36
|
+
```
|
23
37
|
|
24
|
-
|
38
|
+
1. Rename the backup directory from `mymail_domain.com`
|
39
|
+
to `mymail-old_domain.com`.
|
25
40
|
|
26
|
-
1. Set up a new account giving access to the new provider `B`
|
27
|
-
|
41
|
+
1. Set up a new account giving access to the new provider `B`
|
42
|
+
using `imap-backup setup`.
|
43
|
+
1. Now you can use `imap-backup migrate`, optionally adapting
|
44
|
+
[delimiters and prefixes configuration](/docs/delimiters-and-prefixes.md)
|
45
|
+
if need be:
|
28
46
|
|
29
|
-
imap-backup migrate mymail-old@domain.com mymail@domain.com [options]
|
47
|
+
imap-backup migrate mymail-old@domain.com mymail@domain.com [options]
|
@@ -20,7 +20,7 @@ module Imap::Backup
|
|
20
20
|
def run
|
21
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
|
-
account.client
|
23
|
+
account.client.login
|
24
24
|
|
25
25
|
Account::FolderEnsurer.new(account: account).run
|
26
26
|
Account::LocalOnlyFolderDeleter.new(account: account).run if account.mirror_mode
|
@@ -2,8 +2,8 @@ require "socket"
|
|
2
2
|
|
3
3
|
require "email/provider"
|
4
4
|
require "imap/backup/client/apple_mail"
|
5
|
+
require "imap/backup/client/automatic_login_wrapper"
|
5
6
|
require "imap/backup/client/default"
|
6
|
-
require "retry_on_error"
|
7
7
|
|
8
8
|
module Imap; end
|
9
9
|
|
@@ -11,10 +11,6 @@ module Imap::Backup
|
|
11
11
|
class Account; end
|
12
12
|
|
13
13
|
class Account::ClientFactory
|
14
|
-
include RetryOnError
|
15
|
-
|
16
|
-
LOGIN_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::SocketError].freeze
|
17
|
-
|
18
14
|
attr_reader :account
|
19
15
|
|
20
16
|
def initialize(account:)
|
@@ -24,20 +20,17 @@ module Imap::Backup
|
|
24
20
|
end
|
25
21
|
|
26
22
|
def run
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
client.login
|
39
|
-
client
|
40
|
-
end
|
23
|
+
options = provider_options
|
24
|
+
Logger.logger.debug(
|
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
|
33
|
+
Client::AutomaticLoginWrapper.new(client: client)
|
41
34
|
end
|
42
35
|
|
43
36
|
private
|
@@ -30,7 +30,7 @@ module Imap::Backup
|
|
30
30
|
Pathname.glob(glob) do |path|
|
31
31
|
name = source_folder_name(path)
|
32
32
|
serializer = Serializer.new(source_local_path, name)
|
33
|
-
folder =
|
33
|
+
folder = destination_folder_for_path(name)
|
34
34
|
yield serializer, folder
|
35
35
|
end
|
36
36
|
end
|
@@ -55,8 +55,8 @@ module Imap::Backup
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def
|
59
|
-
parts = name.split(
|
58
|
+
def destination_folder_for_path(name)
|
59
|
+
parts = name.split("/")
|
60
60
|
no_source_prefix =
|
61
61
|
if source_prefix_clipped != "" && parts.first == source_prefix_clipped
|
62
62
|
parts[1..]
|
@@ -35,6 +35,7 @@ module Imap::Backup
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def stats
|
38
|
+
Logger.logger.debug("[Stats] loading configuration")
|
38
39
|
config = load_config(**options)
|
39
40
|
account = account(config, email)
|
40
41
|
|
@@ -46,6 +47,7 @@ module Imap::Backup
|
|
46
47
|
|
47
48
|
serializer = Serializer.new(account.local_path, folder.name)
|
48
49
|
local_uids = serializer.uids
|
50
|
+
Logger.logger.debug("[Stats] fetching email list for '#{folder.name}'")
|
49
51
|
remote_uids = folder.uids
|
50
52
|
{
|
51
53
|
folder: folder.name,
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require "imap/backup/cli/backup"
|
2
|
+
require "imap/backup/cli/helpers"
|
3
|
+
require "imap/backup/cli/folder_enumerator"
|
4
|
+
require "imap/backup/migrator"
|
5
|
+
require "imap/backup/mirror"
|
6
|
+
|
7
|
+
module Imap; end
|
8
|
+
|
9
|
+
module Imap::Backup
|
10
|
+
class CLI::Transfer < Thor
|
11
|
+
include Thor::Actions
|
12
|
+
include CLI::Helpers
|
13
|
+
|
14
|
+
ACTIONS = %i(migrate mirror).freeze
|
15
|
+
|
16
|
+
attr_reader :action
|
17
|
+
attr_accessor :automatic_namespaces
|
18
|
+
attr_accessor :config_path
|
19
|
+
attr_accessor :destination_delimiter
|
20
|
+
attr_reader :destination_email
|
21
|
+
attr_accessor :destination_prefix
|
22
|
+
attr_reader :options
|
23
|
+
attr_accessor :reset
|
24
|
+
attr_accessor :source_delimiter
|
25
|
+
attr_reader :source_email
|
26
|
+
attr_accessor :source_prefix
|
27
|
+
|
28
|
+
def initialize(action, source_email, destination_email, options)
|
29
|
+
super([])
|
30
|
+
@action = action
|
31
|
+
@source_email = source_email
|
32
|
+
@destination_email = destination_email
|
33
|
+
@options = options
|
34
|
+
@automatic_namespaces = nil
|
35
|
+
@config_path = nil
|
36
|
+
@destination_delimiter = nil
|
37
|
+
@destination_prefix = nil
|
38
|
+
@reset = nil
|
39
|
+
@source_delimiter = nil
|
40
|
+
@source_prefix = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
no_commands do
|
44
|
+
def run
|
45
|
+
raise "Unknown action '#{action}'" if !ACTIONS.include?(action)
|
46
|
+
|
47
|
+
process_options!
|
48
|
+
prepare_mirror if action == :mirror
|
49
|
+
|
50
|
+
folders.each do |serializer, folder|
|
51
|
+
case action
|
52
|
+
when :migrate
|
53
|
+
Migrator.new(serializer, folder, reset: reset).run
|
54
|
+
when :mirror
|
55
|
+
Mirror.new(serializer, folder).run
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_options!
|
61
|
+
self.automatic_namespaces = options[:automatic_namespaces] || false
|
62
|
+
self.config_path = options[:config]
|
63
|
+
self.destination_delimiter = options[:destination_delimiter]
|
64
|
+
self.destination_prefix = options[:destination_prefix]
|
65
|
+
self.source_delimiter = options[:source_delimiter]
|
66
|
+
self.source_prefix = options[:source_prefix]
|
67
|
+
self.reset = options[:reset] || false
|
68
|
+
check_accounts!
|
69
|
+
choose_prefixes_and_delimiters!
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_accounts!
|
73
|
+
if destination_email == source_email
|
74
|
+
raise "Source and destination accounts cannot be the same!"
|
75
|
+
end
|
76
|
+
|
77
|
+
raise "Account '#{destination_email}' does not exist" if !destination_account
|
78
|
+
|
79
|
+
raise "Account '#{source_email}' does not exist" if !source_account
|
80
|
+
end
|
81
|
+
|
82
|
+
def choose_prefixes_and_delimiters!
|
83
|
+
if automatic_namespaces
|
84
|
+
ensure_no_prefix_or_delimiter_parameters!
|
85
|
+
query_servers_for_settings
|
86
|
+
else
|
87
|
+
add_prefix_and_delimiter_defaults
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def ensure_no_prefix_or_delimiter_parameters!
|
92
|
+
if destination_delimiter
|
93
|
+
raise "--automatic-namespaces is incompatible with --destination-delimiter"
|
94
|
+
end
|
95
|
+
if destination_prefix
|
96
|
+
raise "--automatic-namespaces is incompatible with --destination-prefix"
|
97
|
+
end
|
98
|
+
raise "--automatic-namespaces is incompatible with --source-delimiter" if source_delimiter
|
99
|
+
raise "--automatic-namespaces is incompatible with --source-prefix" if source_prefix
|
100
|
+
end
|
101
|
+
|
102
|
+
def query_servers_for_settings
|
103
|
+
self.destination_prefix, self.destination_delimiter = account_settings(destination_account)
|
104
|
+
self.source_prefix, self.source_delimiter = account_settings(source_account)
|
105
|
+
end
|
106
|
+
|
107
|
+
def account_settings(account)
|
108
|
+
namespaces = account.client.namespace
|
109
|
+
personal = namespaces.personal.first
|
110
|
+
[personal.prefix, personal.delim]
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_prefix_and_delimiter_defaults
|
114
|
+
self.destination_delimiter ||= "/"
|
115
|
+
self.destination_prefix ||= ""
|
116
|
+
self.source_delimiter ||= "/"
|
117
|
+
self.source_prefix ||= ""
|
118
|
+
end
|
119
|
+
|
120
|
+
def prepare_mirror
|
121
|
+
warn_if_source_account_is_not_in_mirror_mode
|
122
|
+
|
123
|
+
CLI::Backup.new(config: config_path, accounts: source_email).run
|
124
|
+
end
|
125
|
+
|
126
|
+
def warn_if_source_account_is_not_in_mirror_mode
|
127
|
+
return if source_account.mirror_mode
|
128
|
+
|
129
|
+
message =
|
130
|
+
"The account '#{source_account.username}' " \
|
131
|
+
"is not set up to make mirror backups"
|
132
|
+
Logger.logger.warn message
|
133
|
+
end
|
134
|
+
|
135
|
+
def config
|
136
|
+
@config ||= load_config(config: config_path)
|
137
|
+
end
|
138
|
+
|
139
|
+
def enumerator_options
|
140
|
+
{
|
141
|
+
destination: destination_account,
|
142
|
+
destination_delimiter: destination_delimiter,
|
143
|
+
destination_prefix: destination_prefix,
|
144
|
+
source: source_account,
|
145
|
+
source_delimiter: source_delimiter,
|
146
|
+
source_prefix: source_prefix
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def folders
|
151
|
+
CLI::FolderEnumerator.new(**enumerator_options)
|
152
|
+
end
|
153
|
+
|
154
|
+
def destination_account
|
155
|
+
config.accounts.find { |a| a.username == destination_email }
|
156
|
+
end
|
157
|
+
|
158
|
+
def source_account
|
159
|
+
config.accounts.find { |a| a.username == source_email }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/imap/backup/cli.rb
CHANGED
@@ -10,18 +10,38 @@ module Imap::Backup
|
|
10
10
|
autoload :Backup, "imap/backup/cli/backup"
|
11
11
|
autoload :Folders, "imap/backup/cli/folders"
|
12
12
|
autoload :Local, "imap/backup/cli/local"
|
13
|
-
autoload :Migrate, "imap/backup/cli/migrate"
|
14
|
-
autoload :Mirror, "imap/backup/cli/mirror"
|
15
13
|
autoload :Remote, "imap/backup/cli/remote"
|
16
14
|
autoload :Restore, "imap/backup/cli/restore"
|
17
15
|
autoload :Setup, "imap/backup/cli/setup"
|
18
16
|
autoload :Stats, "imap/backup/cli/stats"
|
17
|
+
autoload :Transfer, "imap/backup/cli/transfer"
|
19
18
|
autoload :Utils, "imap/backup/cli/utils"
|
20
19
|
|
21
20
|
include Helpers
|
22
21
|
|
23
22
|
VERSION_ARGUMENTS = %w(-v --version).freeze
|
24
23
|
|
24
|
+
NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
|
25
|
+
Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
|
26
|
+
while others, while others concatenate the names of subfolders
|
27
|
+
with a charater ("delimiter") other than "/".
|
28
|
+
|
29
|
+
In these cases there are two choices.
|
30
|
+
|
31
|
+
You can use the `--automatic-namespaces` option.
|
32
|
+
This wil query the source and detination servers for their
|
33
|
+
namespace configuration and will adapt paths accordingly.
|
34
|
+
This option requires that both the source and destination
|
35
|
+
servers are available and work with the provided parameters
|
36
|
+
and authentication.
|
37
|
+
|
38
|
+
If automatic configuration does not work as desired, there are the
|
39
|
+
`--source-prefix=`, `--source-delimiter=`,
|
40
|
+
`--destination-prefix=` and `--destination-delimiter=` parameters.
|
41
|
+
To check what values you should use, check the output of the
|
42
|
+
`imap-backup remote namespaces EMAIL` command.
|
43
|
+
DESC
|
44
|
+
|
25
45
|
default_task :backup
|
26
46
|
|
27
47
|
def self.start(*args)
|
@@ -83,19 +103,22 @@ module Imap::Backup
|
|
83
103
|
All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
|
84
104
|
uploaded to the "destination account" (DESTINATION_EMAIL).
|
85
105
|
|
86
|
-
|
87
|
-
use the `--source-prefix=` and/or `--destination-prefix=` options.
|
106
|
+
Some configuration may be necessary, as follows:
|
88
107
|
|
89
|
-
|
90
|
-
use the `--source-delimiter=` and/or `--destination-delimiter=` options.
|
108
|
+
#{NAMESPACE_CONFIGURATION_DESCRIPTION}
|
91
109
|
|
92
|
-
|
110
|
+
Finally, if you want to delete existing emails in destination folders,
|
93
111
|
use the `--reset` option. In this case, all existing emails are
|
94
112
|
deleted before uploading the migrated emails.
|
95
113
|
DESC
|
96
114
|
config_option
|
97
115
|
quiet_option
|
98
116
|
verbose_option
|
117
|
+
method_option(
|
118
|
+
"automatic-namespaces",
|
119
|
+
type: :boolean,
|
120
|
+
desc: "automatically choose delimiters and prefixes"
|
121
|
+
)
|
99
122
|
method_option(
|
100
123
|
"destination-delimiter",
|
101
124
|
type: :string,
|
@@ -126,7 +149,7 @@ module Imap::Backup
|
|
126
149
|
)
|
127
150
|
def migrate(source_email, destination_email)
|
128
151
|
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
129
|
-
|
152
|
+
Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
|
130
153
|
end
|
131
154
|
|
132
155
|
desc(
|
@@ -140,7 +163,7 @@ module Imap::Backup
|
|
140
163
|
If a folder list is configured for the SOURCE_EMAIL account,
|
141
164
|
only the folders indicated by the setting are copied.
|
142
165
|
|
143
|
-
First, runs the download of the SOURCE_EMAIL account.
|
166
|
+
First, it runs the download of the SOURCE_EMAIL account.
|
144
167
|
If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
|
145
168
|
a warning is printed.
|
146
169
|
|
@@ -152,6 +175,11 @@ module Imap::Backup
|
|
152
175
|
config_option
|
153
176
|
quiet_option
|
154
177
|
verbose_option
|
178
|
+
method_option(
|
179
|
+
"automatic-namespaces",
|
180
|
+
type: :boolean,
|
181
|
+
desc: "automatically choose delimiters and prefixes"
|
182
|
+
)
|
155
183
|
method_option(
|
156
184
|
"destination-delimiter",
|
157
185
|
type: :string,
|
@@ -176,7 +204,7 @@ module Imap::Backup
|
|
176
204
|
)
|
177
205
|
def mirror(source_email, destination_email)
|
178
206
|
non_logging_options = Imap::Backup::Logger.setup_logging(options)
|
179
|
-
|
207
|
+
Transfer.new(:mirror, source_email, destination_email, non_logging_options).run
|
180
208
|
end
|
181
209
|
|
182
210
|
desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
|
@@ -211,9 +239,14 @@ module Imap::Backup
|
|
211
239
|
|
212
240
|
desc "stats EMAIL [OPTIONS]", "Print stats for each account folder"
|
213
241
|
long_desc <<~DESC
|
214
|
-
For each account folder, lists
|
215
|
-
|
216
|
-
|
242
|
+
For each account folder, lists three counts of emails:
|
243
|
+
|
244
|
+
1. "server" - those yet to be downloaded,
|
245
|
+
|
246
|
+
2. "both" - those that exist on server and are backed up,
|
247
|
+
|
248
|
+
3. "local" - those which are only present in the backup (as they have been deleted
|
249
|
+
on the server).
|
217
250
|
DESC
|
218
251
|
config_option
|
219
252
|
format_option
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "retry_on_error"
|
2
|
+
|
3
|
+
module Imap; end
|
4
|
+
|
5
|
+
module Imap::Backup
|
6
|
+
module Client; end
|
7
|
+
|
8
|
+
class Client::AutomaticLoginWrapper
|
9
|
+
include RetryOnError
|
10
|
+
|
11
|
+
LOGIN_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::SocketError].freeze
|
12
|
+
|
13
|
+
attr_reader :client
|
14
|
+
attr_reader :login_called
|
15
|
+
|
16
|
+
def initialize(client:)
|
17
|
+
@client = client
|
18
|
+
@login_called = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(method_name, *arguments, &block)
|
22
|
+
if login_called
|
23
|
+
client.send(method_name, *arguments, &block)
|
24
|
+
else
|
25
|
+
do_first_login
|
26
|
+
client.send(method_name, *arguments, &block) if method_name != :login
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def respond_to_missing?(method_name, _include_private = false)
|
31
|
+
client.respond_to?(method_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def do_first_login
|
37
|
+
retry_on_error(errors: LOGIN_RETRY_CLASSES) do
|
38
|
+
client.login
|
39
|
+
@login_called = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -29,7 +29,7 @@ module Imap::Backup
|
|
29
29
|
rollback_on_error do
|
30
30
|
serialized = to_serialized(message)
|
31
31
|
mbox.append serialized
|
32
|
-
imap.append uid, serialized.
|
32
|
+
imap.append uid, serialized.bytesize, flags: flags
|
33
33
|
rescue StandardError => e
|
34
34
|
raise <<-ERROR.gsub(/^\s*/m, "")
|
35
35
|
[#{folder}] failed to append message #{uid}: #{message}.
|
@@ -33,7 +33,7 @@ module Imap::Backup
|
|
33
33
|
tsx.fail_outside_transaction!(:append)
|
34
34
|
mboxrd_message = Email::Mboxrd::Message.new(message)
|
35
35
|
serialized = mboxrd_message.to_serialized
|
36
|
-
tsx.data[:metadata] << {uid: uid, length: serialized.
|
36
|
+
tsx.data[:metadata] << {uid: uid, length: serialized.bytesize, flags: flags}
|
37
37
|
mbox.append(serialized)
|
38
38
|
end
|
39
39
|
|
@@ -15,6 +15,9 @@ module Imap::Backup
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def run
|
18
|
+
Logger.logger.debug(
|
19
|
+
"[IntegrityChecker] checking '#{imap.pathname}' against '#{mbox.pathname}'"
|
20
|
+
)
|
18
21
|
if !imap.valid?
|
19
22
|
message = ".imap file '#{imap.pathname}' is corrupt"
|
20
23
|
raise Serializer::FolderIntegrityError, message
|
@@ -56,14 +59,18 @@ module Imap::Backup
|
|
56
59
|
def check_mbox_length!
|
57
60
|
last = imap.messages[-1]
|
58
61
|
|
59
|
-
|
62
|
+
expected = last.offset + last.length
|
63
|
+
Logger.logger.debug(
|
64
|
+
"[IntegrityChecker] mbox length is #{mbox.length}, expected length is #{expected}"
|
65
|
+
)
|
66
|
+
if mbox.length < expected
|
60
67
|
message =
|
61
68
|
".mbox file '#{mbox.pathname}' is shorter than indicated by " \
|
62
69
|
".imap file '#{imap.pathname}'"
|
63
70
|
raise Serializer::FolderIntegrityError, message
|
64
71
|
end
|
65
72
|
|
66
|
-
if mbox.length >
|
73
|
+
if mbox.length > expected
|
67
74
|
message =
|
68
75
|
".mbox file '#{mbox.pathname}' is longer than indicated by " \
|
69
76
|
".imap file '#{imap.pathname}'"
|
@@ -77,6 +84,11 @@ module Imap::Backup
|
|
77
84
|
|
78
85
|
next if text.start_with?("From ")
|
79
86
|
|
87
|
+
Logger.logger.debug(
|
88
|
+
"[IntegrityChecker] looking for message with UID #{m.uid} " \
|
89
|
+
"at offset #{m.offset}, " \
|
90
|
+
"mbox starts with '#{text[0..200]}', expecting 'From '"
|
91
|
+
)
|
80
92
|
message =
|
81
93
|
"Message #{m.uid} not found at expected offset #{m.offset} " \
|
82
94
|
"in file '#{mbox.pathname}'"
|
@@ -7,11 +7,9 @@ class Imap::Backup::Setup; end
|
|
7
7
|
class Imap::Backup::Setup::GlobalOptions
|
8
8
|
class DownloadStrategyChooser
|
9
9
|
attr_reader :config
|
10
|
-
attr_reader :highline
|
11
10
|
|
12
|
-
def initialize(config
|
11
|
+
def initialize(config:)
|
13
12
|
@config = config
|
14
|
-
@highline = highline
|
15
13
|
end
|
16
14
|
|
17
15
|
def run
|
@@ -81,5 +79,9 @@ class Imap::Backup::Setup::GlobalOptions
|
|
81
79
|
highline.ask "Press a key "
|
82
80
|
end
|
83
81
|
end
|
82
|
+
|
83
|
+
def highline
|
84
|
+
Imap::Backup::Setup.highline
|
85
|
+
end
|
84
86
|
end
|
85
87
|
end
|
@@ -8,11 +8,9 @@ module Imap::Backup
|
|
8
8
|
|
9
9
|
class Setup::GlobalOptions
|
10
10
|
attr_reader :config
|
11
|
-
attr_reader :highline
|
12
11
|
|
13
|
-
def initialize(config
|
12
|
+
def initialize(config:)
|
14
13
|
@config = config
|
15
|
-
@highline = highline
|
16
14
|
end
|
17
15
|
|
18
16
|
def run
|
@@ -46,8 +44,12 @@ module Imap::Backup
|
|
46
44
|
current = strategies.find { |s| s[:key] == config.download_strategy }
|
47
45
|
changed = config.download_strategy_modified ? " *" : ""
|
48
46
|
menu.choice("change download strategy (currently: '#{current[:description]}')#{changed}") do
|
49
|
-
DownloadStrategyChooser.new(config: config
|
47
|
+
DownloadStrategyChooser.new(config: config).run
|
50
48
|
end
|
51
49
|
end
|
50
|
+
|
51
|
+
def highline
|
52
|
+
Imap::Backup::Setup.highline
|
53
|
+
end
|
52
54
|
end
|
53
55
|
end
|
data/lib/imap/backup/setup.rb
CHANGED
@@ -76,7 +76,7 @@ module Imap::Backup
|
|
76
76
|
def modify_global_options(menu)
|
77
77
|
changed = config.modified? ? " *" : ""
|
78
78
|
menu.choice("modify global options#{changed}") do
|
79
|
-
GlobalOptions.new(config: config
|
79
|
+
GlobalOptions.new(config: config).run
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
data/lib/imap/backup/version.rb
CHANGED
data/lib/retry_on_error.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module RetryOnError
|
2
|
-
def retry_on_error(errors:, limit: 10)
|
2
|
+
def retry_on_error(errors:, limit: 10, on_error: nil)
|
3
3
|
tries ||= 1
|
4
4
|
yield
|
5
5
|
rescue *errors => e
|
6
6
|
if tries < limit
|
7
7
|
message = "#{e}, attempt #{tries} of #{limit}"
|
8
8
|
Imap::Backup::Logger.logger.debug message
|
9
|
+
on_error&.call
|
9
10
|
tries += 1
|
10
11
|
retry
|
11
12
|
end
|
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:
|
4
|
+
version: 12.1.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: 2023-
|
11
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -247,14 +247,14 @@ files:
|
|
247
247
|
- lib/imap/backup/cli/helpers.rb
|
248
248
|
- lib/imap/backup/cli/local.rb
|
249
249
|
- lib/imap/backup/cli/local/check.rb
|
250
|
-
- lib/imap/backup/cli/migrate.rb
|
251
|
-
- lib/imap/backup/cli/mirror.rb
|
252
250
|
- lib/imap/backup/cli/remote.rb
|
253
251
|
- lib/imap/backup/cli/restore.rb
|
254
252
|
- lib/imap/backup/cli/setup.rb
|
255
253
|
- lib/imap/backup/cli/stats.rb
|
254
|
+
- lib/imap/backup/cli/transfer.rb
|
256
255
|
- lib/imap/backup/cli/utils.rb
|
257
256
|
- lib/imap/backup/client/apple_mail.rb
|
257
|
+
- lib/imap/backup/client/automatic_login_wrapper.rb
|
258
258
|
- lib/imap/backup/client/default.rb
|
259
259
|
- lib/imap/backup/configuration.rb
|
260
260
|
- lib/imap/backup/downloader.rb
|
@@ -1,87 +0,0 @@
|
|
1
|
-
require "imap/backup/cli/folder_enumerator"
|
2
|
-
require "imap/backup/migrator"
|
3
|
-
|
4
|
-
module Imap; end
|
5
|
-
|
6
|
-
module Imap::Backup
|
7
|
-
class CLI::Migrate < Thor
|
8
|
-
include Thor::Actions
|
9
|
-
include CLI::Helpers
|
10
|
-
|
11
|
-
attr_reader :destination_delimiter
|
12
|
-
attr_reader :destination_email
|
13
|
-
attr_reader :destination_prefix
|
14
|
-
attr_reader :config_path
|
15
|
-
attr_reader :reset
|
16
|
-
attr_reader :source_delimiter
|
17
|
-
attr_reader :source_email
|
18
|
-
attr_reader :source_prefix
|
19
|
-
|
20
|
-
def initialize(
|
21
|
-
source_email,
|
22
|
-
destination_email,
|
23
|
-
config: nil,
|
24
|
-
destination_delimiter: "/",
|
25
|
-
destination_prefix: "",
|
26
|
-
reset: false,
|
27
|
-
source_delimiter: "/",
|
28
|
-
source_prefix: ""
|
29
|
-
)
|
30
|
-
super([])
|
31
|
-
@destination_delimiter = destination_delimiter
|
32
|
-
@destination_email = destination_email
|
33
|
-
@destination_prefix = destination_prefix
|
34
|
-
@config_path = config
|
35
|
-
@reset = reset
|
36
|
-
@source_delimiter = source_delimiter
|
37
|
-
@source_email = source_email
|
38
|
-
@source_prefix = source_prefix
|
39
|
-
end
|
40
|
-
|
41
|
-
no_commands do
|
42
|
-
def run
|
43
|
-
check_accounts!
|
44
|
-
folders.each do |serializer, folder|
|
45
|
-
Migrator.new(serializer, folder, reset: reset).run
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def check_accounts!
|
50
|
-
if destination_email == source_email
|
51
|
-
raise "Source and destination accounts cannot be the same!"
|
52
|
-
end
|
53
|
-
|
54
|
-
raise "Account '#{destination_email}' does not exist" if !destination_account
|
55
|
-
|
56
|
-
raise "Account '#{source_email}' does not exist" if !source_account
|
57
|
-
end
|
58
|
-
|
59
|
-
def config
|
60
|
-
@config ||= load_config(config: config_path)
|
61
|
-
end
|
62
|
-
|
63
|
-
def enumerator_options
|
64
|
-
{
|
65
|
-
destination: destination_account,
|
66
|
-
destination_delimiter: destination_delimiter,
|
67
|
-
destination_prefix: destination_prefix,
|
68
|
-
source: source_account,
|
69
|
-
source_delimiter: source_delimiter,
|
70
|
-
source_prefix: source_prefix
|
71
|
-
}
|
72
|
-
end
|
73
|
-
|
74
|
-
def folders
|
75
|
-
CLI::FolderEnumerator.new(**enumerator_options)
|
76
|
-
end
|
77
|
-
|
78
|
-
def destination_account
|
79
|
-
config.accounts.find { |a| a.username == destination_email }
|
80
|
-
end
|
81
|
-
|
82
|
-
def source_account
|
83
|
-
config.accounts.find { |a| a.username == source_email }
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,97 +0,0 @@
|
|
1
|
-
require "imap/backup/cli/folder_enumerator"
|
2
|
-
require "imap/backup/mirror"
|
3
|
-
|
4
|
-
module Imap; end
|
5
|
-
|
6
|
-
module Imap::Backup
|
7
|
-
class CLI::Mirror < Thor
|
8
|
-
include Thor::Actions
|
9
|
-
include CLI::Helpers
|
10
|
-
|
11
|
-
attr_reader :destination_delimiter
|
12
|
-
attr_reader :destination_email
|
13
|
-
attr_reader :destination_prefix
|
14
|
-
attr_reader :config_path
|
15
|
-
attr_reader :source_delimiter
|
16
|
-
attr_reader :source_email
|
17
|
-
attr_reader :source_prefix
|
18
|
-
|
19
|
-
def initialize(
|
20
|
-
source_email,
|
21
|
-
destination_email,
|
22
|
-
config: nil,
|
23
|
-
destination_delimiter: "/",
|
24
|
-
destination_prefix: "",
|
25
|
-
source_delimiter: "/",
|
26
|
-
source_prefix: ""
|
27
|
-
)
|
28
|
-
super([])
|
29
|
-
@destination_delimiter = destination_delimiter
|
30
|
-
@destination_email = destination_email
|
31
|
-
@destination_prefix = destination_prefix
|
32
|
-
@config_path = config
|
33
|
-
@source_delimiter = source_delimiter
|
34
|
-
@source_email = source_email
|
35
|
-
@source_prefix = source_prefix
|
36
|
-
end
|
37
|
-
|
38
|
-
no_commands do
|
39
|
-
def run
|
40
|
-
check_accounts!
|
41
|
-
warn_if_source_account_is_not_in_mirror_mode
|
42
|
-
|
43
|
-
CLI::Backup.new(config: config_path, accounts: source_email).run
|
44
|
-
|
45
|
-
folders.each do |serializer, folder|
|
46
|
-
Mirror.new(serializer, folder).run
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def check_accounts!
|
51
|
-
if destination_email == source_email
|
52
|
-
raise "Source and destination accounts cannot be the same!"
|
53
|
-
end
|
54
|
-
|
55
|
-
raise "Account '#{destination_email}' does not exist" if !destination_account
|
56
|
-
|
57
|
-
raise "Account '#{source_email}' does not exist" if !source_account
|
58
|
-
end
|
59
|
-
|
60
|
-
def warn_if_source_account_is_not_in_mirror_mode
|
61
|
-
return if source_account.mirror_mode
|
62
|
-
|
63
|
-
message =
|
64
|
-
"The account '#{source_account.username}' " \
|
65
|
-
"is not set up to make mirror backups"
|
66
|
-
Logger.logger.warn message
|
67
|
-
end
|
68
|
-
|
69
|
-
def config
|
70
|
-
@config = load_config(config: config_path)
|
71
|
-
end
|
72
|
-
|
73
|
-
def enumerator_options
|
74
|
-
{
|
75
|
-
destination: destination_account,
|
76
|
-
destination_delimiter: destination_delimiter,
|
77
|
-
destination_prefix: destination_prefix,
|
78
|
-
source: source_account,
|
79
|
-
source_delimiter: source_delimiter,
|
80
|
-
source_prefix: source_prefix
|
81
|
-
}
|
82
|
-
end
|
83
|
-
|
84
|
-
def folders
|
85
|
-
CLI::FolderEnumerator.new(**enumerator_options)
|
86
|
-
end
|
87
|
-
|
88
|
-
def destination_account
|
89
|
-
config.accounts.find { |a| a.username == destination_email }
|
90
|
-
end
|
91
|
-
|
92
|
-
def source_account
|
93
|
-
config.accounts.find { |a| a.username == source_email }
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|