imap-backup 16.3.0 → 16.4.1
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 +3 -92
- data/docs/development.md +8 -0
- data/docs/testing.md +93 -0
- data/imap-backup.gemspec +12 -11
- data/lib/imap/backup/account/backup.rb +32 -10
- data/lib/imap/backup/account/backup_folders.rb +2 -0
- data/lib/imap/backup/account/client_factory.rb +1 -0
- data/lib/imap/backup/account/folder.rb +9 -0
- data/lib/imap/backup/account/folder_backup.rb +3 -0
- data/lib/imap/backup/account/folder_ensurer.rb +1 -0
- data/lib/imap/backup/account/folder_mapper.rb +6 -0
- data/lib/imap/backup/account/local_only_folder_deleter.rb +1 -0
- data/lib/imap/backup/account/locker.rb +40 -0
- data/lib/imap/backup/account/restore.rb +13 -2
- data/lib/imap/backup/account/serialized_folders.rb +1 -0
- data/lib/imap/backup/account.rb +50 -18
- data/lib/imap/backup/cli/backup.rb +6 -0
- data/lib/imap/backup/cli/local/check.rb +5 -0
- data/lib/imap/backup/cli/local.rb +1 -0
- data/lib/imap/backup/cli/options.rb +1 -0
- data/lib/imap/backup/cli/remote.rb +5 -2
- data/lib/imap/backup/cli/restore.rb +12 -5
- data/lib/imap/backup/cli/setup.rb +3 -0
- data/lib/imap/backup/cli/single/backup.rb +20 -0
- data/lib/imap/backup/cli/stats.rb +11 -0
- data/lib/imap/backup/cli/transfer.rb +32 -8
- data/lib/imap/backup/cli/utils.rb +17 -9
- data/lib/imap/backup/client/automatic_login_wrapper.rb +1 -0
- data/lib/imap/backup/client/default.rb +2 -0
- data/lib/imap/backup/configuration.rb +5 -3
- data/lib/imap/backup/downloader.rb +4 -0
- data/lib/imap/backup/email/mboxrd/message.rb +1 -0
- data/lib/imap/backup/file_mode.rb +1 -0
- data/lib/imap/backup/flag_refresher.rb +2 -0
- data/lib/imap/backup/local_only_message_deleter.rb +2 -0
- data/lib/imap/backup/lockfile.rb +94 -0
- data/lib/imap/backup/logger.rb +1 -1
- data/lib/imap/backup/migrator.rb +3 -0
- data/lib/imap/backup/mirror/map.rb +2 -1
- data/lib/imap/backup/mirror.rb +6 -2
- data/lib/imap/backup/serializer/mbox.rb +1 -1
- data/lib/imap/backup/serializer/message.rb +1 -1
- data/lib/imap/backup/serializer/permission_checker.rb +1 -1
- data/lib/imap/backup/serializer/version2_migrator.rb +1 -1
- data/lib/imap/backup/setup/asker.rb +1 -0
- data/lib/imap/backup/setup.rb +1 -0
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -0
- data/lib/imap/backup/version.rb +2 -2
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3ffe8d1bd32587d59e12a514b005d68175244e2cb73a9d138dd3a5d273c785f
|
|
4
|
+
data.tar.gz: 62d6364641799f1ce20466affc34052f8614a2a2d5633f53a05b4cac31a4e0f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4fdd4254948ef5dd7fba17e3683aa13bf84a19a77ffd935bad3f830355d4bf0d14ff48c731be2542ce56332ed8b4903b1b7971a16e7e947f4406ba4473b4a44
|
|
7
|
+
data.tar.gz: 4ca3ca9bb9eda64a9b8b9a6cc0889e5815f446e2a798f762f1d55e228928e99988c186bdaad4dfd7d033cd21667de7d906ea3cbd1f11d947dc77d68f4bbd1090
|
data/README.md
CHANGED
|
@@ -25,102 +25,13 @@ The {file:CHANGELOG.md CHANGELOG} has a history of the changes to the program.
|
|
|
25
25
|
* Restartable - calculate start point based on already downloaded messages
|
|
26
26
|
* Standalone - do not rely on an email client or MTA
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Development
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
better `git blame` output:
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
git config --local blame.ignoreRevsFile .git-blame-ignore-revs
|
|
35
|
-
```
|
|
30
|
+
See the [development documentation](/docs/development.md).
|
|
36
31
|
|
|
37
32
|
# Testing
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Specs under `specs/features` are integration specs.
|
|
42
|
-
Some of these specs run against two local IMAP servers
|
|
43
|
-
controlled by Podman (or Docker) Compose.
|
|
44
|
-
|
|
45
|
-
Start them before running the test suite
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
$ podman-compose -f dev/compose.yml up -d
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
or, with Docker
|
|
52
|
-
|
|
53
|
-
```sh
|
|
54
|
-
$ docker-compose -f dev/compose.yml up -d
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Then, run all specs
|
|
58
|
-
|
|
59
|
-
```sh
|
|
60
|
-
$ rspec
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
To exclude container-based tests
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
$ rspec --tag ~container
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
To run **just** the feature specs
|
|
70
|
-
|
|
71
|
-
```sh
|
|
72
|
-
rspec spec/features/**/*_spec.rb
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Full Test Run
|
|
76
|
-
|
|
77
|
-
The full test run includes RSpec specs **and** Rubocop checks
|
|
78
|
-
|
|
79
|
-
```sh
|
|
80
|
-
rake
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
# Test Debugging
|
|
84
|
-
|
|
85
|
-
The feature specs are run 'out of process' via the Aruba gem.
|
|
86
|
-
In order to see debugging output from the process,
|
|
87
|
-
use `last_command_started.output`.
|
|
88
|
-
|
|
89
|
-
# Older Rubies
|
|
90
|
-
|
|
91
|
-
A Containerfile is available to allow testing with all available Ruby versions,
|
|
92
|
-
see the README in the `dev` directory.
|
|
93
|
-
|
|
94
|
-
# Performance Specs
|
|
95
|
-
|
|
96
|
-
```sh
|
|
97
|
-
PERFORMANCE=1 rspec --order=defined
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Beware: the performance spec (just backup for now) takes a very
|
|
101
|
-
long time to run, approximately 24 hours!
|
|
102
|
-
|
|
103
|
-
# Access Docker imap server
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
106
|
-
require "net/imap"
|
|
107
|
-
require_relative "spec/features/support/30_email_server_helpers"
|
|
108
|
-
|
|
109
|
-
include EmailServerHelpers
|
|
110
|
-
|
|
111
|
-
test_connection = test_server_connection_parameters
|
|
112
|
-
|
|
113
|
-
test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
|
|
114
|
-
test_imap.login(test_connection[:username], test_connection[:password])
|
|
115
|
-
|
|
116
|
-
message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
|
|
117
|
-
response = test_imap.append("INBOX", message, nil, nil)
|
|
118
|
-
|
|
119
|
-
test_imap.examine("INBOX")
|
|
120
|
-
uids = test_imap.uid_search(["ALL"]).sort
|
|
121
|
-
|
|
122
|
-
fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
|
|
123
|
-
```
|
|
34
|
+
See the [testing documentation](/docs/testing.md).
|
|
124
35
|
|
|
125
36
|
# Contributing
|
|
126
37
|
|
data/docs/development.md
ADDED
data/docs/testing.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Setup
|
|
2
|
+
|
|
3
|
+
Specs under `spec/features` are integration specs.
|
|
4
|
+
Some of these specs run against two local IMAP servers
|
|
5
|
+
controlled by Podman (or Docker) Compose.
|
|
6
|
+
|
|
7
|
+
Start them before running the test suite
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
$ podman-compose -f dev/compose.yml up -d
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or, with Docker
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
$ docker-compose -f dev/compose.yml up -d
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
# Invocations
|
|
20
|
+
|
|
21
|
+
Run all specs
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
$ rake spec
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run **just** the unit specs
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
$ rake spec_unit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run **just** the feature specs
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
$ rake spec_feature
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To exclude the slow container-based tests
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
$ rake spec_non_container
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Full Test Run
|
|
46
|
+
|
|
47
|
+
The full test run includes RSpec specs **and** Rubocop checks
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
rake test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
# Debugging
|
|
54
|
+
|
|
55
|
+
The feature specs are run 'out of process' via the Aruba gem.
|
|
56
|
+
In order to see debugging output from the process,
|
|
57
|
+
use `last_command_started.output`.
|
|
58
|
+
|
|
59
|
+
# Older Rubies
|
|
60
|
+
|
|
61
|
+
A Containerfile is available to allow testing with all available Ruby versions,
|
|
62
|
+
see the README in the `dev` directory.
|
|
63
|
+
|
|
64
|
+
# Performance Specs
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
PERFORMANCE=1 rspec --order=defined
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Beware: the performance spec (just backup for now) takes a very
|
|
71
|
+
long time to run, approximately 24 hours!
|
|
72
|
+
|
|
73
|
+
# Access Docker imap server
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
require "net/imap"
|
|
77
|
+
require_relative "spec/features/support/30_email_server_helpers"
|
|
78
|
+
|
|
79
|
+
include EmailServerHelpers
|
|
80
|
+
|
|
81
|
+
test_connection = test_server_connection_parameters
|
|
82
|
+
|
|
83
|
+
test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
|
|
84
|
+
test_imap.login(test_connection[:username], test_connection[:password])
|
|
85
|
+
|
|
86
|
+
message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
|
|
87
|
+
response = test_imap.append("INBOX", message, nil, nil)
|
|
88
|
+
|
|
89
|
+
test_imap.examine("INBOX")
|
|
90
|
+
uids = test_imap.uid_search(["ALL"]).sort
|
|
91
|
+
|
|
92
|
+
fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
|
|
93
|
+
```
|
data/imap-backup.gemspec
CHANGED
|
@@ -18,18 +18,19 @@ Gem::Specification.new do |gem|
|
|
|
18
18
|
|
|
19
19
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
20
20
|
gem.require_paths = ["lib"]
|
|
21
|
-
gem.required_ruby_version = ">= 3.
|
|
21
|
+
gem.required_ruby_version = ">= 3.2"
|
|
22
22
|
|
|
23
|
-
gem.
|
|
24
|
-
gem.
|
|
25
|
-
gem.
|
|
26
|
-
gem.
|
|
27
|
-
gem.
|
|
28
|
-
gem.
|
|
29
|
-
gem.
|
|
30
|
-
gem.
|
|
31
|
-
gem.
|
|
32
|
-
gem.
|
|
23
|
+
gem.add_dependency "highline"
|
|
24
|
+
gem.add_dependency "logger"
|
|
25
|
+
gem.add_dependency "mail", "2.7.1"
|
|
26
|
+
gem.add_dependency "net-imap", ">= 0.3.2"
|
|
27
|
+
gem.add_dependency "net-smtp"
|
|
28
|
+
gem.add_dependency "os"
|
|
29
|
+
gem.add_dependency "ostruct"
|
|
30
|
+
gem.add_dependency "rake"
|
|
31
|
+
gem.add_dependency "sys-proctable"
|
|
32
|
+
gem.add_dependency "thor", "~> 1.1"
|
|
33
|
+
gem.add_dependency "thunderbird", "0.3.0"
|
|
33
34
|
|
|
34
35
|
gem.metadata = {
|
|
35
36
|
"rubygems_mfa_required" => "true"
|
|
@@ -2,6 +2,7 @@ require "imap/backup/account/backup_folders"
|
|
|
2
2
|
require "imap/backup/account/folder_backup"
|
|
3
3
|
require "imap/backup/account/folder_ensurer"
|
|
4
4
|
require "imap/backup/account/local_only_folder_deleter"
|
|
5
|
+
require "imap/backup/account/locker"
|
|
5
6
|
|
|
6
7
|
module Imap; end
|
|
7
8
|
|
|
@@ -10,6 +11,8 @@ module Imap::Backup
|
|
|
10
11
|
|
|
11
12
|
# Carries out the backup of the configured folders of the account
|
|
12
13
|
class Account::Backup
|
|
14
|
+
# @param account [Account] the account to back up
|
|
15
|
+
# @param refresh [Boolean] true to refresh folder metadata even if mirror is disabled
|
|
13
16
|
def initialize(account:, refresh: false)
|
|
14
17
|
@account = account
|
|
15
18
|
@refresh = refresh
|
|
@@ -22,19 +25,17 @@ module Imap::Backup
|
|
|
22
25
|
# start the connection so we get logging messages in the right order
|
|
23
26
|
account.client.login
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
).to_a
|
|
28
|
+
ensure_folder
|
|
29
|
+
delete_local_only_folders if account.mirror_mode
|
|
30
|
+
|
|
29
31
|
if backup_folders.none?
|
|
30
32
|
Logger.logger.warn "No folders found to backup for account '#{account.username}'"
|
|
31
33
|
return
|
|
32
34
|
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
locker.with_lock do
|
|
37
|
+
perform_backup
|
|
36
38
|
end
|
|
37
|
-
Logger.logger.debug "Backup of account '#{account.username}' complete"
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
private
|
|
@@ -42,9 +43,30 @@ module Imap::Backup
|
|
|
42
43
|
attr_reader :account
|
|
43
44
|
attr_reader :refresh
|
|
44
45
|
|
|
45
|
-
def
|
|
46
|
+
def backup_folders
|
|
47
|
+
@backup_folders ||= Account::BackupFolders.new(
|
|
48
|
+
client: account.client, account: account
|
|
49
|
+
).to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete_local_only_folders
|
|
53
|
+
Account::LocalOnlyFolderDeleter.new(account: account).run
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ensure_folder
|
|
46
57
|
Account::FolderEnsurer.new(account: account).run
|
|
47
|
-
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def locker
|
|
61
|
+
@locker ||= Account::Locker.new(account: account)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def perform_backup
|
|
65
|
+
Logger.logger.debug "Starting backup of #{backup_folders.count} folders"
|
|
66
|
+
backup_folders.each do |folder|
|
|
67
|
+
Account::FolderBackup.new(account: account, folder: folder, refresh: refresh).run
|
|
68
|
+
end
|
|
69
|
+
Logger.logger.debug "Backup of account '#{account.username}' complete"
|
|
48
70
|
end
|
|
49
71
|
end
|
|
50
72
|
end
|
|
@@ -9,6 +9,8 @@ module Imap::Backup
|
|
|
9
9
|
class Account::BackupFolders
|
|
10
10
|
include Enumerable
|
|
11
11
|
|
|
12
|
+
# @param client [Client::AutomaticLoginWrapper] the IMAP client that lists folders
|
|
13
|
+
# @param account [Account] the account whose folders are being backed up
|
|
12
14
|
def initialize(client:, account:)
|
|
13
15
|
@client = client
|
|
14
16
|
@account = account
|
|
@@ -23,6 +23,8 @@ module Imap::Backup
|
|
|
23
23
|
# @return [String] the name of the folder
|
|
24
24
|
attr_reader :name
|
|
25
25
|
|
|
26
|
+
# @param client [Client::Default] the IMAP client used to talk to the server
|
|
27
|
+
# @param name [String] the UTF-7 encoded folder name
|
|
26
28
|
def initialize(client, name)
|
|
27
29
|
@client = client
|
|
28
30
|
@name = name
|
|
@@ -184,6 +186,13 @@ module Imap::Backup
|
|
|
184
186
|
CREATE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
|
|
185
187
|
EXAMINE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
|
|
186
188
|
PERMITTED_FLAGS = %i(Answered Draft Flagged Seen).freeze
|
|
189
|
+
private_constant :BODY_ATTRIBUTE,
|
|
190
|
+
:UID_FETCH_RETRY_CLASSES,
|
|
191
|
+
:UID_SEARCH_RETRY_CLASSES,
|
|
192
|
+
:APPEND_RETRY_CLASSES,
|
|
193
|
+
:CREATE_RETRY_CLASSES,
|
|
194
|
+
:EXAMINE_RETRY_CLASSES,
|
|
195
|
+
:PERMITTED_FLAGS
|
|
187
196
|
|
|
188
197
|
def examine
|
|
189
198
|
client.examine(utf7_encoded_name)
|
|
@@ -12,6 +12,9 @@ module Imap::Backup
|
|
|
12
12
|
|
|
13
13
|
# Implements backup for a single folder
|
|
14
14
|
class Account::FolderBackup
|
|
15
|
+
# @param account [Account] the account that owns the folder
|
|
16
|
+
# @param folder [Account::Folder] the online folder being backed up
|
|
17
|
+
# @param refresh [Boolean] true to refresh metadata regardless of mirror mode
|
|
15
18
|
def initialize(account:, folder:, refresh: false)
|
|
16
19
|
@account = account
|
|
17
20
|
@folder = folder
|
|
@@ -11,6 +11,12 @@ module Imap::Backup
|
|
|
11
11
|
|
|
12
12
|
# Implements a folder enumerator for backed-up accounts
|
|
13
13
|
class Account::FolderMapper
|
|
14
|
+
# @param account [Account] the account whose local folders are being iterated
|
|
15
|
+
# @param destination [Account] the destination account
|
|
16
|
+
# @param destination_delimiter [String] the delimiter to use for destination folder names
|
|
17
|
+
# @param destination_prefix [String] a prefix applied to destination folder names
|
|
18
|
+
# @param source_delimiter [String] the delimiter used in the source account
|
|
19
|
+
# @param source_prefix [String] a prefix applied to source folder names
|
|
14
20
|
def initialize(
|
|
15
21
|
account:,
|
|
16
22
|
destination:,
|
|
@@ -10,6 +10,7 @@ module Imap::Backup
|
|
|
10
10
|
# This is used in mirror mode, where local copies are only kept as long as they
|
|
11
11
|
# exist on the server.
|
|
12
12
|
class Account::LocalOnlyFolderDeleter
|
|
13
|
+
# @param account [Account] the account whose serialized folders are being pruned
|
|
13
14
|
def initialize(account:)
|
|
14
15
|
@account = account
|
|
15
16
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "imap/backup/account/folder_ensurer"
|
|
2
|
+
require "imap/backup/lockfile"
|
|
3
|
+
|
|
4
|
+
module Imap; end
|
|
5
|
+
|
|
6
|
+
module Imap::Backup
|
|
7
|
+
class Account; end
|
|
8
|
+
|
|
9
|
+
class Account::Locker
|
|
10
|
+
attr_reader :account
|
|
11
|
+
|
|
12
|
+
# @param account [Account] the account whose backup must be locked
|
|
13
|
+
def initialize(account:)
|
|
14
|
+
@account = account
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def with_lock(&block)
|
|
18
|
+
lockfile = Lockfile.new(path: account.lockfile_path)
|
|
19
|
+
if lockfile.exists?
|
|
20
|
+
if !lockfile.stale?
|
|
21
|
+
raise Lockfile::LockfileExistsError,
|
|
22
|
+
"Lockfile '#{account.lockfile_path}' exists and is not stale."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Logger.logger.info("Stale lockfile '#{account.lockfile_path}' found. Removing it.")
|
|
26
|
+
lockfile.remove
|
|
27
|
+
else
|
|
28
|
+
Account::FolderEnsurer.new(account: account).run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
lockfile.with_lock do
|
|
33
|
+
block.call
|
|
34
|
+
end
|
|
35
|
+
rescue Lockfile::ProcessStartTimeUnavailableError
|
|
36
|
+
block.call
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "imap/backup/account/folder_mapper"
|
|
2
|
+
require "imap/backup/account/locker"
|
|
2
3
|
require "imap/backup/uploader"
|
|
3
4
|
|
|
4
5
|
module Imap; end
|
|
@@ -8,17 +9,23 @@ module Imap::Backup
|
|
|
8
9
|
|
|
9
10
|
# Restores all backed up folders to the server
|
|
10
11
|
class Account::Restore
|
|
12
|
+
# @param account [Account] the account whose backups will be restored
|
|
13
|
+
# @param delimiter [String] the destination folder delimiter
|
|
14
|
+
# @param prefix [String] a prefix applied to restored folder names
|
|
11
15
|
def initialize(account:, delimiter: "/", prefix: "")
|
|
12
16
|
@account = account
|
|
13
17
|
@destination_delimiter = delimiter
|
|
14
18
|
@destination_prefix = prefix
|
|
19
|
+
@locker = nil
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
# Runs the restore operation
|
|
18
23
|
# @return [void]
|
|
19
24
|
def run
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
locker.with_lock do
|
|
26
|
+
folders.each do |serializer, folder|
|
|
27
|
+
Uploader.new(folder, serializer).run
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
end
|
|
24
31
|
|
|
@@ -40,5 +47,9 @@ module Imap::Backup
|
|
|
40
47
|
def folders
|
|
41
48
|
Account::FolderMapper.new(**enumerator_options)
|
|
42
49
|
end
|
|
50
|
+
|
|
51
|
+
def locker
|
|
52
|
+
@locker ||= Account::Locker.new(account: account)
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
end
|
data/lib/imap/backup/account.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "imap/backup/account/client_factory"
|
|
|
4
4
|
|
|
5
5
|
module Imap; end
|
|
6
6
|
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
8
|
module Imap::Backup
|
|
8
9
|
# Contains the attributes relating to an email account.
|
|
9
10
|
class Account
|
|
@@ -64,6 +65,25 @@ module Imap::Backup
|
|
|
64
65
|
# @return [String] one of "active" (the default), "archived", or "offline"
|
|
65
66
|
attr_reader :status
|
|
66
67
|
|
|
68
|
+
# @param options [Hash] Account attributes
|
|
69
|
+
# @option opts [String] :username the username of the account (usually the same as the email
|
|
70
|
+
# address)
|
|
71
|
+
# @option opts [String] :password the password of the account
|
|
72
|
+
# @option opts [String] :local_path the path where backups will be saved
|
|
73
|
+
# @option opts [Array<String>] :folders (nil) the list of folders that have been configured for
|
|
74
|
+
# the Account
|
|
75
|
+
# @option opts [Boolean] :folder_blacklist (false) whether the folders attribute is a blacklist
|
|
76
|
+
# @option opts [Boolean] :mirror_mode (false) whether to run in mirror mode
|
|
77
|
+
# @option opts [String] :server the address of the IMAP server
|
|
78
|
+
# @option opts [Hash] :connection_options (nil) additional connection options for the IMAP
|
|
79
|
+
# server
|
|
80
|
+
# @option opts [String] :download_strategy (nil) the name of the download strategy to adopt
|
|
81
|
+
# during backups
|
|
82
|
+
# @option opts [Integer] :multi_fetch_size (nil) the number of emails to fetch from the IMAP
|
|
83
|
+
# server at a time
|
|
84
|
+
# @option opts [Boolean] :reset_seen_flags_after_fetch (false) whether to reset seen flags after
|
|
85
|
+
# fetching messages
|
|
86
|
+
# @option opts [String] :status ("active") the status of the account
|
|
67
87
|
def initialize(options)
|
|
68
88
|
check_options!(options)
|
|
69
89
|
@username = options[:username]
|
|
@@ -130,19 +150,19 @@ module Imap::Backup
|
|
|
130
150
|
# @return [Hash] all Account data for serialization
|
|
131
151
|
def to_h
|
|
132
152
|
h = {
|
|
133
|
-
username:
|
|
134
|
-
password:
|
|
153
|
+
username: username,
|
|
154
|
+
password: password,
|
|
135
155
|
status: status
|
|
136
156
|
}
|
|
137
|
-
h[:local_path] =
|
|
138
|
-
h[:folders] =
|
|
139
|
-
h[:folder_blacklist] = true if
|
|
140
|
-
h[:mirror_mode] = true if
|
|
141
|
-
h[:server] =
|
|
142
|
-
h[:connection_options] =
|
|
157
|
+
h[:local_path] = local_path if local_path
|
|
158
|
+
h[:folders] = folders if folders
|
|
159
|
+
h[:folder_blacklist] = true if folder_blacklist
|
|
160
|
+
h[:mirror_mode] = true if mirror_mode
|
|
161
|
+
h[:server] = server if server
|
|
162
|
+
h[:connection_options] = connection_options if connection_options
|
|
143
163
|
h[:multi_fetch_size] = multi_fetch_size
|
|
144
|
-
if
|
|
145
|
-
h[:reset_seen_flags_after_fetch] =
|
|
164
|
+
if reset_seen_flags_after_fetch
|
|
165
|
+
h[:reset_seen_flags_after_fetch] = reset_seen_flags_after_fetch
|
|
146
166
|
end
|
|
147
167
|
h
|
|
148
168
|
end
|
|
@@ -168,6 +188,14 @@ module Imap::Backup
|
|
|
168
188
|
update(:local_path, value)
|
|
169
189
|
end
|
|
170
190
|
|
|
191
|
+
# @raise [RuntimeError] if the local_path is not set
|
|
192
|
+
# @return [String] the path to the lockfile for the account
|
|
193
|
+
def lockfile_path
|
|
194
|
+
raise "local_path is not set" if !local_path
|
|
195
|
+
|
|
196
|
+
File.join(local_path, "imap-backup.lock")
|
|
197
|
+
end
|
|
198
|
+
|
|
171
199
|
# @raise [RuntimeError] if the supplied value is not an Array
|
|
172
200
|
# @return [void]
|
|
173
201
|
def folders=(value)
|
|
@@ -207,12 +235,11 @@ module Imap::Backup
|
|
|
207
235
|
def connection_options=(value)
|
|
208
236
|
# Ensure we've loaded the connection_options
|
|
209
237
|
connection_options
|
|
210
|
-
parsed =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end
|
|
238
|
+
parsed = if value == ""
|
|
239
|
+
nil
|
|
240
|
+
else
|
|
241
|
+
JSON.parse(value, symbolize_names: true)
|
|
242
|
+
end
|
|
216
243
|
update(:connection_options, parsed)
|
|
217
244
|
end
|
|
218
245
|
|
|
@@ -283,14 +310,19 @@ module Imap::Backup
|
|
|
283
310
|
|
|
284
311
|
attr_reader :changes
|
|
285
312
|
|
|
286
|
-
REQUIRED_ATTRIBUTES = %i[password username].freeze
|
|
313
|
+
REQUIRED_ATTRIBUTES = %i[password username local_path].freeze
|
|
287
314
|
OPTIONAL_ATTRIBUTES = %i[
|
|
288
|
-
connection_options download_strategy folders folder_blacklist
|
|
315
|
+
connection_options download_strategy folders folder_blacklist mirror_mode
|
|
289
316
|
multi_fetch_size reset_seen_flags_after_fetch server status
|
|
290
317
|
].freeze
|
|
291
318
|
KNOWN_ATTRIBUTES = REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES
|
|
292
319
|
VALID_STATUSES = %w[active archived offline].freeze
|
|
293
320
|
DEFAULT_STATUS = "active".freeze
|
|
321
|
+
private_constant :REQUIRED_ATTRIBUTES,
|
|
322
|
+
:OPTIONAL_ATTRIBUTES,
|
|
323
|
+
:KNOWN_ATTRIBUTES,
|
|
324
|
+
:VALID_STATUSES,
|
|
325
|
+
:DEFAULT_STATUS
|
|
294
326
|
|
|
295
327
|
def check_options!(options)
|
|
296
328
|
missing_required = REQUIRED_ATTRIBUTES - options.keys
|
|
@@ -15,6 +15,10 @@ module Imap::Backup
|
|
|
15
15
|
include Thor::Actions
|
|
16
16
|
include CLI::Helpers
|
|
17
17
|
|
|
18
|
+
# @param options [Hash] CLI options controlling output
|
|
19
|
+
# @option opts [String] :config (nil) the path to the configuration file
|
|
20
|
+
# @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
|
|
21
|
+
# @option opts [Boolean] :refresh (false) whether to force refresh of folder metadata
|
|
18
22
|
def initialize(options)
|
|
19
23
|
super([])
|
|
20
24
|
@options = options
|
|
@@ -64,6 +68,8 @@ module Imap::Backup
|
|
|
64
68
|
case exception
|
|
65
69
|
when Net::IMAP::NoResponseError, Errno::ECONNREFUSED
|
|
66
70
|
111
|
|
71
|
+
when Lockfile::LockfileExistsError
|
|
72
|
+
112
|
|
67
73
|
else
|
|
68
74
|
1
|
|
69
75
|
end
|
|
@@ -14,6 +14,11 @@ module Imap::Backup
|
|
|
14
14
|
class CLI::Local::Check
|
|
15
15
|
include CLI::Helpers
|
|
16
16
|
|
|
17
|
+
# @param options [Hash] CLI options controlling output
|
|
18
|
+
# @option opts [String] :config (nil) the path to the configuration file
|
|
19
|
+
# @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
|
|
20
|
+
# @option opts [Boolean] :delete_corrupt (false) whether to delete corrupt folders
|
|
21
|
+
# @option opts [String] :format ("text") the output format, either "text" or "json"
|
|
17
22
|
def initialize(options)
|
|
18
23
|
@options = options
|
|
19
24
|
end
|