imap-backup 1.0.5 → 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/.travis.yml +1 -1
- data/Gemfile +0 -1
- data/README.md +0 -1
- data/Rakefile +4 -9
- data/bin/imap-backup +0 -1
- data/imap-backup.gemspec +0 -1
- data/lib/email/mboxrd/message.rb +31 -32
- data/lib/imap/backup.rb +17 -4
- data/lib/imap/backup/account/connection.rb +70 -64
- data/lib/imap/backup/account/folder.rb +21 -26
- data/lib/imap/backup/configuration/account.rb +127 -73
- data/lib/imap/backup/configuration/asker.rb +38 -31
- data/lib/imap/backup/configuration/connection_tester.rb +9 -14
- data/lib/imap/backup/configuration/folder_chooser.rb +43 -48
- data/lib/imap/backup/configuration/list.rb +19 -24
- data/lib/imap/backup/configuration/setup.rb +56 -51
- data/lib/imap/backup/configuration/store.rb +47 -52
- data/lib/imap/backup/downloader.rb +11 -14
- data/lib/imap/backup/serializer/base.rb +8 -11
- data/lib/imap/backup/serializer/directory.rb +36 -41
- data/lib/imap/backup/serializer/mbox.rb +83 -88
- data/lib/imap/backup/utils.rb +0 -3
- data/lib/imap/backup/version.rb +1 -2
- data/spec/gather_rspec_coverage.rb +0 -1
- data/spec/spec_helper.rb +2 -7
- data/spec/unit/account/connection_spec.rb +0 -1
- data/spec/unit/account/folder_spec.rb +0 -1
- data/spec/unit/configuration/account_spec.rb +207 -136
- data/spec/unit/configuration/asker_spec.rb +59 -85
- data/spec/unit/configuration/connection_tester_spec.rb +36 -26
- data/spec/unit/configuration/folder_chooser_spec.rb +3 -6
- data/spec/unit/configuration/list_spec.rb +0 -1
- data/spec/unit/configuration/setup_spec.rb +8 -9
- data/spec/unit/configuration/store_spec.rb +1 -4
- data/spec/unit/downloader_spec.rb +0 -1
- data/spec/unit/email/mboxrd/message_spec.rb +0 -1
- data/spec/unit/serializer/base_spec.rb +0 -1
- data/spec/unit/serializer/directory_spec.rb +0 -1
- data/spec/unit/serializer/mbox_spec.rb +0 -1
- data/spec/unit/utils_spec.rb +0 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28c9159f1439c7b12a52c7149284c714218cef47
|
4
|
+
data.tar.gz: ff1ed3afecd0e1753734bc2bc8f6bd0ddd42d063
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd6d123a800c8e1cfea9897c3f7edd70b5be9e7ad36581ae9aeb90e129d52dbc058372880cb677db40637e5ba4d68015220f8d6ca0c45e7087b1e748568aaae7
|
7
|
+
data.tar.gz: b52ce2115eae0a0bc175fe2264eb9842a8b535f16d6a5237e9df4f250cedcc5e4b4fe26ded3e06a43f0f5b4636b839bcf91c983f217efaf2b05e4549bb122507
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -4,24 +4,19 @@ require 'rspec/core/rake_task'
|
|
4
4
|
|
5
5
|
task :default => :spec
|
6
6
|
|
7
|
-
RSpec::Core::RakeTask.new do |
|
7
|
+
RSpec::Core::RakeTask.new do |t|
|
8
8
|
t.pattern = 'spec/**/*_spec.rb'
|
9
9
|
end
|
10
10
|
|
11
11
|
if RUBY_VERSION < '1.9'
|
12
|
-
|
13
|
-
RSpec::Core::RakeTask.new( 'spec:coverage' ) do |t|
|
12
|
+
RSpec::Core::RakeTask.new('spec:coverage') do |t|
|
14
13
|
t.pattern = 'spec/**/*_spec.rb'
|
15
14
|
t.rcov = true
|
16
|
-
t.rcov_opts = [
|
15
|
+
t.rcov_opts = ['--exclude', 'spec/,/gems/,vendor/']
|
17
16
|
end
|
18
|
-
|
19
17
|
else
|
20
|
-
|
21
18
|
desc 'Run specs and create coverage output'
|
22
|
-
RSpec::Core::RakeTask.new(
|
19
|
+
RSpec::Core::RakeTask.new('spec:coverage') do |t|
|
23
20
|
t.pattern = ['spec/gather_rspec_coverage.rb', 'spec/**/*_spec.rb']
|
24
21
|
end
|
25
|
-
|
26
22
|
end
|
27
|
-
|
data/bin/imap-backup
CHANGED
data/imap-backup.gemspec
CHANGED
data/lib/email/mboxrd/message.rb
CHANGED
@@ -1,37 +1,36 @@
|
|
1
1
|
require 'mail'
|
2
2
|
|
3
|
-
module Email
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
3
|
+
module Email; end
|
4
|
+
|
5
|
+
module Email::Mboxrd
|
6
|
+
class Message
|
7
|
+
def initialize(body)
|
8
|
+
@body = body.clone
|
9
|
+
@body.force_encoding('binary') if RUBY_VERSION >= '1.9.0'
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
'From ' + from + "\n" + body + "\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parsed
|
19
|
+
@parsed ||= Mail.new(@body)
|
20
|
+
end
|
21
|
+
|
22
|
+
def from
|
23
|
+
parsed.from[0] + ' ' + asctime
|
24
|
+
end
|
25
|
+
|
26
|
+
def body
|
27
|
+
mbox = @body.gsub(/\n(>*From)/, "\n>\\1")
|
28
|
+
mbox + "\n" unless mbox.end_with?("\n")
|
29
|
+
mbox
|
30
|
+
end
|
31
|
+
|
32
|
+
def asctime
|
33
|
+
parsed.date.asctime
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
data/lib/imap/backup.rb
CHANGED
@@ -14,9 +14,22 @@ require 'imap/backup/serializer/directory'
|
|
14
14
|
require 'imap/backup/serializer/mbox'
|
15
15
|
require 'imap/backup/version'
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
require 'logger'
|
18
|
+
|
19
|
+
module Imap::Backup
|
20
|
+
class ConfigurationNotFound < StandardError; end
|
21
|
+
|
22
|
+
class Logger
|
23
|
+
include Singleton
|
24
|
+
|
25
|
+
attr_reader :logger
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@logger = ::Logger.new(STDOUT)
|
29
|
+
end
|
20
30
|
end
|
21
|
-
end
|
22
31
|
|
32
|
+
def self.logger
|
33
|
+
Imap::Backup::Logger.instance.logger
|
34
|
+
end
|
35
|
+
end
|
@@ -1,79 +1,85 @@
|
|
1
1
|
require 'net/imap'
|
2
2
|
|
3
|
-
module Imap
|
4
|
-
|
5
|
-
|
6
|
-
class Connection
|
7
|
-
attr_reader :username, :local_path, :backup_folders, :server
|
3
|
+
module Imap::Backup::Account
|
4
|
+
class Connection
|
5
|
+
attr_reader :username, :local_path, :backup_folders, :server
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
def initialize(options)
|
8
|
+
@username, @password = options[:username], options[:password]
|
9
|
+
@local_path, @backup_folders = options[:local_path], options[:folders]
|
10
|
+
@server = options[:server] || host_for(username)
|
11
|
+
end
|
12
|
+
|
13
|
+
def folders
|
14
|
+
root = root_for(username)
|
15
|
+
imap.list(root, '*')
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
backup_folders.map do |folder|
|
20
|
+
f = Imap::Backup::Account::Folder.new(self, folder[:name])
|
21
|
+
s = Imap::Backup::Serializer::Directory.new(local_path, folder[:name])
|
22
|
+
{:name => folder[:name], :local => s.uids, :remote => f.uids}
|
23
|
+
end
|
24
|
+
end
|
14
25
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
26
|
+
def run_backup
|
27
|
+
backup_folders.each do |folder|
|
28
|
+
f = Imap::Backup::Account::Folder.new(self, folder[:name])
|
29
|
+
s = Imap::Backup::Serializer::Mbox.new(local_path, folder[:name])
|
30
|
+
d = Imap::Backup::Downloader.new(f, s)
|
31
|
+
d.run
|
32
|
+
end
|
33
|
+
end
|
19
34
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
s = Imap::Backup::Serializer::Directory.new(local_path, folder[:name])
|
24
|
-
{:name => folder[:name], :local => s.uids, :remote => f.uids}
|
25
|
-
end
|
26
|
-
end
|
35
|
+
def disconnect
|
36
|
+
imap.disconnect
|
37
|
+
end
|
27
38
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
def imap
|
40
|
+
return @imap unless @imap.nil?
|
41
|
+
options = options_for(username)
|
42
|
+
Imap::Backup.logger.debug "Creating IMAP instance: #{server}, options: #{options.inspect}"
|
43
|
+
@imap = Net::IMAP.new(server, options)
|
44
|
+
Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
|
45
|
+
@imap.login(username, password)
|
46
|
+
@imap
|
47
|
+
end
|
36
48
|
|
37
|
-
|
38
|
-
imap.disconnect
|
39
|
-
end
|
49
|
+
private
|
40
50
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@imap = Net::IMAP.new(server, options)
|
45
|
-
@imap.login(username, @password)
|
46
|
-
@imap
|
47
|
-
end
|
51
|
+
def password
|
52
|
+
@password
|
53
|
+
end
|
48
54
|
|
49
|
-
|
55
|
+
def masked_password
|
56
|
+
password.gsub(/./, 'x')
|
57
|
+
end
|
50
58
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
+
def host_for(username)
|
60
|
+
case username
|
61
|
+
when /@gmail\.com/
|
62
|
+
'imap.gmail.com'
|
63
|
+
when /@fastmail\.fm/
|
64
|
+
'mail.messagingengine.com'
|
65
|
+
end
|
66
|
+
end
|
59
67
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
+
def root_for(username)
|
69
|
+
case username
|
70
|
+
when /@gmail\.com/
|
71
|
+
'/'
|
72
|
+
when /@fastmail\.fm/
|
73
|
+
'INBOX'
|
74
|
+
end
|
75
|
+
end
|
68
76
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
76
|
-
end
|
77
|
+
def options_for(username)
|
78
|
+
case username
|
79
|
+
when /@gmail\.com/
|
80
|
+
{:port => 993, :ssl => true}
|
81
|
+
when /@fastmail\.fm/
|
82
|
+
{:port => 993, :ssl => true}
|
77
83
|
end
|
78
84
|
end
|
79
85
|
end
|
@@ -1,34 +1,29 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
module Imap
|
4
|
-
|
5
|
-
|
6
|
-
class Folder
|
7
|
-
REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
|
3
|
+
module Imap::Backup::Account
|
4
|
+
class Folder
|
5
|
+
REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
def initialize(connection, folder)
|
8
|
+
@connection, @folder = connection, folder
|
9
|
+
end
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
def uids
|
12
|
+
@connection.imap.examine(@folder)
|
13
|
+
@connection.imap.uid_search(['ALL']).sort
|
14
|
+
rescue Net::IMAP::NoResponseError => e
|
15
|
+
warn "Folder '#{@folder}' does not exist"
|
16
|
+
[]
|
17
|
+
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
19
|
+
def fetch(uid)
|
20
|
+
@connection.imap.examine(@folder)
|
21
|
+
message = @connection.imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)[0][1]
|
22
|
+
message['RFC822'].force_encoding('utf-8') if RUBY_VERSION > '1.9'
|
23
|
+
message
|
24
|
+
rescue Net::IMAP::NoResponseError => e
|
25
|
+
warn "Folder '#{@folder}' does not exist"
|
26
|
+
nil
|
31
27
|
end
|
32
28
|
end
|
33
29
|
end
|
34
|
-
|
@@ -1,84 +1,138 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
module Imap
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Imap::Backup::Configuration
|
4
|
+
class Account < Struct.new(:store, :account, :highline)
|
5
|
+
def run
|
6
|
+
catch :done do
|
7
|
+
loop do
|
8
|
+
system('clear')
|
9
|
+
create_menu
|
9
10
|
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def create_menu
|
17
|
+
highline.choose do |menu|
|
18
|
+
header menu
|
19
|
+
modify_email menu
|
20
|
+
modify_password menu
|
21
|
+
modify_server menu
|
22
|
+
modify_backup_path menu
|
23
|
+
choose_folders menu
|
24
|
+
test_connection menu
|
25
|
+
delete_account menu
|
26
|
+
menu.choice('return to main menu') { throw :done }
|
27
|
+
menu.hidden('quit') { throw :done }
|
28
|
+
end
|
29
|
+
end
|
10
30
|
|
11
|
-
|
12
|
-
|
13
|
-
system('clear')
|
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
|
31
|
+
def header(menu)
|
32
|
+
menu.header = <<-EOT
|
22
33
|
Account:
|
23
|
-
email: #{
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
34
|
+
email: #{account[:username]}
|
35
|
+
server: #{account[:server]}
|
36
|
+
path: #{account[:local_path]}
|
37
|
+
folders: #{folders.map { |f| f[:name] }.join(', ')}
|
38
|
+
password: #{masked_password}
|
39
|
+
EOT
|
40
|
+
end
|
41
|
+
|
42
|
+
def modify_email(menu)
|
43
|
+
menu.choice('modify email') do
|
44
|
+
username = Asker.email(username)
|
45
|
+
puts "username: #{username}"
|
46
|
+
others = store.data[:accounts].select { |a| a != account}.map { |a| a[:username] }
|
47
|
+
puts "others: #{others.inspect}"
|
48
|
+
if others.include?(username)
|
49
|
+
puts 'There is already an account set up with that email address'
|
50
|
+
else
|
51
|
+
account[:username] = username
|
52
|
+
if account[:server].nil? or account[:server] == ''
|
53
|
+
account[:server] = default_server(username)
|
78
54
|
end
|
79
55
|
end
|
80
56
|
end
|
81
57
|
end
|
58
|
+
|
59
|
+
def modify_password(menu)
|
60
|
+
menu.choice('modify password') do
|
61
|
+
password = Asker.password
|
62
|
+
if ! password.nil?
|
63
|
+
account[:password] = password
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def modify_server(menu)
|
69
|
+
menu.choice('modify server') do
|
70
|
+
server = highline.ask('server: ')
|
71
|
+
if ! server.nil?
|
72
|
+
account[:server] = server
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def modify_backup_path(menu)
|
78
|
+
menu.choice('modify backup path') do
|
79
|
+
validator = lambda do |p|
|
80
|
+
same = store.data[:accounts].find do |a|
|
81
|
+
a[:username] != account[:username] && a[:local_path] == p
|
82
|
+
end
|
83
|
+
if same
|
84
|
+
puts "The path '#{p}' is used to backup the account '#{same[:username]}'"
|
85
|
+
false
|
86
|
+
else
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
account[:local_path] = Asker.backup_path(account[:local_path], validator)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def choose_folders(menu)
|
95
|
+
menu.choice('choose backup folders') do
|
96
|
+
FolderChooser.new(account).run
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_connection(menu)
|
101
|
+
menu.choice('test connection') do
|
102
|
+
result = ConnectionTester.test(account)
|
103
|
+
puts result
|
104
|
+
highline.ask 'Press a key '
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def delete_account(menu)
|
109
|
+
menu.choice('delete') do
|
110
|
+
if highline.agree("Are you sure? (y/n) ")
|
111
|
+
store.data[:accounts].reject! { |a| a[:username] == account[:username] }
|
112
|
+
throw :done
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def folders
|
118
|
+
account[:folders] || []
|
119
|
+
end
|
120
|
+
|
121
|
+
def masked_password
|
122
|
+
if account[:password] == '' or account[:password].nil?
|
123
|
+
'(unset)'
|
124
|
+
else
|
125
|
+
account[:password].gsub(/./, 'x')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def default_server(username)
|
130
|
+
case username
|
131
|
+
when /@gmail\.com/
|
132
|
+
'imap.gmail.com'
|
133
|
+
when /@fastmail\.fm/
|
134
|
+
'mail.messagingengine.com'
|
135
|
+
end
|
136
|
+
end
|
82
137
|
end
|
83
138
|
end
|
84
|
-
|