imap-backup 11.1.0 → 12.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|