imap-backup 9.4.0.pre1 → 10.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/docs/development.md +6 -0
  3. data/lib/email/mboxrd/message.rb +5 -13
  4. data/lib/imap/backup/account/backup.rb +65 -0
  5. data/lib/imap/backup/account/{connection/backup_folders.rb → backup_folders.rb} +12 -5
  6. data/lib/imap/backup/account/{connection/client_factory.rb → client_factory.rb} +11 -11
  7. data/lib/imap/backup/account/folder.rb +8 -14
  8. data/lib/imap/backup/account/folder_ensurer.rb +24 -0
  9. data/lib/imap/backup/account/local_only_folder_deleter.rb +26 -0
  10. data/lib/imap/backup/account/restore.rb +20 -0
  11. data/lib/imap/backup/account/serialized_folders.rb +41 -0
  12. data/lib/imap/backup/account.rb +16 -4
  13. data/lib/imap/backup/cli/backup.rb +6 -7
  14. data/lib/imap/backup/cli/folder_enumerator.rb +1 -1
  15. data/lib/imap/backup/cli/helpers.rb +15 -15
  16. data/lib/imap/backup/cli/local.rb +31 -19
  17. data/lib/imap/backup/cli/mirror.rb +1 -1
  18. data/lib/imap/backup/cli/remote.rb +8 -8
  19. data/lib/imap/backup/cli/restore.rb +10 -14
  20. data/lib/imap/backup/cli/stats.rb +8 -3
  21. data/lib/imap/backup/cli/utils.rb +14 -5
  22. data/lib/imap/backup/cli.rb +13 -9
  23. data/lib/imap/backup/client/default.rb +28 -5
  24. data/lib/imap/backup/configuration.rb +8 -2
  25. data/lib/imap/backup/downloader.rb +4 -1
  26. data/lib/imap/backup/file_mode.rb +16 -0
  27. data/lib/imap/backup/mirror.rb +1 -2
  28. data/lib/imap/backup/serializer/directory.rb +6 -4
  29. data/lib/imap/backup/serializer/folder_maker.rb +33 -0
  30. data/lib/imap/backup/serializer/permission_checker.rb +26 -0
  31. data/lib/imap/backup/serializer.rb +9 -3
  32. data/lib/imap/backup/setup/connection_tester.rb +1 -7
  33. data/lib/imap/backup/setup/folder_chooser.rb +9 -9
  34. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +34 -7
  35. data/lib/imap/backup/uploader.rb +1 -1
  36. data/lib/imap/backup/version.rb +3 -3
  37. data/lib/imap/backup.rb +0 -2
  38. metadata +14 -9
  39. data/lib/imap/backup/account/connection/folder_names.rb +0 -26
  40. data/lib/imap/backup/account/connection.rb +0 -136
  41. data/lib/imap/backup/utils.rb +0 -40
@@ -16,26 +16,22 @@ module Imap::Backup
16
16
  def run
17
17
  config = load_config(**options)
18
18
  case
19
- when email && !emails
20
- connection = connection(config, email)
21
- connection.restore
22
- when !email && !emails
19
+ when email && !options.key?(:accounts)
20
+ account = account(config, email)
21
+ account.restore
22
+ when !email && !options.key?(:accounts)
23
23
  Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
24
- each_connection(config, [], &:restore)
25
- when email && emails.any?
26
- raise "Pass either an email or the --accounts option, not both"
27
- when emails.any?
24
+ config.accounts.map(&:restore)
25
+ when email && options.key?(:accounts)
26
+ raise "Missing EMAIL parameter"
27
+ when !email && options.key?(:accounts)
28
28
  Logger.logger.info(
29
29
  "Calling restore with the --account option is deprected, " \
30
- "please pass a single EMAIL argument"
30
+ "please pass a single EMAIL parameter"
31
31
  )
32
- each_connection(config, emails, &:restore)
32
+ requested_accounts(config).each(&:restore)
33
33
  end
34
34
  end
35
-
36
- def emails
37
- @emails ||= options[:accounts].split(",") if options.key?(:accounts)
38
- end
39
35
  end
40
36
  end
41
37
  end
@@ -1,3 +1,5 @@
1
+ require "imap/backup/account/backup_folders"
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Stats < Thor
3
5
  include Thor::Actions
@@ -32,12 +34,15 @@ module Imap::Backup
32
34
 
33
35
  def stats
34
36
  config = load_config(**options)
35
- connection = connection(config, email)
37
+ account = account(config, email)
36
38
 
37
- connection.backup_folders.map do |folder|
39
+ backup_folders = Account::BackupFolders.new(
40
+ client: account.client, account: account
41
+ )
42
+ backup_folders.map do |folder|
38
43
  next if !folder.exist?
39
44
 
40
- serializer = Serializer.new(connection.account.local_path, folder.name)
45
+ serializer = Serializer.new(account.local_path, folder.name)
41
46
  local_uids = serializer.uids
42
47
  remote_uids = folder.uids
43
48
  {
@@ -1,3 +1,5 @@
1
+ require "imap/backup/account/backup_folders"
2
+ require "imap/backup/account/serialized_folders"
1
3
  require "imap/backup/thunderbird/mailbox_exporter"
2
4
 
3
5
  module Imap::Backup
@@ -14,12 +16,15 @@ module Imap::Backup
14
16
  def ignore_history(email)
15
17
  Logger.setup_logging options
16
18
  config = load_config(**options)
17
- connection = connection(config, email)
19
+ account = account(config, email)
18
20
 
19
- connection.backup_folders.each do |folder|
21
+ backup_folders = Account::BackupFolders.new(
22
+ client: account.client, account: account
23
+ )
24
+ backup_folders.each do |folder|
20
25
  next if !folder.exist?
21
26
 
22
- serializer = Serializer.new(connection.account.local_path, folder.name)
27
+ serializer = Serializer.new(account.local_path, folder.name)
23
28
  do_ignore_folder_history(folder, serializer)
24
29
  end
25
30
  end
@@ -52,7 +57,7 @@ module Imap::Backup
52
57
  profile_name = options[:profile]
53
58
 
54
59
  config = load_config(**options)
55
- connection = connection(config, email)
60
+ account = account(config, email)
56
61
  profile = thunderbird_profile(profile_name)
57
62
 
58
63
  if !profile
@@ -61,7 +66,11 @@ module Imap::Backup
61
66
  raise "Default Thunderbird profile not found"
62
67
  end
63
68
 
64
- connection.local_folders.each do |serializer, _folder|
69
+ serialized_folders = Account::SerializedFolders.new(account: account)
70
+
71
+ raise "No serialized folders were found for account '#{email}'" if serialized_folders.none?
72
+
73
+ serialized_folders.each do |serializer, _folder|
65
74
  Thunderbird::MailboxExporter.new(
66
75
  email, serializer, profile, force: force
67
76
  ).run
@@ -20,9 +20,17 @@ module Imap::Backup
20
20
 
21
21
  include Helpers
22
22
 
23
+ VERSION_ARGUMENTS = %w(-v --version).freeze
24
+
23
25
  default_task :backup
24
26
 
25
27
  def self.start(*args)
28
+ version_argument = ARGV & VERSION_ARGUMENTS
29
+ if version_argument.any?
30
+ new.version
31
+ exit 0
32
+ end
33
+
26
34
  # By default, commands like `imap-backup help foo bar`
27
35
  # are handled by listing all `foo` methods, whereas the user
28
36
  # probably wants the detailed help for the `bar` method.
@@ -41,15 +49,6 @@ module Imap::Backup
41
49
  true
42
50
  end
43
51
 
44
- def self.accounts_option
45
- method_option(
46
- "accounts",
47
- type: :string,
48
- desc: "a comma-separated list of accounts (defaults to all configured accounts)",
49
- aliases: ["-a"]
50
- )
51
- end
52
-
53
52
  desc "backup [OPTIONS]", "Run the backup"
54
53
  long_desc <<~DESC
55
54
  Downloads any emails not yet present locally.
@@ -227,5 +226,10 @@ module Imap::Backup
227
226
 
228
227
  desc "utils SUBCOMMAND [OPTIONS]", "Various utilities"
229
228
  subcommand "utils", Utils
229
+
230
+ desc "version", "Print the imap-backup version"
231
+ def version
232
+ Kernel.puts "imap-backup #{Imap::Backup::VERSION}"
233
+ end
230
234
  end
231
235
  end
@@ -7,15 +7,19 @@ module Imap::Backup
7
7
  class Client::Default
8
8
  extend Forwardable
9
9
  def_delegators :imap, *%i(
10
- append authenticate create expunge login namespace
10
+ append authenticate create expunge namespace
11
11
  responses uid_fetch uid_search uid_store
12
12
  )
13
13
 
14
- attr_reader :args
14
+ attr_reader :account
15
+ attr_reader :options
16
+ attr_reader :server
15
17
  attr_accessor :state
16
18
 
17
- def initialize(*args)
18
- @args = args
19
+ def initialize(server, account, options)
20
+ @account = account
21
+ @options = options
22
+ @server = server
19
23
  @state = nil
20
24
  end
21
25
 
@@ -28,6 +32,21 @@ module Imap::Backup
28
32
  mailbox_lists.map { |ml| extract_name(ml) }
29
33
  end
30
34
 
35
+ def login
36
+ Logger.logger.debug "Logging in: #{account.username}/#{masked_password}"
37
+ imap.login(account.username, account.password)
38
+ Logger.logger.debug "Login complete"
39
+ end
40
+
41
+ def reconnect
42
+ disconnect
43
+ login
44
+ end
45
+
46
+ def username
47
+ account.username
48
+ end
49
+
31
50
  # Track mailbox selection during delegation to Net::IMAP instance
32
51
 
33
52
  def disconnect
@@ -53,7 +72,7 @@ module Imap::Backup
53
72
  private
54
73
 
55
74
  def imap
56
- @imap ||= Net::IMAP.new(*args)
75
+ @imap ||= Net::IMAP.new(server, options)
57
76
  end
58
77
 
59
78
  def extract_name(mailbox_list)
@@ -61,6 +80,10 @@ module Imap::Backup
61
80
  Net::IMAP.decode_utf7(utf7_encoded)
62
81
  end
63
82
 
83
+ def masked_password
84
+ account.password.gsub(/./, "x")
85
+ end
86
+
64
87
  # 6.3.8. LIST Command
65
88
  # An empty ("" string) mailbox name argument is a special request to
66
89
  # return the hierarchy delimiter and the root name of the name given
@@ -1,7 +1,10 @@
1
+ require "fileutils"
1
2
  require "json"
2
3
  require "os"
3
4
 
4
5
  require "imap/backup/account"
6
+ require "imap/backup/file_mode"
7
+ require "imap/backup/serializer/permission_checker"
5
8
 
6
9
  module Imap::Backup
7
10
  class Configuration
@@ -66,7 +69,10 @@ module Imap::Backup
66
69
  def data
67
70
  @data ||=
68
71
  if File.exist?(pathname)
69
- Utils.check_permissions(pathname, 0o600) if !windows?
72
+ permission_checker = Serializer::PermissionChecker.new(
73
+ filename: pathname, limit: 0o600
74
+ )
75
+ permission_checker.run if !windows?
70
76
  contents = File.read(pathname)
71
77
  JSON.parse(contents, symbolize_names: true)
72
78
  else
@@ -83,7 +89,7 @@ module Imap::Backup
83
89
  end
84
90
 
85
91
  def make_private(path)
86
- FileUtils.chmod(0o700, path) if Utils.mode(path) != 0o700
92
+ FileUtils.chmod(0o700, path) if FileMode.new(filename: path).mode != 0o700
87
93
  end
88
94
 
89
95
  def windows?
@@ -24,7 +24,7 @@ module Imap::Backup
24
24
  end
25
25
 
26
26
  def run
27
- debug "#{uids.count} new messages"
27
+ info("#{uids.count} new messages") if uids.any?
28
28
 
29
29
  uids.each_slice(multi_fetch_size).with_index do |block, i|
30
30
  multifetch_failed = download_block(block, i)
@@ -35,6 +35,9 @@ module Imap::Backup
35
35
  @multi_fetch_size = 1
36
36
  @uids = nil
37
37
  retry
38
+ rescue Net::IMAP::ByeResponseError
39
+ folder.client.reconnect
40
+ retry
38
41
  end
39
42
 
40
43
  private
@@ -0,0 +1,16 @@
1
+ module Imap::Backup
2
+ class FileMode
3
+ attr_reader :filename
4
+
5
+ def initialize(filename:)
6
+ @filename = filename
7
+ end
8
+
9
+ def mode
10
+ return nil if !File.exist?(filename)
11
+
12
+ stat = File.stat(filename)
13
+ stat.mode & 0o777
14
+ end
15
+ end
16
+ end
@@ -94,8 +94,7 @@ module Imap::Backup
94
94
  end
95
95
 
96
96
  def destination_email
97
- # TODO: is there a more elegant way to get the email?
98
- folder.connection.account.username
97
+ folder.client.username
99
98
  end
100
99
  end
101
100
  end
@@ -1,5 +1,7 @@
1
1
  require "os"
2
2
 
3
+ require "imap/backup/serializer/folder_maker"
4
+
3
5
  module Imap::Backup
4
6
  class Serializer; end
5
7
 
@@ -16,13 +18,13 @@ module Imap::Backup
16
18
 
17
19
  def ensure_exists
18
20
  if !File.directory?(full_path)
19
- Utils.make_folder(
20
- path, relative, DIRECTORY_PERMISSIONS
21
- )
21
+ Serializer::FolderMaker.new(
22
+ base: path, path: relative, permissions: DIRECTORY_PERMISSIONS
23
+ ).run
22
24
  end
23
25
 
24
26
  return if OS.windows?
25
- return if Utils.mode(full_path) == DIRECTORY_PERMISSIONS
27
+ return if FileMode.new(filename: full_path).mode == DIRECTORY_PERMISSIONS
26
28
 
27
29
  FileUtils.chmod DIRECTORY_PERMISSIONS, full_path
28
30
  end
@@ -0,0 +1,33 @@
1
+ require "fileutils"
2
+
3
+ module Imap::Backup
4
+ class Serializer; end
5
+
6
+ class Serializer::FolderMaker
7
+ attr_reader :base
8
+ attr_reader :path
9
+ attr_reader :permissions
10
+
11
+ def initialize(base:, path:, permissions:)
12
+ @base = base
13
+ @path = path
14
+ @permissions = permissions
15
+ end
16
+
17
+ def run
18
+ parts = path.split("/")
19
+ return if parts.empty?
20
+
21
+ FileUtils.mkdir_p(full_path) if !File.exist?(full_path)
22
+ full = base
23
+ parts.each do |part|
24
+ full = File.join(full, part)
25
+ FileUtils.chmod permissions, full
26
+ end
27
+ end
28
+
29
+ def full_path
30
+ File.join(base, path)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ module Imap::Backup
2
+ class Serializer::PermissionChecker
3
+ attr_reader :filename
4
+ attr_reader :limit
5
+
6
+ def initialize(filename:, limit:)
7
+ @filename = filename
8
+ @limit = limit
9
+ end
10
+
11
+ def run
12
+ actual = FileMode.new(filename: filename).mode
13
+ return nil if actual.nil?
14
+
15
+ mask = ~limit & 0o777
16
+ return if (actual & mask).zero?
17
+
18
+ message = format(
19
+ "Permissions on '%<filename>s' " \
20
+ "should be 0%<limit>o, not 0%<actual>o",
21
+ filename: filename, limit: limit, actual: actual
22
+ )
23
+ raise message
24
+ end
25
+ end
26
+ end
@@ -30,14 +30,20 @@ module Imap::Backup
30
30
  def initialize(path, folder)
31
31
  @path = path
32
32
  @folder = folder
33
+ @validated = nil
33
34
  end
34
35
 
35
36
  # Returns true if there are existing, valid files
36
37
  # false otherwise (in which case any existing files are deleted)
37
38
  def validate!
39
+ return true if @validated
40
+
38
41
  optionally_migrate2to3
39
42
 
40
- return true if imap.valid? && mbox.valid?
43
+ if imap.valid? && mbox.valid?
44
+ @validated = true
45
+ return true
46
+ end
41
47
 
42
48
  delete
43
49
 
@@ -113,7 +119,7 @@ module Imap::Backup
113
119
  # NOOP
114
120
  nil
115
121
  else
116
- apply_new_uid_validity value
122
+ apply_new_uid_validity(value)
117
123
  end
118
124
  end
119
125
 
@@ -216,7 +222,7 @@ module Imap::Backup
216
222
  Logger.logger.info <<~MESSAGE
217
223
  Local metadata for folder '#{folder_path}' is currently stored in the version 2 format.
218
224
 
219
- Migrating the version 3 format...
225
+ This will now be transformed into the version 3 format.
220
226
  MESSAGE
221
227
 
222
228
  migrator.run
@@ -9,18 +9,12 @@ module Imap::Backup
9
9
  end
10
10
 
11
11
  def test
12
- connection.client
12
+ account.client
13
13
  "Connection successful"
14
14
  rescue Net::IMAP::NoResponseError
15
15
  "No response"
16
16
  rescue StandardError => e
17
17
  "Unexpected error: #{e}"
18
18
  end
19
-
20
- private
21
-
22
- def connection
23
- Account::Connection.new(account)
24
- end
25
19
  end
26
20
  end
@@ -11,13 +11,12 @@ module Imap::Backup
11
11
  end
12
12
 
13
13
  def run
14
- if connection.nil?
15
- Logger.logger.warn "Connection failed"
14
+ if client.nil?
16
15
  highline.ask "Press a key "
17
16
  return
18
17
  end
19
18
 
20
- if imap_folders.nil?
19
+ if folder_names.empty?
21
20
  Logger.logger.warn "Unable to get folder list"
22
21
  highline.ask "Press a key "
23
22
  return
@@ -50,7 +49,7 @@ module Imap::Backup
50
49
  end
51
50
 
52
51
  def add_folders(menu)
53
- imap_folders.each do |folder|
52
+ folder_names.each do |folder|
54
53
  mark = selected?(folder) ? "+" : "-"
55
54
  menu.choice("#{mark} #{folder}") do
56
55
  toggle_selection folder
@@ -69,7 +68,7 @@ module Imap::Backup
69
68
  removed = []
70
69
  config_folders = []
71
70
  account.folders.each do |f|
72
- found = imap_folders.find { |folder| folder == f[:name] }
71
+ found = folder_names.find { |folder| folder == f[:name] }
73
72
  if found
74
73
  config_folders << f
75
74
  else
@@ -98,14 +97,15 @@ module Imap::Backup
98
97
  end
99
98
  end
100
99
 
101
- def connection
102
- @connection ||= Account::Connection.new(account)
100
+ def client
101
+ @client ||= account.client
103
102
  rescue StandardError
103
+ Logger.logger.warn "Connection failed"
104
104
  nil
105
105
  end
106
106
 
107
- def imap_folders
108
- @imap_folders ||= connection.folder_names
107
+ def folder_names
108
+ @folder_names ||= client.list
109
109
  end
110
110
 
111
111
  def highline
@@ -18,8 +18,18 @@ module Imap::Backup
18
18
  end
19
19
 
20
20
  def run
21
+ if !profile_set_up
22
+ error "The Thunderbird profile '#{profile.title}' " \
23
+ "has not been set up. " \
24
+ "Please set it up before trying to export"
25
+ return false
26
+ end
27
+
21
28
  local_folder_ok = local_folder.set_up
22
- return false if !local_folder_ok
29
+ if !local_folder_ok
30
+ error "Failed to set up local folder"
31
+ return false
32
+ end
23
33
 
24
34
  skip_for_msf = check_msf
25
35
  return false if skip_for_msf
@@ -27,6 +37,7 @@ module Imap::Backup
27
37
  skip_for_local_folder = check_local_folder
28
38
  return false if skip_for_local_folder
29
39
 
40
+ info "Exporting account '#{email}' to folder '#{local_folder.full_path}'"
30
41
  copy_messages
31
42
 
32
43
  true
@@ -34,15 +45,19 @@ module Imap::Backup
34
45
 
35
46
  private
36
47
 
48
+ def profile_set_up
49
+ File.exist?(profile.local_folders_path)
50
+ end
51
+
37
52
  def check_local_folder
38
53
  return false if !local_folder.exists?
39
54
 
40
55
  if force
41
- Kernel.puts "Overwriting '#{local_folder.path}' as --force option was supplied"
56
+ info "Overwriting '#{local_folder.path}' as --force option was supplied"
42
57
  return false
43
58
  end
44
59
 
45
- Kernel.puts "Skipping export of '#{serializer.folder}' as '#{local_folder.path}' exists"
60
+ warning "Skipping export of '#{serializer.folder}' as '#{local_folder.full_path}' exists"
46
61
  true
47
62
  end
48
63
 
@@ -50,12 +65,12 @@ module Imap::Backup
50
65
  return false if !local_folder.msf_exists?
51
66
 
52
67
  if force
53
- Kernel.puts "Deleting '#{local_folder.msf_path}' as --force option was supplied"
68
+ info "Deleting '#{local_folder.msf_path}' as --force option was supplied"
54
69
  File.unlink local_folder.msf_path
55
70
  return false
56
71
  end
57
72
 
58
- Kernel.puts(
73
+ warning(
59
74
  "Skipping export of '#{serializer.folder}' " \
60
75
  "as '#{local_folder.msf_path}' exists"
61
76
  )
@@ -66,8 +81,8 @@ module Imap::Backup
66
81
  File.open(local_folder.full_path, "w") do |f|
67
82
  serializer.messages.each do |message|
68
83
  timestamp = Time.now.strftime("%a %b %d %H:%M:%S %Y")
69
- thunderbird_fom_line = "From - #{timestamp}"
70
- output = "#{thunderbird_fom_line}\n#{message.body}\n"
84
+ thunderbird_from_line = "From - #{timestamp}"
85
+ output = "#{thunderbird_from_line}\n#{message.body}\n"
71
86
  f.write output
72
87
  end
73
88
  end
@@ -80,5 +95,17 @@ module Imap::Backup
80
95
  Thunderbird::LocalFolder.new(profile, prefixed_folder_path)
81
96
  end
82
97
  end
98
+
99
+ def error(message)
100
+ Logger.logger.error("[Thunderbird::MailboxExporter] #{message}")
101
+ end
102
+
103
+ def info(message)
104
+ Logger.logger.info("[Thunderbird::MailboxExporter] #{message}")
105
+ end
106
+
107
+ def warning(message)
108
+ Logger.logger.warn("[Thunderbird::MailboxExporter] #{message}")
109
+ end
83
110
  end
84
111
  end
@@ -64,7 +64,7 @@ module Imap::Backup
64
64
  Logger.logger.debug(
65
65
  "Backup '#{serializer.folder}' renamed and restored to '#{new_name}'"
66
66
  )
67
- @folder = Account::Folder.new(folder.connection, new_name)
67
+ @folder = Account::Folder.new(folder.client, new_name)
68
68
  folder.create
69
69
  @serializer = Serializer.new(serializer.path, new_name)
70
70
  serializer.force_uid_validity(@folder.uid_validity)
@@ -1,9 +1,9 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 9
5
- MINOR = 4
4
+ MAJOR = 10
5
+ MINOR = 0
6
6
  REVISION = 0
7
- PRE = "pre1"
7
+ PRE = nil
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
data/lib/imap/backup.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  module Imap; end
2
2
 
3
- require "imap/backup/utils"
4
- require "imap/backup/account/connection"
5
3
  require "imap/backup/account/folder"
6
4
  require "imap/backup/configuration"
7
5
  require "imap/backup/downloader"