imap-backup 1.0.5 → 1.0.6
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.
- 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
|
-
|