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 +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
|