imap-backup 6.0.0.rc2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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 +11 -95
- data/lib/imap/backup/account/folder.rb +9 -6
- data/lib/imap/backup/account.rb +3 -5
- 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 +48 -30
- 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/message_enumerator.rb +29 -0
- data/lib/imap/backup/serializer/unused_name_finder.rb +27 -0
- data/lib/imap/backup/serializer.rb +24 -78
- data/lib/imap/backup/setup/account/header.rb +75 -0
- data/lib/imap/backup/setup/account.rb +14 -91
- 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 +1 -1
- data/lib/imap/backup/setup.rb +5 -4
- 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 +1 -1
- data/lib/imap/backup.rb +0 -1
- metadata +31 -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
|
@@ -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)
|
|
@@ -10,52 +10,70 @@ module Imap::Backup
|
|
|
10
10
|
@folder = folder
|
|
11
11
|
@serializer = serializer
|
|
12
12
|
@multi_fetch_size = multi_fetch_size
|
|
13
|
+
@uids = nil
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
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
|
|
17
|
+
debug "#{uids.count} new messages"
|
|
30
18
|
|
|
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
|
|
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
|
|
45
22
|
end
|
|
46
23
|
rescue MultiFetchFailedError
|
|
24
|
+
@count = nil
|
|
47
25
|
@multi_fetch_size = 1
|
|
26
|
+
@uids = nil
|
|
48
27
|
retry
|
|
49
28
|
end
|
|
50
29
|
|
|
51
30
|
private
|
|
52
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
|
+
|
|
53
71
|
def info(message)
|
|
54
|
-
|
|
72
|
+
Logger.logger.info("[#{folder.name}] #{message}")
|
|
55
73
|
end
|
|
56
74
|
|
|
57
75
|
def debug(message)
|
|
58
|
-
|
|
76
|
+
Logger.logger.debug("[#{folder.name}] #{message}")
|
|
59
77
|
end
|
|
60
78
|
end
|
|
61
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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "email/mboxrd/message"
|
|
2
|
+
require "imap/backup/serializer/mbox_enumerator"
|
|
3
|
+
|
|
4
|
+
module Imap::Backup
|
|
5
|
+
class Serializer::MessageEnumerator
|
|
6
|
+
attr_reader :imap
|
|
7
|
+
attr_reader :mbox
|
|
8
|
+
|
|
9
|
+
def initialize(imap:, mbox:)
|
|
10
|
+
@imap = imap
|
|
11
|
+
@mbox = mbox
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run(uids:)
|
|
15
|
+
indexes = uids.each.with_object({}) do |uid_maybe_string, acc|
|
|
16
|
+
uid = uid_maybe_string.to_i
|
|
17
|
+
index = imap.index(uid)
|
|
18
|
+
acc[index] = uid if index
|
|
19
|
+
end
|
|
20
|
+
enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
|
|
21
|
+
enumerator.each.with_index do |raw, i|
|
|
22
|
+
uid = indexes[i]
|
|
23
|
+
next if !uid
|
|
24
|
+
|
|
25
|
+
yield uid, Email::Mboxrd::Message.from_serialized(raw)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Imap::Backup
|
|
2
|
+
class Serializer::UnusedNameFinder
|
|
3
|
+
attr_reader :serializer
|
|
4
|
+
|
|
5
|
+
def initialize(serializer:)
|
|
6
|
+
@serializer = serializer
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
digit = 0
|
|
11
|
+
name = nil
|
|
12
|
+
|
|
13
|
+
loop do
|
|
14
|
+
extra = digit.zero? ? "" : "-#{digit}"
|
|
15
|
+
name = "#{serializer.folder}-#{serializer.uid_validity}#{extra}"
|
|
16
|
+
new_folder_path = Serializer.folder_path_for(path: serializer.path, folder: name)
|
|
17
|
+
test_mbox = Serializer::Mbox.new(new_folder_path)
|
|
18
|
+
test_imap = Serializer::Imap.new(new_folder_path)
|
|
19
|
+
break if !test_mbox.exist? && !test_imap.exist?
|
|
20
|
+
|
|
21
|
+
digit += 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
require "forwardable"
|
|
2
2
|
|
|
3
3
|
require "email/mboxrd/message"
|
|
4
|
+
require "imap/backup/serializer/appender"
|
|
4
5
|
require "imap/backup/serializer/imap"
|
|
5
6
|
require "imap/backup/serializer/mbox"
|
|
6
7
|
require "imap/backup/serializer/mbox_enumerator"
|
|
8
|
+
require "imap/backup/serializer/message_enumerator"
|
|
9
|
+
require "imap/backup/serializer/unused_name_finder"
|
|
7
10
|
|
|
8
11
|
module Imap::Backup
|
|
9
12
|
class Serializer
|
|
13
|
+
def self.folder_path_for(path:, folder:)
|
|
14
|
+
relative = File.join(path, folder)
|
|
15
|
+
File.expand_path(relative)
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
extend Forwardable
|
|
11
19
|
|
|
12
20
|
def_delegator :mbox, :pathname, :mbox_pathname
|
|
@@ -38,17 +46,8 @@ module Imap::Backup
|
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def append(uid, message)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
uid = uid.to_i
|
|
44
|
-
if imap.include?(uid)
|
|
45
|
-
Logger.logger.debug(
|
|
46
|
-
"[#{folder}] message #{uid} already downloaded - skipping"
|
|
47
|
-
)
|
|
48
|
-
return
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
do_append uid, message
|
|
49
|
+
appender = Serializer::Appender.new(folder: folder, imap: imap, mbox: mbox)
|
|
50
|
+
appender.run(uid: uid, message: message)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
def load(uid_maybe_string)
|
|
@@ -69,59 +68,26 @@ module Imap::Backup
|
|
|
69
68
|
nil
|
|
70
69
|
end
|
|
71
70
|
|
|
72
|
-
def each_message(required_uids)
|
|
73
|
-
return enum_for(:each_message, required_uids) if !
|
|
71
|
+
def each_message(required_uids, &block)
|
|
72
|
+
return enum_for(:each_message, required_uids) if !block
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
index = imap.index(uid)
|
|
78
|
-
acc[index] = uid if index
|
|
79
|
-
end
|
|
80
|
-
enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
|
|
81
|
-
enumerator.each.with_index do |raw, i|
|
|
82
|
-
uid = indexes[i]
|
|
83
|
-
next if !uid
|
|
84
|
-
|
|
85
|
-
yield uid, Email::Mboxrd::Message.from_serialized(raw)
|
|
86
|
-
end
|
|
74
|
+
enumerator = Serializer::MessageEnumerator.new(imap: imap, mbox: mbox)
|
|
75
|
+
enumerator.run(uids: required_uids, &block)
|
|
87
76
|
end
|
|
88
77
|
|
|
89
78
|
def rename(new_name)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
ensure_containing_directory
|
|
95
|
-
mbox.rename folder_path
|
|
96
|
-
imap.rename folder_path
|
|
79
|
+
destination = self.class.folder_path_for(path: path, folder: new_name)
|
|
80
|
+
ensure_containing_directory(new_name)
|
|
81
|
+
mbox.rename destination
|
|
82
|
+
imap.rename destination
|
|
97
83
|
end
|
|
98
84
|
|
|
99
85
|
private
|
|
100
86
|
|
|
101
|
-
def do_append(uid, message)
|
|
102
|
-
mboxrd_message = Email::Mboxrd::Message.new(message)
|
|
103
|
-
initial = mbox.length || 0
|
|
104
|
-
mbox_appended = false
|
|
105
|
-
begin
|
|
106
|
-
mbox.append mboxrd_message.to_serialized
|
|
107
|
-
mbox_appended = true
|
|
108
|
-
imap.append uid
|
|
109
|
-
rescue StandardError => e
|
|
110
|
-
mbox.rewind(initial) if mbox_appended
|
|
111
|
-
|
|
112
|
-
message = <<-ERROR.gsub(/^\s*/m, "")
|
|
113
|
-
[#{folder}] failed to append message #{uid}:
|
|
114
|
-
#{message}. #{e}:
|
|
115
|
-
#{e.backtrace.join("\n")}"
|
|
116
|
-
ERROR
|
|
117
|
-
Logger.logger.warn message
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
87
|
def mbox
|
|
122
88
|
@mbox ||=
|
|
123
89
|
begin
|
|
124
|
-
ensure_containing_directory
|
|
90
|
+
ensure_containing_directory(folder)
|
|
125
91
|
Serializer::Mbox.new(folder_path)
|
|
126
92
|
end
|
|
127
93
|
end
|
|
@@ -129,21 +95,16 @@ module Imap::Backup
|
|
|
129
95
|
def imap
|
|
130
96
|
@imap ||=
|
|
131
97
|
begin
|
|
132
|
-
ensure_containing_directory
|
|
98
|
+
ensure_containing_directory(folder)
|
|
133
99
|
Serializer::Imap.new(folder_path)
|
|
134
100
|
end
|
|
135
101
|
end
|
|
136
102
|
|
|
137
103
|
def folder_path
|
|
138
|
-
folder_path_for(path, folder)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def folder_path_for(path, folder)
|
|
142
|
-
relative = File.join(path, folder)
|
|
143
|
-
File.expand_path(relative)
|
|
104
|
+
self.class.folder_path_for(path: path, folder: folder)
|
|
144
105
|
end
|
|
145
106
|
|
|
146
|
-
def ensure_containing_directory
|
|
107
|
+
def ensure_containing_directory(folder)
|
|
147
108
|
relative = File.dirname(folder)
|
|
148
109
|
directory = Serializer::Directory.new(path, relative)
|
|
149
110
|
directory.ensure_exists
|
|
@@ -160,23 +121,8 @@ module Imap::Backup
|
|
|
160
121
|
end
|
|
161
122
|
|
|
162
123
|
def rename_existing_folder
|
|
163
|
-
|
|
164
|
-
new_name
|
|
165
|
-
loop do
|
|
166
|
-
extra = digit.zero? ? "" : "-#{digit}"
|
|
167
|
-
new_name = "#{folder}-#{imap.uid_validity}#{extra}"
|
|
168
|
-
new_folder_path = folder_path_for(path, new_name)
|
|
169
|
-
test_mbox = Serializer::Mbox.new(new_folder_path)
|
|
170
|
-
test_imap = Serializer::Imap.new(new_folder_path)
|
|
171
|
-
break if !test_mbox.exist? && !test_imap.exist?
|
|
172
|
-
|
|
173
|
-
digit += 1
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
previous = folder
|
|
177
|
-
rename(new_name)
|
|
178
|
-
@folder = previous
|
|
179
|
-
|
|
124
|
+
new_name = Serializer::UnusedNameFinder.new(serializer: self).run
|
|
125
|
+
rename new_name
|
|
180
126
|
new_name
|
|
181
127
|
end
|
|
182
128
|
end
|