imap-backup 6.0.0.rc2 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/imap-backup.gemspec +5 -1
- data/lib/cli_coverage.rb +11 -11
- data/lib/email/provider/apple_mail.rb +4 -0
- data/lib/email/provider/base.rb +6 -0
- data/lib/email/provider/purelymail.rb +11 -0
- data/lib/email/provider/unknown.rb +2 -0
- data/lib/email/provider.rb +5 -0
- data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
- data/lib/imap/backup/account/connection/client_factory.rb +55 -0
- data/lib/imap/backup/account/connection/folder_names.rb +26 -0
- data/lib/imap/backup/account/connection.rb +16 -96
- data/lib/imap/backup/account/folder.rb +31 -9
- data/lib/imap/backup/account.rb +15 -6
- data/lib/imap/backup/cli/backup.rb +1 -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 +4 -10
- 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 +4 -6
- data/lib/imap/backup/cli.rb +24 -3
- data/lib/imap/backup/configuration.rb +9 -11
- data/lib/imap/backup/downloader.rb +75 -31
- 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/imap.rb +27 -3
- data/lib/imap/backup/serializer/mbox.rb +18 -2
- 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 +64 -84
- data/lib/imap/backup/setup/account/header.rb +81 -0
- data/lib/imap/backup/setup/account.rb +28 -91
- data/lib/imap/backup/setup/asker.rb +4 -15
- data/lib/imap/backup/setup/backup_path.rb +45 -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 +1 -1
- data/lib/imap/backup/setup.rb +7 -6
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +39 -20
- data/lib/imap/backup/uploader.rb +46 -8
- data/lib/imap/backup/utils.rb +1 -1
- data/lib/imap/backup/version.rb +2 -2
- data/lib/imap/backup.rb +0 -1
- metadata +32 -134
- data/spec/features/backup_spec.rb +0 -100
- 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 -78
- 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 -433
- data/spec/unit/imap/backup/account/folder_spec.rb +0 -261
- data/spec/unit/imap/backup/account_spec.rb +0 -246
- data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -58
- data/spec/unit/imap/backup/cli/backup_spec.rb +0 -19
- data/spec/unit/imap/backup/cli/folders_spec.rb +0 -39
- data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
- data/spec/unit/imap/backup/cli/local_spec.rb +0 -100
- data/spec/unit/imap/backup/cli/migrate_spec.rb +0 -80
- data/spec/unit/imap/backup/cli/restore_spec.rb +0 -67
- data/spec/unit/imap/backup/cli/setup_spec.rb +0 -17
- data/spec/unit/imap/backup/cli/utils_spec.rb +0 -125
- data/spec/unit/imap/backup/cli_spec.rb +0 -93
- data/spec/unit/imap/backup/client/apple_mail_spec.rb +0 -9
- 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 -96
- 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/sanitizer_spec.rb +0 -42
- data/spec/unit/imap/backup/serializer/directory_spec.rb +0 -37
- data/spec/unit/imap/backup/serializer/imap_spec.rb +0 -218
- data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -101
- data/spec/unit/imap/backup/serializer_spec.rb +0 -296
- data/spec/unit/imap/backup/setup/account_spec.rb +0 -461
- 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/helpers_spec.rb +0 -15
- data/spec/unit/imap/backup/setup_spec.rb +0 -301
- data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +0 -116
- data/spec/unit/imap/backup/uploader_spec.rb +0 -54
- data/spec/unit/imap/backup/utils_spec.rb +0 -92
- data/spec/unit/retry_on_error_spec.rb +0 -34
@@ -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
|
@@ -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
|
@@ -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
|
|
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,7 +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
|
-
VERSION = "2.0"
|
9
|
+
VERSION = "2.0".freeze
|
10
10
|
|
11
11
|
attr_reader :pathname
|
12
12
|
|
@@ -81,23 +81,21 @@ module Imap::Backup
|
|
81
81
|
|
82
82
|
def data
|
83
83
|
@data ||=
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
{accounts: []}
|
91
|
-
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: []}
|
92
90
|
end
|
93
91
|
end
|
94
92
|
|
95
93
|
def remove_modified_flags
|
96
|
-
accounts.each
|
94
|
+
accounts.each(&:clear_changes)
|
97
95
|
end
|
98
96
|
|
99
97
|
def remove_deleted_accounts
|
100
|
-
accounts.reject!
|
98
|
+
accounts.reject!(&:marked_for_deletion?)
|
101
99
|
end
|
102
100
|
|
103
101
|
def make_private(path)
|
@@ -5,57 +5,101 @@ module Imap::Backup
|
|
5
5
|
attr_reader :folder
|
6
6
|
attr_reader :serializer
|
7
7
|
attr_reader :multi_fetch_size
|
8
|
+
# Some IMAP providers, notably Apple Mail, set the '\Seen' flag
|
9
|
+
# on emails when they are fetched. By setting `:reset_seen_flags_after_fetch`,
|
10
|
+
# a workaround is activated which checks which emails are 'unseen' before
|
11
|
+
# and after the fetch, and removes the '\Seen' flag from those which have changed.
|
12
|
+
# As this check is susceptible to 'race conditions', i.e. when a different
|
13
|
+
# client sets the '\Seen' flag while imap-backup is fetching, it is best
|
14
|
+
# to only use it when required (i.e. for IMAP providers which always
|
15
|
+
# mark messages as '\Seen' when accessed).
|
16
|
+
attr_reader :reset_seen_flags_after_fetch
|
8
17
|
|
9
|
-
def initialize(folder, serializer, multi_fetch_size: 1)
|
18
|
+
def initialize(folder, serializer, multi_fetch_size: 1, reset_seen_flags_after_fetch: false)
|
10
19
|
@folder = folder
|
11
20
|
@serializer = serializer
|
12
21
|
@multi_fetch_size = multi_fetch_size
|
22
|
+
@reset_seen_flags_after_fetch = reset_seen_flags_after_fetch
|
23
|
+
@uids = nil
|
13
24
|
end
|
14
25
|
|
15
26
|
def run
|
16
|
-
uids
|
17
|
-
count = uids.count
|
18
|
-
debug "#{count} new messages"
|
19
|
-
uids.each_slice(multi_fetch_size).with_index do |block, i|
|
20
|
-
uids_and_bodies = folder.fetch_multi(block)
|
21
|
-
if uids_and_bodies.nil?
|
22
|
-
if multi_fetch_size > 1
|
23
|
-
debug("Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
|
24
|
-
raise MultiFetchFailedError
|
25
|
-
else
|
26
|
-
debug("Fetch failed for UID #{block[0]} - skipping")
|
27
|
-
next
|
28
|
-
end
|
29
|
-
end
|
27
|
+
debug "#{uids.count} new messages"
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
body = uid_and_body[:body]
|
35
|
-
case
|
36
|
-
when !body
|
37
|
-
info("Fetch returned empty body - skipping")
|
38
|
-
when !uid
|
39
|
-
info("Fetch returned empty UID - skipping")
|
40
|
-
else
|
41
|
-
debug("uid: #{uid} (#{offset + j}/#{count}) - #{body.size} bytes")
|
42
|
-
serializer.append uid, body
|
43
|
-
end
|
44
|
-
end
|
29
|
+
uids.each_slice(multi_fetch_size).with_index do |block, i|
|
30
|
+
multifetch_failed = download_block(block, i)
|
31
|
+
raise MultiFetchFailedError if multifetch_failed
|
45
32
|
end
|
46
33
|
rescue MultiFetchFailedError
|
34
|
+
@count = nil
|
47
35
|
@multi_fetch_size = 1
|
36
|
+
@uids = nil
|
48
37
|
retry
|
49
38
|
end
|
50
39
|
|
51
40
|
private
|
52
41
|
|
42
|
+
def download_block(block, index)
|
43
|
+
uids_and_bodies =
|
44
|
+
if reset_seen_flags_after_fetch
|
45
|
+
before_unseen = folder.unseen(block)
|
46
|
+
debug "Pre-fetch unseen messages: #{before_unseen.join(', ')}"
|
47
|
+
uids_and_bodies = folder.fetch_multi(block)
|
48
|
+
after_unseen = folder.unseen(block)
|
49
|
+
debug "Post-fetch unseen messages: #{after_unseen.join(', ')}"
|
50
|
+
changed = before_unseen - after_unseen
|
51
|
+
if changed.any?
|
52
|
+
ids = changed.join(", ")
|
53
|
+
debug "Removing '\Seen' flag for the following messages: #{ids}"
|
54
|
+
folder.unset_flags(changed, [:Seen])
|
55
|
+
end
|
56
|
+
uids_and_bodies
|
57
|
+
else
|
58
|
+
folder.fetch_multi(block)
|
59
|
+
end
|
60
|
+
if uids_and_bodies.nil?
|
61
|
+
if multi_fetch_size > 1
|
62
|
+
uids = block.join(", ")
|
63
|
+
debug("Multi fetch failed for UIDs #{uids}, switching to single fetches")
|
64
|
+
return true
|
65
|
+
else
|
66
|
+
debug("Fetch failed for UID #{block[0]} - skipping")
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
offset = (index * multi_fetch_size) + 1
|
72
|
+
uids_and_bodies.each.with_index do |uid_and_body, j|
|
73
|
+
handle_uid_and_body uid_and_body, offset + j
|
74
|
+
end
|
75
|
+
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_uid_and_body(uid_and_body, index)
|
80
|
+
uid = uid_and_body[:uid]
|
81
|
+
body = uid_and_body[:body]
|
82
|
+
case
|
83
|
+
when !body
|
84
|
+
info("Fetch returned empty body - skipping")
|
85
|
+
when !uid
|
86
|
+
info("Fetch returned empty UID - skipping")
|
87
|
+
else
|
88
|
+
debug("uid: #{uid} (#{index}/#{uids.count}) - #{body.size} bytes")
|
89
|
+
serializer.append uid, body
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def uids
|
94
|
+
@uids ||= folder.uids - serializer.uids
|
95
|
+
end
|
96
|
+
|
53
97
|
def info(message)
|
54
|
-
|
98
|
+
Logger.logger.info("[#{folder.name}] #{message}")
|
55
99
|
end
|
56
100
|
|
57
101
|
def debug(message)
|
58
|
-
|
102
|
+
Logger.logger.debug("[#{folder.name}] #{message}")
|
59
103
|
end
|
60
104
|
end
|
61
105
|
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
|
@@ -12,6 +12,15 @@ module Imap::Backup
|
|
12
12
|
@loaded = false
|
13
13
|
@uid_validity = nil
|
14
14
|
@uids = nil
|
15
|
+
@version = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid?
|
19
|
+
return false if !exist?
|
20
|
+
return false if version != CURRENT_VERSION
|
21
|
+
return false if !uid_validity
|
22
|
+
|
23
|
+
true
|
15
24
|
end
|
16
25
|
|
17
26
|
def append(uid)
|
@@ -19,8 +28,10 @@ module Imap::Backup
|
|
19
28
|
save
|
20
29
|
end
|
21
30
|
|
22
|
-
def
|
23
|
-
|
31
|
+
def delete
|
32
|
+
return if !exist?
|
33
|
+
|
34
|
+
File.unlink(pathname)
|
24
35
|
end
|
25
36
|
|
26
37
|
def include?(uid)
|
@@ -66,12 +77,21 @@ module Imap::Backup
|
|
66
77
|
save
|
67
78
|
end
|
68
79
|
|
80
|
+
def version
|
81
|
+
ensure_loaded
|
82
|
+
@version
|
83
|
+
end
|
84
|
+
|
69
85
|
private
|
70
86
|
|
71
87
|
def pathname
|
72
88
|
"#{folder_path}.imap"
|
73
89
|
end
|
74
90
|
|
91
|
+
def exist?
|
92
|
+
File.exist?(pathname)
|
93
|
+
end
|
94
|
+
|
75
95
|
def ensure_loaded
|
76
96
|
return if loaded
|
77
97
|
|
@@ -79,9 +99,11 @@ module Imap::Backup
|
|
79
99
|
if data
|
80
100
|
@uids = data[:uids].map(&:to_i)
|
81
101
|
@uid_validity = data[:uid_validity]
|
102
|
+
@version = data[:version]
|
82
103
|
else
|
83
104
|
@uids = []
|
84
105
|
@uid_validity = nil
|
106
|
+
@version = CURRENT_VERSION
|
85
107
|
end
|
86
108
|
@loaded = true
|
87
109
|
end
|
@@ -97,6 +119,8 @@ module Imap::Backup
|
|
97
119
|
return nil
|
98
120
|
end
|
99
121
|
|
122
|
+
return nil if !data.key?(:version)
|
123
|
+
return nil if !data.key?(:uid_validity)
|
100
124
|
return nil if !data.key?(:uids)
|
101
125
|
return nil if !data[:uids].is_a?(Array)
|
102
126
|
|
@@ -109,7 +133,7 @@ module Imap::Backup
|
|
109
133
|
raise "Cannot save metadata without a uid_validity" if !uid_validity
|
110
134
|
|
111
135
|
data = {
|
112
|
-
version:
|
136
|
+
version: @version,
|
113
137
|
uid_validity: @uid_validity,
|
114
138
|
uids: @uids
|
115
139
|
}
|