imap-backup 0.0.5 → 1.0.0
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/README.md +43 -6
- data/bin/imap-backup +0 -1
- data/imap-backup.gemspec +1 -1
- data/lib/email/mboxrd/message.rb +37 -0
- data/lib/imap/backup.rb +2 -0
- data/lib/imap/backup/account/connection.rb +2 -4
- data/lib/imap/backup/account/folder.rb +0 -2
- data/lib/imap/backup/configuration/account.rb +0 -2
- data/lib/imap/backup/configuration/asker.rb +0 -2
- data/lib/imap/backup/configuration/folder_chooser.rb +0 -2
- data/lib/imap/backup/configuration/list.rb +17 -16
- data/lib/imap/backup/configuration/setup.rb +1 -3
- data/lib/imap/backup/configuration/store.rb +19 -13
- data/lib/imap/backup/downloader.rb +0 -2
- data/lib/imap/backup/serializer/base.rb +17 -0
- data/lib/imap/backup/serializer/directory.rb +3 -7
- data/lib/imap/backup/serializer/mbox.rb +96 -0
- data/lib/imap/backup/utils.rb +6 -3
- data/lib/imap/backup/version.rb +2 -2
- data/spec/unit/account/connection_spec.rb +12 -13
- data/spec/unit/account/folder_spec.rb +1 -9
- data/spec/unit/configuration/account_spec.rb +1 -16
- data/spec/unit/configuration/asker_spec.rb +1 -9
- data/spec/unit/configuration/connection_tester_spec.rb +1 -5
- data/spec/unit/configuration/folder_chooser_spec.rb +1 -7
- data/spec/unit/configuration/list_spec.rb +24 -20
- data/spec/unit/configuration/setup_spec.rb +3 -9
- data/spec/unit/configuration/store_spec.rb +11 -20
- data/spec/unit/downloader_spec.rb +1 -11
- data/spec/unit/email/mboxrd/message_spec.rb +51 -0
- data/spec/unit/serializer/base_spec.rb +19 -0
- data/spec/unit/serializer/directory_spec.rb +51 -74
- data/spec/unit/serializer/mbox_spec.rb +113 -0
- data/spec/unit/utils_spec.rb +3 -9
- metadata +29 -4
data/README.md
CHANGED
@@ -14,19 +14,23 @@
|
|
14
14
|
|
15
15
|
# Installation
|
16
16
|
|
17
|
-
|
17
|
+
```shell
|
18
|
+
$ gem install 'imap-backup'
|
19
|
+
```
|
18
20
|
|
19
21
|
# Setup
|
20
22
|
|
21
23
|
Run:
|
22
24
|
|
23
|
-
|
25
|
+
```shell
|
26
|
+
$ imap-backup setup
|
27
|
+
```
|
24
28
|
|
25
29
|
The setup system is a menu-driven command line application.
|
26
30
|
|
27
31
|
It creates ~/.imap-backup directory and configuration file. E.g.:
|
28
32
|
|
29
|
-
```
|
33
|
+
```json
|
30
34
|
{
|
31
35
|
"accounts":
|
32
36
|
[
|
@@ -44,6 +48,27 @@ It creates ~/.imap-backup directory and configuration file. E.g.:
|
|
44
48
|
}
|
45
49
|
```
|
46
50
|
|
51
|
+
It connects to GMail by default, but you can also specify a server:
|
52
|
+
|
53
|
+
```json
|
54
|
+
{
|
55
|
+
"accounts":
|
56
|
+
[
|
57
|
+
{
|
58
|
+
"username": "my.user@gmail.com",
|
59
|
+
"password": "secret",
|
60
|
+
"server": "my.imap.example.com",
|
61
|
+
"local_path": "/path/to/backup/root",
|
62
|
+
"folders":
|
63
|
+
[
|
64
|
+
{"name": "[Gmail]/All Mail"},
|
65
|
+
{"name": "my_folder"}
|
66
|
+
]
|
67
|
+
}
|
68
|
+
]
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
47
72
|
# Security
|
48
73
|
|
49
74
|
Note that email usernames and passwords are held in plain text
|
@@ -57,19 +82,31 @@ by your user.
|
|
57
82
|
|
58
83
|
Manually, from the command line:
|
59
84
|
|
60
|
-
|
85
|
+
```shell
|
86
|
+
$ imap-backup
|
87
|
+
```
|
61
88
|
|
62
89
|
Altertatively, add it to your crontab.
|
63
90
|
|
91
|
+
# Result
|
92
|
+
|
93
|
+
Each folder is saved to an mbox file.
|
94
|
+
Alongside each mbox is a fine *.imap which lists the source IMAP UIDs to allow
|
95
|
+
a full restore.
|
96
|
+
|
64
97
|
# Other Usage
|
65
98
|
|
66
99
|
List IMAP folders:
|
67
100
|
|
68
|
-
|
101
|
+
```shell
|
102
|
+
$ imap-backup folders
|
103
|
+
```
|
69
104
|
|
70
105
|
Get statistics of emails to download per folder:
|
71
106
|
|
72
|
-
|
107
|
+
```shell
|
108
|
+
$ imap-backup status
|
109
|
+
```
|
73
110
|
|
74
111
|
# Design Goals
|
75
112
|
|
data/bin/imap-backup
CHANGED
data/imap-backup.gemspec
CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |gem|
|
|
18
18
|
|
19
19
|
gem.add_runtime_dependency 'rake'
|
20
20
|
gem.add_runtime_dependency 'highline'
|
21
|
+
gem.add_runtime_dependency 'mail'
|
21
22
|
if RUBY_VERSION < '1.9'
|
22
23
|
gem.add_runtime_dependency 'json'
|
23
24
|
end
|
@@ -30,6 +31,5 @@ Gem::Specification.new do |gem|
|
|
30
31
|
else
|
31
32
|
gem.add_development_dependency 'simplecov'
|
32
33
|
end
|
33
|
-
|
34
34
|
end
|
35
35
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module Email
|
4
|
+
module Mboxrd
|
5
|
+
class Message
|
6
|
+
def initialize(body)
|
7
|
+
@body = body.clone
|
8
|
+
@body.force_encoding('binary') if RUBY_VERSION >= '1.9.0'
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
'From ' + from + "\n" + body + "\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def parsed
|
18
|
+
@parsed ||= Mail.new(@body)
|
19
|
+
end
|
20
|
+
|
21
|
+
def from
|
22
|
+
parsed.from[0] + ' ' + asctime
|
23
|
+
end
|
24
|
+
|
25
|
+
def body
|
26
|
+
mbox = @body.gsub(/\n(>*From)/, "\n>\\1")
|
27
|
+
mbox + "\n" unless mbox.end_with?("\n")
|
28
|
+
mbox
|
29
|
+
end
|
30
|
+
|
31
|
+
def asctime
|
32
|
+
parsed.date.asctime
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
data/lib/imap/backup.rb
CHANGED
@@ -9,7 +9,9 @@ require 'imap/backup/configuration/list'
|
|
9
9
|
require 'imap/backup/configuration/setup'
|
10
10
|
require 'imap/backup/configuration/store'
|
11
11
|
require 'imap/backup/downloader'
|
12
|
+
require 'imap/backup/serializer/base'
|
12
13
|
require 'imap/backup/serializer/directory'
|
14
|
+
require 'imap/backup/serializer/mbox'
|
13
15
|
require 'imap/backup/version'
|
14
16
|
|
15
17
|
module Imap
|
@@ -4,14 +4,13 @@ module Imap
|
|
4
4
|
module Backup
|
5
5
|
module Account
|
6
6
|
class Connection
|
7
|
-
|
8
7
|
attr_reader :username
|
9
8
|
attr_reader :imap
|
10
9
|
|
11
10
|
def initialize(options)
|
12
11
|
@username = options[:username]
|
13
12
|
@local_path, @backup_folders = options[:local_path], options[:folders]
|
14
|
-
@imap = Net::IMAP.new('imap.gmail.com', 993, true)
|
13
|
+
@imap = Net::IMAP.new(options[:server] || 'imap.gmail.com', 993, true)
|
15
14
|
@imap.login(@username, options[:password])
|
16
15
|
end
|
17
16
|
|
@@ -34,12 +33,11 @@ module Imap
|
|
34
33
|
def run_backup
|
35
34
|
@backup_folders.each do |folder|
|
36
35
|
f = Imap::Backup::Account::Folder.new(self, folder[:name])
|
37
|
-
s = Imap::Backup::Serializer::
|
36
|
+
s = Imap::Backup::Serializer::Mbox.new(@local_path, folder[:name])
|
38
37
|
d = Imap::Backup::Downloader.new(f, s)
|
39
38
|
d.run
|
40
39
|
end
|
41
40
|
end
|
42
|
-
|
43
41
|
end
|
44
42
|
end
|
45
43
|
end
|
@@ -4,7 +4,6 @@ module Imap
|
|
4
4
|
module Backup
|
5
5
|
module Account
|
6
6
|
class Folder
|
7
|
-
|
8
7
|
REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
|
9
8
|
|
10
9
|
def initialize(connection, folder)
|
@@ -22,7 +21,6 @@ module Imap
|
|
22
21
|
message['RFC822'].force_encoding('utf-8') if RUBY_VERSION > '1.9'
|
23
22
|
message
|
24
23
|
end
|
25
|
-
|
26
24
|
end
|
27
25
|
end
|
28
26
|
end
|
@@ -4,7 +4,6 @@ module Imap
|
|
4
4
|
module Backup
|
5
5
|
module Configuration
|
6
6
|
module Asker
|
7
|
-
|
8
7
|
EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i
|
9
8
|
|
10
9
|
def self.email(default = '')
|
@@ -34,7 +33,6 @@ module Imap
|
|
34
33
|
q.responses[:not_valid] = 'Choose a different directory '
|
35
34
|
end
|
36
35
|
end
|
37
|
-
|
38
36
|
end
|
39
37
|
end
|
40
38
|
end
|
@@ -4,28 +4,29 @@ module Imap
|
|
4
4
|
module Backup
|
5
5
|
module Configuration
|
6
6
|
class List
|
7
|
+
attr_reader :accounts
|
7
8
|
|
8
|
-
|
9
|
+
def initialize(accounts = nil)
|
10
|
+
if not Imap::Backup::Configuration::Store.exist?
|
11
|
+
raise ConfigurationNotFound.new("Configuration file '#{Imap::Backup::Configuration::Store.default_pathname}' not found")
|
12
|
+
end
|
13
|
+
@config = Imap::Backup::Configuration::Store.new
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
else
|
16
|
-
@accounts = @config.data[:accounts].select{ |account| accounts.include?(account[:username]) }
|
15
|
+
if accounts.nil?
|
16
|
+
@accounts = @config.data[:accounts]
|
17
|
+
else
|
18
|
+
@accounts = @config.data[:accounts].select{ |account| accounts.include?(account[:username]) }
|
19
|
+
end
|
17
20
|
end
|
18
|
-
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def each_connection
|
23
|
+
@accounts.each do |account|
|
24
|
+
connection = Imap::Backup::Account::Connection.new(account)
|
25
|
+
yield connection
|
26
|
+
connection.disconnect
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
27
|
-
|
28
|
-
end
|
29
30
|
end
|
30
31
|
end
|
31
32
|
end
|
@@ -6,14 +6,13 @@ module Imap
|
|
6
6
|
module Backup
|
7
7
|
module Configuration
|
8
8
|
class Setup
|
9
|
-
|
10
9
|
class << self
|
11
10
|
attr_accessor :highline
|
12
11
|
end
|
13
12
|
self.highline = HighLine.new
|
14
13
|
|
15
14
|
def initialize
|
16
|
-
@config = Imap::Backup::Configuration::Store.new
|
15
|
+
@config = Imap::Backup::Configuration::Store.new
|
17
16
|
end
|
18
17
|
|
19
18
|
def run
|
@@ -60,7 +59,6 @@ module Imap
|
|
60
59
|
end
|
61
60
|
Account.new(@config, account).run
|
62
61
|
end
|
63
|
-
|
64
62
|
end
|
65
63
|
end
|
66
64
|
end
|
@@ -6,41 +6,44 @@ module Imap
|
|
6
6
|
module Backup
|
7
7
|
module Configuration
|
8
8
|
class Store
|
9
|
-
|
10
9
|
CONFIGURATION_DIRECTORY = File.expand_path('~/.imap-backup')
|
11
10
|
|
12
11
|
attr_reader :data
|
13
12
|
attr_reader :path
|
14
13
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
def self.default_pathname
|
15
|
+
File.join(CONFIGURATION_DIRECTORY, 'config.json')
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.exist?(pathname = default_pathname)
|
19
|
+
File.exist?(pathname)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(pathname = self.class.default_pathname)
|
23
|
+
@pathname = pathname
|
24
|
+
if File.directory?(path)
|
25
|
+
Imap::Backup::Utils.check_permissions path, 0700
|
20
26
|
end
|
21
27
|
if File.exist?(@pathname)
|
22
28
|
Imap::Backup::Utils.check_permissions @pathname, 0600
|
23
29
|
@data = JSON.parse(File.read(@pathname), :symbolize_names => true)
|
24
30
|
else
|
25
|
-
if fail_if_missing
|
26
|
-
raise ConfigurationNotFound.new("Configuration file '#{@pathname}' not found")
|
27
|
-
end
|
28
31
|
@data = {:accounts => []}
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
32
35
|
def save
|
33
|
-
mkdir_private
|
36
|
+
mkdir_private path
|
34
37
|
File.open(@pathname, 'w') { |f| f.write(JSON.pretty_generate(@data)) }
|
35
38
|
FileUtils.chmod 0600, @pathname
|
36
39
|
@data[:accounts].each do |account|
|
37
40
|
mkdir_private account[:local_path]
|
38
41
|
account[:folders].each do |f|
|
39
42
|
parts = f[:name].split('/')
|
40
|
-
|
43
|
+
p = account[:local_path].clone
|
41
44
|
parts.each do |part|
|
42
|
-
|
43
|
-
mkdir_private
|
45
|
+
p = File.join(p, part)
|
46
|
+
mkdir_private p
|
44
47
|
end
|
45
48
|
end
|
46
49
|
end
|
@@ -57,6 +60,9 @@ module Imap
|
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
63
|
+
def path
|
64
|
+
File.dirname(@pathname)
|
65
|
+
end
|
60
66
|
end
|
61
67
|
end
|
62
68
|
end
|
@@ -5,7 +5,6 @@ require 'json'
|
|
5
5
|
module Imap
|
6
6
|
module Backup
|
7
7
|
class Downloader
|
8
|
-
|
9
8
|
def initialize(folder, serializer)
|
10
9
|
@folder, @serializer = folder, serializer
|
11
10
|
end
|
@@ -17,7 +16,6 @@ module Imap
|
|
17
16
|
@serializer.save(uid, message)
|
18
17
|
end
|
19
18
|
end
|
20
|
-
|
21
19
|
end
|
22
20
|
end
|
23
21
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Imap
|
4
|
+
module Backup
|
5
|
+
module Serializer
|
6
|
+
DIRECTORY_PERMISSIONS = 0700
|
7
|
+
FILE_PERMISSIONS = 0600
|
8
|
+
class Base
|
9
|
+
def initialize(path, folder)
|
10
|
+
@path, @folder = path, folder
|
11
|
+
Imap::Backup::Utils.check_permissions(@path, DIRECTORY_PERMISSIONS)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -4,13 +4,10 @@ require 'fileutils'
|
|
4
4
|
module Imap
|
5
5
|
module Backup
|
6
6
|
module Serializer
|
7
|
-
class Directory
|
8
|
-
|
7
|
+
class Directory < Base
|
9
8
|
def initialize(path, folder)
|
10
|
-
|
11
|
-
|
12
|
-
Imap::Backup::Utils.check_permissions(@path, permissions)
|
13
|
-
Imap::Backup::Utils.make_folder(@path, @folder, permissions)
|
9
|
+
super
|
10
|
+
Imap::Backup::Utils.make_folder(@path, @folder, DIRECTORY_PERMISSIONS)
|
14
11
|
end
|
15
12
|
|
16
13
|
def uids
|
@@ -43,7 +40,6 @@ module Imap
|
|
43
40
|
def filename(uid)
|
44
41
|
"#{directory}/%012u.json" % uid.to_i
|
45
42
|
end
|
46
|
-
|
47
43
|
end
|
48
44
|
end
|
49
45
|
end
|