imap-backup 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -1
- data/.travis.yml +1 -1
- data/README.md +12 -0
- data/bin/imap-backup +11 -6
- data/docker-compose.yml +15 -0
- data/imap-backup.gemspec +3 -2
- data/lib/email/mboxrd/message.rb +30 -4
- data/lib/email/provider.rb +2 -2
- data/lib/imap/backup.rb +0 -1
- data/lib/imap/backup/account/connection.rb +29 -15
- data/lib/imap/backup/account/folder.rb +3 -3
- data/lib/imap/backup/configuration/account.rb +15 -12
- data/lib/imap/backup/configuration/asker.rb +3 -1
- data/lib/imap/backup/configuration/folder_chooser.rb +5 -2
- data/lib/imap/backup/configuration/list.rb +12 -9
- data/lib/imap/backup/configuration/setup.rb +5 -3
- data/lib/imap/backup/configuration/store.rb +4 -4
- data/lib/imap/backup/downloader.rb +6 -2
- data/lib/imap/backup/serializer/base.rb +2 -2
- data/lib/imap/backup/serializer/mbox.rb +16 -7
- data/lib/imap/backup/utils.rb +8 -3
- data/lib/imap/backup/version.rb +1 -1
- data/spec/features/backup_spec.rb +27 -0
- data/spec/features/helper.rb +2 -0
- data/spec/features/support/backup_directory.rb +27 -0
- data/spec/features/support/email_server.rb +40 -0
- data/spec/features/support/shared/connection_context.rb +12 -0
- data/spec/features/support/shared/message_fixtures.rb +6 -0
- data/spec/fixtures/connection.yml +7 -0
- data/spec/support/fixtures.rb +6 -0
- data/spec/unit/account/connection_spec.rb +19 -7
- data/spec/unit/account/folder_spec.rb +15 -5
- data/spec/unit/configuration/account_spec.rb +18 -8
- data/spec/unit/configuration/asker_spec.rb +6 -3
- data/spec/unit/configuration/connection_tester_spec.rb +1 -1
- data/spec/unit/configuration/folder_chooser_spec.rb +21 -11
- data/spec/unit/configuration/list_spec.rb +13 -6
- data/spec/unit/configuration/setup_spec.rb +5 -3
- data/spec/unit/configuration/store_spec.rb +13 -8
- data/spec/unit/downloader_spec.rb +3 -1
- data/spec/unit/email/mboxrd/message_spec.rb +32 -13
- data/spec/unit/email/provider_spec.rb +7 -3
- data/spec/unit/serializer/base_spec.rb +3 -2
- data/spec/unit/serializer/mbox_spec.rb +9 -6
- data/spec/unit/utils_spec.rb +15 -13
- metadata +35 -5
- data/lib/imap/backup/serializer/directory.rb +0 -43
- data/spec/unit/serializer/directory_spec.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4665c4dcce3e9de07be72563f7f52045789d1da30120462e290ae5c3207598db
|
4
|
+
data.tar.gz: 3f455617d7b7a0d392856b74adb0aa75e528037e8716469a72e3a8b9aa405841
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57ef0a8b879f915630ba3d3ecbf339973ecf11a0eb0ae30b1e520bd346a9daaaab501fc90c509e0f1ad5d4b0fbee6cf6a4424da3b10caeb72bc52b96663bc9cf
|
7
|
+
data.tar.gz: f834d433145dcdc60ece0b3e03e973d36c0bb884060e13378922a886310e9362f92db901a4a9882e7a31a300594253377c7450085a5af66097646c6480cd2ed7
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.rubocop.yml
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
inherit_from: https://
|
1
|
+
inherit_from: https://gitlab.com/snippets/1744945/raw
|
2
2
|
|
3
3
|
AllCops:
|
4
|
+
TargetRubyVersion: 2.1
|
4
5
|
Exclude:
|
5
6
|
- "bin/stubs/*"
|
6
7
|
DisplayCopNames:
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -186,6 +186,18 @@ $ imap-backup status
|
|
186
186
|
* https://github.com/rgrove/larch - copies between IMAP servers
|
187
187
|
* https://github.com/OfflineIMAP/offlineimap
|
188
188
|
|
189
|
+
# Testing
|
190
|
+
|
191
|
+
## Integration Tests
|
192
|
+
|
193
|
+
Integration tests are run against a Docker image
|
194
|
+
(antespi/docker-imap-devel:latest).
|
195
|
+
|
196
|
+
Currently, the integration tests with Docker are excluded from the CI run.
|
197
|
+
|
198
|
+
The image has a pre-existing user:
|
199
|
+
`address@example.org` with password `pass`
|
200
|
+
|
189
201
|
## Contributing
|
190
202
|
|
191
203
|
1. Fork it
|
data/bin/imap-backup
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require "optparse"
|
3
3
|
|
4
|
-
$LOAD_PATH.unshift(File.expand_path("
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
|
5
5
|
require "imap/backup"
|
6
6
|
|
7
7
|
KNOWN_COMMANDS = [
|
@@ -10,21 +10,26 @@ KNOWN_COMMANDS = [
|
|
10
10
|
{name: "folders", help: "List folders for all (or selected) accounts"},
|
11
11
|
{name: "status", help: "List count of non backed-up emails per folder"},
|
12
12
|
{name: "help", help: "Show usage"}
|
13
|
-
]
|
13
|
+
].freeze
|
14
14
|
|
15
15
|
options = {command: "backup"}
|
16
|
-
|
16
|
+
parser = OptionParser.new do |opts|
|
17
17
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options] COMMAND"
|
18
18
|
|
19
19
|
opts.separator ""
|
20
20
|
opts.separator "Commands:"
|
21
21
|
KNOWN_COMMANDS.each do |command|
|
22
|
-
opts.separator "\t%-
|
22
|
+
opts.separator format("\t%- 20<name>s %<help>s", command)
|
23
23
|
end
|
24
24
|
opts.separator ""
|
25
25
|
opts.separator "Common options:"
|
26
26
|
|
27
|
-
opts.on(
|
27
|
+
opts.on(
|
28
|
+
"-a",
|
29
|
+
"--accounts ACCOUNT1[,ACCOUNT2,...]",
|
30
|
+
Array,
|
31
|
+
"only these accounts"
|
32
|
+
) do |account|
|
28
33
|
options[:accounts] = account
|
29
34
|
end
|
30
35
|
|
@@ -38,7 +43,7 @@ opts = OptionParser.new do |opts|
|
|
38
43
|
exit
|
39
44
|
end
|
40
45
|
end
|
41
|
-
|
46
|
+
parser.parse!
|
42
47
|
|
43
48
|
if ARGV.size > 0
|
44
49
|
options[:command] = ARGV.shift
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file adapted from github.com/antespi/docker-imap-devel
|
2
|
+
version: "3"
|
3
|
+
|
4
|
+
services:
|
5
|
+
imap:
|
6
|
+
image: antespi/docker-imap-devel:latest
|
7
|
+
container_name: imap
|
8
|
+
ports:
|
9
|
+
- "8025:25"
|
10
|
+
- "8143:143"
|
11
|
+
- "8993:993"
|
12
|
+
environment:
|
13
|
+
- MAILNAME=example.org
|
14
|
+
- MAIL_ADDRESS=address@example.org
|
15
|
+
- MAIL_PASS=pass
|
data/imap-backup.gemspec
CHANGED
@@ -3,8 +3,8 @@ require "imap/backup/version"
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.name = "imap-backup"
|
6
|
-
gem.description =
|
7
|
-
gem.summary =
|
6
|
+
gem.description = "Backup GMail, or any other IMAP email service, to disk."
|
7
|
+
gem.summary = "Backup GMail (or other IMAP) accounts to disk"
|
8
8
|
gem.authors = ["Joe Yates"]
|
9
9
|
gem.email = ["joe.g.yates@gmail.com"]
|
10
10
|
gem.homepage = "https://github.com/joeyates/imap-backup"
|
@@ -20,6 +20,7 @@ Gem::Specification.new do |gem|
|
|
20
20
|
gem.add_runtime_dependency "mail"
|
21
21
|
|
22
22
|
gem.add_development_dependency "codeclimate-test-reporter", "~> 0.4.8"
|
23
|
+
gem.add_development_dependency "pry-byebug"
|
23
24
|
gem.add_development_dependency "rspec", ">= 3.0.0"
|
24
25
|
gem.add_development_dependency "rubocop-rspec"
|
25
26
|
gem.add_development_dependency "simplecov"
|
data/lib/email/mboxrd/message.rb
CHANGED
@@ -6,12 +6,22 @@ module Email::Mboxrd
|
|
6
6
|
class Message
|
7
7
|
attr_reader :supplied_body
|
8
8
|
|
9
|
+
def self.from_serialized(serialized)
|
10
|
+
cleaned = serialized.gsub(/^>(>*From)/, "\\1")
|
11
|
+
# Serialized messages in this format *should* start with a line
|
12
|
+
# From xxx yy zz
|
13
|
+
if cleaned.start_with?("From ")
|
14
|
+
cleaned = cleaned.sub(/^From .*[\r\n]*/, "")
|
15
|
+
end
|
16
|
+
new(cleaned)
|
17
|
+
end
|
18
|
+
|
9
19
|
def initialize(supplied_body)
|
10
20
|
@supplied_body = supplied_body.clone
|
11
21
|
@supplied_body.force_encoding("binary")
|
12
22
|
end
|
13
23
|
|
14
|
-
def
|
24
|
+
def to_serialized
|
15
25
|
"From " + from + "\n" + mboxrd_body + "\n"
|
16
26
|
end
|
17
27
|
|
@@ -36,24 +46,40 @@ module Email::Mboxrd
|
|
36
46
|
end
|
37
47
|
|
38
48
|
def from
|
39
|
-
|
49
|
+
@from ||=
|
50
|
+
begin
|
51
|
+
from = best_from
|
52
|
+
from << " " + asctime if asctime != ""
|
53
|
+
from
|
54
|
+
end
|
40
55
|
end
|
41
56
|
|
42
57
|
def mboxrd_body
|
43
58
|
@mboxrd_body ||=
|
44
59
|
begin
|
45
|
-
@mboxrd_body = supplied_body
|
60
|
+
@mboxrd_body = add_extra_quote(supplied_body)
|
46
61
|
@mboxrd_body += "\n" if !@mboxrd_body.end_with?("\n")
|
47
62
|
@mboxrd_body
|
48
63
|
end
|
49
64
|
end
|
50
65
|
|
66
|
+
def add_extra_quote(body)
|
67
|
+
# The mboxrd format requires that lines starting with 'From'
|
68
|
+
# be prefixed with a '>' so that any remaining lines which start with
|
69
|
+
# 'From ' can be taken as the beginning of messages.
|
70
|
+
# http://www.digitalpreservation.gov/formats/fdd/fdd000385.shtml
|
71
|
+
# Here we add an extra '>' before any "From" or ">From".
|
72
|
+
body.gsub(/\n(>*From)/, "\n>\\1")
|
73
|
+
end
|
74
|
+
|
51
75
|
def asctime
|
52
|
-
date ? date.asctime : ""
|
76
|
+
@asctime ||= date ? date.asctime : ""
|
53
77
|
end
|
54
78
|
|
55
79
|
def date
|
56
80
|
parsed.date
|
81
|
+
rescue
|
82
|
+
nil
|
57
83
|
end
|
58
84
|
end
|
59
85
|
end
|
data/lib/email/provider.rb
CHANGED
@@ -2,7 +2,7 @@ module Email; end
|
|
2
2
|
|
3
3
|
class Email::Provider
|
4
4
|
def self.for_address(address)
|
5
|
-
case
|
5
|
+
case
|
6
6
|
when address.end_with?("@gmail.com")
|
7
7
|
new(:gmail)
|
8
8
|
when address.end_with?("@fastmail.fm")
|
@@ -21,7 +21,7 @@ class Email::Provider
|
|
21
21
|
def options
|
22
22
|
case provider
|
23
23
|
when :gmail
|
24
|
-
{port: 993, ssl:
|
24
|
+
{port: 993, ssl: {ssl_version: :TLSv1_2}}
|
25
25
|
when :fastmail
|
26
26
|
{port: 993, ssl: true}
|
27
27
|
else
|
data/lib/imap/backup.rb
CHANGED
@@ -12,7 +12,6 @@ require "imap/backup/configuration/setup"
|
|
12
12
|
require "imap/backup/configuration/store"
|
13
13
|
require "imap/backup/downloader"
|
14
14
|
require "imap/backup/serializer/base"
|
15
|
-
require "imap/backup/serializer/directory"
|
16
15
|
require "imap/backup/serializer/mbox"
|
17
16
|
require "imap/backup/version"
|
18
17
|
require "email/provider"
|
@@ -4,9 +4,10 @@ module Imap::Backup
|
|
4
4
|
module Account; end
|
5
5
|
|
6
6
|
class Account::Connection
|
7
|
-
attr_reader :username
|
8
|
-
attr_reader :local_path
|
9
7
|
attr_reader :connection_options
|
8
|
+
attr_reader :local_path
|
9
|
+
attr_reader :password
|
10
|
+
attr_reader :username
|
10
11
|
|
11
12
|
def initialize(options)
|
12
13
|
@username, @password = options[:username], options[:password]
|
@@ -19,18 +20,23 @@ module Imap::Backup
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def folders
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
@folders ||=
|
24
|
+
begin
|
25
|
+
root = provider_root
|
26
|
+
@folders = imap.list(root, "*")
|
27
|
+
if @folders.nil?
|
28
|
+
Imap::Backup.logger.warn(
|
29
|
+
"Unable to get folder list for account #{username}"
|
30
|
+
)
|
31
|
+
end
|
32
|
+
@folders
|
33
|
+
end
|
28
34
|
end
|
29
35
|
|
30
36
|
def status
|
31
37
|
backup_folders.map do |folder|
|
32
38
|
f = Account::Folder.new(self, folder[:name])
|
33
|
-
s = Serializer::
|
39
|
+
s = Serializer::Mbox.new(local_path, folder[:name])
|
34
40
|
{name: folder[:name], local: s.uids, remote: f.uids}
|
35
41
|
end
|
36
42
|
end
|
@@ -52,7 +58,9 @@ module Imap::Backup
|
|
52
58
|
def imap
|
53
59
|
return @imap unless @imap.nil?
|
54
60
|
options = provider_options
|
55
|
-
Imap::Backup.logger.debug
|
61
|
+
Imap::Backup.logger.debug(
|
62
|
+
"Creating IMAP instance: #{server}, options: #{options.inspect}"
|
63
|
+
)
|
56
64
|
@imap = Net::IMAP.new(server, options)
|
57
65
|
Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
|
58
66
|
@imap.login(username, password)
|
@@ -78,14 +86,10 @@ module Imap::Backup
|
|
78
86
|
)
|
79
87
|
end
|
80
88
|
|
81
|
-
def password
|
82
|
-
@password
|
83
|
-
end
|
84
|
-
|
85
89
|
def masked_password
|
86
90
|
password.gsub(/./, "x")
|
87
91
|
end
|
88
|
-
|
92
|
+
|
89
93
|
def backup_folders
|
90
94
|
return @backup_folders if @backup_folders && (@backup_folders.size > 0)
|
91
95
|
(folders || []).map { |f| {name: f.name} }
|
@@ -104,5 +108,15 @@ module Imap::Backup
|
|
104
108
|
def provider_options
|
105
109
|
provider.options.merge(connection_options)
|
106
110
|
end
|
111
|
+
|
112
|
+
# 6.3.8. LIST Command
|
113
|
+
# An empty ("" string) mailbox name argument is a special request to
|
114
|
+
# return the hierarchy delimiter and the root name of the name given
|
115
|
+
# in the reference.
|
116
|
+
def provider_root
|
117
|
+
return @provider_root if @provider_root
|
118
|
+
root_info = imap.list("", "")[0]
|
119
|
+
@provider_root = root_info.name
|
120
|
+
end
|
107
121
|
end
|
108
122
|
end
|
@@ -6,7 +6,7 @@ module Imap::Backup
|
|
6
6
|
class Account::Folder
|
7
7
|
extend Forwardable
|
8
8
|
|
9
|
-
REQUESTED_ATTRIBUTES = ["RFC822", "FLAGS", "INTERNALDATE"]
|
9
|
+
REQUESTED_ATTRIBUTES = ["RFC822", "FLAGS", "INTERNALDATE"].freeze
|
10
10
|
|
11
11
|
attr_reader :connection
|
12
12
|
attr_reader :name
|
@@ -25,7 +25,7 @@ module Imap::Backup
|
|
25
25
|
def uids
|
26
26
|
imap.examine(name)
|
27
27
|
imap.uid_search(["ALL"]).sort
|
28
|
-
rescue Net::IMAP::NoResponseError
|
28
|
+
rescue Net::IMAP::NoResponseError
|
29
29
|
Imap::Backup.logger.warn "Folder '#{name}' does not exist"
|
30
30
|
[]
|
31
31
|
end
|
@@ -38,7 +38,7 @@ module Imap::Backup
|
|
38
38
|
attributes = fetch_data_item.attr
|
39
39
|
attributes["RFC822"].force_encoding("utf-8")
|
40
40
|
attributes
|
41
|
-
rescue Net::IMAP::NoResponseError
|
41
|
+
rescue Net::IMAP::NoResponseError
|
42
42
|
Imap::Backup.logger.warn "Folder '#{name}' does not exist"
|
43
43
|
nil
|
44
44
|
end
|
@@ -33,21 +33,22 @@ module Imap::Backup
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def header(menu)
|
36
|
-
menu.header = <<-
|
37
|
-
Account:
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
menu.header = <<-HEADER.gsub(/^\s{8}/m, "")
|
37
|
+
Account:
|
38
|
+
email: #{account[:username]}
|
39
|
+
server: #{account[:server]}
|
40
|
+
path: #{account[:local_path]}
|
41
|
+
folders: #{folders.map { |f| f[:name] }.join(', ')}
|
42
|
+
password: #{masked_password}
|
43
|
+
HEADER
|
44
44
|
end
|
45
45
|
|
46
46
|
def modify_email(menu)
|
47
47
|
menu.choice("modify email") do
|
48
48
|
username = Configuration::Asker.email(username)
|
49
49
|
puts "username: #{username}"
|
50
|
-
|
50
|
+
other_accounts = store.accounts.reject { |a| a == account }
|
51
|
+
others = other_accounts.map { |a| a[:username] }
|
51
52
|
puts "others: #{others.inspect}"
|
52
53
|
if others.include?(username)
|
53
54
|
puts "There is already an account set up with that email address"
|
@@ -83,19 +84,21 @@ Account:
|
|
83
84
|
|
84
85
|
def modify_backup_path(menu)
|
85
86
|
menu.choice("modify backup path") do
|
86
|
-
validator =
|
87
|
+
validator = ->(p) do
|
87
88
|
same = store.accounts.find do |a|
|
88
89
|
a[:username] != account[:username] && a[:local_path] == p
|
89
90
|
end
|
90
91
|
if same
|
91
|
-
puts "The path '#{p}' is used to backup
|
92
|
+
puts "The path '#{p}' is used to backup " \
|
93
|
+
"the account '#{same[:username]}'"
|
92
94
|
false
|
93
95
|
else
|
94
96
|
true
|
95
97
|
end
|
96
98
|
end
|
97
99
|
existing = account[:local_path].clone
|
98
|
-
account[:local_path] =
|
100
|
+
account[:local_path] =
|
101
|
+
Configuration::Asker.backup_path(account[:local_path], validator)
|
99
102
|
account[:modified] = true if existing != account[:local_path]
|
100
103
|
end
|
101
104
|
end
|
@@ -21,7 +21,9 @@ module Imap::Backup
|
|
21
21
|
password = highline.ask("password: ") { |q| q.echo = false }
|
22
22
|
confirmation = highline.ask("repeat password: ") { |q| q.echo = false }
|
23
23
|
if password != confirmation
|
24
|
-
return nil
|
24
|
+
return nil if !highline.agree(
|
25
|
+
"the password and confirmation did not match.\nContinue? (y/n) "
|
26
|
+
)
|
25
27
|
return self.password
|
26
28
|
end
|
27
29
|
password
|
@@ -52,7 +52,9 @@ module Imap::Backup
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def is_selected?(folder_name)
|
55
|
-
account[:folders]
|
55
|
+
backup_folders = account[:folders]
|
56
|
+
return false if backup_folders.nil?
|
57
|
+
backup_folders.find { |f| f[:name] == folder_name }
|
56
58
|
end
|
57
59
|
|
58
60
|
def toggle_selection(folder_name)
|
@@ -60,6 +62,7 @@ module Imap::Backup
|
|
60
62
|
changed = account[:folders].reject! { |f| f[:name] == folder_name }
|
61
63
|
account[:modified] = true if changed
|
62
64
|
else
|
65
|
+
account[:folders] ||= []
|
63
66
|
account[:folders] << {name: folder_name}
|
64
67
|
account[:modified] = true
|
65
68
|
end
|
@@ -67,7 +70,7 @@ module Imap::Backup
|
|
67
70
|
|
68
71
|
def connection
|
69
72
|
@connection ||= Account::Connection.new(account)
|
70
|
-
rescue
|
73
|
+
rescue
|
71
74
|
nil
|
72
75
|
end
|
73
76
|
|