imap-backup 6.0.0.rc2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) 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/apple_mail.rb +4 -0
  5. data/lib/email/provider/base.rb +6 -0
  6. data/lib/email/provider/purelymail.rb +11 -0
  7. data/lib/email/provider/unknown.rb +2 -0
  8. data/lib/email/provider.rb +5 -0
  9. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  10. data/lib/imap/backup/account/connection/client_factory.rb +55 -0
  11. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  12. data/lib/imap/backup/account/connection.rb +16 -96
  13. data/lib/imap/backup/account/folder.rb +31 -9
  14. data/lib/imap/backup/account.rb +15 -6
  15. data/lib/imap/backup/cli/backup.rb +1 -3
  16. data/lib/imap/backup/cli/helpers.rb +24 -22
  17. data/lib/imap/backup/cli/local.rb +20 -13
  18. data/lib/imap/backup/cli/migrate.rb +4 -10
  19. data/lib/imap/backup/cli/restore.rb +8 -7
  20. data/lib/imap/backup/cli/setup.rb +10 -8
  21. data/lib/imap/backup/cli/stats.rb +78 -0
  22. data/lib/imap/backup/cli/status.rb +2 -2
  23. data/lib/imap/backup/cli/utils.rb +4 -6
  24. data/lib/imap/backup/cli.rb +24 -3
  25. data/lib/imap/backup/configuration.rb +9 -11
  26. data/lib/imap/backup/downloader.rb +75 -31
  27. data/lib/imap/backup/migrator.rb +5 -5
  28. data/lib/imap/backup/sanitizer.rb +3 -2
  29. data/lib/imap/backup/serializer/appender.rb +49 -0
  30. data/lib/imap/backup/serializer/imap.rb +27 -3
  31. data/lib/imap/backup/serializer/mbox.rb +18 -2
  32. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  33. data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
  34. data/lib/imap/backup/serializer.rb +64 -84
  35. data/lib/imap/backup/setup/account/header.rb +81 -0
  36. data/lib/imap/backup/setup/account.rb +28 -91
  37. data/lib/imap/backup/setup/asker.rb +4 -15
  38. data/lib/imap/backup/setup/backup_path.rb +45 -0
  39. data/lib/imap/backup/setup/email.rb +45 -0
  40. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  41. data/lib/imap/backup/setup/helpers.rb +1 -1
  42. data/lib/imap/backup/setup.rb +7 -6
  43. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +39 -20
  44. data/lib/imap/backup/uploader.rb +46 -8
  45. data/lib/imap/backup/utils.rb +1 -1
  46. data/lib/imap/backup/version.rb +2 -2
  47. data/lib/imap/backup.rb +0 -1
  48. metadata +32 -134
  49. data/spec/features/backup_spec.rb +0 -100
  50. data/spec/features/configuration/minimal_configuration.rb +0 -15
  51. data/spec/features/configuration/missing_configuration.rb +0 -14
  52. data/spec/features/folders_spec.rb +0 -36
  53. data/spec/features/helper.rb +0 -2
  54. data/spec/features/local/list_accounts_spec.rb +0 -12
  55. data/spec/features/local/list_emails_spec.rb +0 -21
  56. data/spec/features/local/list_folders_spec.rb +0 -21
  57. data/spec/features/local/show_an_email_spec.rb +0 -34
  58. data/spec/features/migrate_spec.rb +0 -35
  59. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  60. data/spec/features/restore_spec.rb +0 -162
  61. data/spec/features/status_spec.rb +0 -43
  62. data/spec/features/support/aruba.rb +0 -78
  63. data/spec/features/support/backup_directory.rb +0 -43
  64. data/spec/features/support/email_server.rb +0 -110
  65. data/spec/features/support/shared/connection_context.rb +0 -14
  66. data/spec/features/support/shared/message_fixtures.rb +0 -16
  67. data/spec/fixtures/connection.yml +0 -7
  68. data/spec/spec_helper.rb +0 -15
  69. data/spec/support/fixtures.rb +0 -11
  70. data/spec/support/higline_test_helpers.rb +0 -8
  71. data/spec/support/silence_logging.rb +0 -7
  72. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  73. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  74. data/spec/unit/email/provider/base_spec.rb +0 -11
  75. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  76. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  77. data/spec/unit/email/provider_spec.rb +0 -27
  78. data/spec/unit/imap/backup/account/connection_spec.rb +0 -433
  79. data/spec/unit/imap/backup/account/folder_spec.rb +0 -261
  80. data/spec/unit/imap/backup/account_spec.rb +0 -246
  81. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -58
  82. data/spec/unit/imap/backup/cli/backup_spec.rb +0 -19
  83. data/spec/unit/imap/backup/cli/folders_spec.rb +0 -39
  84. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  85. data/spec/unit/imap/backup/cli/local_spec.rb +0 -100
  86. data/spec/unit/imap/backup/cli/migrate_spec.rb +0 -80
  87. data/spec/unit/imap/backup/cli/restore_spec.rb +0 -67
  88. data/spec/unit/imap/backup/cli/setup_spec.rb +0 -17
  89. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -125
  90. data/spec/unit/imap/backup/cli_spec.rb +0 -93
  91. data/spec/unit/imap/backup/client/apple_mail_spec.rb +0 -9
  92. data/spec/unit/imap/backup/client/default_spec.rb +0 -22
  93. data/spec/unit/imap/backup/configuration_spec.rb +0 -238
  94. data/spec/unit/imap/backup/downloader_spec.rb +0 -96
  95. data/spec/unit/imap/backup/logger_spec.rb +0 -48
  96. data/spec/unit/imap/backup/migrator_spec.rb +0 -58
  97. data/spec/unit/imap/backup/sanitizer_spec.rb +0 -42
  98. data/spec/unit/imap/backup/serializer/directory_spec.rb +0 -37
  99. data/spec/unit/imap/backup/serializer/imap_spec.rb +0 -218
  100. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
  101. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -101
  102. data/spec/unit/imap/backup/serializer_spec.rb +0 -296
  103. data/spec/unit/imap/backup/setup/account_spec.rb +0 -461
  104. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  105. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  106. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  107. data/spec/unit/imap/backup/setup/helpers_spec.rb +0 -15
  108. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  109. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +0 -116
  110. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  111. data/spec/unit/imap/backup/utils_spec.rb +0 -92
  112. data/spec/unit/retry_on_error_spec.rb +0 -34
@@ -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
@@ -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
@@ -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)
@@ -5,57 +5,101 @@ module Imap::Backup
5
5
  attr_reader :folder
6
6
  attr_reader :serializer
7
7
  attr_reader :multi_fetch_size
8
+ # Some IMAP providers, notably Apple Mail, set the '\Seen' flag
9
+ # on emails when they are fetched. By setting `:reset_seen_flags_after_fetch`,
10
+ # a workaround is activated which checks which emails are 'unseen' before
11
+ # and after the fetch, and removes the '\Seen' flag from those which have changed.
12
+ # As this check is susceptible to 'race conditions', i.e. when a different
13
+ # client sets the '\Seen' flag while imap-backup is fetching, it is best
14
+ # to only use it when required (i.e. for IMAP providers which always
15
+ # mark messages as '\Seen' when accessed).
16
+ attr_reader :reset_seen_flags_after_fetch
8
17
 
9
- def initialize(folder, serializer, multi_fetch_size: 1)
18
+ def initialize(folder, serializer, multi_fetch_size: 1, reset_seen_flags_after_fetch: false)
10
19
  @folder = folder
11
20
  @serializer = serializer
12
21
  @multi_fetch_size = multi_fetch_size
22
+ @reset_seen_flags_after_fetch = reset_seen_flags_after_fetch
23
+ @uids = nil
13
24
  end
14
25
 
15
26
  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
27
+ debug "#{uids.count} new messages"
30
28
 
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
29
+ uids.each_slice(multi_fetch_size).with_index do |block, i|
30
+ multifetch_failed = download_block(block, i)
31
+ raise MultiFetchFailedError if multifetch_failed
45
32
  end
46
33
  rescue MultiFetchFailedError
34
+ @count = nil
47
35
  @multi_fetch_size = 1
36
+ @uids = nil
48
37
  retry
49
38
  end
50
39
 
51
40
  private
52
41
 
42
+ def download_block(block, index)
43
+ uids_and_bodies =
44
+ if reset_seen_flags_after_fetch
45
+ before_unseen = folder.unseen(block)
46
+ debug "Pre-fetch unseen messages: #{before_unseen.join(', ')}"
47
+ uids_and_bodies = folder.fetch_multi(block)
48
+ after_unseen = folder.unseen(block)
49
+ debug "Post-fetch unseen messages: #{after_unseen.join(', ')}"
50
+ changed = before_unseen - after_unseen
51
+ if changed.any?
52
+ ids = changed.join(", ")
53
+ debug "Removing '\Seen' flag for the following messages: #{ids}"
54
+ folder.unset_flags(changed, [:Seen])
55
+ end
56
+ uids_and_bodies
57
+ else
58
+ folder.fetch_multi(block)
59
+ end
60
+ if uids_and_bodies.nil?
61
+ if multi_fetch_size > 1
62
+ uids = block.join(", ")
63
+ debug("Multi fetch failed for UIDs #{uids}, switching to single fetches")
64
+ return true
65
+ else
66
+ debug("Fetch failed for UID #{block[0]} - skipping")
67
+ return false
68
+ end
69
+ end
70
+
71
+ offset = (index * multi_fetch_size) + 1
72
+ uids_and_bodies.each.with_index do |uid_and_body, j|
73
+ handle_uid_and_body uid_and_body, offset + j
74
+ end
75
+
76
+ false
77
+ end
78
+
79
+ def handle_uid_and_body(uid_and_body, index)
80
+ uid = uid_and_body[:uid]
81
+ body = uid_and_body[:body]
82
+ case
83
+ when !body
84
+ info("Fetch returned empty body - skipping")
85
+ when !uid
86
+ info("Fetch returned empty UID - skipping")
87
+ else
88
+ debug("uid: #{uid} (#{index}/#{uids.count}) - #{body.size} bytes")
89
+ serializer.append uid, body
90
+ end
91
+ end
92
+
93
+ def uids
94
+ @uids ||= folder.uids - serializer.uids
95
+ end
96
+
53
97
  def info(message)
54
- Imap::Backup::Logger.logger.info("[#{folder.name}] #{message}")
98
+ Logger.logger.info("[#{folder.name}] #{message}")
55
99
  end
56
100
 
57
101
  def debug(message)
58
- Imap::Backup::Logger.logger.debug("[#{folder.name}] #{message}")
102
+ Logger.logger.debug("[#{folder.name}] #{message}")
59
103
  end
60
104
  end
61
105
  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
@@ -12,6 +12,15 @@ module Imap::Backup
12
12
  @loaded = false
13
13
  @uid_validity = nil
14
14
  @uids = nil
15
+ @version = nil
16
+ end
17
+
18
+ def valid?
19
+ return false if !exist?
20
+ return false if version != CURRENT_VERSION
21
+ return false if !uid_validity
22
+
23
+ true
15
24
  end
16
25
 
17
26
  def append(uid)
@@ -19,8 +28,10 @@ module Imap::Backup
19
28
  save
20
29
  end
21
30
 
22
- def exist?
23
- File.exist?(pathname)
31
+ def delete
32
+ return if !exist?
33
+
34
+ File.unlink(pathname)
24
35
  end
25
36
 
26
37
  def include?(uid)
@@ -66,12 +77,21 @@ module Imap::Backup
66
77
  save
67
78
  end
68
79
 
80
+ def version
81
+ ensure_loaded
82
+ @version
83
+ end
84
+
69
85
  private
70
86
 
71
87
  def pathname
72
88
  "#{folder_path}.imap"
73
89
  end
74
90
 
91
+ def exist?
92
+ File.exist?(pathname)
93
+ end
94
+
75
95
  def ensure_loaded
76
96
  return if loaded
77
97
 
@@ -79,9 +99,11 @@ module Imap::Backup
79
99
  if data
80
100
  @uids = data[:uids].map(&:to_i)
81
101
  @uid_validity = data[:uid_validity]
102
+ @version = data[:version]
82
103
  else
83
104
  @uids = []
84
105
  @uid_validity = nil
106
+ @version = CURRENT_VERSION
85
107
  end
86
108
  @loaded = true
87
109
  end
@@ -97,6 +119,8 @@ module Imap::Backup
97
119
  return nil
98
120
  end
99
121
 
122
+ return nil if !data.key?(:version)
123
+ return nil if !data.key?(:uid_validity)
100
124
  return nil if !data.key?(:uids)
101
125
  return nil if !data[:uids].is_a?(Array)
102
126
 
@@ -109,7 +133,7 @@ module Imap::Backup
109
133
  raise "Cannot save metadata without a uid_validity" if !uid_validity
110
134
 
111
135
  data = {
112
- version: CURRENT_VERSION,
136
+ version: @version,
113
137
  uid_validity: @uid_validity,
114
138
  uids: @uids
115
139
  }