imap-backup 5.2.0 → 6.0.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 +9 -2
- data/docs/development.md +10 -4
- data/imap-backup.gemspec +5 -1
- data/lib/cli_coverage.rb +11 -11
- data/lib/email/provider/base.rb +2 -0
- data/lib/email/provider/unknown.rb +2 -0
- data/lib/email/provider.rb +2 -0
- data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
- data/lib/imap/backup/account/connection/client_factory.rb +54 -0
- data/lib/imap/backup/account/connection/folder_names.rb +26 -0
- data/lib/imap/backup/account/connection.rb +17 -105
- data/lib/imap/backup/account/folder.rb +9 -6
- data/lib/imap/backup/account.rb +36 -16
- data/lib/imap/backup/cli/backup.rb +1 -3
- data/lib/imap/backup/cli/folders.rb +3 -3
- data/lib/imap/backup/cli/helpers.rb +24 -22
- data/lib/imap/backup/cli/local.rb +20 -13
- data/lib/imap/backup/cli/migrate.rb +5 -11
- data/lib/imap/backup/cli/restore.rb +8 -7
- data/lib/imap/backup/cli/setup.rb +10 -8
- data/lib/imap/backup/cli/stats.rb +78 -0
- data/lib/imap/backup/cli/status.rb +2 -2
- data/lib/imap/backup/cli/utils.rb +6 -8
- data/lib/imap/backup/cli.rb +24 -3
- data/lib/imap/backup/configuration.rb +9 -21
- data/lib/imap/backup/downloader.rb +56 -34
- data/lib/imap/backup/migrator.rb +5 -5
- data/lib/imap/backup/sanitizer.rb +3 -2
- data/lib/imap/backup/serializer/appender.rb +49 -0
- data/lib/imap/backup/serializer/directory.rb +37 -0
- data/lib/imap/backup/serializer/imap.rb +144 -0
- data/lib/imap/backup/serializer/mbox.rb +33 -88
- data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
- data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
- data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
- data/lib/imap/backup/serializer.rb +160 -3
- data/lib/imap/backup/setup/account/header.rb +75 -0
- data/lib/imap/backup/setup/account.rb +41 -95
- data/lib/imap/backup/setup/asker.rb +4 -15
- data/lib/imap/backup/setup/backup_path.rb +41 -0
- data/lib/imap/backup/setup/email.rb +45 -0
- data/lib/imap/backup/setup/folder_chooser.rb +3 -3
- data/lib/imap/backup/setup/helpers.rb +2 -2
- data/lib/imap/backup/setup.rb +5 -4
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +41 -22
- data/lib/imap/backup/uploader.rb +46 -8
- data/lib/imap/backup/utils.rb +1 -1
- data/lib/imap/backup/version.rb +3 -3
- data/lib/imap/backup.rb +0 -2
- metadata +31 -105
- data/lib/imap/backup/serializer/mbox_store.rb +0 -217
- data/spec/features/backup_spec.rb +0 -108
- data/spec/features/configuration/minimal_configuration.rb +0 -15
- data/spec/features/configuration/missing_configuration.rb +0 -14
- data/spec/features/folders_spec.rb +0 -36
- data/spec/features/helper.rb +0 -2
- data/spec/features/local/list_accounts_spec.rb +0 -12
- data/spec/features/local/list_emails_spec.rb +0 -21
- data/spec/features/local/list_folders_spec.rb +0 -21
- data/spec/features/local/show_an_email_spec.rb +0 -34
- data/spec/features/migrate_spec.rb +0 -35
- data/spec/features/remote/list_account_folders_spec.rb +0 -16
- data/spec/features/restore_spec.rb +0 -162
- data/spec/features/status_spec.rb +0 -43
- data/spec/features/support/aruba.rb +0 -77
- data/spec/features/support/backup_directory.rb +0 -43
- data/spec/features/support/email_server.rb +0 -110
- data/spec/features/support/shared/connection_context.rb +0 -14
- data/spec/features/support/shared/message_fixtures.rb +0 -16
- data/spec/fixtures/connection.yml +0 -7
- data/spec/spec_helper.rb +0 -15
- data/spec/support/fixtures.rb +0 -11
- data/spec/support/higline_test_helpers.rb +0 -8
- data/spec/support/silence_logging.rb +0 -7
- data/spec/unit/email/mboxrd/message_spec.rb +0 -177
- data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
- data/spec/unit/email/provider/base_spec.rb +0 -11
- data/spec/unit/email/provider/fastmail_spec.rb +0 -7
- data/spec/unit/email/provider/gmail_spec.rb +0 -7
- data/spec/unit/email/provider_spec.rb +0 -27
- data/spec/unit/imap/backup/account/connection_spec.rb +0 -405
- data/spec/unit/imap/backup/account/folder_spec.rb +0 -251
- data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -47
- data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
- data/spec/unit/imap/backup/cli/local_spec.rb +0 -81
- data/spec/unit/imap/backup/cli/utils_spec.rb +0 -62
- data/spec/unit/imap/backup/client/default_spec.rb +0 -22
- data/spec/unit/imap/backup/configuration_spec.rb +0 -238
- data/spec/unit/imap/backup/downloader_spec.rb +0 -44
- data/spec/unit/imap/backup/logger_spec.rb +0 -48
- data/spec/unit/imap/backup/migrator_spec.rb +0 -58
- data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -222
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
- data/spec/unit/imap/backup/setup/account_spec.rb +0 -366
- data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
- data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
- data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
- data/spec/unit/imap/backup/setup_spec.rb +0 -301
- data/spec/unit/imap/backup/uploader_spec.rb +0 -54
- data/spec/unit/imap/backup/utils_spec.rb +0 -92
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
require "imap/backup"
|
|
2
2
|
require "imap/backup/cli/accounts"
|
|
3
3
|
|
|
4
|
-
module Imap::Backup
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
module Imap::Backup
|
|
5
|
+
module CLI::Helpers
|
|
6
|
+
def symbolized(options)
|
|
7
|
+
options.each.with_object({}) do |(k, v), acc|
|
|
8
|
+
key = k.gsub("-", "_").intern
|
|
9
|
+
acc[key] = v
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
|
-
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
def account(email)
|
|
14
|
+
accounts = CLI::Accounts.new
|
|
15
|
+
account = accounts.find { |a| a.username == email }
|
|
16
|
+
raise "#{email} is not a configured account" if !account
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
account
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
def connection(email)
|
|
22
|
+
account = account(email)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
Account::Connection.new(account)
|
|
25
|
+
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
def each_connection(names)
|
|
28
|
+
accounts = CLI::Accounts.new(names)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
accounts.each do |account|
|
|
31
|
+
yield account.connection
|
|
32
|
+
end
|
|
33
|
+
rescue ConfigurationNotFound
|
|
34
|
+
raise "imap-backup is not configured. Run `imap-backup setup`"
|
|
31
35
|
end
|
|
32
|
-
rescue Imap::Backup::ConfigurationNotFound
|
|
33
|
-
raise "imap-backup is not configured. Run `imap-backup setup`"
|
|
34
36
|
end
|
|
35
37
|
end
|
|
@@ -5,6 +5,8 @@ module Imap::Backup
|
|
|
5
5
|
include Thor::Actions
|
|
6
6
|
include CLI::Helpers
|
|
7
7
|
|
|
8
|
+
MAX_SUBJECT = 60
|
|
9
|
+
|
|
8
10
|
desc "accounts", "List locally backed-up accounts"
|
|
9
11
|
def accounts
|
|
10
12
|
accounts = CLI::Accounts.new
|
|
@@ -29,26 +31,16 @@ module Imap::Backup
|
|
|
29
31
|
end
|
|
30
32
|
raise "Folder '#{folder_name}' not found" if !folder_serializer
|
|
31
33
|
|
|
32
|
-
max_subject = 60
|
|
33
34
|
Kernel.puts format(
|
|
34
|
-
"%-10<uid>s %-#{
|
|
35
|
+
"%-10<uid>s %-#{MAX_SUBJECT}<subject>s - %<date>s",
|
|
35
36
|
{uid: "UID", subject: "Subject", date: "Date"}
|
|
36
37
|
)
|
|
37
|
-
Kernel.puts "-" * (12 +
|
|
38
|
+
Kernel.puts "-" * (12 + MAX_SUBJECT + 28)
|
|
38
39
|
|
|
39
40
|
uids = folder_serializer.uids
|
|
40
41
|
|
|
41
42
|
folder_serializer.each_message(uids).map do |uid, message|
|
|
42
|
-
|
|
43
|
-
uid: uid,
|
|
44
|
-
date: message.date.to_s,
|
|
45
|
-
subject: message.subject || ""
|
|
46
|
-
}
|
|
47
|
-
if m[:subject].length > max_subject
|
|
48
|
-
Kernel.puts format("% 10<uid>u: %.#{max_subject - 3}<subject>s... - %<date>s", m)
|
|
49
|
-
else
|
|
50
|
-
Kernel.puts format("% 10<uid>u: %-#{max_subject}<subject>s - %<date>s", m)
|
|
51
|
-
end
|
|
43
|
+
list_message uid, message
|
|
52
44
|
end
|
|
53
45
|
end
|
|
54
46
|
|
|
@@ -78,5 +70,20 @@ module Imap::Backup
|
|
|
78
70
|
Kernel.puts message.supplied_body
|
|
79
71
|
end
|
|
80
72
|
end
|
|
73
|
+
|
|
74
|
+
no_commands do
|
|
75
|
+
def list_message(uid, message)
|
|
76
|
+
m = {
|
|
77
|
+
uid: uid,
|
|
78
|
+
date: message.date.to_s,
|
|
79
|
+
subject: message.subject || ""
|
|
80
|
+
}
|
|
81
|
+
if m[:subject].length > MAX_SUBJECT
|
|
82
|
+
Kernel.puts format("% 10<uid>u: %.#{MAX_SUBJECT - 3}<subject>s... - %<date>s", m)
|
|
83
|
+
else
|
|
84
|
+
Kernel.puts format("% 10<uid>u: %-#{MAX_SUBJECT}<subject>s - %<date>s", m)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
@@ -38,13 +38,9 @@ module Imap::Backup
|
|
|
38
38
|
raise "Source and destination accounts cannot be the same!"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
if !destination_account
|
|
42
|
-
raise "Account #{destination_email} does not exist"
|
|
43
|
-
end
|
|
41
|
+
raise "Account '#{destination_email}' does not exist" if !destination_account
|
|
44
42
|
|
|
45
|
-
if !source_account
|
|
46
|
-
raise "Account #{source_email} does not exist"
|
|
47
|
-
end
|
|
43
|
+
raise "Account '#{source_email}' does not exist" if !source_account
|
|
48
44
|
end
|
|
49
45
|
|
|
50
46
|
def config
|
|
@@ -61,7 +57,7 @@ module Imap::Backup
|
|
|
61
57
|
glob = File.join(source_local_path, "**", "*.imap")
|
|
62
58
|
Pathname.glob(glob) do |path|
|
|
63
59
|
name = source_folder_name(path)
|
|
64
|
-
serializer = Serializer
|
|
60
|
+
serializer = Serializer.new(source_local_path, name)
|
|
65
61
|
folder = folder_for(name)
|
|
66
62
|
yield serializer, folder
|
|
67
63
|
end
|
|
@@ -69,16 +65,14 @@ module Imap::Backup
|
|
|
69
65
|
|
|
70
66
|
def folder_for(source_folder)
|
|
71
67
|
no_source_prefix =
|
|
72
|
-
if source_prefix != "" &&
|
|
73
|
-
source_folder.start_with?(source_prefix)
|
|
68
|
+
if source_prefix != "" && source_folder.start_with?(source_prefix)
|
|
74
69
|
source_folder.delete_prefix(source_prefix)
|
|
75
70
|
else
|
|
76
71
|
source_folder.to_s
|
|
77
72
|
end
|
|
78
73
|
|
|
79
74
|
with_destination_prefix =
|
|
80
|
-
if destination_prefix &&
|
|
81
|
-
destination_prefix != ""
|
|
75
|
+
if destination_prefix && destination_prefix != ""
|
|
82
76
|
destination_prefix + no_source_prefix
|
|
83
77
|
else
|
|
84
78
|
no_source_prefix
|
|
@@ -7,11 +7,9 @@ module Imap::Backup
|
|
|
7
7
|
attr_reader :account_names
|
|
8
8
|
|
|
9
9
|
def initialize(email = nil, options)
|
|
10
|
+
super([])
|
|
10
11
|
@email = email
|
|
11
|
-
@account_names =
|
|
12
|
-
if options.key?(:accounts)
|
|
13
|
-
options[:accounts].split(",")
|
|
14
|
-
end
|
|
12
|
+
@account_names = options[:accounts].split(",") if options.key?(:accounts)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
no_commands do
|
|
@@ -22,12 +20,15 @@ module Imap::Backup
|
|
|
22
20
|
connection.restore
|
|
23
21
|
when !email && !account_names
|
|
24
22
|
Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
|
|
25
|
-
each_connection([]
|
|
23
|
+
each_connection([], &:restore)
|
|
26
24
|
when email && account_names.any?
|
|
27
25
|
raise "Pass either an email or the --accounts option, not both"
|
|
28
26
|
when account_names.any?
|
|
29
|
-
Logger.logger.info
|
|
30
|
-
|
|
27
|
+
Logger.logger.info(
|
|
28
|
+
"Calling restore with the --account option is deprected, " \
|
|
29
|
+
"please pass a single EMAIL argument"
|
|
30
|
+
)
|
|
31
|
+
each_connection(account_names, &:restore)
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module Imap::Backup
|
|
2
|
+
class CLI::Setup < Thor
|
|
3
|
+
include Thor::Actions
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
def initialize
|
|
6
|
+
super([])
|
|
7
|
+
end
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
no_commands do
|
|
10
|
+
def run
|
|
11
|
+
Setup.new.run
|
|
12
|
+
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Imap::Backup
|
|
2
|
+
class CLI::Stats < Thor
|
|
3
|
+
include Thor::Actions
|
|
4
|
+
include CLI::Helpers
|
|
5
|
+
|
|
6
|
+
TEXT_COLUMNS = [
|
|
7
|
+
{name: :folder, width: 20, alignment: :left},
|
|
8
|
+
{name: :remote, width: 8, alignment: :right},
|
|
9
|
+
{name: :both, width: 8, alignment: :right},
|
|
10
|
+
{name: :local, width: 8, alignment: :right}
|
|
11
|
+
].freeze
|
|
12
|
+
ALIGNMENT_FORMAT_SYMBOL = {left: "-", right: " "}.freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :email
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
def initialize(email, options = {})
|
|
18
|
+
super([])
|
|
19
|
+
@email = email
|
|
20
|
+
@options = options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
no_commands do
|
|
24
|
+
def run
|
|
25
|
+
case options[:format]
|
|
26
|
+
when "json"
|
|
27
|
+
Kernel.puts stats.to_json
|
|
28
|
+
else
|
|
29
|
+
format_text stats
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stats
|
|
34
|
+
connection = connection(email)
|
|
35
|
+
|
|
36
|
+
connection.backup_folders.map do |folder|
|
|
37
|
+
next if !folder.exist?
|
|
38
|
+
|
|
39
|
+
serializer = Serializer.new(connection.account.local_path, folder.name)
|
|
40
|
+
local_uids = serializer.uids
|
|
41
|
+
remote_uids = folder.uids
|
|
42
|
+
{
|
|
43
|
+
folder: folder.name,
|
|
44
|
+
remote: (remote_uids - local_uids).count,
|
|
45
|
+
both: (serializer.uids & folder.uids).count,
|
|
46
|
+
local: (local_uids - remote_uids).count
|
|
47
|
+
}
|
|
48
|
+
end.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format_text(stats)
|
|
52
|
+
Kernel.puts text_header
|
|
53
|
+
|
|
54
|
+
stats.each do |stat|
|
|
55
|
+
columns = TEXT_COLUMNS.map do |column|
|
|
56
|
+
symbol = ALIGNMENT_FORMAT_SYMBOL[column[:alignment]]
|
|
57
|
+
count = stat[column[:name]]
|
|
58
|
+
format("%#{symbol}#{column[:width]}s", count)
|
|
59
|
+
end.join("|")
|
|
60
|
+
|
|
61
|
+
Kernel.puts columns
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def text_header
|
|
66
|
+
titles = TEXT_COLUMNS.map do |column|
|
|
67
|
+
format("%-#{column[:width]}s", column[:name])
|
|
68
|
+
end.join("|")
|
|
69
|
+
|
|
70
|
+
underline = TEXT_COLUMNS.map do |column|
|
|
71
|
+
"-" * column[:width]
|
|
72
|
+
end.join("|")
|
|
73
|
+
|
|
74
|
+
"#{titles}\n#{underline}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -13,11 +13,11 @@ module Imap::Backup
|
|
|
13
13
|
no_commands do
|
|
14
14
|
def run
|
|
15
15
|
each_connection(account_names) do |connection|
|
|
16
|
-
puts connection.account.username
|
|
16
|
+
Kernel.puts connection.account.username
|
|
17
17
|
folders = connection.status
|
|
18
18
|
folders.each do |f|
|
|
19
19
|
missing_locally = f[:remote] - f[:local]
|
|
20
|
-
puts "#{f[:name]}: #{missing_locally.size}"
|
|
20
|
+
Kernel.puts "#{f[:name]}: #{missing_locally.size}"
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -14,7 +14,7 @@ module Imap::Backup
|
|
|
14
14
|
connection.backup_folders.each do |folder|
|
|
15
15
|
next if !folder.exist?
|
|
16
16
|
|
|
17
|
-
serializer = Serializer
|
|
17
|
+
serializer = Serializer.new(connection.account.local_path, folder.name)
|
|
18
18
|
do_ignore_folder_history(folder, serializer)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -47,11 +47,9 @@ module Imap::Backup
|
|
|
47
47
|
profile = thunderbird_profile(profile_name)
|
|
48
48
|
|
|
49
49
|
if !profile
|
|
50
|
-
if profile_name
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
raise "Default Thunderbird profile not found"
|
|
54
|
-
end
|
|
50
|
+
raise "Thunderbird profile '#{profile_name}' not found" if profile_name
|
|
51
|
+
|
|
52
|
+
raise "Default Thunderbird profile not found"
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
connection.local_folders.each do |serializer, _folder|
|
|
@@ -64,7 +62,7 @@ module Imap::Backup
|
|
|
64
62
|
no_commands do
|
|
65
63
|
def do_ignore_folder_history(folder, serializer)
|
|
66
64
|
uids = folder.uids - serializer.uids
|
|
67
|
-
|
|
65
|
+
Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
|
|
68
66
|
|
|
69
67
|
serializer.apply_uid_validity(folder.uid_validity)
|
|
70
68
|
|
|
@@ -75,7 +73,7 @@ module Imap::Backup
|
|
|
75
73
|
Skipped #{uid}
|
|
76
74
|
MESSAGE
|
|
77
75
|
|
|
78
|
-
serializer.
|
|
76
|
+
serializer.append uid, message
|
|
79
77
|
end
|
|
80
78
|
end
|
|
81
79
|
|
data/lib/imap/backup/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Imap::Backup
|
|
|
13
13
|
autoload :Remote, "imap/backup/cli/remote"
|
|
14
14
|
autoload :Restore, "imap/backup/cli/restore"
|
|
15
15
|
autoload :Setup, "imap/backup/cli/setup"
|
|
16
|
+
autoload :Stats, "imap/backup/cli/stats"
|
|
16
17
|
autoload :Status, "imap/backup/cli/status"
|
|
17
18
|
autoload :Utils, "imap/backup/cli/utils"
|
|
18
19
|
|
|
@@ -61,8 +62,11 @@ module Imap::Backup
|
|
|
61
62
|
desc "local SUBCOMMAND [OPTIONS]", "View local info"
|
|
62
63
|
subcommand "local", Local
|
|
63
64
|
|
|
64
|
-
desc
|
|
65
|
-
"
|
|
65
|
+
desc(
|
|
66
|
+
"migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
|
|
67
|
+
"[Experimental] " \
|
|
68
|
+
"Uploads backed-up emails from account SOURCE_EMAIL to account DESTINATION_EMAIL"
|
|
69
|
+
)
|
|
66
70
|
long_desc <<~DESC
|
|
67
71
|
All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
|
|
68
72
|
uploaded to the "destination account" (DESTINATION_EMAIL).
|
|
@@ -125,9 +129,26 @@ module Imap::Backup
|
|
|
125
129
|
Setup.new.run
|
|
126
130
|
end
|
|
127
131
|
|
|
128
|
-
desc "
|
|
132
|
+
desc "stats EMAIL [OPTIONS]", "Print stats for each account folder"
|
|
133
|
+
long_desc <<~DESC
|
|
134
|
+
For each account folder, lists emails that are yet to be downloaded "server",
|
|
135
|
+
are downloaded (exist on server and locally) "both" and those which
|
|
136
|
+
are only present in the backup (as they have been deleted on the server) "local".
|
|
137
|
+
DESC
|
|
138
|
+
method_option(
|
|
139
|
+
"format",
|
|
140
|
+
type: :string,
|
|
141
|
+
desc: "the output type, text (plain text) or json",
|
|
142
|
+
aliases: ["-f"]
|
|
143
|
+
)
|
|
144
|
+
def stats(email)
|
|
145
|
+
Stats.new(email, symbolized(options)).run
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
desc "status", "This command is deprecated, use `imap-backup stats ACCOUNT`"
|
|
129
149
|
long_desc <<~DESC
|
|
130
150
|
For each configured account and folder, lists the number of emails yet to be downloaded.
|
|
151
|
+
This command is deprecated.
|
|
131
152
|
DESC
|
|
132
153
|
accounts_option
|
|
133
154
|
def status
|
|
@@ -6,8 +6,7 @@ require "imap/backup/account"
|
|
|
6
6
|
module Imap::Backup
|
|
7
7
|
class Configuration
|
|
8
8
|
CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
|
|
9
|
-
|
|
10
|
-
VERSION = "2.0"
|
|
9
|
+
VERSION = "2.0".freeze
|
|
11
10
|
|
|
12
11
|
attr_reader :pathname
|
|
13
12
|
|
|
@@ -52,15 +51,6 @@ module Imap::Backup
|
|
|
52
51
|
end
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
def download_block_size
|
|
56
|
-
size = ENV["DOWNLOAD_BLOCK_SIZE"].to_i
|
|
57
|
-
if size > 0
|
|
58
|
-
size
|
|
59
|
-
else
|
|
60
|
-
DEFAULT_DOWNLOAD_BLOCK_SIZE
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
54
|
def modified?
|
|
65
55
|
ensure_loaded!
|
|
66
56
|
return true if @saved_debug != @debug
|
|
@@ -91,23 +81,21 @@ module Imap::Backup
|
|
|
91
81
|
|
|
92
82
|
def data
|
|
93
83
|
@data ||=
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{accounts: []}
|
|
101
|
-
end
|
|
84
|
+
if File.exist?(pathname)
|
|
85
|
+
Utils.check_permissions(pathname, 0o600) if !windows?
|
|
86
|
+
contents = File.read(pathname)
|
|
87
|
+
JSON.parse(contents, symbolize_names: true)
|
|
88
|
+
else
|
|
89
|
+
{accounts: []}
|
|
102
90
|
end
|
|
103
91
|
end
|
|
104
92
|
|
|
105
93
|
def remove_modified_flags
|
|
106
|
-
accounts.each
|
|
94
|
+
accounts.each(&:clear_changes)
|
|
107
95
|
end
|
|
108
96
|
|
|
109
97
|
def remove_deleted_accounts
|
|
110
|
-
accounts.reject!
|
|
98
|
+
accounts.reject!(&:marked_for_deletion?)
|
|
111
99
|
end
|
|
112
100
|
|
|
113
101
|
def make_private(path)
|
|
@@ -1,57 +1,79 @@
|
|
|
1
1
|
module Imap::Backup
|
|
2
|
+
class MultiFetchFailedError < StandardError; end
|
|
3
|
+
|
|
2
4
|
class Downloader
|
|
3
5
|
attr_reader :folder
|
|
4
6
|
attr_reader :serializer
|
|
5
|
-
attr_reader :
|
|
7
|
+
attr_reader :multi_fetch_size
|
|
6
8
|
|
|
7
|
-
def initialize(folder, serializer,
|
|
9
|
+
def initialize(folder, serializer, multi_fetch_size: 1)
|
|
8
10
|
@folder = folder
|
|
9
11
|
@serializer = serializer
|
|
10
|
-
@
|
|
12
|
+
@multi_fetch_size = multi_fetch_size
|
|
13
|
+
@uids = nil
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def run
|
|
14
|
-
uids
|
|
15
|
-
count = uids.count
|
|
16
|
-
debug "#{count} new messages"
|
|
17
|
-
uids.each_slice(block_size).with_index do |block, i|
|
|
18
|
-
uids_and_bodies = folder.fetch_multi(block)
|
|
19
|
-
if uids_and_bodies.nil?
|
|
20
|
-
if block_size > 1
|
|
21
|
-
debug("Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
|
|
22
|
-
@block_size = 1
|
|
23
|
-
redo
|
|
24
|
-
else
|
|
25
|
-
debug("Fetch failed for UID #{block[0]} - skipping")
|
|
26
|
-
next
|
|
27
|
-
end
|
|
28
|
-
end
|
|
17
|
+
debug "#{uids.count} new messages"
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
body = uid_and_body[:body]
|
|
34
|
-
case
|
|
35
|
-
when !body
|
|
36
|
-
info("Fetch returned empty body - skipping")
|
|
37
|
-
when !uid
|
|
38
|
-
info("Fetch returned empty UID - skipping")
|
|
39
|
-
else
|
|
40
|
-
debug("uid: #{uid} (#{offset + j}/#{count}) - #{body.size} bytes")
|
|
41
|
-
serializer.save(uid, body)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
19
|
+
uids.each_slice(multi_fetch_size).with_index do |block, i|
|
|
20
|
+
multifetch_failed = download_block(block, i)
|
|
21
|
+
raise MultiFetchFailedError if multifetch_failed
|
|
44
22
|
end
|
|
23
|
+
rescue MultiFetchFailedError
|
|
24
|
+
@count = nil
|
|
25
|
+
@multi_fetch_size = 1
|
|
26
|
+
@uids = nil
|
|
27
|
+
retry
|
|
45
28
|
end
|
|
46
29
|
|
|
47
30
|
private
|
|
48
31
|
|
|
32
|
+
def download_block(block, index)
|
|
33
|
+
uids_and_bodies = folder.fetch_multi(block)
|
|
34
|
+
if uids_and_bodies.nil?
|
|
35
|
+
if multi_fetch_size > 1
|
|
36
|
+
uids = block.join(", ")
|
|
37
|
+
debug("Multi fetch failed for UIDs #{uids}, switching to single fetches")
|
|
38
|
+
return true
|
|
39
|
+
else
|
|
40
|
+
debug("Fetch failed for UID #{block[0]} - skipping")
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
offset = (index * multi_fetch_size) + 1
|
|
46
|
+
uids_and_bodies.each.with_index do |uid_and_body, j|
|
|
47
|
+
handle_uid_and_body uid_and_body, offset + j
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_uid_and_body(uid_and_body, index)
|
|
54
|
+
uid = uid_and_body[:uid]
|
|
55
|
+
body = uid_and_body[:body]
|
|
56
|
+
case
|
|
57
|
+
when !body
|
|
58
|
+
info("Fetch returned empty body - skipping")
|
|
59
|
+
when !uid
|
|
60
|
+
info("Fetch returned empty UID - skipping")
|
|
61
|
+
else
|
|
62
|
+
debug("uid: #{uid} (#{index}/#{uids.count}) - #{body.size} bytes")
|
|
63
|
+
serializer.append uid, body
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def uids
|
|
68
|
+
@uids ||= folder.uids - serializer.uids
|
|
69
|
+
end
|
|
70
|
+
|
|
49
71
|
def info(message)
|
|
50
|
-
|
|
72
|
+
Logger.logger.info("[#{folder.name}] #{message}")
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def debug(message)
|
|
54
|
-
|
|
76
|
+
Logger.logger.debug("[#{folder.name}] #{message}")
|
|
55
77
|
end
|
|
56
78
|
end
|
|
57
79
|
end
|
data/lib/imap/backup/migrator.rb
CHANGED
|
@@ -15,12 +15,12 @@ module Imap::Backup
|
|
|
15
15
|
folder.create
|
|
16
16
|
ensure_destination_empty!
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Logger.logger.debug "[#{folder.name}] #{count} to migrate"
|
|
19
19
|
serializer.each_message(serializer.uids).with_index do |(uid, message), i|
|
|
20
20
|
next if message.nil?
|
|
21
21
|
|
|
22
22
|
log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
|
|
23
|
-
|
|
23
|
+
Logger.logger.debug(
|
|
24
24
|
"#{log_prefix} #{message.supplied_body.size} bytes"
|
|
25
25
|
)
|
|
26
26
|
|
|
@@ -42,9 +42,9 @@ module Imap::Backup
|
|
|
42
42
|
return if folder.uids.empty?
|
|
43
43
|
|
|
44
44
|
raise <<~ERROR
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
The destination folder '#{folder.name}' is not empty.
|
|
46
|
+
Pass the --reset flag if you want to clear existing emails from destination
|
|
47
|
+
folders before uploading.
|
|
48
48
|
ERROR
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -19,6 +19,7 @@ module Imap::Backup
|
|
|
19
19
|
loop do
|
|
20
20
|
line, newline, rest = @current.partition("\n")
|
|
21
21
|
break if newline != "\n"
|
|
22
|
+
|
|
22
23
|
clean = sanitize(line)
|
|
23
24
|
output.puts clean
|
|
24
25
|
@current = rest
|
|
@@ -34,9 +35,9 @@ module Imap::Backup
|
|
|
34
35
|
|
|
35
36
|
private
|
|
36
37
|
|
|
37
|
-
def sanitize(
|
|
38
|
+
def sanitize(text)
|
|
38
39
|
# Hide password in Net::IMAP debug output
|
|
39
|
-
|
|
40
|
+
text.gsub(
|
|
40
41
|
/\A(C: RUBY\d+ LOGIN \S+) \S+/,
|
|
41
42
|
"\\1 [PASSWORD REDACTED]"
|
|
42
43
|
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Imap::Backup
|
|
2
|
+
class Serializer::Appender
|
|
3
|
+
attr_reader :imap
|
|
4
|
+
attr_reader :folder
|
|
5
|
+
attr_reader :mbox
|
|
6
|
+
|
|
7
|
+
def initialize(folder:, imap:, mbox:)
|
|
8
|
+
@folder = folder
|
|
9
|
+
@imap = imap
|
|
10
|
+
@mbox = mbox
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(uid:, message:)
|
|
14
|
+
raise "Can't add messages without uid_validity" if !imap.uid_validity
|
|
15
|
+
|
|
16
|
+
uid = uid.to_i
|
|
17
|
+
if imap.include?(uid)
|
|
18
|
+
Logger.logger.debug(
|
|
19
|
+
"[#{folder}] message #{uid} already downloaded - skipping"
|
|
20
|
+
)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
do_append uid, message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def do_append(uid, message)
|
|
30
|
+
mboxrd_message = Email::Mboxrd::Message.new(message)
|
|
31
|
+
initial = mbox.length || 0
|
|
32
|
+
mbox_appended = false
|
|
33
|
+
begin
|
|
34
|
+
mbox.append mboxrd_message.to_serialized
|
|
35
|
+
mbox_appended = true
|
|
36
|
+
imap.append uid
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
mbox.rewind(initial) if mbox_appended
|
|
39
|
+
|
|
40
|
+
message = <<-ERROR.gsub(/^\s*/m, "")
|
|
41
|
+
[#{folder}] failed to append message #{uid}:
|
|
42
|
+
#{message}. #{e}:
|
|
43
|
+
#{e.backtrace.join("\n")}"
|
|
44
|
+
ERROR
|
|
45
|
+
Logger.logger.warn message
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|