imap-backup 5.2.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -2
  3. data/docs/development.md +10 -4
  4. data/imap-backup.gemspec +5 -1
  5. data/lib/cli_coverage.rb +11 -11
  6. data/lib/email/provider/base.rb +2 -0
  7. data/lib/email/provider/unknown.rb +2 -0
  8. data/lib/email/provider.rb +2 -0
  9. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  10. data/lib/imap/backup/account/connection/client_factory.rb +54 -0
  11. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  12. data/lib/imap/backup/account/connection.rb +17 -105
  13. data/lib/imap/backup/account/folder.rb +9 -6
  14. data/lib/imap/backup/account.rb +36 -16
  15. data/lib/imap/backup/cli/backup.rb +1 -3
  16. data/lib/imap/backup/cli/folders.rb +3 -3
  17. data/lib/imap/backup/cli/helpers.rb +24 -22
  18. data/lib/imap/backup/cli/local.rb +20 -13
  19. data/lib/imap/backup/cli/migrate.rb +5 -11
  20. data/lib/imap/backup/cli/restore.rb +8 -7
  21. data/lib/imap/backup/cli/setup.rb +10 -8
  22. data/lib/imap/backup/cli/stats.rb +78 -0
  23. data/lib/imap/backup/cli/status.rb +2 -2
  24. data/lib/imap/backup/cli/utils.rb +6 -8
  25. data/lib/imap/backup/cli.rb +24 -3
  26. data/lib/imap/backup/configuration.rb +9 -21
  27. data/lib/imap/backup/downloader.rb +56 -34
  28. data/lib/imap/backup/migrator.rb +5 -5
  29. data/lib/imap/backup/sanitizer.rb +3 -2
  30. data/lib/imap/backup/serializer/appender.rb +49 -0
  31. data/lib/imap/backup/serializer/directory.rb +37 -0
  32. data/lib/imap/backup/serializer/imap.rb +144 -0
  33. data/lib/imap/backup/serializer/mbox.rb +33 -88
  34. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  35. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  36. data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
  37. data/lib/imap/backup/serializer.rb +160 -3
  38. data/lib/imap/backup/setup/account/header.rb +75 -0
  39. data/lib/imap/backup/setup/account.rb +41 -95
  40. data/lib/imap/backup/setup/asker.rb +4 -15
  41. data/lib/imap/backup/setup/backup_path.rb +41 -0
  42. data/lib/imap/backup/setup/email.rb +45 -0
  43. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  44. data/lib/imap/backup/setup/helpers.rb +2 -2
  45. data/lib/imap/backup/setup.rb +5 -4
  46. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +41 -22
  47. data/lib/imap/backup/uploader.rb +46 -8
  48. data/lib/imap/backup/utils.rb +1 -1
  49. data/lib/imap/backup/version.rb +3 -3
  50. data/lib/imap/backup.rb +0 -2
  51. metadata +31 -105
  52. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  53. data/spec/features/backup_spec.rb +0 -108
  54. data/spec/features/configuration/minimal_configuration.rb +0 -15
  55. data/spec/features/configuration/missing_configuration.rb +0 -14
  56. data/spec/features/folders_spec.rb +0 -36
  57. data/spec/features/helper.rb +0 -2
  58. data/spec/features/local/list_accounts_spec.rb +0 -12
  59. data/spec/features/local/list_emails_spec.rb +0 -21
  60. data/spec/features/local/list_folders_spec.rb +0 -21
  61. data/spec/features/local/show_an_email_spec.rb +0 -34
  62. data/spec/features/migrate_spec.rb +0 -35
  63. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  64. data/spec/features/restore_spec.rb +0 -162
  65. data/spec/features/status_spec.rb +0 -43
  66. data/spec/features/support/aruba.rb +0 -77
  67. data/spec/features/support/backup_directory.rb +0 -43
  68. data/spec/features/support/email_server.rb +0 -110
  69. data/spec/features/support/shared/connection_context.rb +0 -14
  70. data/spec/features/support/shared/message_fixtures.rb +0 -16
  71. data/spec/fixtures/connection.yml +0 -7
  72. data/spec/spec_helper.rb +0 -15
  73. data/spec/support/fixtures.rb +0 -11
  74. data/spec/support/higline_test_helpers.rb +0 -8
  75. data/spec/support/silence_logging.rb +0 -7
  76. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  77. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  78. data/spec/unit/email/provider/base_spec.rb +0 -11
  79. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  80. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  81. data/spec/unit/email/provider_spec.rb +0 -27
  82. data/spec/unit/imap/backup/account/connection_spec.rb +0 -405
  83. data/spec/unit/imap/backup/account/folder_spec.rb +0 -251
  84. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -47
  85. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  86. data/spec/unit/imap/backup/cli/local_spec.rb +0 -81
  87. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -62
  88. data/spec/unit/imap/backup/client/default_spec.rb +0 -22
  89. data/spec/unit/imap/backup/configuration_spec.rb +0 -238
  90. data/spec/unit/imap/backup/downloader_spec.rb +0 -44
  91. data/spec/unit/imap/backup/logger_spec.rb +0 -48
  92. data/spec/unit/imap/backup/migrator_spec.rb +0 -58
  93. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
  94. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -222
  95. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
  96. data/spec/unit/imap/backup/setup/account_spec.rb +0 -366
  97. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  98. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  99. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  100. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  101. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  102. data/spec/unit/imap/backup/utils_spec.rb +0 -92
@@ -0,0 +1,37 @@
1
+ require "os"
2
+
3
+ module Imap::Backup
4
+ class Serializer; end
5
+
6
+ class Serializer::Directory
7
+ DIRECTORY_PERMISSIONS = 0o700
8
+
9
+ attr_reader :relative
10
+ attr_reader :path
11
+
12
+ def initialize(path, relative)
13
+ @path = path
14
+ @relative = relative
15
+ end
16
+
17
+ def ensure_exists
18
+ if !File.directory?(full_path)
19
+ Utils.make_folder(
20
+ path, relative, DIRECTORY_PERMISSIONS
21
+ )
22
+ end
23
+
24
+ return if OS.windows?
25
+ return if Utils.mode(full_path) == DIRECTORY_PERMISSIONS
26
+
27
+ FileUtils.chmod DIRECTORY_PERMISSIONS, full_path
28
+ end
29
+
30
+ private
31
+
32
+ def full_path
33
+ containing_directory = File.join(path, relative)
34
+ File.expand_path(containing_directory)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,144 @@
1
+ require "json"
2
+
3
+ module Imap::Backup
4
+ class Serializer::Imap
5
+ CURRENT_VERSION = 2
6
+
7
+ attr_reader :folder_path
8
+ attr_reader :loaded
9
+
10
+ def initialize(folder_path)
11
+ @folder_path = folder_path
12
+ @loaded = false
13
+ @uid_validity = nil
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
24
+ end
25
+
26
+ def append(uid)
27
+ uids << uid
28
+ save
29
+ end
30
+
31
+ def delete
32
+ return if !exist?
33
+
34
+ File.unlink(pathname)
35
+ end
36
+
37
+ def include?(uid)
38
+ uids.include?(uid)
39
+ end
40
+
41
+ def index(uid)
42
+ uids.find_index(uid)
43
+ end
44
+
45
+ def rename(new_path)
46
+ if exist?
47
+ old_pathname = pathname
48
+ @folder_path = new_path
49
+ File.rename(old_pathname, pathname)
50
+ else
51
+ @folder_path = new_path
52
+ end
53
+ end
54
+
55
+ def uid_validity
56
+ ensure_loaded
57
+ @uid_validity
58
+ end
59
+
60
+ def uid_validity=(value)
61
+ ensure_loaded
62
+ @uid_validity = value
63
+ @uids ||= []
64
+ save
65
+ end
66
+
67
+ def uids
68
+ ensure_loaded
69
+ @uids || []
70
+ end
71
+
72
+ def update_uid(old, new)
73
+ index = uids.find_index(old.to_i)
74
+ return if index.nil?
75
+
76
+ uids[index] = new.to_i
77
+ save
78
+ end
79
+
80
+ def version
81
+ ensure_loaded
82
+ @version
83
+ end
84
+
85
+ private
86
+
87
+ def pathname
88
+ "#{folder_path}.imap"
89
+ end
90
+
91
+ def exist?
92
+ File.exist?(pathname)
93
+ end
94
+
95
+ def ensure_loaded
96
+ return if loaded
97
+
98
+ data = load
99
+ if data
100
+ @uids = data[:uids].map(&:to_i)
101
+ @uid_validity = data[:uid_validity]
102
+ @version = data[:version]
103
+ else
104
+ @uids = []
105
+ @uid_validity = nil
106
+ @version = CURRENT_VERSION
107
+ end
108
+ @loaded = true
109
+ end
110
+
111
+ def load
112
+ return nil if !exist?
113
+
114
+ data = nil
115
+ begin
116
+ content = File.read(pathname)
117
+ data = JSON.parse(content, symbolize_names: true)
118
+ rescue JSON::ParserError
119
+ return nil
120
+ end
121
+
122
+ return nil if !data.key?(:version)
123
+ return nil if !data.key?(:uid_validity)
124
+ return nil if !data.key?(:uids)
125
+ return nil if !data[:uids].is_a?(Array)
126
+
127
+ data
128
+ end
129
+
130
+ def save
131
+ ensure_loaded
132
+
133
+ raise "Cannot save metadata without a uid_validity" if !uid_validity
134
+
135
+ data = {
136
+ version: @version,
137
+ uid_validity: @uid_validity,
138
+ uids: @uids
139
+ }
140
+ content = data.to_json
141
+ File.open(pathname, "w") { |f| f.write content }
142
+ end
143
+ end
144
+ end
@@ -1,116 +1,61 @@
1
- require "forwardable"
2
-
3
- require "imap/backup/serializer/mbox_store"
4
-
5
1
  module Imap::Backup
6
2
  class Serializer::Mbox
7
- extend Forwardable
8
- def_delegators :store, :mbox_pathname
9
-
10
- attr_reader :path
11
- attr_reader :folder
12
-
13
- def initialize(path, folder)
14
- @path = path
15
- @folder = folder
16
- end
17
-
18
- def apply_uid_validity(value)
19
- case
20
- when store.uid_validity.nil?
21
- store.uid_validity = value
22
- nil
23
- when store.uid_validity == value
24
- # NOOP
25
- nil
26
- else
27
- apply_new_uid_validity value
28
- end
29
- end
3
+ attr_reader :folder_path
30
4
 
31
- def force_uid_validity(value)
32
- store.uid_validity = value
5
+ def initialize(folder_path)
6
+ @folder_path = folder_path
33
7
  end
34
8
 
35
- def uids
36
- store.uids
9
+ def valid?
10
+ exist?
37
11
  end
38
12
 
39
- def load(uid)
40
- store.load(uid)
13
+ def append(message)
14
+ File.open(pathname, "ab") do |file|
15
+ file.write message
16
+ end
41
17
  end
42
18
 
43
- def each_message(uids)
44
- store.each_message(uids)
45
- end
19
+ def delete
20
+ return if !exist?
46
21
 
47
- def save(uid, message)
48
- store.add(uid, message)
22
+ File.unlink(pathname)
49
23
  end
50
24
 
51
- def rename(new_name)
52
- @folder = new_name
53
- store.rename new_name
54
- end
25
+ def length
26
+ return nil if !exist?
55
27
 
56
- def update_uid(old, new)
57
- store.update_uid old, new
28
+ File.stat(pathname).size
58
29
  end
59
30
 
60
- private
61
-
62
- def store
63
- @store ||=
64
- begin
65
- create_containing_directory
66
- Serializer::MboxStore.new(path, folder)
67
- end
31
+ def pathname
32
+ "#{folder_path}.mbox"
68
33
  end
69
34
 
70
- def apply_new_uid_validity(value)
71
- digit = 0
72
- new_name = nil
73
- loop do
74
- extra = digit.zero? ? "" : "-#{digit}"
75
- new_name = "#{folder}-#{store.uid_validity}#{extra}"
76
- test_store = Serializer::MboxStore.new(path, new_name)
77
- break if !test_store.exist?
78
-
79
- digit += 1
35
+ def rename(new_path)
36
+ if exist?
37
+ old_pathname = pathname
38
+ @folder_path = new_path
39
+ File.rename(old_pathname, pathname)
40
+ else
41
+ @folder_path = new_path
80
42
  end
81
- rename_store new_name, value
82
- end
83
-
84
- def rename_store(new_name, value)
85
- store.rename new_name
86
- @store = nil
87
- store.uid_validity = value
88
- new_name
89
43
  end
90
44
 
91
- def relative_path
92
- File.dirname(folder)
93
- end
94
-
95
- def containing_directory
96
- File.join(path, relative_path)
45
+ def rewind(length)
46
+ File.open(pathname, File::RDWR | File::CREAT, 0o644) do |f|
47
+ f.truncate(length)
48
+ end
97
49
  end
98
50
 
99
- def full_path
100
- File.expand_path(containing_directory)
51
+ def touch
52
+ File.open(pathname, "a") {}
101
53
  end
102
54
 
103
- def create_containing_directory
104
- if !File.directory?(full_path)
105
- Utils.make_folder(
106
- path, relative_path, Serializer::DIRECTORY_PERMISSIONS
107
- )
108
- end
55
+ private
109
56
 
110
- if Utils.mode(full_path) !=
111
- Serializer::DIRECTORY_PERMISSIONS
112
- FileUtils.chmod Serializer::DIRECTORY_PERMISSIONS, full_path
113
- end
57
+ def exist?
58
+ File.exist?(pathname)
114
59
  end
115
60
  end
116
61
  end
@@ -1,4 +1,6 @@
1
1
  module Imap::Backup
2
+ class Serializer; end
3
+
2
4
  class Serializer::MboxEnumerator
3
5
  attr_reader :mbox_pathname
4
6
 
@@ -0,0 +1,29 @@
1
+ require "email/mboxrd/message"
2
+ require "imap/backup/serializer/mbox_enumerator"
3
+
4
+ module Imap::Backup
5
+ class Serializer::MessageEnumerator
6
+ attr_reader :imap
7
+ attr_reader :mbox
8
+
9
+ def initialize(imap:, mbox:)
10
+ @imap = imap
11
+ @mbox = mbox
12
+ end
13
+
14
+ def run(uids:)
15
+ indexes = uids.each.with_object({}) do |uid_maybe_string, acc|
16
+ uid = uid_maybe_string.to_i
17
+ index = imap.index(uid)
18
+ acc[index] = uid if index
19
+ end
20
+ enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
21
+ enumerator.each.with_index do |raw, i|
22
+ uid = indexes[i]
23
+ next if !uid
24
+
25
+ yield uid, Email::Mboxrd::Message.from_serialized(raw)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Imap::Backup
2
+ class Serializer::UnusedNameFinder
3
+ attr_reader :serializer
4
+
5
+ def initialize(serializer:)
6
+ @serializer = serializer
7
+ end
8
+
9
+ def run
10
+ digit = 0
11
+ folder = nil
12
+
13
+ loop do
14
+ extra = digit.zero? ? "" : "-#{digit}"
15
+ folder = "#{serializer.folder}-#{serializer.uid_validity}#{extra}"
16
+ test = Serializer.new(serializer.path, folder)
17
+ break if !test.validate!
18
+
19
+ digit += 1
20
+ end
21
+
22
+ folder
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,163 @@
1
+ require "forwardable"
2
+
3
+ require "email/mboxrd/message"
4
+ require "imap/backup/serializer/appender"
5
+ require "imap/backup/serializer/imap"
6
+ require "imap/backup/serializer/mbox"
7
+ require "imap/backup/serializer/mbox_enumerator"
8
+ require "imap/backup/serializer/message_enumerator"
9
+ require "imap/backup/serializer/unused_name_finder"
10
+
1
11
  module Imap::Backup
2
- module Serializer
3
- DIRECTORY_PERMISSIONS = 0o700
4
- FILE_PERMISSIONS = 0o600
12
+ class Serializer
13
+ def self.folder_path_for(path:, folder:)
14
+ relative = File.join(path, folder)
15
+ File.expand_path(relative)
16
+ end
17
+
18
+ extend Forwardable
19
+
20
+ def_delegator :mbox, :pathname, :mbox_pathname
21
+ def_delegators :imap, :uid_validity, :uids, :update_uid
22
+
23
+ attr_reader :folder
24
+ attr_reader :path
25
+
26
+ def initialize(path, folder)
27
+ @path = path
28
+ @folder = folder
29
+ end
30
+
31
+ # Returns true if there are existing, valid files
32
+ # false otherwise (in which case any existing files are deleted)
33
+ def validate!
34
+ return true if imap.valid? && mbox.valid?
35
+
36
+ imap.delete
37
+ @imap = nil
38
+ mbox.delete
39
+ @mbox = nil
40
+
41
+ false
42
+ end
43
+
44
+ def apply_uid_validity(value)
45
+ validate!
46
+
47
+ case
48
+ when uid_validity.nil?
49
+ internal_force_uid_validity(value)
50
+ nil
51
+ when uid_validity == value
52
+ # NOOP
53
+ nil
54
+ else
55
+ apply_new_uid_validity value
56
+ end
57
+ end
58
+
59
+ def force_uid_validity(value)
60
+ validate!
61
+
62
+ internal_force_uid_validity(value)
63
+ end
64
+
65
+ def append(uid, message)
66
+ validate!
67
+
68
+ appender = Serializer::Appender.new(folder: folder, imap: imap, mbox: mbox)
69
+ appender.run(uid: uid, message: message)
70
+ end
71
+
72
+ def load(uid_maybe_string)
73
+ validate!
74
+
75
+ uid = uid_maybe_string.to_i
76
+ message_index = imap.index(uid)
77
+ return nil if message_index.nil?
78
+
79
+ internal_load_nth(message_index)
80
+ end
81
+
82
+ def load_nth(index)
83
+ validate!
84
+
85
+ internal_load_nth(index)
86
+ end
87
+
88
+ def each_message(required_uids, &block)
89
+ validate!
90
+
91
+ return enum_for(:each_message, required_uids) if !block
92
+
93
+ enumerator = Serializer::MessageEnumerator.new(imap: imap, mbox: mbox)
94
+ enumerator.run(uids: required_uids, &block)
95
+ end
96
+
97
+ private
98
+
99
+ def rename(new_name)
100
+ destination = self.class.folder_path_for(path: path, folder: new_name)
101
+ ensure_containing_directory(new_name)
102
+ mbox.rename destination
103
+ imap.rename destination
104
+ end
105
+
106
+ def internal_force_uid_validity(value)
107
+ imap.uid_validity = value
108
+ mbox.touch
109
+ end
110
+
111
+ def internal_load_nth(index)
112
+ enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
113
+ enumerator.each.with_index do |raw, i|
114
+ next if i != index
115
+
116
+ return Email::Mboxrd::Message.from_serialized(raw)
117
+ end
118
+ nil
119
+ end
120
+
121
+ def mbox
122
+ @mbox ||=
123
+ begin
124
+ ensure_containing_directory(folder)
125
+ Serializer::Mbox.new(folder_path)
126
+ end
127
+ end
128
+
129
+ def imap
130
+ @imap ||=
131
+ begin
132
+ ensure_containing_directory(folder)
133
+ Serializer::Imap.new(folder_path)
134
+ end
135
+ end
136
+
137
+ def folder_path
138
+ self.class.folder_path_for(path: path, folder: folder)
139
+ end
140
+
141
+ def ensure_containing_directory(folder)
142
+ relative = File.dirname(folder)
143
+ directory = Serializer::Directory.new(path, relative)
144
+ directory.ensure_exists
145
+ end
146
+
147
+ def apply_new_uid_validity(value)
148
+ new_name = rename_existing_folder
149
+ # Clear memoization so we get empty data
150
+ @mbox = nil
151
+ @imap = nil
152
+ internal_force_uid_validity(value)
153
+
154
+ new_name
155
+ end
156
+
157
+ def rename_existing_folder
158
+ new_name = Serializer::UnusedNameFinder.new(serializer: self).run
159
+ rename new_name
160
+ new_name
161
+ end
5
162
  end
6
163
  end
@@ -0,0 +1,75 @@
1
+ require "imap/backup/setup/helpers"
2
+
3
+ module Imap::Backup
4
+ class Setup; end
5
+ class Setup::Account; end
6
+
7
+ class Setup::Account::Header
8
+ attr_reader :account
9
+ attr_reader :menu
10
+
11
+ def initialize(menu:, account:)
12
+ @menu = menu
13
+ @account = account
14
+ end
15
+
16
+ def run
17
+ menu.header = <<~HEADER.chomp
18
+ #{helpers.title_prefix} Account#{modified_flag}
19
+
20
+ email #{space}#{account.username}
21
+ password#{space}#{masked_password}
22
+ path #{space}#{local_path}
23
+ folders #{space}#{folders.map { |f| f[:name] }.join(', ')}#{multi_fetch_size}
24
+ server #{space}#{account.server}#{connection_options}
25
+
26
+ Choose an action
27
+ HEADER
28
+ end
29
+
30
+ private
31
+
32
+ def folders
33
+ account.folders || []
34
+ end
35
+
36
+ def helpers
37
+ Setup::Helpers.new
38
+ end
39
+
40
+ def modified_flag
41
+ account.modified? ? "*" : ""
42
+ end
43
+
44
+ def multi_fetch_size
45
+ "\nmulti-fetch #{account.multi_fetch_size}" if account.multi_fetch_size > 1
46
+ end
47
+
48
+ def connection_options
49
+ return nil if !account.connection_options
50
+
51
+ escaped = JSON.generate(account.connection_options)
52
+ escaped.gsub!('"', '\"')
53
+ "\nconnection options '#{escaped}'"
54
+ end
55
+
56
+ def space
57
+ account.connection_options ? " " * 12 : " " * 4
58
+ end
59
+
60
+ def masked_password
61
+ if (account.password == "") || account.password.nil?
62
+ "(unset)"
63
+ else
64
+ account.password.gsub(/./, "x")
65
+ end
66
+ end
67
+
68
+ def local_path
69
+ # In order to handle backslashes, as Highline effectively
70
+ # does an eval (!) on its templates, we need to doubly
71
+ # escape them
72
+ account.local_path.gsub("\\", "\\\\\\\\")
73
+ end
74
+ end
75
+ end