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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/imap-backup.gemspec +5 -1
  3. data/lib/cli_coverage.rb +11 -11
  4. data/lib/email/provider/base.rb +2 -0
  5. data/lib/email/provider/unknown.rb +2 -0
  6. data/lib/email/provider.rb +2 -0
  7. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  8. data/lib/imap/backup/account/connection/client_factory.rb +54 -0
  9. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  10. data/lib/imap/backup/account/connection.rb +11 -95
  11. data/lib/imap/backup/account/folder.rb +9 -6
  12. data/lib/imap/backup/account.rb +3 -5
  13. data/lib/imap/backup/cli/backup.rb +1 -3
  14. data/lib/imap/backup/cli/helpers.rb +24 -22
  15. data/lib/imap/backup/cli/local.rb +20 -13
  16. data/lib/imap/backup/cli/migrate.rb +4 -10
  17. data/lib/imap/backup/cli/restore.rb +8 -7
  18. data/lib/imap/backup/cli/setup.rb +10 -8
  19. data/lib/imap/backup/cli/stats.rb +78 -0
  20. data/lib/imap/backup/cli/status.rb +2 -2
  21. data/lib/imap/backup/cli/utils.rb +4 -6
  22. data/lib/imap/backup/cli.rb +24 -3
  23. data/lib/imap/backup/configuration.rb +9 -11
  24. data/lib/imap/backup/downloader.rb +48 -30
  25. data/lib/imap/backup/migrator.rb +5 -5
  26. data/lib/imap/backup/sanitizer.rb +3 -2
  27. data/lib/imap/backup/serializer/appender.rb +49 -0
  28. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  29. data/lib/imap/backup/serializer/unused_name_finder.rb +27 -0
  30. data/lib/imap/backup/serializer.rb +24 -78
  31. data/lib/imap/backup/setup/account/header.rb +75 -0
  32. data/lib/imap/backup/setup/account.rb +14 -91
  33. data/lib/imap/backup/setup/asker.rb +4 -15
  34. data/lib/imap/backup/setup/backup_path.rb +41 -0
  35. data/lib/imap/backup/setup/email.rb +45 -0
  36. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  37. data/lib/imap/backup/setup/helpers.rb +1 -1
  38. data/lib/imap/backup/setup.rb +5 -4
  39. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +39 -20
  40. data/lib/imap/backup/uploader.rb +46 -8
  41. data/lib/imap/backup/utils.rb +1 -1
  42. data/lib/imap/backup/version.rb +1 -1
  43. data/lib/imap/backup.rb +0 -1
  44. metadata +31 -134
  45. data/spec/features/backup_spec.rb +0 -100
  46. data/spec/features/configuration/minimal_configuration.rb +0 -15
  47. data/spec/features/configuration/missing_configuration.rb +0 -14
  48. data/spec/features/folders_spec.rb +0 -36
  49. data/spec/features/helper.rb +0 -2
  50. data/spec/features/local/list_accounts_spec.rb +0 -12
  51. data/spec/features/local/list_emails_spec.rb +0 -21
  52. data/spec/features/local/list_folders_spec.rb +0 -21
  53. data/spec/features/local/show_an_email_spec.rb +0 -34
  54. data/spec/features/migrate_spec.rb +0 -35
  55. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  56. data/spec/features/restore_spec.rb +0 -162
  57. data/spec/features/status_spec.rb +0 -43
  58. data/spec/features/support/aruba.rb +0 -78
  59. data/spec/features/support/backup_directory.rb +0 -43
  60. data/spec/features/support/email_server.rb +0 -110
  61. data/spec/features/support/shared/connection_context.rb +0 -14
  62. data/spec/features/support/shared/message_fixtures.rb +0 -16
  63. data/spec/fixtures/connection.yml +0 -7
  64. data/spec/spec_helper.rb +0 -15
  65. data/spec/support/fixtures.rb +0 -11
  66. data/spec/support/higline_test_helpers.rb +0 -8
  67. data/spec/support/silence_logging.rb +0 -7
  68. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  69. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  70. data/spec/unit/email/provider/base_spec.rb +0 -11
  71. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  72. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  73. data/spec/unit/email/provider_spec.rb +0 -27
  74. data/spec/unit/imap/backup/account/connection_spec.rb +0 -433
  75. data/spec/unit/imap/backup/account/folder_spec.rb +0 -261
  76. data/spec/unit/imap/backup/account_spec.rb +0 -246
  77. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -58
  78. data/spec/unit/imap/backup/cli/backup_spec.rb +0 -19
  79. data/spec/unit/imap/backup/cli/folders_spec.rb +0 -39
  80. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  81. data/spec/unit/imap/backup/cli/local_spec.rb +0 -100
  82. data/spec/unit/imap/backup/cli/migrate_spec.rb +0 -80
  83. data/spec/unit/imap/backup/cli/restore_spec.rb +0 -67
  84. data/spec/unit/imap/backup/cli/setup_spec.rb +0 -17
  85. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -125
  86. data/spec/unit/imap/backup/cli_spec.rb +0 -93
  87. data/spec/unit/imap/backup/client/apple_mail_spec.rb +0 -9
  88. data/spec/unit/imap/backup/client/default_spec.rb +0 -22
  89. data/spec/unit/imap/backup/configuration_spec.rb +0 -238
  90. data/spec/unit/imap/backup/downloader_spec.rb +0 -96
  91. data/spec/unit/imap/backup/logger_spec.rb +0 -48
  92. data/spec/unit/imap/backup/migrator_spec.rb +0 -58
  93. data/spec/unit/imap/backup/sanitizer_spec.rb +0 -42
  94. data/spec/unit/imap/backup/serializer/directory_spec.rb +0 -37
  95. data/spec/unit/imap/backup/serializer/imap_spec.rb +0 -218
  96. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
  97. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -101
  98. data/spec/unit/imap/backup/serializer_spec.rb +0 -296
  99. data/spec/unit/imap/backup/setup/account_spec.rb +0 -461
  100. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  101. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  102. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  103. data/spec/unit/imap/backup/setup/helpers_spec.rb +0 -15
  104. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  105. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +0 -116
  106. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  107. data/spec/unit/imap/backup/utils_spec.rb +0 -92
  108. 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
- raise "Thunderbird profile '#{profile_name}' not found"
52
- else
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
- Imap::Backup::Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
65
+ Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
68
66
 
69
67
  serializer.apply_uid_validity(folder.uid_validity)
70
68
 
@@ -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 "migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
65
- "[Experimental] Uploads backed-up emails from account SOURCE_EMAIL to account DESTINATION_EMAIL"
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 "status", "Show backup status"
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
- begin
85
- if File.exist?(pathname)
86
- Utils.check_permissions(pathname, 0o600) if !windows?
87
- contents = File.read(pathname)
88
- JSON.parse(contents, symbolize_names: true)
89
- else
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 { |a| a.clear_changes }
94
+ accounts.each(&:clear_changes)
97
95
  end
98
96
 
99
97
  def remove_deleted_accounts
100
- accounts.reject! { |a| a.marked_for_deletion? }
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 = folder.uids - serializer.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
- offset = i * multi_fetch_size + 1
32
- uids_and_bodies.each.with_index do |uid_and_body, j|
33
- uid = uid_and_body[:uid]
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
- Imap::Backup::Logger.logger.info("[#{folder.name}] #{message}")
72
+ Logger.logger.info("[#{folder.name}] #{message}")
55
73
  end
56
74
 
57
75
  def debug(message)
58
- Imap::Backup::Logger.logger.debug("[#{folder.name}] #{message}")
76
+ Logger.logger.debug("[#{folder.name}] #{message}")
59
77
  end
60
78
  end
61
79
  end
@@ -15,12 +15,12 @@ module Imap::Backup
15
15
  folder.create
16
16
  ensure_destination_empty!
17
17
 
18
- Imap::Backup::Logger.logger.debug "[#{folder.name}] #{count} to migrate"
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
- Imap::Backup::Logger.logger.debug(
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
- 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.
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(t)
38
+ def sanitize(text)
38
39
  # Hide password in Net::IMAP debug output
39
- t.gsub(
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
- raise "Can't add messages without uid_validity" if !imap.uid_validity
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 !block_given?
71
+ def each_message(required_uids, &block)
72
+ return enum_for(:each_message, required_uids) if !block
74
73
 
75
- indexes = required_uids.each.with_object({}) do |uid_maybe_string, acc|
76
- uid = uid_maybe_string.to_i
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
- # Initialize so we get memoized instances with the correct folder_path
91
- mbox
92
- imap
93
- @folder = new_name
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
- digit = 0
164
- new_name = nil
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