imap-backup 10.0.0 → 11.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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: