imap-backup 10.0.0 → 11.0.0.rc1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -0
  3. data/docs/development.md +9 -0
  4. data/lib/imap/backup/account/backup.rb +11 -2
  5. data/lib/imap/backup/account/backup_folders.rb +2 -0
  6. data/lib/imap/backup/account/client_factory.rb +2 -0
  7. data/lib/imap/backup/account/folder.rb +2 -0
  8. data/lib/imap/backup/account/folder_ensurer.rb +2 -0
  9. data/lib/imap/backup/account/local_only_folder_deleter.rb +2 -0
  10. data/lib/imap/backup/account/restore.rb +2 -0
  11. data/lib/imap/backup/account/serialized_folders.rb +2 -0
  12. data/lib/imap/backup/account.rb +16 -7
  13. data/lib/imap/backup/cli/backup.rb +2 -0
  14. data/lib/imap/backup/cli/folder_enumerator.rb +2 -0
  15. data/lib/imap/backup/cli/helpers.rb +2 -0
  16. data/lib/imap/backup/cli/local.rb +2 -1
  17. data/lib/imap/backup/cli/migrate.rb +2 -0
  18. data/lib/imap/backup/cli/mirror.rb +2 -0
  19. data/lib/imap/backup/cli/remote.rb +18 -0
  20. data/lib/imap/backup/cli/restore.rb +2 -0
  21. data/lib/imap/backup/cli/setup.rb +2 -0
  22. data/lib/imap/backup/cli/stats.rb +2 -0
  23. data/lib/imap/backup/cli/utils.rb +2 -0
  24. data/lib/imap/backup/client/apple_mail.rb +2 -0
  25. data/lib/imap/backup/client/default.rb +3 -1
  26. data/lib/imap/backup/configuration.rb +31 -2
  27. data/lib/imap/backup/downloader.rb +2 -0
  28. data/lib/imap/backup/file_mode.rb +2 -0
  29. data/lib/imap/backup/flag_refresher.rb +2 -0
  30. data/lib/imap/backup/local_only_message_deleter.rb +2 -0
  31. data/lib/imap/backup/logger.rb +2 -0
  32. data/lib/imap/backup/migrator.rb +2 -0
  33. data/lib/imap/backup/mirror/map.rb +2 -0
  34. data/lib/imap/backup/mirror.rb +2 -0
  35. data/lib/imap/backup/naming.rb +2 -0
  36. data/lib/imap/backup/serializer/appender.rb +33 -17
  37. data/lib/imap/backup/serializer/directory.rb +2 -0
  38. data/lib/imap/backup/serializer/folder_maker.rb +2 -0
  39. data/lib/imap/backup/serializer/imap.rb +36 -4
  40. data/lib/imap/backup/serializer/mbox.rb +32 -4
  41. data/lib/imap/backup/serializer/message.rb +2 -0
  42. data/lib/imap/backup/serializer/message_enumerator.rb +2 -0
  43. data/lib/imap/backup/serializer/permission_checker.rb +2 -0
  44. data/lib/imap/backup/serializer/unused_name_finder.rb +2 -0
  45. data/lib/imap/backup/serializer/version2_migrator.rb +2 -0
  46. data/lib/imap/backup/serializer.rb +52 -6
  47. data/lib/imap/backup/setup/account/header.rb +2 -0
  48. data/lib/imap/backup/setup/account.rb +2 -0
  49. data/lib/imap/backup/setup/asker.rb +2 -0
  50. data/lib/imap/backup/setup/backup_path.rb +2 -0
  51. data/lib/imap/backup/setup/connection_tester.rb +2 -0
  52. data/lib/imap/backup/setup/email.rb +2 -0
  53. data/lib/imap/backup/setup/folder_chooser.rb +2 -0
  54. data/lib/imap/backup/setup/helpers.rb +2 -0
  55. data/lib/imap/backup/setup.rb +13 -0
  56. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +2 -0
  57. data/lib/imap/backup/uploader.rb +2 -0
  58. data/lib/imap/backup/version.rb +2 -2
  59. data/lib/imap/backup.rb +2 -2
  60. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a244374a14bae1b8d9bee48293eb744266d4d9fa67be560a3ca52a25b49a0cfa
4
- data.tar.gz: 03fc3fe13037bcc3c9927117a5fb9edd4a85e053ea3d8e8f2380d8daee514c9d
3
+ metadata.gz: e9cff069fefde8147d1995b254f3f0613575ff4f52232c2fc64c91dc00c49bde
4
+ data.tar.gz: a1b8cb3d48f41b2773fd20b842b8d3e3b71e673b6797c83a345e341d087060d8
5
5
  SHA512:
6
- metadata.gz: fc751d9d7f9af7f483536251234b5b40ffa9813d947d0739229ab14bf3d35d8878af6656d2180a490ad21b06d5b99c746547a95b75b6925c968fca51b91ba917
7
- data.tar.gz: 8f4944c21e5458dda098554b3338c5baaecfe2e111fc5acc23ca0bcf1a20794202a80d0139a9932bba28960702ed8531881ea5bc7030af1b04a2e98afdc6dd22
6
+ metadata.gz: 4e7b83bac1004b853287689e98ed4d03d7c3c87bd05c4a22831ffe592782ff8916ab576778e41de572f872505e3beab5b923ff5d1d78ccf3e1a99d701ff95737
7
+ data.tar.gz: a0cc1cc27b225c315a68743dff999fc2d8b0d0c9fd5d68f03641b59d53ddc491da21f318b9c075927263367b9db0a50e80af727de0445c86a049bc55d2a32fdf
data/README.md CHANGED
@@ -100,6 +100,54 @@ For more information about a command, run
100
100
  imap-backup help COMMAND
101
101
  ```
102
102
 
103
+ # Performace
104
+
105
+ There are a couple of performance tweaks that you can use
106
+ to improve backup speed.
107
+
108
+ These are activated via two settings:
109
+
110
+ * Global setting "Delay download writes"
111
+ * Account setting "Multi-fetch size"
112
+
113
+ As with all performance tweaks, there are trade-offs.
114
+ If you are using a small virtual server or Raspberry Pi
115
+ to run your backups, you will probably want to leave
116
+ the deafult settings.
117
+ If, on the other hand, you are using a computer with a
118
+ fair bit of RAM, and you are dealing with a *lot* of email,
119
+ then changing these settings may be worthwhile.
120
+
121
+ ## Delay download writes
122
+
123
+ This setting affects all account backups.
124
+
125
+ When not set, each message is written to disk, one at a time.
126
+ Doing so means the message itself is appended to the MBox file,
127
+ but more importantly, the JSON metadata is rewritten to disk
128
+ from scratch.
129
+
130
+ When in use, all of a mailboxes unbackupped messages are
131
+ downloaded first, and then written to disk just once.
132
+
133
+ This speeds up backup as the metadata file is not rewritten
134
+ after each message is added, but it potentially uses much more memory.
135
+
136
+ ## Multi-fetch Size
137
+
138
+ By default, during backup, each message is downloaded one-by-one.
139
+
140
+ Using this setting, you can download chunks of emails at a time,
141
+ potentially speeding up the process.
142
+
143
+ If you're not using "Delayed downlaod writes",
144
+ using multi-fetch *will* mean that the backup process will use
145
+ more memory - equivalent to the size of the greater number
146
+ of messages downloaded at a time.
147
+
148
+ This behaviour may also exceed limits on your email provider,
149
+ so it's best to check before cranking it up!
150
+
103
151
  # Troubleshooting
104
152
 
105
153
  If you have problems:
data/docs/development.md CHANGED
@@ -43,6 +43,15 @@ or
43
43
  $ rspec --tag ~docker
44
44
  ```
45
45
 
46
+ # Performance Specs
47
+
48
+ ```sh
49
+ PERFORMANCE=1 rspec --order=defined
50
+ ```
51
+
52
+ Beware: the performance spec (just backup for now) takes a very
53
+ long time to run, approximately 24 hours!
54
+
46
55
  ### Debugging
47
56
 
48
57
  The feature specs are run 'out of process' via the Aruba gem.
@@ -5,6 +5,8 @@ require "imap/backup/downloader"
5
5
  require "imap/backup/flag_refresher"
6
6
  require "imap/backup/local_only_message_deleter"
7
7
 
8
+ module Imap; end
9
+
8
10
  module Imap::Backup
9
11
  class Account; end
10
12
 
@@ -36,12 +38,19 @@ module Imap::Backup
36
38
 
37
39
  Logger.logger.debug "[#{folder.name}] running backup"
38
40
  serializer.apply_uid_validity(folder.uid_validity)
39
- Downloader.new(
41
+ downloader = Downloader.new(
40
42
  folder,
41
43
  serializer,
42
44
  multi_fetch_size: account.multi_fetch_size,
43
45
  reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
44
- ).run
46
+ )
47
+ if account.delay_download_writes
48
+ serializer.transaction do
49
+ downloader.run
50
+ end
51
+ else
52
+ downloader.run
53
+ end
45
54
  if account.mirror_mode
46
55
  Logger.logger.info "Mirror mode - Deleting messages only present locally"
47
56
  LocalOnlyMessageDeleter.new(folder, serializer).run
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Account; end
3
5
 
@@ -5,6 +5,8 @@ require "imap/backup/client/apple_mail"
5
5
  require "imap/backup/client/default"
6
6
  require "retry_on_error"
7
7
 
8
+ module Imap; end
9
+
8
10
  module Imap::Backup
9
11
  class Account; end
10
12
 
@@ -3,6 +3,8 @@ require "net/imap"
3
3
 
4
4
  require "retry_on_error"
5
5
 
6
+ module Imap; end
7
+
6
8
  module Imap::Backup
7
9
  class Account; end
8
10
 
@@ -1,6 +1,8 @@
1
1
  require "imap/backup/serializer/directory"
2
2
  require "imap/backup/serializer/folder_maker"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  class Account; end
6
8
 
@@ -1,6 +1,8 @@
1
1
  require "imap/backup/account/backup_folders"
2
2
  require "imap/backup/account/serialized_folders"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  class Account; end
6
8
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/account/serialized_folders"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Account; end
5
7
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/account/folder_ensurer"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Account; end
5
7
 
@@ -15,6 +15,7 @@ module Imap::Backup
15
15
  attr_reader :mirror_mode
16
16
  attr_reader :server
17
17
  attr_reader :connection_options
18
+ attr_accessor :delay_download_writes
18
19
  attr_reader :reset_seen_flags_after_fetch
19
20
  attr_reader :changes
20
21
 
@@ -27,7 +28,8 @@ module Imap::Backup
27
28
  @mirror_mode = options[:mirror_mode]
28
29
  @server = options[:server]
29
30
  @connection_options = options[:connection_options]
30
- @multi_fetch_size = options[:multi_fetch_size]
31
+ @delay_download_writes = true
32
+ @multi_fetch_size_orignal = options[:multi_fetch_size]
31
33
  @reset_seen_flags_after_fetch = options[:reset_seen_flags_after_fetch]
32
34
  @client = nil
33
35
  @changes = {}
@@ -42,6 +44,10 @@ module Imap::Backup
42
44
  client.namespace
43
45
  end
44
46
 
47
+ def capabilities
48
+ client.capability
49
+ end
50
+
45
51
  def restore
46
52
  restore = Account::Restore.new(account: self)
47
53
  restore.run
@@ -75,7 +81,8 @@ module Imap::Backup
75
81
  h[:mirror_mode] = true if @mirror_mode
76
82
  h[:server] = @server if @server
77
83
  h[:connection_options] = @connection_options if @connection_options
78
- h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
84
+ h[:delay_download_writes] = delay_download_writes
85
+ h[:multi_fetch_size] = multi_fetch_size
79
86
  if @reset_seen_flags_after_fetch
80
87
  h[:reset_seen_flags_after_fetch] = @reset_seen_flags_after_fetch
81
88
  end
@@ -123,11 +130,13 @@ module Imap::Backup
123
130
  end
124
131
 
125
132
  def multi_fetch_size
126
- int = @multi_fetch_size.to_i
127
- if int.positive?
128
- int
129
- else
130
- DEFAULT_MULTI_FETCH_SIZE
133
+ @multi_fetch_size ||= begin
134
+ int = @multi_fetch_size_orignal.to_i
135
+ if int.positive?
136
+ int
137
+ else
138
+ DEFAULT_MULTI_FETCH_SIZE
139
+ end
131
140
  end
132
141
  end
133
142
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/account/backup"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class CLI::Backup < Thor
5
7
  include Thor::Actions
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class CLI::FolderEnumerator
3
5
  attr_reader :destination
@@ -1,5 +1,7 @@
1
1
  require "imap/backup"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  module CLI::Helpers
5
7
  def self.included(base)
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Local < Thor
3
5
  include Thor::Actions
@@ -39,7 +41,6 @@ module Imap::Backup
39
41
  results = requested_accounts(config).map do |account|
40
42
  serialized_folders = Account::SerializedFolders.new(account: account)
41
43
  folder_results = serialized_folders.map do |serializer, _folder|
42
- puts "serializer: #{serializer.inspect}"
43
44
  serializer.check_integrity!
44
45
  {name: serializer.folder, result: "OK"}
45
46
  rescue Serializer::FolderIntegrityError => e
@@ -1,6 +1,8 @@
1
1
  require "imap/backup/cli/folder_enumerator"
2
2
  require "imap/backup/migrator"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  class CLI::Migrate < Thor
6
8
  include Thor::Actions
@@ -1,6 +1,8 @@
1
1
  require "imap/backup/cli/folder_enumerator"
2
2
  require "imap/backup/mirror"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  class CLI::Mirror < Thor
6
8
  include Thor::Actions
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/logger"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class CLI::Remote < Thor
5
7
  include Thor::Actions
@@ -21,6 +23,22 @@ module Imap::Backup
21
23
  end
22
24
  end
23
25
 
26
+ desc "capabilities EMAIL", "List server capabilities"
27
+ long_desc <<~DESC
28
+ Lists the IMAP capabilities supported by the IMAP server.
29
+ DESC
30
+ config_option
31
+ format_option
32
+ quiet_option
33
+ verbose_option
34
+ def capabilities(email)
35
+ Imap::Backup::Logger.setup_logging options
36
+ config = load_config(**options)
37
+ account = account(config, email)
38
+ capabilities = account.capabilities
39
+ Kernel.puts capabilities.join(", ")
40
+ end
41
+
24
42
  desc "namespaces EMAIL", "List account namespaces"
25
43
  long_desc <<~DESC
26
44
  Lists namespaces defined for an email account.
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Restore < Thor
3
5
  include Thor::Actions
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Setup < Thor
3
5
  include Thor::Actions
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/account/backup_folders"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class CLI::Stats < Thor
5
7
  include Thor::Actions
@@ -2,6 +2,8 @@ require "imap/backup/account/backup_folders"
2
2
  require "imap/backup/account/serialized_folders"
3
3
  require "imap/backup/thunderbird/mailbox_exporter"
4
4
 
5
+ module Imap; end
6
+
5
7
  module Imap::Backup
6
8
  class CLI::Utils < Thor
7
9
  include Thor::Actions
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/client/default"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Client::AppleMail < Client::Default
5
7
  # With Apple Mails's IMAP, passing "/" to list
@@ -1,13 +1,15 @@
1
1
  require "forwardable"
2
2
  require "net/imap"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  module Client; end
6
8
 
7
9
  class Client::Default
8
10
  extend Forwardable
9
11
  def_delegators :imap, *%i(
10
- append authenticate create expunge namespace
12
+ append authenticate capability create expunge namespace
11
13
  responses uid_fetch uid_search uid_store
12
14
  )
13
15
 
@@ -6,12 +6,16 @@ require "imap/backup/account"
6
6
  require "imap/backup/file_mode"
7
7
  require "imap/backup/serializer/permission_checker"
8
8
 
9
+ module Imap; end
10
+
9
11
  module Imap::Backup
10
12
  class Configuration
11
13
  CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
12
14
  VERSION = "2.0".freeze
13
15
 
14
16
  attr_reader :pathname
17
+ attr_reader :delay_download_writes
18
+ attr_reader :delay_download_writes_modified
15
19
 
16
20
  def self.default_pathname
17
21
  File.join(CONFIGURATION_DIRECTORY, "config.json")
@@ -23,6 +27,8 @@ module Imap::Backup
23
27
 
24
28
  def initialize(path: nil)
25
29
  @pathname = path || self.class.default_pathname
30
+ @delay_download_writes = false
31
+ @delay_download_writes_modified = false
26
32
  end
27
33
 
28
34
  def path
@@ -37,7 +43,8 @@ module Imap::Backup
37
43
  remove_deleted_accounts
38
44
  save_data = {
39
45
  version: VERSION,
40
- accounts: accounts.map(&:to_h)
46
+ accounts: accounts.map(&:to_h),
47
+ delay_download_writes: delay_download_writes
41
48
  }
42
49
  File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
43
50
  FileUtils.chmod(0o600, pathname) if !windows?
@@ -47,13 +54,26 @@ module Imap::Backup
47
54
  def accounts
48
55
  @accounts ||= begin
49
56
  ensure_loaded!
50
- data[:accounts].map { |data| Account.new(data) }
57
+ accounts = data[:accounts].map do |attr|
58
+ Account.new(attr)
59
+ end
60
+ inject_global_attributes(accounts)
51
61
  end
52
62
  end
53
63
 
64
+ def delay_download_writes=(value)
65
+ ensure_loaded!
66
+
67
+ @delay_download_writes = value
68
+ @delay_download_writes_modified = true
69
+ inject_global_attributes(accounts)
70
+ end
71
+
54
72
  def modified?
55
73
  ensure_loaded!
56
74
 
75
+ return true if delay_download_writes_modified
76
+
57
77
  accounts.any? { |a| a.modified? || a.marked_for_deletion? }
58
78
  end
59
79
 
@@ -63,6 +83,7 @@ module Imap::Backup
63
83
  return true if @data
64
84
 
65
85
  data
86
+ @delay_download_writes = data[:delay_download_writes]
66
87
  true
67
88
  end
68
89
 
@@ -81,6 +102,7 @@ module Imap::Backup
81
102
  end
82
103
 
83
104
  def remove_modified_flags
105
+ @delay_download_writes_modified = false
84
106
  accounts.each(&:clear_changes)
85
107
  end
86
108
 
@@ -88,6 +110,13 @@ module Imap::Backup
88
110
  accounts.reject!(&:marked_for_deletion?)
89
111
  end
90
112
 
113
+ def inject_global_attributes(accounts)
114
+ accounts.map do |a|
115
+ a.delay_download_writes = delay_download_writes
116
+ a
117
+ end
118
+ end
119
+
91
120
  def make_private(path)
92
121
  FileUtils.chmod(0o700, path) if FileMode.new(filename: path).mode != 0o700
93
122
  end
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class MultiFetchFailedError < StandardError; end
3
5
 
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class FileMode
3
5
  attr_reader :filename
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class FlagRefresher
3
5
  attr_reader :folder
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class LocalOnlyMessageDeleter
3
5
  attr_reader :folder
@@ -4,6 +4,8 @@ require "singleton"
4
4
  require "imap/backup/configuration"
5
5
  require "text/sanitizer"
6
6
 
7
+ module Imap; end
8
+
7
9
  module Imap::Backup
8
10
  class Logger
9
11
  include Singleton
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Migrator
3
5
  attr_reader :folder
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Mirror; end
3
5
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/mirror/map"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Mirror
5
7
  attr_reader :serializer
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Naming
3
5
  INVALID_FILENAME_CHARACTERS = ":%;".freeze
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Serializer; end
3
5
 
@@ -12,7 +14,7 @@ module Imap::Backup
12
14
  @mbox = mbox
13
15
  end
14
16
 
15
- def run(uid:, message:, flags:)
17
+ def single(uid:, message:, flags:)
16
18
  raise "Can't add messages without uid_validity" if !imap.uid_validity
17
19
 
18
20
  uid = uid.to_i
@@ -24,29 +26,43 @@ module Imap::Backup
24
26
  return
25
27
  end
26
28
 
27
- do_append uid, message, flags
29
+ rollback_on_error do
30
+ do_append uid, message, flags
31
+ rescue StandardError => e
32
+ raise <<-ERROR.gsub(/^\s*/m, "")
33
+ [#{folder}] failed to append message #{uid}:
34
+ #{message}. #{e}:
35
+ #{e.backtrace.join("\n")}"
36
+ ERROR
37
+ end
38
+ end
39
+
40
+ def multi(appends)
41
+ rollback_on_error do
42
+ appends.each do |a|
43
+ do_append a[:uid], a[:message], a[:flags]
44
+ end
45
+ end
28
46
  end
29
47
 
30
48
  private
31
49
 
32
50
  def do_append(uid, message, flags)
33
51
  mboxrd_message = Email::Mboxrd::Message.new(message)
34
- initial = mbox.length || 0
35
- mbox_appended = false
36
- begin
37
- serialized = mboxrd_message.to_serialized
38
- mbox.append serialized
39
- mbox_appended = true
40
- imap.append uid, serialized.length, flags
41
- rescue StandardError => e
42
- mbox.rewind(initial) if mbox_appended
52
+ serialized = mboxrd_message.to_serialized
53
+ mbox.append serialized
54
+ imap.append uid, serialized.length, flags: flags
55
+ end
43
56
 
44
- error = <<-ERROR.gsub(/^\s*/m, "")
45
- [#{folder}] failed to append message #{uid}:
46
- #{message}. #{e}:
47
- #{e.backtrace.join("\n")}"
48
- ERROR
49
- Logger.logger.warn error
57
+ def rollback_on_error(&block)
58
+ imap.transaction do
59
+ mbox.transaction do
60
+ block.call
61
+ rescue StandardError => e
62
+ Logger.logger.error e
63
+ imap.rollback
64
+ mbox.rollback
65
+ end
50
66
  end
51
67
  end
52
68
  end
@@ -2,6 +2,8 @@ require "os"
2
2
 
3
3
  require "imap/backup/serializer/folder_maker"
4
4
 
5
+ module Imap; end
6
+
5
7
  module Imap::Backup
6
8
  class Serializer; end
7
9
 
@@ -1,5 +1,7 @@
1
1
  require "fileutils"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Serializer; end
5
7
 
@@ -1,5 +1,7 @@
1
1
  require "json"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Serializer::Imap
5
7
  CURRENT_VERSION = 3
@@ -13,6 +15,26 @@ module Imap::Backup
13
15
  @uid_validity = nil
14
16
  @messages = nil
15
17
  @version = nil
18
+ @savepoint = nil
19
+ end
20
+
21
+ def transaction(&block)
22
+ fail_in_transaction!(message: "Serializer::Imap: nested transactions are not supported")
23
+
24
+ ensure_loaded
25
+ @savepoint = {messages: messages.dup, uid_validity: uid_validity}
26
+
27
+ block.call
28
+
29
+ @savepoint = nil
30
+ save
31
+ end
32
+
33
+ def rollback
34
+ fail_outside_transaction!
35
+
36
+ @messages = @savepoint[:messages]
37
+ @uid_validity = @savepoint[:uid_validity]
16
38
  end
17
39
 
18
40
  def pathname
@@ -31,7 +53,7 @@ module Imap::Backup
31
53
  true
32
54
  end
33
55
 
34
- def append(uid, length, flags = [])
56
+ def append(uid, length, flags: [])
35
57
  offset =
36
58
  if messages.empty?
37
59
  0
@@ -107,14 +129,16 @@ module Imap::Backup
107
129
  end
108
130
 
109
131
  def save
132
+ return if @savepoint
133
+
110
134
  ensure_loaded
111
135
 
112
136
  raise "Cannot save metadata without a uid_validity" if !uid_validity
113
137
 
114
138
  data = {
115
- version: @version,
116
- uid_validity: @uid_validity,
117
- messages: @messages.map(&:to_h)
139
+ version: version,
140
+ uid_validity: uid_validity,
141
+ messages: messages.map(&:to_h)
118
142
  }
119
143
  content = data.to_json
120
144
  File.open(pathname, "w") { |f| f.write content }
@@ -160,5 +184,13 @@ module Imap::Backup
160
184
  def mbox
161
185
  @mbox ||= Serializer::Mbox.new(folder_path)
162
186
  end
187
+
188
+ def fail_in_transaction!(message: "Serializer::Imap: method not supported inside trasactions")
189
+ raise message if @savepoint
190
+ end
191
+
192
+ def fail_outside_transaction!
193
+ raise "This method can only be called inside a transaction" if !@savepoint
194
+ end
163
195
  end
164
196
  end
@@ -1,9 +1,27 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Serializer::Mbox
3
5
  attr_reader :folder_path
6
+ attr_reader :savepoint
4
7
 
5
8
  def initialize(folder_path)
6
9
  @folder_path = folder_path
10
+ @savepoint = nil
11
+ end
12
+
13
+ def transaction(&block)
14
+ fail_in_transaction!(message: "Nested transactions are not supported")
15
+
16
+ @savepoint = {length: length}
17
+ block.call
18
+ @savepoint = nil
19
+ end
20
+
21
+ def rollback
22
+ fail_outside_transaction!
23
+
24
+ rewind(@savepoint[:length])
7
25
  end
8
26
 
9
27
  def valid?
@@ -29,6 +47,10 @@ module Imap::Backup
29
47
  File.unlink(pathname)
30
48
  end
31
49
 
50
+ def exist?
51
+ File.exist?(pathname)
52
+ end
53
+
32
54
  def length
33
55
  return nil if !exist?
34
56
 
@@ -49,18 +71,24 @@ module Imap::Backup
49
71
  end
50
72
  end
51
73
 
74
+ def touch
75
+ File.open(pathname, "a") {}
76
+ end
77
+
78
+ private
79
+
52
80
  def rewind(length)
53
81
  File.open(pathname, File::RDWR | File::CREAT, 0o644) do |f|
54
82
  f.truncate(length)
55
83
  end
56
84
  end
57
85
 
58
- def touch
59
- File.open(pathname, "a") {}
86
+ def fail_in_transaction!(message: "Method not supported inside trasactions")
87
+ raise message if savepoint
60
88
  end
61
89
 
62
- def exist?
63
- File.exist?(pathname)
90
+ def fail_outside_transaction!
91
+ raise "This method can only be called inside a transaction" if !savepoint
64
92
  end
65
93
  end
66
94
  end
@@ -1,5 +1,7 @@
1
1
  require "email/mboxrd/message"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Serializer::Message
5
7
  attr_accessor :uid
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Serializer::MessageEnumerator
3
5
  attr_reader :imap
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Serializer::PermissionChecker
3
5
  attr_reader :filename
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Serializer::UnusedNameFinder
3
5
  attr_reader :serializer
@@ -1,5 +1,7 @@
1
1
  require "json"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Serializer::Version2Migrator
5
7
  attr_reader :folder_path
@@ -10,6 +10,8 @@ require "imap/backup/serializer/message_enumerator"
10
10
  require "imap/backup/serializer/version2_migrator"
11
11
  require "imap/backup/serializer/unused_name_finder"
12
12
 
13
+ module Imap; end
14
+
13
15
  module Imap::Backup
14
16
  class Serializer
15
17
  def self.folder_path_for(path:, folder:)
@@ -26,16 +28,36 @@ module Imap::Backup
26
28
 
27
29
  attr_reader :folder
28
30
  attr_reader :path
31
+ attr_reader :dirty
29
32
 
30
33
  def initialize(path, folder)
31
34
  @path = path
32
35
  @folder = folder
33
36
  @validated = nil
37
+ @dirty = nil
38
+ end
39
+
40
+ def transaction(&block)
41
+ fail_in_transaction!(:transaction, message: "nested transactions are not supported")
42
+
43
+ validate!
44
+ @dirty = {append: []}
45
+
46
+ block.call
47
+
48
+ if dirty[:append].any?
49
+ appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
50
+ appender.multi(dirty[:append])
51
+ end
52
+
53
+ @dirty = nil
34
54
  end
35
55
 
36
56
  # Returns true if there are existing, valid files
37
57
  # false otherwise (in which case any existing files are deleted)
38
58
  def validate!
59
+ fail_in_transaction!(:validate!)
60
+
39
61
  return true if @validated
40
62
 
41
63
  optionally_migrate2to3
@@ -51,6 +73,8 @@ module Imap::Backup
51
73
  end
52
74
 
53
75
  def check_integrity!
76
+ fail_in_transaction!(:check_integrity!)
77
+
54
78
  if !imap.valid?
55
79
  message = ".imap file '#{imap.pathname}' is corrupt"
56
80
  raise FolderIntegrityError, message
@@ -102,6 +126,8 @@ module Imap::Backup
102
126
  end
103
127
 
104
128
  def delete
129
+ fail_in_transaction!(:delete)
130
+
105
131
  imap.delete
106
132
  @imap = nil
107
133
  mbox.delete
@@ -109,6 +135,7 @@ module Imap::Backup
109
135
  end
110
136
 
111
137
  def apply_uid_validity(value)
138
+ fail_in_transaction!(:apply_uid_validity)
112
139
  validate!
113
140
 
114
141
  case
@@ -124,19 +151,26 @@ module Imap::Backup
124
151
  end
125
152
 
126
153
  def force_uid_validity(value)
154
+ fail_in_transaction!(:force_uid_validity)
127
155
  validate!
128
156
 
129
157
  internal_force_uid_validity(value)
130
158
  end
131
159
 
132
160
  def append(uid, message, flags)
133
- validate!
161
+ if dirty
162
+ dirty[:append] << {uid: uid, message: message, flags: flags}
163
+ else
164
+ validate!
134
165
 
135
- appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
136
- appender.run(uid: uid, message: message, flags: flags)
166
+ appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
167
+ appender.single(uid: uid, message: message, flags: flags)
168
+ end
137
169
  end
138
170
 
139
171
  def update(uid, flags: nil)
172
+ fail_in_transaction!(:update)
173
+
140
174
  message = imap.get(uid)
141
175
  return if !message
142
176
 
@@ -145,17 +179,21 @@ module Imap::Backup
145
179
  end
146
180
 
147
181
  def each_message(required_uids = nil, &block)
182
+ fail_in_transaction!(:each_message)
183
+
184
+ return enum_for(:each_message, required_uids) if !block
185
+
148
186
  required_uids ||= uids
149
187
 
150
188
  validate!
151
189
 
152
- return enum_for(:each_message, required_uids) if !block
153
-
154
190
  enumerator = Serializer::MessageEnumerator.new(imap: imap)
155
191
  enumerator.run(uids: required_uids, &block)
156
192
  end
157
193
 
158
194
  def filter(&block)
195
+ fail_in_transaction!(:filter)
196
+
159
197
  temp_name = Serializer::UnusedNameFinder.new(serializer: self).run
160
198
  temp_folder_path = self.class.folder_path_for(path: path, folder: temp_name)
161
199
  new_mbox = Serializer::Mbox.new(temp_folder_path)
@@ -165,7 +203,7 @@ module Imap::Backup
165
203
  enumerator = Serializer::MessageEnumerator.new(imap: imap)
166
204
  enumerator.run(uids: uids) do |message|
167
205
  keep = block.call(message)
168
- appender.run(uid: message.uid, message: message.body, flags: message.flags) if keep
206
+ appender.single(uid: message.uid, message: message.body, flags: message.flags) if keep
169
207
  end
170
208
  imap.delete
171
209
  new_imap.rename imap.folder_path
@@ -249,5 +287,13 @@ module Imap::Backup
249
287
  rename new_name
250
288
  new_name
251
289
  end
290
+
291
+ def fail_in_transaction!(method, message: "not supported inside trasactions")
292
+ raise "Serializer##{method} #{message}" if dirty
293
+ end
294
+
295
+ def fail_outside_transaction!(method)
296
+ raise "Serializer##{method} can only be called inside a transaction" if !dirty
297
+ end
252
298
  end
253
299
  end
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/setup/helpers"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Setup; end
5
7
  class Setup::Account; end
@@ -2,6 +2,8 @@ require "imap/backup/setup/account/header"
2
2
  require "imap/backup/setup/backup_path"
3
3
  require "imap/backup/setup/email"
4
4
 
5
+ module Imap; end
6
+
5
7
  module Imap::Backup
6
8
  class Setup; end
7
9
 
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Setup; end
3
5
 
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Setup; end
3
5
 
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Setup; end
3
5
 
@@ -1,5 +1,7 @@
1
1
  require "email/provider"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Setup; end
5
7
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/setup/helpers"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Setup; end
5
7
 
@@ -1,5 +1,7 @@
1
1
  require "imap/backup/version"
2
2
 
3
+ module Imap; end
4
+
3
5
  module Imap::Backup
4
6
  class Setup; end
5
7
 
@@ -4,6 +4,8 @@ require "email/provider"
4
4
  require "imap/backup/account"
5
5
  require "imap/backup/setup/helpers"
6
6
 
7
+ module Imap; end
8
+
7
9
  module Imap::Backup
8
10
  class Setup
9
11
  class << self
@@ -37,6 +39,7 @@ module Imap::Backup
37
39
  MENU
38
40
  account_items menu
39
41
  add_account_item menu
42
+ toggle_delay_download_writes menu
40
43
  if config.modified?
41
44
  menu.choice("save and exit") do
42
45
  config.save
@@ -68,6 +71,16 @@ module Imap::Backup
68
71
  end
69
72
  end
70
73
 
74
+ def toggle_delay_download_writes(menu)
75
+ new_value = config.delay_download_writes ? false : true
76
+ modified = config.delay_download_writes_modified ? " *" : ""
77
+ change = config.delay_download_writes ? "don't delay" : "delay"
78
+ menu_item = "#{change} download writes#{modified}"
79
+ menu.choice(menu_item) do
80
+ config.delay_download_writes = new_value
81
+ end
82
+ end
83
+
71
84
  def default_account_config(username)
72
85
  Imap::Backup::Account.new(
73
86
  username: username,
@@ -1,6 +1,8 @@
1
1
  require "thunderbird/local_folder"
2
2
  require "thunderbird/profiles"
3
3
 
4
+ module Imap; end
5
+
4
6
  module Imap::Backup
5
7
  class Thunderbird::MailboxExporter
6
8
  EXPORT_PREFIX = "imap-backup".freeze
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Uploader
3
5
  attr_reader :folder
@@ -1,9 +1,9 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 10
4
+ MAJOR = 11
5
5
  MINOR = 0
6
6
  REVISION = 0
7
- PRE = nil
7
+ PRE = "rc1".freeze
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
data/lib/imap/backup.rb CHANGED
@@ -1,5 +1,3 @@
1
- module Imap; end
2
-
3
1
  require "imap/backup/account/folder"
4
2
  require "imap/backup/configuration"
5
3
  require "imap/backup/downloader"
@@ -13,6 +11,8 @@ require "imap/backup/setup/connection_tester"
13
11
  require "imap/backup/setup/folder_chooser"
14
12
  require "imap/backup/version"
15
13
 
14
+ module Imap; end
15
+
16
16
  module Imap::Backup
17
17
  class ConfigurationNotFound < StandardError; end
18
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.0.0
4
+ version: 11.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-16 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -305,9 +305,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
305
305
  version: '2.6'
306
306
  required_rubygems_version: !ruby/object:Gem::Requirement
307
307
  requirements:
308
- - - ">="
308
+ - - ">"
309
309
  - !ruby/object:Gem::Version
310
- version: '0'
310
+ version: 1.3.1
311
311
  requirements: []
312
312
  rubygems_version: 3.3.7
313
313
  signing_key: