imap-backup 5.2.0 → 6.0.1

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -2
  3. data/docs/development.md +10 -4
  4. data/imap-backup.gemspec +5 -1
  5. data/lib/cli_coverage.rb +11 -11
  6. data/lib/email/provider/base.rb +2 -0
  7. data/lib/email/provider/unknown.rb +2 -0
  8. data/lib/email/provider.rb +2 -0
  9. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  10. data/lib/imap/backup/account/connection/client_factory.rb +54 -0
  11. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  12. data/lib/imap/backup/account/connection.rb +17 -105
  13. data/lib/imap/backup/account/folder.rb +9 -6
  14. data/lib/imap/backup/account.rb +36 -16
  15. data/lib/imap/backup/cli/backup.rb +1 -3
  16. data/lib/imap/backup/cli/folders.rb +3 -3
  17. data/lib/imap/backup/cli/helpers.rb +24 -22
  18. data/lib/imap/backup/cli/local.rb +20 -13
  19. data/lib/imap/backup/cli/migrate.rb +5 -11
  20. data/lib/imap/backup/cli/restore.rb +8 -7
  21. data/lib/imap/backup/cli/setup.rb +10 -8
  22. data/lib/imap/backup/cli/stats.rb +78 -0
  23. data/lib/imap/backup/cli/status.rb +2 -2
  24. data/lib/imap/backup/cli/utils.rb +6 -8
  25. data/lib/imap/backup/cli.rb +24 -3
  26. data/lib/imap/backup/configuration.rb +9 -21
  27. data/lib/imap/backup/downloader.rb +56 -34
  28. data/lib/imap/backup/migrator.rb +5 -5
  29. data/lib/imap/backup/sanitizer.rb +3 -2
  30. data/lib/imap/backup/serializer/appender.rb +49 -0
  31. data/lib/imap/backup/serializer/directory.rb +37 -0
  32. data/lib/imap/backup/serializer/imap.rb +144 -0
  33. data/lib/imap/backup/serializer/mbox.rb +33 -88
  34. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  35. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  36. data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
  37. data/lib/imap/backup/serializer.rb +160 -3
  38. data/lib/imap/backup/setup/account/header.rb +75 -0
  39. data/lib/imap/backup/setup/account.rb +41 -95
  40. data/lib/imap/backup/setup/asker.rb +4 -15
  41. data/lib/imap/backup/setup/backup_path.rb +41 -0
  42. data/lib/imap/backup/setup/email.rb +45 -0
  43. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  44. data/lib/imap/backup/setup/helpers.rb +2 -2
  45. data/lib/imap/backup/setup.rb +5 -4
  46. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +41 -22
  47. data/lib/imap/backup/uploader.rb +46 -8
  48. data/lib/imap/backup/utils.rb +1 -1
  49. data/lib/imap/backup/version.rb +3 -3
  50. data/lib/imap/backup.rb +0 -2
  51. metadata +31 -105
  52. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  53. data/spec/features/backup_spec.rb +0 -108
  54. data/spec/features/configuration/minimal_configuration.rb +0 -15
  55. data/spec/features/configuration/missing_configuration.rb +0 -14
  56. data/spec/features/folders_spec.rb +0 -36
  57. data/spec/features/helper.rb +0 -2
  58. data/spec/features/local/list_accounts_spec.rb +0 -12
  59. data/spec/features/local/list_emails_spec.rb +0 -21
  60. data/spec/features/local/list_folders_spec.rb +0 -21
  61. data/spec/features/local/show_an_email_spec.rb +0 -34
  62. data/spec/features/migrate_spec.rb +0 -35
  63. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  64. data/spec/features/restore_spec.rb +0 -162
  65. data/spec/features/status_spec.rb +0 -43
  66. data/spec/features/support/aruba.rb +0 -77
  67. data/spec/features/support/backup_directory.rb +0 -43
  68. data/spec/features/support/email_server.rb +0 -110
  69. data/spec/features/support/shared/connection_context.rb +0 -14
  70. data/spec/features/support/shared/message_fixtures.rb +0 -16
  71. data/spec/fixtures/connection.yml +0 -7
  72. data/spec/spec_helper.rb +0 -15
  73. data/spec/support/fixtures.rb +0 -11
  74. data/spec/support/higline_test_helpers.rb +0 -8
  75. data/spec/support/silence_logging.rb +0 -7
  76. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  77. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  78. data/spec/unit/email/provider/base_spec.rb +0 -11
  79. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  80. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  81. data/spec/unit/email/provider_spec.rb +0 -27
  82. data/spec/unit/imap/backup/account/connection_spec.rb +0 -405
  83. data/spec/unit/imap/backup/account/folder_spec.rb +0 -251
  84. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -47
  85. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  86. data/spec/unit/imap/backup/cli/local_spec.rb +0 -81
  87. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -62
  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 -44
  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/serializer/mbox_enumerator_spec.rb +0 -45
  94. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -222
  95. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
  96. data/spec/unit/imap/backup/setup/account_spec.rb +0 -366
  97. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  98. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  99. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  100. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  101. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  102. data/spec/unit/imap/backup/utils_spec.rb +0 -92
@@ -1,35 +1,37 @@
1
1
  require "imap/backup"
2
2
  require "imap/backup/cli/accounts"
3
3
 
4
- module Imap::Backup::CLI::Helpers
5
- def symbolized(options)
6
- options.each.with_object({}) do |(k, v), acc|
7
- key = k.gsub("-", "_").intern
8
- acc[key] = v
4
+ module Imap::Backup
5
+ module CLI::Helpers
6
+ def symbolized(options)
7
+ options.each.with_object({}) do |(k, v), acc|
8
+ key = k.gsub("-", "_").intern
9
+ acc[key] = v
10
+ end
9
11
  end
10
- end
11
12
 
12
- def account(email)
13
- accounts = Imap::Backup::CLI::Accounts.new
14
- account = accounts.find { |a| a.username == email }
15
- raise "#{email} is not a configured account" if !account
13
+ def account(email)
14
+ accounts = CLI::Accounts.new
15
+ account = accounts.find { |a| a.username == email }
16
+ raise "#{email} is not a configured account" if !account
16
17
 
17
- account
18
- end
18
+ account
19
+ end
19
20
 
20
- def connection(email)
21
- account = account(email)
21
+ def connection(email)
22
+ account = account(email)
22
23
 
23
- Imap::Backup::Account::Connection.new(account)
24
- end
24
+ Account::Connection.new(account)
25
+ end
25
26
 
26
- def each_connection(names)
27
- accounts = Imap::Backup::CLI::Accounts.new(names)
27
+ def each_connection(names)
28
+ accounts = CLI::Accounts.new(names)
28
29
 
29
- accounts.each do |account|
30
- yield account.connection
30
+ accounts.each do |account|
31
+ yield account.connection
32
+ end
33
+ rescue ConfigurationNotFound
34
+ raise "imap-backup is not configured. Run `imap-backup setup`"
31
35
  end
32
- rescue Imap::Backup::ConfigurationNotFound
33
- raise "imap-backup is not configured. Run `imap-backup setup`"
34
36
  end
35
37
  end
@@ -5,6 +5,8 @@ module Imap::Backup
5
5
  include Thor::Actions
6
6
  include CLI::Helpers
7
7
 
8
+ MAX_SUBJECT = 60
9
+
8
10
  desc "accounts", "List locally backed-up accounts"
9
11
  def accounts
10
12
  accounts = CLI::Accounts.new
@@ -29,26 +31,16 @@ module Imap::Backup
29
31
  end
30
32
  raise "Folder '#{folder_name}' not found" if !folder_serializer
31
33
 
32
- max_subject = 60
33
34
  Kernel.puts format(
34
- "%-10<uid>s %-#{max_subject}<subject>s - %<date>s",
35
+ "%-10<uid>s %-#{MAX_SUBJECT}<subject>s - %<date>s",
35
36
  {uid: "UID", subject: "Subject", date: "Date"}
36
37
  )
37
- Kernel.puts "-" * (12 + max_subject + 28)
38
+ Kernel.puts "-" * (12 + MAX_SUBJECT + 28)
38
39
 
39
40
  uids = folder_serializer.uids
40
41
 
41
42
  folder_serializer.each_message(uids).map do |uid, message|
42
- m = {
43
- uid: uid,
44
- date: message.date.to_s,
45
- subject: message.subject || ""
46
- }
47
- if m[:subject].length > max_subject
48
- Kernel.puts format("% 10<uid>u: %.#{max_subject - 3}<subject>s... - %<date>s", m)
49
- else
50
- Kernel.puts format("% 10<uid>u: %-#{max_subject}<subject>s - %<date>s", m)
51
- end
43
+ list_message uid, message
52
44
  end
53
45
  end
54
46
 
@@ -78,5 +70,20 @@ module Imap::Backup
78
70
  Kernel.puts message.supplied_body
79
71
  end
80
72
  end
73
+
74
+ no_commands do
75
+ def list_message(uid, message)
76
+ m = {
77
+ uid: uid,
78
+ date: message.date.to_s,
79
+ subject: message.subject || ""
80
+ }
81
+ if m[:subject].length > MAX_SUBJECT
82
+ Kernel.puts format("% 10<uid>u: %.#{MAX_SUBJECT - 3}<subject>s... - %<date>s", m)
83
+ else
84
+ Kernel.puts format("% 10<uid>u: %-#{MAX_SUBJECT}<subject>s - %<date>s", m)
85
+ end
86
+ end
87
+ end
81
88
  end
82
89
  end
@@ -38,13 +38,9 @@ module Imap::Backup
38
38
  raise "Source and destination accounts cannot be the same!"
39
39
  end
40
40
 
41
- if !destination_account
42
- raise "Account #{destination_email} does not exist"
43
- end
41
+ raise "Account '#{destination_email}' does not exist" if !destination_account
44
42
 
45
- if !source_account
46
- raise "Account #{source_email} does not exist"
47
- end
43
+ raise "Account '#{source_email}' does not exist" if !source_account
48
44
  end
49
45
 
50
46
  def config
@@ -61,7 +57,7 @@ module Imap::Backup
61
57
  glob = File.join(source_local_path, "**", "*.imap")
62
58
  Pathname.glob(glob) do |path|
63
59
  name = source_folder_name(path)
64
- serializer = Serializer::Mbox.new(source_local_path, name)
60
+ serializer = Serializer.new(source_local_path, name)
65
61
  folder = folder_for(name)
66
62
  yield serializer, folder
67
63
  end
@@ -69,16 +65,14 @@ module Imap::Backup
69
65
 
70
66
  def folder_for(source_folder)
71
67
  no_source_prefix =
72
- if source_prefix != "" &&
73
- source_folder.start_with?(source_prefix)
68
+ if source_prefix != "" && source_folder.start_with?(source_prefix)
74
69
  source_folder.delete_prefix(source_prefix)
75
70
  else
76
71
  source_folder.to_s
77
72
  end
78
73
 
79
74
  with_destination_prefix =
80
- if destination_prefix &&
81
- destination_prefix != ""
75
+ if destination_prefix && destination_prefix != ""
82
76
  destination_prefix + no_source_prefix
83
77
  else
84
78
  no_source_prefix
@@ -7,11 +7,9 @@ module Imap::Backup
7
7
  attr_reader :account_names
8
8
 
9
9
  def initialize(email = nil, options)
10
+ super([])
10
11
  @email = email
11
- @account_names =
12
- if options.key?(:accounts)
13
- options[:accounts].split(",")
14
- end
12
+ @account_names = options[:accounts].split(",") if options.key?(:accounts)
15
13
  end
16
14
 
17
15
  no_commands do
@@ -22,12 +20,15 @@ module Imap::Backup
22
20
  connection.restore
23
21
  when !email && !account_names
24
22
  Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
25
- each_connection([]) { |connection| connection.restore }
23
+ each_connection([], &:restore)
26
24
  when email && account_names.any?
27
25
  raise "Pass either an email or the --accounts option, not both"
28
26
  when account_names.any?
29
- Logger.logger.info "Calling restore with the --account option is deprected, please pass a single EMAIL argument"
30
- each_connection(account_names) { |connection| connection.restore }
27
+ Logger.logger.info(
28
+ "Calling restore with the --account option is deprected, " \
29
+ "please pass a single EMAIL argument"
30
+ )
31
+ each_connection(account_names, &:restore)
31
32
  end
32
33
  end
33
34
  end
@@ -1,13 +1,15 @@
1
- class Imap::Backup::CLI::Setup < Thor
2
- include Thor::Actions
1
+ module Imap::Backup
2
+ class CLI::Setup < Thor
3
+ include Thor::Actions
3
4
 
4
- def initialize
5
- super([])
6
- end
5
+ def initialize
6
+ super([])
7
+ end
7
8
 
8
- no_commands do
9
- def run
10
- Imap::Backup::Setup.new.run
9
+ no_commands do
10
+ def run
11
+ Setup.new.run
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -0,0 +1,78 @@
1
+ module Imap::Backup
2
+ class CLI::Stats < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ TEXT_COLUMNS = [
7
+ {name: :folder, width: 20, alignment: :left},
8
+ {name: :remote, width: 8, alignment: :right},
9
+ {name: :both, width: 8, alignment: :right},
10
+ {name: :local, width: 8, alignment: :right}
11
+ ].freeze
12
+ ALIGNMENT_FORMAT_SYMBOL = {left: "-", right: " "}.freeze
13
+
14
+ attr_reader :email
15
+ attr_reader :options
16
+
17
+ def initialize(email, options = {})
18
+ super([])
19
+ @email = email
20
+ @options = options
21
+ end
22
+
23
+ no_commands do
24
+ def run
25
+ case options[:format]
26
+ when "json"
27
+ Kernel.puts stats.to_json
28
+ else
29
+ format_text stats
30
+ end
31
+ end
32
+
33
+ def stats
34
+ connection = connection(email)
35
+
36
+ connection.backup_folders.map do |folder|
37
+ next if !folder.exist?
38
+
39
+ serializer = Serializer.new(connection.account.local_path, folder.name)
40
+ local_uids = serializer.uids
41
+ remote_uids = folder.uids
42
+ {
43
+ folder: folder.name,
44
+ remote: (remote_uids - local_uids).count,
45
+ both: (serializer.uids & folder.uids).count,
46
+ local: (local_uids - remote_uids).count
47
+ }
48
+ end.compact
49
+ end
50
+
51
+ def format_text(stats)
52
+ Kernel.puts text_header
53
+
54
+ stats.each do |stat|
55
+ columns = TEXT_COLUMNS.map do |column|
56
+ symbol = ALIGNMENT_FORMAT_SYMBOL[column[:alignment]]
57
+ count = stat[column[:name]]
58
+ format("%#{symbol}#{column[:width]}s", count)
59
+ end.join("|")
60
+
61
+ Kernel.puts columns
62
+ end
63
+ end
64
+
65
+ def text_header
66
+ titles = TEXT_COLUMNS.map do |column|
67
+ format("%-#{column[:width]}s", column[:name])
68
+ end.join("|")
69
+
70
+ underline = TEXT_COLUMNS.map do |column|
71
+ "-" * column[:width]
72
+ end.join("|")
73
+
74
+ "#{titles}\n#{underline}"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -13,11 +13,11 @@ module Imap::Backup
13
13
  no_commands do
14
14
  def run
15
15
  each_connection(account_names) do |connection|
16
- puts connection.account.username
16
+ Kernel.puts connection.account.username
17
17
  folders = connection.status
18
18
  folders.each do |f|
19
19
  missing_locally = f[:remote] - f[:local]
20
- puts "#{f[:name]}: #{missing_locally.size}"
20
+ Kernel.puts "#{f[:name]}: #{missing_locally.size}"
21
21
  end
22
22
  end
23
23
  end
@@ -14,7 +14,7 @@ module Imap::Backup
14
14
  connection.backup_folders.each do |folder|
15
15
  next if !folder.exist?
16
16
 
17
- serializer = Serializer::Mbox.new(connection.account.local_path, folder.name)
17
+ serializer = Serializer.new(connection.account.local_path, folder.name)
18
18
  do_ignore_folder_history(folder, serializer)
19
19
  end
20
20
  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
 
@@ -75,7 +73,7 @@ module Imap::Backup
75
73
  Skipped #{uid}
76
74
  MESSAGE
77
75
 
78
- serializer.save(uid, message)
76
+ serializer.append uid, message
79
77
  end
80
78
  end
81
79
 
@@ -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,8 +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
- DEFAULT_DOWNLOAD_BLOCK_SIZE = 1
10
- VERSION = "2.0"
9
+ VERSION = "2.0".freeze
11
10
 
12
11
  attr_reader :pathname
13
12
 
@@ -52,15 +51,6 @@ module Imap::Backup
52
51
  end
53
52
  end
54
53
 
55
- def download_block_size
56
- size = ENV["DOWNLOAD_BLOCK_SIZE"].to_i
57
- if size > 0
58
- size
59
- else
60
- DEFAULT_DOWNLOAD_BLOCK_SIZE
61
- end
62
- end
63
-
64
54
  def modified?
65
55
  ensure_loaded!
66
56
  return true if @saved_debug != @debug
@@ -91,23 +81,21 @@ module Imap::Backup
91
81
 
92
82
  def data
93
83
  @data ||=
94
- begin
95
- if File.exist?(pathname)
96
- Utils.check_permissions(pathname, 0o600) if !windows?
97
- contents = File.read(pathname)
98
- JSON.parse(contents, symbolize_names: true)
99
- else
100
- {accounts: []}
101
- 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: []}
102
90
  end
103
91
  end
104
92
 
105
93
  def remove_modified_flags
106
- accounts.each { |a| a.clear_changes! }
94
+ accounts.each(&:clear_changes)
107
95
  end
108
96
 
109
97
  def remove_deleted_accounts
110
- accounts.reject! { |a| a.marked_for_deletion? }
98
+ accounts.reject!(&:marked_for_deletion?)
111
99
  end
112
100
 
113
101
  def make_private(path)
@@ -1,57 +1,79 @@
1
1
  module Imap::Backup
2
+ class MultiFetchFailedError < StandardError; end
3
+
2
4
  class Downloader
3
5
  attr_reader :folder
4
6
  attr_reader :serializer
5
- attr_reader :block_size
7
+ attr_reader :multi_fetch_size
6
8
 
7
- def initialize(folder, serializer, block_size: 1)
9
+ def initialize(folder, serializer, multi_fetch_size: 1)
8
10
  @folder = folder
9
11
  @serializer = serializer
10
- @block_size = block_size
12
+ @multi_fetch_size = multi_fetch_size
13
+ @uids = nil
11
14
  end
12
15
 
13
16
  def run
14
- uids = folder.uids - serializer.uids
15
- count = uids.count
16
- debug "#{count} new messages"
17
- uids.each_slice(block_size).with_index do |block, i|
18
- uids_and_bodies = folder.fetch_multi(block)
19
- if uids_and_bodies.nil?
20
- if block_size > 1
21
- debug("Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
22
- @block_size = 1
23
- redo
24
- else
25
- debug("Fetch failed for UID #{block[0]} - skipping")
26
- next
27
- end
28
- end
17
+ debug "#{uids.count} new messages"
29
18
 
30
- offset = i * block_size + 1
31
- uids_and_bodies.each.with_index do |uid_and_body, j|
32
- uid = uid_and_body[:uid]
33
- body = uid_and_body[:body]
34
- case
35
- when !body
36
- info("Fetch returned empty body - skipping")
37
- when !uid
38
- info("Fetch returned empty UID - skipping")
39
- else
40
- debug("uid: #{uid} (#{offset + j}/#{count}) - #{body.size} bytes")
41
- serializer.save(uid, body)
42
- end
43
- 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
44
22
  end
23
+ rescue MultiFetchFailedError
24
+ @count = nil
25
+ @multi_fetch_size = 1
26
+ @uids = nil
27
+ retry
45
28
  end
46
29
 
47
30
  private
48
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
+
49
71
  def info(message)
50
- Imap::Backup::Logger.logger.info("[#{folder.name}] #{message}")
72
+ Logger.logger.info("[#{folder.name}] #{message}")
51
73
  end
52
74
 
53
75
  def debug(message)
54
- Imap::Backup::Logger.logger.debug("[#{folder.name}] #{message}")
76
+ Logger.logger.debug("[#{folder.name}] #{message}")
55
77
  end
56
78
  end
57
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