imap-backup 1.4.2 → 2.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.
- checksums.yaml +4 -4
- data/.rspec-all +2 -0
- data/README.md +36 -3
- data/bin/imap-backup +5 -0
- data/imap-backup.gemspec +7 -0
- data/lib/email/mboxrd/message.rb +13 -10
- data/lib/email/provider.rb +11 -2
- data/lib/imap/backup.rb +2 -1
- data/lib/imap/backup/account/connection.rb +41 -0
- data/lib/imap/backup/account/folder.rb +42 -2
- data/lib/imap/backup/serializer.rb +6 -0
- data/lib/imap/backup/serializer/mbox.rb +63 -83
- data/lib/imap/backup/serializer/mbox_store.rb +214 -0
- data/lib/imap/backup/uploader.rb +26 -0
- data/lib/imap/backup/utils.rb +3 -3
- data/lib/imap/backup/version.rb +5 -4
- data/spec/features/backup_spec.rb +85 -7
- data/spec/features/restore_spec.rb +112 -0
- data/spec/features/support/backup_directory.rb +21 -1
- data/spec/features/support/email_server.rb +66 -3
- data/spec/features/support/shared/message_fixtures.rb +2 -0
- data/spec/fixtures/connection.yml +2 -3
- data/spec/unit/account/connection_spec.rb +19 -5
- data/spec/unit/configuration/account_spec.rb +2 -1
- data/spec/unit/email/provider_spec.rb +1 -5
- data/spec/unit/serializer/mbox_spec.rb +52 -89
- data/spec/unit/serializer/mbox_store_spec.rb +117 -0
- data/spec/unit/utils_spec.rb +3 -3
- metadata +17 -8
- data/lib/imap/backup/serializer/base.rb +0 -16
- data/spec/unit/serializer/base_spec.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f897dc0bac78683c9cae100718a03cf934adc0348866a4688ce5d2196bbdcfa
|
4
|
+
data.tar.gz: 164bc47c5acb9e448b3c61e6d58d18c3f48cd03fd6a2cf236682e5f5abd11232
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ef7c3cea9a72c2fec7f87491c80eebc1aeeeca50c207fec4c713259a2de34c986a7a2457aed7cc20b36e4173f52fd9d30ac0340bca64600d23792d00433ac3f
|
7
|
+
data.tar.gz: 787c00650b5dd72258e895f51457599e0c20bd4d5897c5ec8c812fd4a54ce96796538b8c88cc689a02365f8fed5122237724a566aae9849dd5b807804a3a644f
|
data/.rspec-all
ADDED
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
|
-
|
199
|
-
|
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
|
|
data/bin/imap-backup
CHANGED
@@ -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
|
data/imap-backup.gemspec
CHANGED
@@ -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"
|
data/lib/email/mboxrd/message.rb
CHANGED
@@ -22,7 +22,15 @@ module Email::Mboxrd
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def to_serialized
|
25
|
-
"From " + from + "\n" + mboxrd_body
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
data/lib/email/provider.rb
CHANGED
@@ -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
|
-
|
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
|
-
"
|
39
|
+
"imap.fastmail.com"
|
31
40
|
end
|
32
41
|
end
|
33
42
|
end
|
data/lib/imap/backup.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
-
|
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
|
@@ -1,112 +1,92 @@
|
|
1
|
-
require "
|
2
|
-
require "email/mboxrd/message"
|
1
|
+
require "imap/backup/serializer/mbox_store"
|
3
2
|
|
4
3
|
module Imap::Backup
|
5
|
-
|
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
|
-
|
10
|
-
|
11
|
-
assert_files
|
9
|
+
@path = path
|
10
|
+
@folder = folder
|
12
11
|
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
30
|
-
|
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
|
-
|
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
|
72
|
-
|
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
|
80
|
-
|
52
|
+
def save(uid, message)
|
53
|
+
store.add(uid, message)
|
81
54
|
end
|
82
55
|
|
83
|
-
def
|
84
|
-
|
56
|
+
def rename(new_name)
|
57
|
+
@folder = new_name
|
58
|
+
store.rename new_name
|
85
59
|
end
|
86
60
|
|
87
|
-
def
|
88
|
-
|
61
|
+
def update_uid(old, new)
|
62
|
+
store.update_uid old, new
|
89
63
|
end
|
90
64
|
|
91
|
-
|
92
|
-
folder + ".mbox"
|
93
|
-
end
|
65
|
+
private
|
94
66
|
|
95
|
-
def
|
96
|
-
|
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
|
100
|
-
|
101
|
-
File.join(path,
|
102
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
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
|