imap-backup 0.0.3 → 0.0.4
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.
- data/bin/imap-backup +18 -7
- data/imap-backup.gemspec +1 -0
- data/lib/imap/backup.rb +13 -1
- data/lib/imap/backup/configuration/account.rb +85 -0
- data/lib/imap/backup/configuration/asker.rb +42 -0
- data/lib/imap/backup/configuration/connection_tester.rb +19 -0
- data/lib/imap/backup/configuration/folder_chooser.rb +56 -0
- data/lib/imap/backup/configuration/list.rb +32 -0
- data/lib/imap/backup/configuration/setup.rb +68 -0
- data/lib/imap/backup/configuration/store.rb +64 -0
- data/lib/imap/backup/downloader.rb +2 -0
- data/lib/imap/backup/serializer/directory.rb +2 -4
- data/lib/imap/backup/utils.rb +11 -5
- data/lib/imap/backup/version.rb +1 -1
- data/spec/spec_helper.rb +26 -0
- data/spec/unit/configuration/account_spec.rb +224 -0
- data/spec/unit/configuration/asker_spec.rb +122 -0
- data/spec/unit/configuration/connection_tester_spec.rb +47 -0
- data/spec/unit/configuration/folder_chooser_spec.rb +131 -0
- data/spec/unit/configuration/list_spec.rb +73 -0
- data/spec/unit/configuration/setup_spec.rb +115 -0
- data/spec/unit/configuration/store_spec.rb +173 -0
- data/spec/unit/serializer/directory_spec.rb +2 -0
- data/spec/unit/utils_spec.rb +11 -9
- metadata +129 -113
- data/lib/imap/backup/settings.rb +0 -35
- data/spec/unit/settings_spec.rb +0 -109
data/bin/imap-backup
CHANGED
@@ -7,10 +7,11 @@ $:.unshift(File.expand_path('../../lib/', __FILE__))
|
|
7
7
|
require 'imap/backup'
|
8
8
|
|
9
9
|
KNOWN_COMMANDS = [
|
10
|
+
{:name => 'help', :help => 'Show usage'},
|
11
|
+
{:name => 'setup', :help => 'Create/edit the configuration file'},
|
10
12
|
{:name => 'backup', :help => 'Do the backup (default)'},
|
11
13
|
{:name => 'folders', :help => 'List folders for all (or selected) accounts'},
|
12
14
|
{:name => 'status', :help => 'List count of non backed-up emails per folder'},
|
13
|
-
{:name => 'help', :help => 'Show usage'},
|
14
15
|
]
|
15
16
|
|
16
17
|
options = {:command => 'backup'}
|
@@ -45,22 +46,32 @@ if KNOWN_COMMANDS.find{|c| c[:name] == options[:command] }.nil?
|
|
45
46
|
raise "Unknown command '#{options[:command]}'"
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
+
if options[:command] == 'help'
|
50
|
+
puts opts
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
begin
|
55
|
+
configuration = Imap::Backup::Configuration::List.new(options[:accounts])
|
56
|
+
rescue Imap::Backup::ConfigurationNotFound
|
57
|
+
Imap::Backup::Configuration::Setup.new(options).run
|
58
|
+
exit
|
59
|
+
end
|
49
60
|
|
50
61
|
case options[:command]
|
51
62
|
when 'backup'
|
52
|
-
|
63
|
+
configuration.each_connection do |connection|
|
53
64
|
connection.run_backup
|
54
65
|
end
|
55
|
-
when 'help'
|
56
|
-
puts opts
|
57
66
|
when 'folders'
|
58
|
-
|
67
|
+
configuration.each_connection do |connection|
|
59
68
|
puts connection.username
|
60
69
|
connection.folders.each { |f| puts "\t" + f.name }
|
61
70
|
end
|
71
|
+
when 'setup'
|
72
|
+
Imap::Backup::Configuration::Setup.new(options).run
|
62
73
|
when 'status'
|
63
|
-
|
74
|
+
configuration.each_connection do |connection|
|
64
75
|
puts connection.username
|
65
76
|
folders = connection.status
|
66
77
|
folders.each do |f|
|
data/imap-backup.gemspec
CHANGED
data/lib/imap/backup.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
1
|
require 'imap/backup/utils'
|
2
2
|
require 'imap/backup/account/connection'
|
3
3
|
require 'imap/backup/account/folder'
|
4
|
+
require 'imap/backup/configuration/account'
|
5
|
+
require 'imap/backup/configuration/asker'
|
6
|
+
require 'imap/backup/configuration/connection_tester'
|
7
|
+
require 'imap/backup/configuration/folder_chooser'
|
8
|
+
require 'imap/backup/configuration/list'
|
9
|
+
require 'imap/backup/configuration/setup'
|
10
|
+
require 'imap/backup/configuration/store'
|
4
11
|
require 'imap/backup/downloader'
|
5
12
|
require 'imap/backup/serializer/directory'
|
6
|
-
require 'imap/backup/settings'
|
7
13
|
require 'imap/backup/version'
|
8
14
|
|
15
|
+
module Imap
|
16
|
+
module Backup
|
17
|
+
class ConfigurationNotFound < StandardError; end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Configuration
|
6
|
+
class Account
|
7
|
+
|
8
|
+
def initialize(store, account)
|
9
|
+
@store, @account = store, account
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
loop do
|
14
|
+
Setup.highline.choose do |menu|
|
15
|
+
password =
|
16
|
+
if @account[:password] == ''
|
17
|
+
'(unset)'
|
18
|
+
else
|
19
|
+
@account[:password].gsub(/./, 'x')
|
20
|
+
end
|
21
|
+
menu.header = <<EOT
|
22
|
+
Account:
|
23
|
+
email: #{@account[:username]}
|
24
|
+
path: #{@account[:local_path]}
|
25
|
+
folders: #{@account[:folders].map { |f| f[:name] }.join(', ')}
|
26
|
+
password: #{password}
|
27
|
+
EOT
|
28
|
+
menu.choice('modify email') do
|
29
|
+
username = Asker.email(username)
|
30
|
+
others = @store.data[:accounts].select { |a| a != @account}.map { |a| a[:username] }
|
31
|
+
if others.include?(username)
|
32
|
+
puts 'There is already an account set up with that email address'
|
33
|
+
else
|
34
|
+
@account[:username] = username
|
35
|
+
end
|
36
|
+
end
|
37
|
+
menu.choice('modify password') do
|
38
|
+
password = Asker.password
|
39
|
+
if ! password.nil?
|
40
|
+
@account[:password] = password
|
41
|
+
end
|
42
|
+
end
|
43
|
+
menu.choice('modify backup path') do
|
44
|
+
validator = lambda do |p|
|
45
|
+
same = @store.data[:accounts].find do |a|
|
46
|
+
a[:username] != @account[:username] && a[:local_path] == p
|
47
|
+
end
|
48
|
+
if same
|
49
|
+
puts "The path '#{p}' is used to backup the account '#{same[:username]}'"
|
50
|
+
false
|
51
|
+
else
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@account[:local_path] = Asker.backup_path(@account[:local_path], validator)
|
56
|
+
end
|
57
|
+
menu.choice('choose backup folders') do
|
58
|
+
FolderChooser.new(@account).run
|
59
|
+
end
|
60
|
+
menu.choice 'test authentication' do
|
61
|
+
result = ConnectionTester.test(@account)
|
62
|
+
puts result
|
63
|
+
Setup.highline.ask 'Press a key '
|
64
|
+
end
|
65
|
+
menu.choice(:delete) do
|
66
|
+
if Setup.highline.agree("Are you sure? (y/n) ")
|
67
|
+
@store.data[:accounts].reject! { |a| a[:username] == @account[:username] }
|
68
|
+
return
|
69
|
+
end
|
70
|
+
end
|
71
|
+
menu.choice('return to main menu') do
|
72
|
+
return
|
73
|
+
end
|
74
|
+
menu.hidden('quit') do
|
75
|
+
return
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Configuration
|
6
|
+
module Asker
|
7
|
+
|
8
|
+
EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i
|
9
|
+
|
10
|
+
def self.email(default = '')
|
11
|
+
Setup.highline.ask('email address: ') do |q|
|
12
|
+
q.default = default
|
13
|
+
q.readline = true
|
14
|
+
q.validate = EMAIL_MATCHER
|
15
|
+
q.responses[:not_valid] = 'Enter a valid email address '
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.password
|
20
|
+
password = Setup.highline.ask('password: ') { |q| q.echo = false }
|
21
|
+
confirmation = Setup.highline.ask('repeat password: ') { |q| q.echo = false }
|
22
|
+
if password != confirmation
|
23
|
+
return nil unless Setup.highline.agree("the password and confirmation did not match.\nContinue? (y/n) ")
|
24
|
+
return self.password
|
25
|
+
end
|
26
|
+
password
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.backup_path(default, validator)
|
30
|
+
Setup.highline.ask('backup directory: ') do |q|
|
31
|
+
q.default = default
|
32
|
+
q.readline = true
|
33
|
+
q.validate = validator
|
34
|
+
q.responses[:not_valid] = 'Choose a different directory '
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Configuration
|
6
|
+
module ConnectionTester
|
7
|
+
def self.test(account)
|
8
|
+
Imap::Backup::Account::Connection.new(account)
|
9
|
+
return 'Connection successful'
|
10
|
+
rescue Net::IMAP::NoResponseError
|
11
|
+
return 'No response'
|
12
|
+
rescue Exception => e
|
13
|
+
return "Unexpected error: #{e}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Configuration
|
6
|
+
class FolderChooser
|
7
|
+
|
8
|
+
def initialize(account)
|
9
|
+
@account = account
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
begin
|
14
|
+
@connection = Imap::Backup::Account::Connection.new(@account)
|
15
|
+
rescue => e
|
16
|
+
puts 'Connection failed'
|
17
|
+
Setup.highline.ask 'Press a key '
|
18
|
+
return
|
19
|
+
end
|
20
|
+
@folders = @connection.folders
|
21
|
+
loop do
|
22
|
+
Setup.highline.choose do |menu|
|
23
|
+
menu.header = 'Add/remove folders'
|
24
|
+
menu.index = :number
|
25
|
+
@folders.each do |folder|
|
26
|
+
name = folder.name
|
27
|
+
found = @account[:folders].find { |f| f[:name] == name }
|
28
|
+
mark =
|
29
|
+
if found
|
30
|
+
'+'
|
31
|
+
else
|
32
|
+
'-'
|
33
|
+
end
|
34
|
+
menu.choice("#{mark} #{name}") do
|
35
|
+
if found
|
36
|
+
@account[:folders].reject! { |f| f[:name] == name }
|
37
|
+
else
|
38
|
+
@account[:folders] << { :name => name }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
menu.choice('return to the account menu') do
|
43
|
+
return
|
44
|
+
end
|
45
|
+
menu.hidden('quit') do
|
46
|
+
return
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Configuration
|
6
|
+
class List
|
7
|
+
|
8
|
+
attr_reader :accounts
|
9
|
+
|
10
|
+
def initialize(accounts = nil)
|
11
|
+
@config = Imap::Backup::Configuration::Store.new
|
12
|
+
|
13
|
+
if accounts.nil?
|
14
|
+
@accounts = @config.data[:accounts]
|
15
|
+
else
|
16
|
+
@accounts = @config.data[:accounts].select{ |account| accounts.include?(account[:username]) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_connection
|
21
|
+
@accounts.each do |account|
|
22
|
+
connection = Imap::Backup::Account::Connection.new(account)
|
23
|
+
yield connection
|
24
|
+
connection.disconnect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
3
|
+
require 'highline'
|
4
|
+
|
5
|
+
module Imap
|
6
|
+
module Backup
|
7
|
+
module Configuration
|
8
|
+
class Setup
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :highline
|
12
|
+
end
|
13
|
+
self.highline = HighLine.new
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@config = Imap::Backup::Configuration::Store.new(false)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
loop do
|
21
|
+
self.class.highline.choose do |menu|
|
22
|
+
menu.header = 'Choose an action'
|
23
|
+
@config.data[:accounts].each do |account|
|
24
|
+
menu.choice("#{account[:username]}") do
|
25
|
+
Account.new(@config, account[:username]).run
|
26
|
+
end
|
27
|
+
end
|
28
|
+
menu.choice('add account') do
|
29
|
+
username = Asker.email
|
30
|
+
edit_account username
|
31
|
+
end
|
32
|
+
menu.choice('save and exit') do
|
33
|
+
@config.save
|
34
|
+
return
|
35
|
+
end
|
36
|
+
menu.choice(:quit) do
|
37
|
+
return
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def add_account(username)
|
46
|
+
account = {
|
47
|
+
:username => username,
|
48
|
+
:password => '',
|
49
|
+
:local_path => File.join(@config.path, username.gsub('@', '_')),
|
50
|
+
:folders => []
|
51
|
+
}
|
52
|
+
@config.data[:accounts] << account
|
53
|
+
account
|
54
|
+
end
|
55
|
+
|
56
|
+
def edit_account(username)
|
57
|
+
account = @config.data[:accounts].find { |a| a[:username] == username }
|
58
|
+
if account.nil?
|
59
|
+
account = add_account(username)
|
60
|
+
end
|
61
|
+
Account.new @config, account[:username]
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rubygems' if RUBY_VERSION < '1.9'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Imap
|
6
|
+
module Backup
|
7
|
+
module Configuration
|
8
|
+
class Store
|
9
|
+
|
10
|
+
CONFIGURATION_DIRECTORY = File.expand_path('~/.imap-backup')
|
11
|
+
|
12
|
+
attr_reader :data
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def initialize(fail_if_missing = true)
|
16
|
+
@path = CONFIGURATION_DIRECTORY
|
17
|
+
@pathname = File.join(@path, 'config.json')
|
18
|
+
if File.directory?(@path)
|
19
|
+
Imap::Backup::Utils.check_permissions @path, 0700
|
20
|
+
end
|
21
|
+
if File.exist?(@pathname)
|
22
|
+
Imap::Backup::Utils.check_permissions @pathname, 0600
|
23
|
+
@data = JSON.parse(File.read(@pathname), :symbolize_names => true)
|
24
|
+
else
|
25
|
+
if fail_if_missing
|
26
|
+
raise ConfigurationNotFound.new("Configuration file '#{@pathname}' not found")
|
27
|
+
end
|
28
|
+
@data = {:accounts => []}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
mkdir_private @path
|
34
|
+
File.open(@pathname, 'w') { |f| f.write(JSON.pretty_generate(@data)) }
|
35
|
+
FileUtils.chmod 0600, @pathname
|
36
|
+
@data[:accounts].each do |account|
|
37
|
+
mkdir_private account[:local_path]
|
38
|
+
account[:folders].each do |f|
|
39
|
+
parts = f[:name].split('/')
|
40
|
+
path = account[:local_path].clone
|
41
|
+
parts.each do |part|
|
42
|
+
path = File.join(path, part)
|
43
|
+
mkdir_private path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def mkdir_private(path)
|
52
|
+
if ! File.directory?(path)
|
53
|
+
FileUtils.mkdir path
|
54
|
+
end
|
55
|
+
if Imap::Backup::Utils::stat(path) != 0700
|
56
|
+
FileUtils.chmod 0700, path
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|