imap-backup 1.4.2 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37b1f605db79f10d3520f0f718e457d38c839003380f53fc7c1259a16293952e
4
- data.tar.gz: e13a7ab06d0c3a9e93ef52709de8f23a6fe4eaba720f51a3535aa2b984468df5
3
+ metadata.gz: 3f897dc0bac78683c9cae100718a03cf934adc0348866a4688ce5d2196bbdcfa
4
+ data.tar.gz: 164bc47c5acb9e448b3c61e6d58d18c3f48cd03fd6a2cf236682e5f5abd11232
5
5
  SHA512:
6
- metadata.gz: 9961d6f0ddde8b511f77d1cf0a1ebec932d2a98a0b6762c18ea36496365125b451818c1d9565527b146ccb8a7c495e17d6f590600caee8aa7a02d128edf8fe19
7
- data.tar.gz: 511c7025476ff56508d6538bc74ba382d2af741ba1c2ee8eabf36ceb6ce002b9d18ef51fc731d1aed4e8f20cfbeb998c3ccd39000528cccaa45c365403ffea3f
6
+ metadata.gz: 1ef7c3cea9a72c2fec7f87491c80eebc1aeeeca50c207fec4c713259a2de34c986a7a2457aed7cc20b36e4173f52fd9d30ac0340bca64600d23792d00433ac3f
7
+ data.tar.gz: 787c00650b5dd72258e895f51457599e0c20bd4d5897c5ec8c812fd4a54ce96796538b8c88cc689a02365f8fed5122237724a566aae9849dd5b807804a3a644f
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/README.md CHANGED
@@ -16,6 +16,12 @@
16
16
  [Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
17
17
  [Continuous Integration]: http://travis-ci.org/joeyates/imap-backup "Build status by Travis-CI"
18
18
 
19
+ ## Version 2
20
+
21
+ With versions above 2.x, this gems stores IMAP metadata in a
22
+ backwardly-incompatible way. When upgrading, all old backups will be gradually
23
+ deleted to allow for the new file format to be introduced.
24
+
19
25
  # Installation
20
26
 
21
27
  ```shell
@@ -158,6 +164,13 @@ If you have problems:
158
164
  "debug": true
159
165
  }
160
166
  ```
167
+ # Restore
168
+
169
+ All missing messages are pushed to the IMAP server.
170
+ Existing messages are left unchanged.
171
+
172
+ This functionality requires that the IMAP server supports the UIDPLUS
173
+ extension to IMAP4.
161
174
 
162
175
  # Other Usage
163
176
 
@@ -190,13 +203,33 @@ $ imap-backup status
190
203
 
191
204
  ## Integration Tests
192
205
 
193
- Integration tests are run against a Docker image
206
+ Integration tests (feature specs) are run against a Docker image
194
207
  (antespi/docker-imap-devel:latest).
195
208
 
209
+ In one shell, run the Docker image:
210
+
211
+ ```
212
+ docker run \
213
+ --env MAIL_ADDRESS=address@example.org \
214
+ --env MAIL_PASS=pass \
215
+ --env MAILNAME=example.org \
216
+ --publish 8993:993 \
217
+ antespi/docker-imap-devel:latest
218
+ ```
219
+
196
220
  Currently, the integration tests with Docker are excluded from the CI run.
197
221
 
198
- The image has a pre-existing user:
199
- `address@example.org` with password `pass`
222
+ To run **just** the Docker-based tests:
223
+
224
+ ```
225
+ rspec -t docker
226
+ ```
227
+
228
+ To run **all** specs, including the integration tests, do the following:
229
+
230
+ ```
231
+ rspec -O .rspec-all
232
+ ```
200
233
 
201
234
  ## Contributing
202
235
 
@@ -8,6 +8,7 @@ KNOWN_COMMANDS = [
8
8
  {name: "setup", help: "Create/edit the configuration file"},
9
9
  {name: "backup", help: "Do the backup (default)"},
10
10
  {name: "folders", help: "List folders for all (or selected) accounts"},
11
+ {name: "restore", help: "Restore emails to server"},
11
12
  {name: "status", help: "List count of non backed-up emails per folder"},
12
13
  {name: "help", help: "Show usage"}
13
14
  ].freeze
@@ -84,6 +85,10 @@ when "folders"
84
85
  end
85
86
  folders.each { |f| puts "\t" + f.name }
86
87
  end
88
+ when "restore"
89
+ configuration.each_connection do |connection|
90
+ connection.restore
91
+ end
87
92
  when "status"
88
93
  configuration.each_connection do |connection|
89
94
  puts connection.username
@@ -15,6 +15,13 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Imap::Backup::VERSION
17
17
 
18
+ gem.post_install_message = <<-MESSAGE.gsub(/^\s{4}/m, "")
19
+ When upgrading #{gem.name} from version 1.x to 2.x not that the
20
+ metadata storage method has changed (from flat file to JSON).
21
+ As a result, on the first run after an upgrade, old backup folders will be
22
+ **deleted** and a full new backup created.
23
+ MESSAGE
24
+
18
25
  gem.add_runtime_dependency "rake"
19
26
  gem.add_runtime_dependency "highline"
20
27
  gem.add_runtime_dependency "mail"
@@ -22,7 +22,15 @@ module Email::Mboxrd
22
22
  end
23
23
 
24
24
  def to_serialized
25
- "From " + from + "\n" + mboxrd_body + "\n"
25
+ "From " + from + "\n" + mboxrd_body
26
+ end
27
+
28
+ def date
29
+ parsed.date
30
+ end
31
+
32
+ def imap_body
33
+ supplied_body.gsub(/(?<!\r)\n/, "\r\n")
26
34
  end
27
35
 
28
36
  private
@@ -57,9 +65,10 @@ module Email::Mboxrd
57
65
  def mboxrd_body
58
66
  @mboxrd_body ||=
59
67
  begin
60
- @mboxrd_body = add_extra_quote(supplied_body)
61
- @mboxrd_body += "\n" if !@mboxrd_body.end_with?("\n")
62
- @mboxrd_body
68
+ mboxrd_body = add_extra_quote(supplied_body.gsub("\r\n", "\n"))
69
+ mboxrd_body += "\n" if !mboxrd_body.end_with?("\n")
70
+ mboxrd_body += "\n" if !mboxrd_body.end_with?("\n\n")
71
+ mboxrd_body
63
72
  end
64
73
  end
65
74
 
@@ -75,11 +84,5 @@ module Email::Mboxrd
75
84
  def asctime
76
85
  @asctime ||= date ? date.asctime : ""
77
86
  end
78
-
79
- def date
80
- parsed.date
81
- rescue
82
- nil
83
- end
84
87
  end
85
88
  end
@@ -3,6 +3,8 @@ module Email; end
3
3
  class Email::Provider
4
4
  def self.for_address(address)
5
5
  case
6
+ when address.end_with?("@fastmail.com")
7
+ new(:fastmail)
6
8
  when address.end_with?("@gmail.com")
7
9
  new(:gmail)
8
10
  when address.end_with?("@fastmail.fm")
@@ -19,7 +21,14 @@ class Email::Provider
19
21
  end
20
22
 
21
23
  def options
22
- {port: 993, ssl: {ssl_version: :TLSv1_2}}
24
+ case provider
25
+ when :gmail
26
+ {port: 993, ssl: true}
27
+ when :fastmail
28
+ {port: 993, ssl: true}
29
+ else
30
+ {port: 993, ssl: true}
31
+ end
23
32
  end
24
33
 
25
34
  def host
@@ -27,7 +36,7 @@ class Email::Provider
27
36
  when :gmail
28
37
  "imap.gmail.com"
29
38
  when :fastmail
30
- "mail.messagingengine.com"
39
+ "imap.fastmail.com"
31
40
  end
32
41
  end
33
42
  end
@@ -11,7 +11,8 @@ require "imap/backup/configuration/list"
11
11
  require "imap/backup/configuration/setup"
12
12
  require "imap/backup/configuration/store"
13
13
  require "imap/backup/downloader"
14
- require "imap/backup/serializer/base"
14
+ require "imap/backup/uploader"
15
+ require "imap/backup/serializer"
15
16
  require "imap/backup/serializer/mbox"
16
17
  require "imap/backup/version"
17
18
  require "email/provider"
@@ -47,14 +47,44 @@ module Imap::Backup
47
47
  imap
48
48
  each_folder do |folder, serializer|
49
49
  Imap::Backup.logger.debug "[#{folder.name}] running backup"
50
+ serializer.set_uid_validity(folder.uid_validity)
50
51
  Downloader.new(folder, serializer).run
51
52
  end
52
53
  end
53
54
 
55
+ def restore
56
+ local_folders do |serializer, folder|
57
+ exists = folder.exist?
58
+ if exists
59
+ new_name = serializer.set_uid_validity(folder.uid_validity)
60
+ old_name = serializer.folder
61
+ if new_name
62
+ Imap::Backup.logger.debug "Backup '#{old_name}' renamed and restored to '#{new_name}'"
63
+ new_serializer = Serializer::Mbox.new(local_path, new_name)
64
+ new_folder = Account::Folder.new(self, new_name)
65
+ new_folder.create
66
+ new_serializer.force_uid_validity(new_folder.uid_validity)
67
+ Uploader.new(new_folder, new_serializer).run
68
+ else
69
+ Uploader.new(folder, serializer).run
70
+ end
71
+ else
72
+ folder.create
73
+ serializer.force_uid_validity(folder.uid_validity)
74
+ Uploader.new(folder, serializer).run
75
+ end
76
+ end
77
+ end
78
+
54
79
  def disconnect
55
80
  imap.disconnect
56
81
  end
57
82
 
83
+ def reconnect
84
+ disconnect
85
+ @imap = nil
86
+ end
87
+
58
88
  def imap
59
89
  return @imap unless @imap.nil?
60
90
  options = provider_options
@@ -95,6 +125,17 @@ module Imap::Backup
95
125
  (folders || []).map { |f| {name: f.name} }
96
126
  end
97
127
 
128
+ def local_folders
129
+ glob = File.join(local_path, "**", "*.imap")
130
+ base = Pathname.new(local_path)
131
+ Pathname.glob(glob) do |path|
132
+ name = path.relative_path_from(base).to_s[0..-6]
133
+ serializer = Serializer::Mbox.new(local_path, name)
134
+ folder = Account::Folder.new(self, name)
135
+ yield serializer, folder
136
+ end
137
+ end
138
+
98
139
  def provider
99
140
  @provider ||= Email::Provider.for_address(username)
100
141
  end
@@ -16,14 +16,36 @@ module Imap::Backup
16
16
  def initialize(connection, name)
17
17
  @connection = connection
18
18
  @name = name
19
+ @uid_validity = nil
19
20
  end
20
21
 
22
+ # Deprecated: use #name
21
23
  def folder
22
24
  name
23
25
  end
24
26
 
27
+ def exist?
28
+ examine
29
+ true
30
+ rescue Net::IMAP::NoResponseError => e
31
+ false
32
+ end
33
+
34
+ def create
35
+ return if exist?
36
+ imap.create(name)
37
+ end
38
+
39
+ def uid_validity
40
+ @uid_validity ||=
41
+ begin
42
+ examine
43
+ imap.responses["UIDVALIDITY"][-1]
44
+ end
45
+ end
46
+
25
47
  def uids
26
- imap.examine(name)
48
+ examine
27
49
  imap.uid_search(["ALL"]).sort
28
50
  rescue Net::IMAP::NoResponseError
29
51
  Imap::Backup.logger.warn "Folder '#{name}' does not exist"
@@ -31,7 +53,7 @@ module Imap::Backup
31
53
  end
32
54
 
33
55
  def fetch(uid)
34
- imap.examine(name)
56
+ examine
35
57
  fetch_data_items = imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)
36
58
  return nil if fetch_data_items.nil?
37
59
  fetch_data_item = fetch_data_items[0]
@@ -42,5 +64,23 @@ module Imap::Backup
42
64
  Imap::Backup.logger.warn "Folder '#{name}' does not exist"
43
65
  nil
44
66
  end
67
+
68
+ def append(message)
69
+ body = message.imap_body
70
+ date = message.date.to_time
71
+ response = imap.append(name, body, nil, date)
72
+ extract_uid(response)
73
+ end
74
+
75
+ private
76
+
77
+ def examine
78
+ imap.examine(name)
79
+ end
80
+
81
+ def extract_uid(response)
82
+ @uid_validity, uid = response.data.code.data.split(" ").map(&:to_i)
83
+ uid
84
+ end
45
85
  end
46
86
  end
@@ -0,0 +1,6 @@
1
+ module Imap::Backup
2
+ module Serializer
3
+ DIRECTORY_PERMISSIONS = 0o700
4
+ FILE_PERMISSIONS = 0o600
5
+ end
6
+ end
@@ -1,112 +1,92 @@
1
- require "csv"
2
- require "email/mboxrd/message"
1
+ require "imap/backup/serializer/mbox_store"
3
2
 
4
3
  module Imap::Backup
5
- module Serializer; end
4
+ class Serializer::Mbox
5
+ attr_reader :path
6
+ attr_reader :folder
6
7
 
7
- class Serializer::Mbox < Serializer::Base
8
8
  def initialize(path, folder)
9
- super
10
- create_containing_directory
11
- assert_files
9
+ @path = path
10
+ @folder = folder
12
11
  end
13
12
 
14
- # TODO: cleanup locks, close file handles
15
-
16
- def uids
17
- return @uids if @uids
18
-
19
- @uids = []
20
- return @uids if !exist?
21
-
22
- CSV.foreach(imap_pathname) do |row|
23
- @uids << row[0]
13
+ def set_uid_validity(value)
14
+ existing_uid_validity = store.uid_validity
15
+ case
16
+ when existing_uid_validity.nil?
17
+ store.uid_validity = value
18
+ nil
19
+ when existing_uid_validity == value
20
+ # NOOP
21
+ nil
22
+ else
23
+ digit = nil
24
+ new_name = nil
25
+ loop do
26
+ extra = digit ? ".#{digit}" : ""
27
+ new_name = "#{folder}.#{existing_uid_validity}#{extra}"
28
+ test_store = Serializer::MboxStore.new(path, new_name)
29
+ break if !test_store.exist?
30
+ digit ||= 0
31
+ digit += 1
32
+ end
33
+ store.rename new_name
34
+ @store = nil
35
+ store.uid_validity = value
36
+ new_name
24
37
  end
25
- @uids = @uids.map(&:to_i).sort
26
- @uids
27
38
  end
28
39
 
29
- def save(uid, message)
30
- uid = uid.to_s
31
- if uids.include?(uid)
32
- Imap::Backup.logger.debug(
33
- "[#{folder}] message #{uid} already downloaded - skipping"
34
- )
35
- return
36
- end
37
-
38
- # invalidate cache
39
- @uids = nil
40
-
41
- body = message["RFC822"]
42
- mboxrd_message = Email::Mboxrd::Message.new(body)
43
- mbox = imap = nil
44
- begin
45
- mbox = File.open(mbox_pathname, "ab")
46
- imap = File.open(imap_pathname, "ab")
47
- mbox.write mboxrd_message.to_serialized
48
- imap.write uid + "\n"
49
- rescue => e
50
- message = <<-ERROR.gsub(/^\s*/m, "")
51
- [#{folder}] failed to save message #{uid}:
52
- #{body}. #{e}:
53
- #{e.backtrace.join("\n")}"
54
- ERROR
55
- Imap::Backup.logger.warn message
56
- ensure
57
- mbox.close if mbox
58
- imap.close if imap
59
- end
40
+ def force_uid_validity(value)
41
+ store.uid_validity = value
60
42
  end
61
43
 
62
- private
63
-
64
- def assert_files
65
- mbox = mbox_exist?
66
- imap = imap_exist?
67
- raise ".imap file missing" if mbox && (!imap)
68
- raise ".mbox file missing" if imap && (!mbox)
44
+ def uids
45
+ store.uids
69
46
  end
70
47
 
71
- def create_containing_directory
72
- mbox_relative_path = File.dirname(mbox_relative_pathname)
73
- return if mbox_relative_path == "."
74
- Utils.make_folder(
75
- path, mbox_relative_path, Serializer::DIRECTORY_PERMISSIONS
76
- )
48
+ def load(uid)
49
+ store.load(uid)
77
50
  end
78
51
 
79
- def exist?
80
- mbox_exist? && imap_exist?
52
+ def save(uid, message)
53
+ store.add(uid, message)
81
54
  end
82
55
 
83
- def mbox_exist?
84
- File.exist?(mbox_pathname)
56
+ def rename(new_name)
57
+ @folder = new_name
58
+ store.rename new_name
85
59
  end
86
60
 
87
- def imap_exist?
88
- File.exist?(imap_pathname)
61
+ def update_uid(old, new)
62
+ store.update_uid old, new
89
63
  end
90
64
 
91
- def mbox_relative_pathname
92
- folder + ".mbox"
93
- end
65
+ private
94
66
 
95
- def mbox_pathname
96
- File.join(path, mbox_relative_pathname)
67
+ def store
68
+ @store ||=
69
+ begin
70
+ create_containing_directory
71
+ Serializer::MboxStore.new(path, folder)
72
+ end
97
73
  end
98
74
 
99
- def imap_pathname
100
- filename = folder + ".imap"
101
- File.join(path, filename)
102
- end
75
+ def create_containing_directory
76
+ relative_path = File.dirname(folder)
77
+ containing_directory = File.join(path, relative_path)
78
+ full_path = File.expand_path(containing_directory)
103
79
 
104
- def lock
105
- # lock mbox and imap files
106
- # create both empty if missing
107
- end
80
+ if !File.directory?(full_path)
81
+ Imap::Backup::Utils.make_folder(
82
+ path, relative_path, Serializer::DIRECTORY_PERMISSIONS
83
+ )
84
+ end
108
85
 
109
- def unlock
86
+ if Imap::Backup::Utils.mode(full_path) !=
87
+ Serializer::DIRECTORY_PERMISSIONS
88
+ FileUtils.chmod Serializer::DIRECTORY_PERMISSIONS, full_path
89
+ end
110
90
  end
111
91
  end
112
92
  end