imap-backup 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|