imap-backup 14.4.4 → 14.5.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/README.md +67 -137
- data/docs/documentation.md +19 -0
- data/imap-backup.gemspec +1 -1
- data/lib/imap/backup/account/backup.rb +2 -0
- data/lib/imap/backup/account/backup_folders.rb +7 -1
- data/lib/imap/backup/account/client_factory.rb +1 -0
- data/lib/imap/backup/account/folder.rb +26 -0
- data/lib/imap/backup/account/folder_backup.rb +4 -1
- data/lib/imap/backup/account/folder_ensurer.rb +3 -0
- data/lib/imap/backup/account/local_only_folder_deleter.rb +3 -1
- data/lib/imap/backup/account/restore.rb +2 -0
- data/lib/imap/backup/account/serialized_folders.rb +31 -1
- data/lib/imap/backup/account.rb +34 -18
- data/lib/imap/backup/cli/backup.rb +3 -0
- data/lib/imap/backup/cli/folder_enumerator.rb +6 -0
- data/lib/imap/backup/cli/helpers.rb +13 -0
- data/lib/imap/backup/cli/local/check.rb +3 -0
- data/lib/imap/backup/cli/local.rb +14 -1
- data/lib/imap/backup/cli/remote.rb +7 -0
- data/lib/imap/backup/cli/restore.rb +4 -0
- data/lib/imap/backup/cli/setup.rb +3 -0
- data/lib/imap/backup/cli/single/backup.rb +3 -0
- data/lib/imap/backup/cli/single.rb +4 -0
- data/lib/imap/backup/cli/stats.rb +3 -0
- data/lib/imap/backup/cli/transfer.rb +8 -0
- data/lib/imap/backup/cli/utils.rb +7 -1
- data/lib/imap/backup/cli.rb +8 -0
- data/lib/imap/backup/client/apple_mail.rb +2 -0
- data/lib/imap/backup/client/automatic_login_wrapper.rb +9 -1
- data/lib/imap/backup/client/default.rb +15 -4
- data/lib/imap/backup/configuration.rb +13 -0
- data/lib/imap/backup/configuration_not_found.rb +1 -0
- data/lib/imap/backup/downloader.rb +4 -0
- data/lib/imap/backup/email/mboxrd/message.rb +14 -0
- data/lib/imap/backup/email/provider/apple_mail.rb +2 -0
- data/lib/imap/backup/email/provider/base.rb +3 -3
- data/lib/imap/backup/email/provider/fastmail.rb +2 -0
- data/lib/imap/backup/email/provider/gmail.rb +2 -0
- data/lib/imap/backup/email/provider/purelymail.rb +2 -0
- data/lib/imap/backup/email/provider/unknown.rb +2 -6
- data/lib/imap/backup/email/provider.rb +5 -0
- data/lib/imap/backup/file_mode.rb +2 -0
- data/lib/imap/backup/flag_refresher.rb +4 -0
- data/lib/imap/backup/local_only_message_deleter.rb +2 -0
- data/lib/imap/backup/logger.rb +18 -0
- data/lib/imap/backup/migrator.rb +3 -0
- data/lib/imap/backup/mirror/map.rb +21 -0
- data/lib/imap/backup/mirror.rb +8 -0
- data/lib/imap/backup/naming.rb +10 -1
- data/lib/imap/backup/retry_on_error.rb +9 -0
- data/lib/imap/backup/serializer/appender.rb +9 -0
- data/lib/imap/backup/serializer/delayed_metadata_serializer.rb +4 -0
- data/lib/imap/backup/serializer/folder_maker.rb +1 -0
- data/lib/imap/backup/serializer/imap.rb +38 -2
- data/lib/imap/backup/serializer/integrity_checker.rb +1 -0
- data/lib/imap/backup/serializer/mbox.rb +26 -0
- data/lib/imap/backup/serializer/message.rb +13 -0
- data/lib/imap/backup/serializer/message_enumerator.rb +10 -2
- data/lib/imap/backup/serializer/permission_checker.rb +6 -0
- data/lib/imap/backup/serializer/transaction.rb +18 -0
- data/lib/imap/backup/serializer/unused_name_finder.rb +4 -0
- data/lib/imap/backup/serializer/version2_migrator.rb +4 -0
- data/lib/imap/backup/serializer.rb +56 -2
- data/lib/imap/backup/setup/account/header.rb +6 -0
- data/lib/imap/backup/setup/account.rb +6 -0
- data/lib/imap/backup/setup/asker.rb +16 -0
- data/lib/imap/backup/setup/backup_path.rb +6 -0
- data/lib/imap/backup/setup/connection_tester.rb +5 -0
- data/lib/imap/backup/setup/email_changer.rb +6 -0
- data/lib/imap/backup/setup/folder_chooser.rb +4 -0
- data/lib/imap/backup/setup/global_options/download_strategy_chooser.rb +4 -0
- data/lib/imap/backup/setup/global_options.rb +4 -0
- data/lib/imap/backup/setup/helpers.rb +3 -0
- data/lib/imap/backup/setup.rb +5 -0
- data/lib/imap/backup/text/sanitizer.rb +9 -0
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +8 -1
- data/lib/imap/backup/uploader.rb +5 -0
- data/lib/imap/backup/version.rb +7 -2
- metadata +11 -13
- data/docs/api.md +0 -20
- data/docs/development.md +0 -110
- data/docs/migrate-server-keep-address.md +0 -47
@@ -9,15 +9,21 @@ require "imap/backup/serializer/permission_checker"
|
|
9
9
|
module Imap; end
|
10
10
|
|
11
11
|
module Imap::Backup
|
12
|
+
# Handles the application's configuration file
|
12
13
|
class Configuration
|
14
|
+
# The default directory of the configuration file
|
13
15
|
CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
|
16
|
+
# The default download strategy key
|
14
17
|
DEFAULT_STRATEGY = "delay_metadata".freeze
|
18
|
+
# The available download strategies
|
15
19
|
DOWNLOAD_STRATEGIES = [
|
16
20
|
{key: "direct", description: "write straight to disk"},
|
17
21
|
{key: DEFAULT_STRATEGY, description: "delay writing metadata"}
|
18
22
|
].freeze
|
23
|
+
# The current file version
|
19
24
|
VERSION = "2.2".freeze
|
20
25
|
|
26
|
+
# @return [String] the default configuration file path
|
21
27
|
def self.default_pathname
|
22
28
|
File.join(CONFIGURATION_DIRECTORY, "config.json")
|
23
29
|
end
|
@@ -33,10 +39,13 @@ module Imap::Backup
|
|
33
39
|
@download_strategy_modified = false
|
34
40
|
end
|
35
41
|
|
42
|
+
# @return [String] the directory containing the configuration file
|
36
43
|
def path
|
37
44
|
File.dirname(pathname)
|
38
45
|
end
|
39
46
|
|
47
|
+
# Saves the configuration file in JSON format
|
48
|
+
# @return [void]
|
40
49
|
def save
|
41
50
|
ensure_loaded!
|
42
51
|
FileUtils.mkdir_p(path) if !File.directory?(path)
|
@@ -53,6 +62,7 @@ module Imap::Backup
|
|
53
62
|
@data = nil
|
54
63
|
end
|
55
64
|
|
65
|
+
# @return [Array<Account>] the configured accounts
|
56
66
|
def accounts
|
57
67
|
@accounts ||= begin
|
58
68
|
ensure_loaded!
|
@@ -63,12 +73,15 @@ module Imap::Backup
|
|
63
73
|
end
|
64
74
|
end
|
65
75
|
|
76
|
+
# @return [String] the cofigured download strategy
|
66
77
|
def download_strategy
|
67
78
|
ensure_loaded!
|
68
79
|
|
69
80
|
@download_strategy
|
70
81
|
end
|
71
82
|
|
83
|
+
# @param value [String] the new strategy
|
84
|
+
# @return [void]
|
72
85
|
def download_strategy=(value)
|
73
86
|
raise "Unknown strategy '#{value}'" if !DOWNLOAD_STRATEGIES.find { |s| s[:key] == value }
|
74
87
|
|
@@ -3,7 +3,9 @@ require "net/imap"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Downloads as yet undownloaded emails from an account's server
|
6
7
|
class Downloader
|
8
|
+
# @private
|
7
9
|
class MultiFetchFailedError < StandardError; end
|
8
10
|
|
9
11
|
def initialize(folder, serializer, multi_fetch_size: 1, reset_seen_flags_after_fetch: false)
|
@@ -14,6 +16,8 @@ module Imap::Backup
|
|
14
16
|
@uids = nil
|
15
17
|
end
|
16
18
|
|
19
|
+
# Runs the downloader
|
20
|
+
# @return [void]
|
17
21
|
def run
|
18
22
|
info("#{uids.count} new messages") if uids.any?
|
19
23
|
|
@@ -6,7 +6,13 @@ module Imap::Backup
|
|
6
6
|
module Email; end
|
7
7
|
|
8
8
|
module Email::Mboxrd
|
9
|
+
# Handles serialization and deserialization of messages
|
9
10
|
class Message
|
11
|
+
# @param serialized [String] an email message
|
12
|
+
#
|
13
|
+
# @return [String] The message without the initial 'From ' line
|
14
|
+
# and with one level of '>' quoting removed from other lines
|
15
|
+
# that start with 'From'
|
10
16
|
def self.clean_serialized(serialized)
|
11
17
|
cleaned = serialized.gsub(/^>(>*From)/, "\\1")
|
12
18
|
# Serialized messages in this format *should* start with a line
|
@@ -19,32 +25,40 @@ module Imap::Backup
|
|
19
25
|
cleaned
|
20
26
|
end
|
21
27
|
|
28
|
+
# @param serialized [String] the on-disk version of the message
|
29
|
+
#
|
30
|
+
# @return [Message] the original message
|
22
31
|
def self.from_serialized(serialized)
|
23
32
|
new(clean_serialized(serialized))
|
24
33
|
end
|
25
34
|
|
35
|
+
# @return [String] the original message body
|
26
36
|
attr_reader :supplied_body
|
27
37
|
|
28
38
|
def initialize(supplied_body)
|
29
39
|
@supplied_body = supplied_body.clone
|
30
40
|
end
|
31
41
|
|
42
|
+
# @return [String] the message with an initial 'From ADDRESS' line
|
32
43
|
def to_serialized
|
33
44
|
from_line = "From #{from}\n"
|
34
45
|
body = mboxrd_body.dup.force_encoding(Encoding::UTF_8)
|
35
46
|
from_line + body
|
36
47
|
end
|
37
48
|
|
49
|
+
# @return [Date, nil] the date of the message
|
38
50
|
def date
|
39
51
|
parsed.date
|
40
52
|
rescue StandardError
|
41
53
|
nil
|
42
54
|
end
|
43
55
|
|
56
|
+
# @return [String] the message's subject line
|
44
57
|
def subject
|
45
58
|
parsed.subject
|
46
59
|
end
|
47
60
|
|
61
|
+
# @return [String] the original message ready for transmission to an IMAP server
|
48
62
|
def imap_body
|
49
63
|
supplied_body.gsub(/(?<!\r)\n/, "\r\n")
|
50
64
|
end
|
@@ -3,7 +3,9 @@ require "imap/backup/email/provider/base"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides overrides for Apple mail accounts
|
6
7
|
class Email::Provider::AppleMail < Email::Provider::Base
|
8
|
+
# @return [String] the Apple Mail IMAP server host name
|
7
9
|
def host
|
8
10
|
"imap.mail.me.com"
|
9
11
|
end
|
@@ -4,11 +4,11 @@ module Imap::Backup
|
|
4
4
|
module Email; end
|
5
5
|
class Email::Provider; end
|
6
6
|
|
7
|
+
# Supplies defaults for email provider behaviour
|
7
8
|
class Email::Provider::Base
|
9
|
+
# @return [Hash] defaults for the Net::IMAP connection
|
8
10
|
def options
|
9
|
-
|
10
|
-
{port: 993, ssl: {ssl_version: :TLSv1_2}}
|
11
|
-
# rubocop:enable Naming/VariableNumber
|
11
|
+
{port: 993, ssl: {min_version: OpenSSL::SSL::TLS1_2_VERSION}}
|
12
12
|
end
|
13
13
|
|
14
14
|
def sets_seen_flags_on_fetch?
|
@@ -3,7 +3,9 @@ require "imap/backup/email/provider/base"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides overrides for Fastmail accounts
|
6
7
|
class Email::Provider::Fastmail < Email::Provider::Base
|
8
|
+
# @return [String] the Fastmail IMAP server host name
|
7
9
|
def host
|
8
10
|
"imap.fastmail.com"
|
9
11
|
end
|
@@ -3,7 +3,9 @@ require "imap/backup/email/provider/base"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides overrides for GMail accounts
|
6
7
|
class Email::Provider::GMail < Email::Provider::Base
|
8
|
+
# @return [String] the GMail IMAP server host name
|
7
9
|
def host
|
8
10
|
"imap.gmail.com"
|
9
11
|
end
|
@@ -3,7 +3,9 @@ require "imap/backup/email/provider/base"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides overrides for Purelymail accounts
|
6
7
|
class Email::Provider::Purelymail < Email::Provider::Base
|
8
|
+
# @return [String] The Purelymail IMAP server host name
|
7
9
|
def host
|
8
10
|
"mailserver.purelymail.com"
|
9
11
|
end
|
@@ -3,15 +3,11 @@ require "imap/backup/email/provider/base"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides overrides when the IMAP provider is not known
|
6
7
|
class Email::Provider::Unknown < Email::Provider::Base
|
7
8
|
# We don't know how to guess the IMAP server
|
9
|
+
# @return [nil]
|
8
10
|
def host
|
9
11
|
end
|
10
|
-
|
11
|
-
def options
|
12
|
-
# rubocop:disable Naming/VariableNumber
|
13
|
-
{port: 993, ssl: {ssl_version: :TLSv1_2}}
|
14
|
-
# rubocop:enable Naming/VariableNumber
|
15
|
-
end
|
16
12
|
end
|
17
13
|
end
|
@@ -9,7 +9,12 @@ module Imap; end
|
|
9
9
|
module Imap::Backup
|
10
10
|
module Email; end
|
11
11
|
|
12
|
+
# Provides a class factory for email account providers
|
12
13
|
class Email::Provider
|
14
|
+
# @param address [String] an email address
|
15
|
+
# @return [Email::Provider::Fastmail, Email::Provider::GMail, Email::Provider::AppleMail,
|
16
|
+
# Email::Provider::Purelymail, Email::Provider::Unknown]
|
17
|
+
# an instance supplying default values for the email's account set up
|
13
18
|
def self.for_address(address)
|
14
19
|
# rubocop:disable Lint/DuplicateBranch
|
15
20
|
case
|
@@ -1,11 +1,13 @@
|
|
1
1
|
module Imap; end
|
2
2
|
|
3
3
|
module Imap::Backup
|
4
|
+
# Accesses a file's access permissions
|
4
5
|
class FileMode
|
5
6
|
def initialize(filename:)
|
6
7
|
@filename = filename
|
7
8
|
end
|
8
9
|
|
10
|
+
# @return [Integer, nil] The user, group and "other" part of the file's "mode"
|
9
11
|
def mode
|
10
12
|
return nil if !File.exist?(filename)
|
11
13
|
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module Imap; end
|
2
2
|
|
3
3
|
module Imap::Backup
|
4
|
+
# Updates the flags on backed-up emails
|
4
5
|
class FlagRefresher
|
6
|
+
# The number of messages to process at a time
|
5
7
|
CHUNK_SIZE = 100
|
6
8
|
|
7
9
|
def initialize(folder, serializer)
|
@@ -9,6 +11,8 @@ module Imap::Backup
|
|
9
11
|
@serializer = serializer
|
10
12
|
end
|
11
13
|
|
14
|
+
# Runs the update
|
15
|
+
# @return [void]
|
12
16
|
def run
|
13
17
|
uids = serializer.uids.clone
|
14
18
|
|
@@ -3,6 +3,7 @@ require "imap/backup/logger"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Deletes locally backed-up emails that are no longer on the server
|
6
7
|
class LocalOnlyMessageDeleter
|
7
8
|
def initialize(folder, serializer)
|
8
9
|
@folder = folder
|
@@ -12,6 +13,7 @@ module Imap::Backup
|
|
12
13
|
# TODO: this method is very slow as it copies all messages.
|
13
14
|
# A quicker method would only remove UIDs from the .imap file,
|
14
15
|
# but that would require a garbage collection later.
|
16
|
+
# @return [void]
|
15
17
|
def run
|
16
18
|
local_only_uids = serializer.uids - folder.uids
|
17
19
|
if local_only_uids.empty?
|
data/lib/imap/backup/logger.rb
CHANGED
@@ -7,13 +7,26 @@ require "imap/backup/text/sanitizer"
|
|
7
7
|
module Imap; end
|
8
8
|
|
9
9
|
module Imap::Backup
|
10
|
+
# Wraps the standard logger, providing configuration and sanitization
|
10
11
|
class Logger
|
11
12
|
include Singleton
|
12
13
|
|
14
|
+
# @return [Imap::Backup::Logger] the singleton instance of this class
|
13
15
|
def self.logger
|
14
16
|
Logger.instance.logger
|
15
17
|
end
|
16
18
|
|
19
|
+
# @param options [Hash] command-line options
|
20
|
+
# @option options [Boolean] :quiet (false) if true, no output will be written
|
21
|
+
# @option options [Array<Boolean>] :verbose ([]) counts how many `--verbose`
|
22
|
+
# parameters were passed (and, potentially subtracts the number of
|
23
|
+
# `--no-verbose` parameters).
|
24
|
+
# If the result is 0, does normal info-level logging,
|
25
|
+
# If the result is 1, does debug logging,
|
26
|
+
# If the result is 2, does debug logging and client-server debug logging.
|
27
|
+
# This option is overridden by the `:verbose` option.
|
28
|
+
#
|
29
|
+
# @return [Hash] the options without the :quiet and :verbose keys
|
17
30
|
def self.setup_logging(options = {})
|
18
31
|
copy = options.clone
|
19
32
|
quiet = copy.delete(:quiet)
|
@@ -35,6 +48,9 @@ module Imap::Backup
|
|
35
48
|
copy
|
36
49
|
end
|
37
50
|
|
51
|
+
# Wraps a block, filtering output to standard error,
|
52
|
+
# hidng passwords and outputs the results to standard out
|
53
|
+
# @return [void]
|
38
54
|
def self.sanitize_stderr
|
39
55
|
sanitizer = Text::Sanitizer.new($stdout)
|
40
56
|
previous_stderr = $stderr
|
@@ -45,10 +61,12 @@ module Imap::Backup
|
|
45
61
|
$stderr = previous_stderr
|
46
62
|
end
|
47
63
|
|
64
|
+
# @private
|
48
65
|
def self.count(verbose)
|
49
66
|
verbose.reduce(1) { |acc, v| acc + (v ? 1 : -1) }
|
50
67
|
end
|
51
68
|
|
69
|
+
# @return [Logger] the configured Logger
|
52
70
|
attr_reader :logger
|
53
71
|
|
54
72
|
def initialize
|
data/lib/imap/backup/migrator.rb
CHANGED
@@ -3,6 +3,7 @@ require "imap/backup/logger"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Copies a folder of backed-up emails to an online folder
|
6
7
|
class Migrator
|
7
8
|
def initialize(serializer, folder, reset: false)
|
8
9
|
@folder = folder
|
@@ -10,6 +11,8 @@ module Imap::Backup
|
|
10
11
|
@serializer = serializer
|
11
12
|
end
|
12
13
|
|
14
|
+
# Runs the migration
|
15
|
+
# @return [void]
|
13
16
|
def run
|
14
17
|
count = serializer.uids.count
|
15
18
|
folder.create
|
@@ -5,6 +5,7 @@ module Imap; end
|
|
5
5
|
module Imap::Backup
|
6
6
|
class Mirror; end
|
7
7
|
|
8
|
+
# Keeps track of the mapping between source and destination UIDs
|
8
9
|
class Mirror::Map
|
9
10
|
def initialize(pathname:, destination:)
|
10
11
|
@pathname = pathname
|
@@ -16,6 +17,8 @@ module Imap::Backup
|
|
16
17
|
@map = nil
|
17
18
|
end
|
18
19
|
|
20
|
+
# @return [Boolean] whether the supplied values match the existing
|
21
|
+
# UID validity values
|
19
22
|
def check_uid_validities(source:, destination:)
|
20
23
|
store
|
21
24
|
return false if source != source_uid_validity
|
@@ -24,6 +27,8 @@ module Imap::Backup
|
|
24
27
|
true
|
25
28
|
end
|
26
29
|
|
30
|
+
# Sets, or resets to an empty state
|
31
|
+
# @return [void]
|
27
32
|
def reset(source_uid_validity:, destination_uid_validity:)
|
28
33
|
destination_store["source_uid_validity"] = source_uid_validity
|
29
34
|
@source_uid_validity = nil
|
@@ -33,6 +38,11 @@ module Imap::Backup
|
|
33
38
|
@map = nil
|
34
39
|
end
|
35
40
|
|
41
|
+
# @param destination_uid [Integer] a message UID from the destination server
|
42
|
+
#
|
43
|
+
# @raise [RuntimeError] if the UID validity is not set
|
44
|
+
# @return [Integer, nil] the source UID that is equivalent to the given destination UID
|
45
|
+
# or nil if it is not found
|
36
46
|
def source_uid(destination_uid)
|
37
47
|
if destination_store == {}
|
38
48
|
raise "Assign UID validities with #reset before calling #source_uid"
|
@@ -41,6 +51,11 @@ module Imap::Backup
|
|
41
51
|
map.key(destination_uid)
|
42
52
|
end
|
43
53
|
|
54
|
+
# @param source_uid [Integer] a message UID from the source server
|
55
|
+
#
|
56
|
+
# @raise [RuntimeError] if the UID validity is not set
|
57
|
+
# @return [Integer, nil] the destination UID that is equivalent to the given source UID
|
58
|
+
# or nil if it is not found
|
44
59
|
def destination_uid(source_uid)
|
45
60
|
if destination_store == {}
|
46
61
|
raise "Assign UID validities with #reset before calling #destination_uid"
|
@@ -49,12 +64,18 @@ module Imap::Backup
|
|
49
64
|
map[source_uid]
|
50
65
|
end
|
51
66
|
|
67
|
+
# Creates a mapping between message UIDs on the source
|
68
|
+
# and destination servers
|
69
|
+
# @raise [RuntimeError] if the UID validity is not set
|
70
|
+
# @return [void]
|
52
71
|
def map_uids(source:, destination:)
|
53
72
|
raise "Assign UID validities with #reset before calling #map_uids" if destination_store == {}
|
54
73
|
|
55
74
|
map[source] = destination
|
56
75
|
end
|
57
76
|
|
77
|
+
# Saves the map to disk as JSON
|
78
|
+
# @return [void]
|
58
79
|
def save
|
59
80
|
File.write(pathname, store.to_json)
|
60
81
|
end
|
data/lib/imap/backup/mirror.rb
CHANGED
@@ -3,12 +3,20 @@ require "imap/backup/mirror/map"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Synchronises a folder between a source and destination
|
6
7
|
class Mirror
|
7
8
|
def initialize(serializer, folder)
|
8
9
|
@serializer = serializer
|
9
10
|
@folder = folder
|
10
11
|
end
|
11
12
|
|
13
|
+
# If necessary, reates the destination folder,
|
14
|
+
# then deletes any messages in the destination folder
|
15
|
+
# that are not in the local store,
|
16
|
+
# sets existing messages' flas
|
17
|
+
# then appends any missing messages
|
18
|
+
# and saves the mapping file
|
19
|
+
# @return [void]
|
12
20
|
def run
|
13
21
|
ensure_destination_folder
|
14
22
|
delete_destination_only_emails
|
data/lib/imap/backup/naming.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
module Imap; end
|
2
2
|
|
3
3
|
module Imap::Backup
|
4
|
+
# Maps between server and file system folder names
|
5
|
+
# `/` is treated as an acceptable character
|
4
6
|
class Naming
|
7
|
+
# The characters that cannot be used in file names
|
5
8
|
INVALID_FILENAME_CHARACTERS = ":%;".freeze
|
9
|
+
# A regular expression that captures each disallowed character
|
6
10
|
INVALID_FILENAME_CHARACTER_MATCH = /([#{INVALID_FILENAME_CHARACTERS}])/.freeze
|
7
11
|
|
8
|
-
#
|
12
|
+
# @param name [String] a folder name
|
13
|
+
# @return [String] the supplied string iwth disallowed characters replaced
|
14
|
+
# by their hexadecimal representation
|
9
15
|
def self.to_local_path(name)
|
10
16
|
name.gsub(INVALID_FILENAME_CHARACTER_MATCH) do |character|
|
11
17
|
hex =
|
@@ -16,6 +22,9 @@ module Imap::Backup
|
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
25
|
+
# @param name [String] a serialized folder name
|
26
|
+
# @return the supplied string with hexadecimal codes ('%xx') replaced with
|
27
|
+
# the characters they represent
|
19
28
|
def self.from_local_path(name)
|
20
29
|
name.gsub(/%(.*?);/) do
|
21
30
|
::Regexp.last_match(1).
|
@@ -3,7 +3,16 @@ require "imap/backup/logger"
|
|
3
3
|
module Imap; end
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
+
# Provides a mechanism for retrying blocks of code which often throw errors
|
6
7
|
module RetryOnError
|
8
|
+
# Calls the supplied block,
|
9
|
+
# traps the given types of errors
|
10
|
+
# retrying up to a given number of times
|
11
|
+
# @param errors [Array<Exception>] the exceptions to trap
|
12
|
+
# @param limit [Integer] the maximum number of retries
|
13
|
+
# @param on_error [Proc] a block to call when an error occurs
|
14
|
+
# @raise any error ocurring more than `limit` times
|
15
|
+
# @return the result of any successful completion of the block
|
7
16
|
def retry_on_error(errors:, limit: 10, on_error: nil)
|
8
17
|
tries ||= 1
|
9
18
|
yield
|
@@ -7,6 +7,9 @@ module Imap::Backup
|
|
7
7
|
|
8
8
|
# Appends messages to the local store
|
9
9
|
class Serializer::Appender
|
10
|
+
# @param folder [String] the name of the folder
|
11
|
+
# @param imap [Serializer::Imap] the metadata serializer for the folder
|
12
|
+
# @param mbox [Serializer::Mbox] the folder's mailbox
|
10
13
|
def initialize(folder:, imap:, mbox:)
|
11
14
|
@folder = folder
|
12
15
|
@imap = imap
|
@@ -15,6 +18,12 @@ module Imap::Backup
|
|
15
18
|
|
16
19
|
# Adds a message to the metadata file and the mailbox.
|
17
20
|
# Wraps any errors with information about the message that caused them.
|
21
|
+
# @raise [RuntimeError] if the UID validity is not set
|
22
|
+
# or when an error occurs during serialization
|
23
|
+
# @param uid [Integer] the message's UID
|
24
|
+
# @param message [String] the on-disk version of the message
|
25
|
+
# @param flags [Array[Symbol]] the message's flags
|
26
|
+
# @return [void]
|
18
27
|
def append(uid:, message:, flags:)
|
19
28
|
raise "Can't add messages without uid_validity" if !imap.uid_validity
|
20
29
|
|
@@ -8,11 +8,13 @@ require "imap/backup/serializer/transaction"
|
|
8
8
|
module Imap; end
|
9
9
|
|
10
10
|
module Imap::Backup
|
11
|
+
# Wraps the Serializer, delaying metadata appends
|
11
12
|
class Serializer::DelayedMetadataSerializer
|
12
13
|
extend Forwardable
|
13
14
|
|
14
15
|
def_delegator :serializer, :uids
|
15
16
|
|
17
|
+
# @param serializer [Serializer] the serializer for a folder
|
16
18
|
def initialize(serializer:)
|
17
19
|
@serializer = serializer
|
18
20
|
@tsx = nil
|
@@ -20,7 +22,9 @@ module Imap::Backup
|
|
20
22
|
|
21
23
|
# Initializes the metadata and mailbox transactions, then calls the supplied block.
|
22
24
|
# Once the block has finished, commits changes to metadata
|
25
|
+
# @param block [block] the block that is wrapped by the transaction
|
23
26
|
#
|
27
|
+
# @raise any error ocurring during the commit phase
|
24
28
|
# @return [void]
|
25
29
|
def transaction(&block)
|
26
30
|
tsx.fail_in_transaction!(:transaction, message: "nested transactions are not supported")
|
@@ -12,6 +12,7 @@ module Imap::Backup
|
|
12
12
|
# The version number to store in the metadata file
|
13
13
|
CURRENT_VERSION = 3
|
14
14
|
|
15
|
+
# @return [String] The path of the imap metadata file, without the '.imap' extension
|
15
16
|
attr_reader :folder_path
|
16
17
|
|
17
18
|
# @param folder_path [String] The path of the imap metadata file, without the '.imap' extension
|
@@ -24,6 +25,10 @@ module Imap::Backup
|
|
24
25
|
@tsx = nil
|
25
26
|
end
|
26
27
|
|
28
|
+
# Opens a transaction
|
29
|
+
# @param block [block] the block that is wrapped by the transaction
|
30
|
+
# @raise any exception ocurring in the block
|
31
|
+
# @return [void]
|
27
32
|
def transaction(&block)
|
28
33
|
tsx.fail_in_transaction!(:transaction, message: "nested transactions are not supported")
|
29
34
|
|
@@ -41,6 +46,8 @@ module Imap::Backup
|
|
41
46
|
# rubocop:enable Lint/RescueException
|
42
47
|
end
|
43
48
|
|
49
|
+
# Discards stored changes to the data
|
50
|
+
# @return [void]
|
44
51
|
def rollback
|
45
52
|
tsx.fail_outside_transaction!(:rollback)
|
46
53
|
|
@@ -50,6 +57,7 @@ module Imap::Backup
|
|
50
57
|
tsx.clear
|
51
58
|
end
|
52
59
|
|
60
|
+
# @return [String] The full path name of the metadata file
|
53
61
|
def pathname
|
54
62
|
"#{folder_path}.imap"
|
55
63
|
end
|
@@ -66,6 +74,11 @@ module Imap::Backup
|
|
66
74
|
true
|
67
75
|
end
|
68
76
|
|
77
|
+
# Append message metadata
|
78
|
+
# @param uid [Integer] the message's UID
|
79
|
+
# @param length [Integer] the length of the message (as stored on disk)
|
80
|
+
# @param flags [Array[Symbol]] the message's flags
|
81
|
+
# @return [void]
|
69
82
|
def append(uid, length, flags: [])
|
70
83
|
offset =
|
71
84
|
if messages.empty?
|
@@ -82,10 +95,16 @@ module Imap::Backup
|
|
82
95
|
save
|
83
96
|
end
|
84
97
|
|
98
|
+
# Get message metadata
|
99
|
+
# @param uid [Integer] a message UID
|
100
|
+
# @return [Serializer::Message]
|
85
101
|
def get(uid)
|
86
102
|
messages.find { |m| m.uid == uid }
|
87
103
|
end
|
88
104
|
|
105
|
+
# Deletes the metadata file
|
106
|
+
# and discards stored attributes
|
107
|
+
# @return [void]
|
89
108
|
def delete
|
90
109
|
return if !exist?
|
91
110
|
|
@@ -96,6 +115,10 @@ module Imap::Backup
|
|
96
115
|
@version = nil
|
97
116
|
end
|
98
117
|
|
118
|
+
# Renames the metadata file, if it exists,
|
119
|
+
# otherwise, simply stores the new name
|
120
|
+
# @param new_path [String] the new path (without extension)
|
121
|
+
# @return [void]
|
99
122
|
def rename(new_path)
|
100
123
|
if exist?
|
101
124
|
old_pathname = pathname
|
@@ -106,28 +129,36 @@ module Imap::Backup
|
|
106
129
|
end
|
107
130
|
end
|
108
131
|
|
132
|
+
# @return [Integer] the UID validity for the folder
|
109
133
|
def uid_validity
|
110
134
|
ensure_loaded
|
111
135
|
@uid_validity
|
112
136
|
end
|
113
137
|
|
138
|
+
# Sets the folder's UID validity and saves the metadata file
|
139
|
+
# @param value [Integer] the new UID validity
|
140
|
+
# @return [void]
|
114
141
|
def uid_validity=(value)
|
115
142
|
ensure_loaded
|
116
143
|
@uid_validity = value
|
117
144
|
save
|
118
145
|
end
|
119
146
|
|
120
|
-
#
|
147
|
+
# @return [Array<Hash>]
|
121
148
|
def messages
|
122
149
|
ensure_loaded
|
123
150
|
@messages
|
124
151
|
end
|
125
152
|
|
126
|
-
#
|
153
|
+
# @return [Array<Integer>] The uids of all messages
|
127
154
|
def uids
|
128
155
|
messages.map(&:uid)
|
129
156
|
end
|
130
157
|
|
158
|
+
# Update a message's metadata, replacing its UID
|
159
|
+
# @param old [Integer] the existing message UID
|
160
|
+
# @param new [Integer] the new UID to apply to the message
|
161
|
+
# @return [void]
|
131
162
|
def update_uid(old, new)
|
132
163
|
index = messages.find_index { |m| m.uid == old }
|
133
164
|
return if index.nil?
|
@@ -136,11 +167,16 @@ module Imap::Backup
|
|
136
167
|
save
|
137
168
|
end
|
138
169
|
|
170
|
+
# @return [String] The format version for the metadata file
|
139
171
|
def version
|
140
172
|
ensure_loaded
|
141
173
|
@version
|
142
174
|
end
|
143
175
|
|
176
|
+
# Saves the file,
|
177
|
+
# except in a transaction when it does nothing
|
178
|
+
# @raise [RuntimeError] if UID validity has not been set
|
179
|
+
# @return [void]
|
144
180
|
def save
|
145
181
|
return if tsx.in_transaction?
|
146
182
|
|