imap-backup 7.0.0.rc1 → 7.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -21
- data/bin/imap-backup +0 -2
- data/docs/development.md +1 -1
- data/lib/email/mboxrd/message.rb +4 -3
- data/lib/imap/backup/account/connection.rb +16 -2
- data/lib/imap/backup/account/folder.rb +27 -12
- data/lib/imap/backup/account.rb +10 -1
- data/lib/imap/backup/cli/backup.rb +5 -1
- data/lib/imap/backup/cli/helpers.rb +22 -0
- data/lib/imap/backup/cli/local.rb +7 -7
- data/lib/imap/backup/cli/mirror.rb +118 -0
- data/lib/imap/backup/cli/utils.rb +6 -0
- data/lib/imap/backup/cli.rb +59 -0
- data/lib/imap/backup/configuration.rb +1 -17
- data/lib/imap/backup/flag_refresher.rb +32 -0
- data/lib/imap/backup/local_only_message_deleter.rb +23 -0
- data/lib/imap/backup/logger.rb +10 -5
- data/lib/imap/backup/migrator.rb +4 -4
- data/lib/imap/backup/mirror/map.rb +85 -0
- data/lib/imap/backup/mirror.rb +101 -0
- data/lib/imap/backup/serializer/appender.rb +4 -3
- data/lib/imap/backup/serializer/imap.rb +27 -32
- data/lib/imap/backup/serializer/message.rb +54 -0
- data/lib/imap/backup/serializer/message_enumerator.rb +2 -10
- data/lib/imap/backup/serializer/version2_migrator.rb +38 -15
- data/lib/imap/backup/serializer.rb +41 -27
- data/lib/imap/backup/setup/account/header.rb +9 -1
- data/lib/imap/backup/setup/account.rb +18 -1
- data/lib/imap/backup/setup.rb +0 -10
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +2 -4
- data/lib/imap/backup/uploader.rb +7 -7
- data/lib/imap/backup/version.rb +2 -2
- metadata +14 -9
- data/lib/imap/backup/serializer/mbox_enumerator.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb709e6431c47afc24c55d39ef7d9213fc0c27b42929c0692843a254cfd83c1d
|
4
|
+
data.tar.gz: 702d93af210afa826e64c0b5b1a4d1e16780c6078201e4a8db13dd4703ac62d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04a0c173fea0dd80e87443db040383402eb9eacbc7f557b4db8339cfd16c20153b068da8f4ade84d82643e3e9d3b9a76f04d654cbbc931620ef197546b24492a
|
7
|
+
data.tar.gz: e3cb1c12f73c421a66e41016b826ecdf3d199fd5920f9e5cbb04e0ef41619984791c8ce436283a91e6264707b04db7ecef94703f5df9af4c64cbcbb75c0fd7d7
|
data/README.md
CHANGED
@@ -11,21 +11,15 @@ The backups can then be restored, used to migrate to another service,
|
|
11
11
|
inspected or exported.
|
12
12
|
|
13
13
|
* [Source Code]
|
14
|
-
* [Documentation]
|
14
|
+
* [Code Documentation]
|
15
15
|
* [Rubygem]
|
16
16
|
* [CI Status]
|
17
17
|
|
18
18
|
[Source Code]: https://github.com/joeyates/imap-backup "Source code at GitHub"
|
19
|
-
[Documentation]: https://rubydoc.info/gems/imap-backup/frames "
|
19
|
+
[Code Documentation]: https://rubydoc.info/gems/imap-backup/frames "Code Documentation at Rubydoc.info"
|
20
20
|
[Rubygem]: https://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
|
21
21
|
[CI Status]: https://github.com/joeyates/imap-backup/actions/workflows/main.yml
|
22
22
|
|
23
|
-
# Backup Emails
|
24
|
-
|
25
|
-
imap-backup downloads emails and stores them on disk.
|
26
|
-
|
27
|
-
The backup is incremental and interruptable, so backups won't get messed if your connection goes down during an operation.
|
28
|
-
|
29
23
|
# Installation
|
30
24
|
|
31
25
|
## Homebrew (macOS)
|
@@ -63,7 +57,7 @@ imap-backup setup
|
|
63
57
|
|
64
58
|
To use imap-backup with GMail, you will need to enable 'App passwords' on your account.
|
65
59
|
|
66
|
-
#
|
60
|
+
# Backup
|
67
61
|
|
68
62
|
Manually, from the command line:
|
69
63
|
|
@@ -73,14 +67,22 @@ imap-backup
|
|
73
67
|
|
74
68
|
Alternatively, add it to your crontab.
|
75
69
|
|
76
|
-
Emails are stored on disk in [Mbox files](./docs/files/mboxrd.md) for each folder, with metadata
|
77
|
-
stored in [Imap files](./docs/files/imap.md).
|
78
|
-
|
79
70
|
# Commands
|
80
71
|
|
81
|
-
* [
|
82
|
-
* [
|
83
|
-
* [
|
72
|
+
* [backup](docs/commands/backup.md)
|
73
|
+
* [folders](docs/commands/folders.md)
|
74
|
+
* [local accounts](docs/commands/local-accounts.md)
|
75
|
+
* [local folders](docs/commands/local-folders.md)
|
76
|
+
* [local list](docs/commands/local-list.md)
|
77
|
+
* [local show](docs/commands/local-show.md)
|
78
|
+
* [migrate](docs/commands/migrate.md)
|
79
|
+
* [mirror](docs/commands/mirror.md)
|
80
|
+
* [remote folders](docs/commands/remote-folders.md)
|
81
|
+
* [restore](docs/commands/restore.md)
|
82
|
+
* [setup](docs/commands/setup.md)
|
83
|
+
* [status](docs/commands/status.md)
|
84
|
+
* [utils export-to-thunderbird](docs/commands/utils-export-to-thunderbird.md)
|
85
|
+
* [utils ignore-history](docs/commands/utils-ignore-history.md)
|
84
86
|
|
85
87
|
For a full list of available commands, run
|
86
88
|
|
@@ -94,12 +96,6 @@ For more information about a command, run
|
|
94
96
|
imap-backup help COMMAND
|
95
97
|
```
|
96
98
|
|
97
|
-
## Configuration
|
98
|
-
|
99
|
-
`imap-backup setup` creates the file `~/.imap-backup/config.json`.
|
100
|
-
|
101
|
-
[More information about configuration is available in the specific documentation](./docs/configuration.md).
|
102
|
-
|
103
99
|
# Troubleshooting
|
104
100
|
|
105
101
|
If you have problems:
|
data/bin/imap-backup
CHANGED
data/docs/development.md
CHANGED
@@ -44,7 +44,7 @@ use `last_command_started.output`.
|
|
44
44
|
require "net/imap"
|
45
45
|
|
46
46
|
imap = Net::IMAP.new("localhost", {port: 8993, ssl: {verify_mode: 0}})
|
47
|
-
username = "address@example.
|
47
|
+
username = "address@example.com"
|
48
48
|
imap.login(username, "pass")
|
49
49
|
|
50
50
|
message = "From: #{username}\nSubject: Some Subject\n\nHello!\n"
|
data/lib/email/mboxrd/message.rb
CHANGED
@@ -5,9 +5,6 @@ module Email; end
|
|
5
5
|
|
6
6
|
module Email::Mboxrd
|
7
7
|
class Message
|
8
|
-
extend Forwardable
|
9
|
-
def_delegators :@parsed, :subject
|
10
|
-
|
11
8
|
attr_reader :supplied_body
|
12
9
|
|
13
10
|
def self.clean_serialized(serialized)
|
@@ -40,6 +37,10 @@ module Email::Mboxrd
|
|
40
37
|
nil
|
41
38
|
end
|
42
39
|
|
40
|
+
def subject
|
41
|
+
parsed.subject
|
42
|
+
end
|
43
|
+
|
43
44
|
def imap_body
|
44
45
|
supplied_body.gsub(/(?<!\r)\n/, "\r\n")
|
45
46
|
end
|
@@ -3,6 +3,8 @@ require "imap/backup/client/default"
|
|
3
3
|
require "imap/backup/account/connection/backup_folders"
|
4
4
|
require "imap/backup/account/connection/client_factory"
|
5
5
|
require "imap/backup/account/connection/folder_names"
|
6
|
+
require "imap/backup/flag_refresher"
|
7
|
+
require "imap/backup/local_only_message_deleter"
|
6
8
|
require "imap/backup/serializer/directory"
|
7
9
|
|
8
10
|
module Imap::Backup
|
@@ -33,11 +35,18 @@ module Imap::Backup
|
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
|
-
def run_backup
|
37
|
-
Logger.logger.
|
38
|
+
def run_backup(refresh: false)
|
39
|
+
Logger.logger.info "Running backup of account: #{account.username}"
|
38
40
|
# start the connection so we get logging messages in the right order
|
39
41
|
client
|
40
42
|
ensure_account_folder
|
43
|
+
if account.mirror_mode
|
44
|
+
# Delete serialized folders that are not to be backed up
|
45
|
+
wanted = backup_folders.map(&:name)
|
46
|
+
local_folders do |serializer, _folder|
|
47
|
+
serializer.delete if !wanted.include?(serializer.folder)
|
48
|
+
end
|
49
|
+
end
|
41
50
|
each_folder do |folder, serializer|
|
42
51
|
begin
|
43
52
|
next if !folder.exist?
|
@@ -57,6 +66,11 @@ module Imap::Backup
|
|
57
66
|
multi_fetch_size: account.multi_fetch_size,
|
58
67
|
reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
|
59
68
|
).run
|
69
|
+
if account.mirror_mode
|
70
|
+
Logger.logger.info "Mirror mode - Deleting messages only present locally"
|
71
|
+
LocalOnlyMessageDeleter.new(folder, serializer).run
|
72
|
+
end
|
73
|
+
FlagRefresher.new(folder, serializer).run if account.mirror_mode || refresh
|
60
74
|
rescue Net::IMAP::ByeResponseError
|
61
75
|
reconnect
|
62
76
|
retry
|
@@ -58,22 +58,22 @@ module Imap::Backup
|
|
58
58
|
rescue FolderNotFound
|
59
59
|
[]
|
60
60
|
rescue NoMethodError
|
61
|
-
message =
|
62
|
-
Folder '#{name}' caused NoMethodError
|
63
|
-
|
64
|
-
`
|
65
|
-
|
66
|
-
|
67
|
-
|
61
|
+
message =
|
62
|
+
"Folder '#{name}' caused a NoMethodError. " \
|
63
|
+
"Probably this was `undefined method `[]' for nil:NilClass (NoMethodError)` " \
|
64
|
+
"in `search_internal` in stdlib net/imap.rb. " \
|
65
|
+
'This is caused by `@responses["SEARCH"] being unset/undefined. ' \
|
66
|
+
"Among others, Apple Mail servers send empty responses when " \
|
67
|
+
"folders are empty, causing this error."
|
68
68
|
Logger.logger.warn message
|
69
69
|
[]
|
70
70
|
end
|
71
71
|
|
72
|
-
def fetch_multi(uids)
|
72
|
+
def fetch_multi(uids, attr = [BODY_ATTRIBUTE, "FLAGS"])
|
73
73
|
examine
|
74
74
|
fetch_data_items =
|
75
75
|
retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
|
76
|
-
client.uid_fetch(uids,
|
76
|
+
client.uid_fetch(uids, attr)
|
77
77
|
end
|
78
78
|
return nil if fetch_data_items.nil?
|
79
79
|
|
@@ -90,18 +90,30 @@ module Imap::Backup
|
|
90
90
|
nil
|
91
91
|
end
|
92
92
|
|
93
|
-
def append(message
|
93
|
+
def append(message)
|
94
94
|
body = message.imap_body
|
95
95
|
date = message.date&.to_time
|
96
96
|
retry_on_error(errors: APPEND_RETRY_CLASSES, limit: 3) do
|
97
|
-
response = client.append(utf7_encoded_name, body, flags, date)
|
97
|
+
response = client.append(utf7_encoded_name, body, message.flags, date)
|
98
98
|
extract_uid(response)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
|
+
def delete_multi(uids)
|
103
|
+
set_flags(uids, [:Deleted])
|
104
|
+
client.expunge
|
105
|
+
end
|
106
|
+
|
107
|
+
def apply_flags(uids, flags)
|
108
|
+
client.select(utf7_encoded_name)
|
109
|
+
flags.reject! { |f| f == :Recent }
|
110
|
+
client.uid_store(uids, "FLAGS", flags)
|
111
|
+
end
|
112
|
+
|
102
113
|
def set_flags(uids, flags)
|
103
114
|
# Use read-write access, via `select`
|
104
115
|
client.select(utf7_encoded_name)
|
116
|
+
flags.reject! { |f| f == :Recent }
|
105
117
|
client.uid_store(uids, "+FLAGS", flags)
|
106
118
|
end
|
107
119
|
|
@@ -111,7 +123,10 @@ module Imap::Backup
|
|
111
123
|
end
|
112
124
|
|
113
125
|
def clear
|
114
|
-
|
126
|
+
existing = uids
|
127
|
+
return if existing.empty?
|
128
|
+
|
129
|
+
set_flags(existing, [:Deleted])
|
115
130
|
client.expunge
|
116
131
|
end
|
117
132
|
|
data/lib/imap/backup/account.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
module Imap; end
|
2
|
+
|
1
3
|
module Imap::Backup
|
2
4
|
class Account
|
3
5
|
DEFAULT_MULTI_FETCH_SIZE = 1
|
@@ -6,6 +8,7 @@ module Imap::Backup
|
|
6
8
|
attr_reader :password
|
7
9
|
attr_reader :local_path
|
8
10
|
attr_reader :folders
|
11
|
+
attr_reader :mirror_mode
|
9
12
|
attr_reader :server
|
10
13
|
attr_reader :connection_options
|
11
14
|
attr_reader :reset_seen_flags_after_fetch
|
@@ -16,6 +19,7 @@ module Imap::Backup
|
|
16
19
|
@password = options[:password]
|
17
20
|
@local_path = options[:local_path]
|
18
21
|
@folders = options[:folders]
|
22
|
+
@mirror_mode = options[:mirror_mode]
|
19
23
|
@server = options[:server]
|
20
24
|
@connection_options = options[:connection_options]
|
21
25
|
@multi_fetch_size = options[:multi_fetch_size]
|
@@ -53,6 +57,7 @@ module Imap::Backup
|
|
53
57
|
h = {username: @username, password: @password}
|
54
58
|
h[:local_path] = @local_path if @local_path
|
55
59
|
h[:folders] = @folders if @folders
|
60
|
+
h[:mirror_mode] = true if @mirror_mode
|
56
61
|
h[:server] = @server if @server
|
57
62
|
h[:connection_options] = @connection_options if @connection_options
|
58
63
|
h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
|
@@ -80,12 +85,16 @@ module Imap::Backup
|
|
80
85
|
update(:folders, value)
|
81
86
|
end
|
82
87
|
|
88
|
+
def mirror_mode=(value)
|
89
|
+
update(:mirror_mode, value)
|
90
|
+
end
|
91
|
+
|
83
92
|
def server=(value)
|
84
93
|
update(:server, value)
|
85
94
|
end
|
86
95
|
|
87
96
|
def connection_options=(value)
|
88
|
-
parsed = JSON.parse(value)
|
97
|
+
parsed = JSON.parse(value, symbolize_names: true)
|
89
98
|
update(:connection_options, parsed)
|
90
99
|
end
|
91
100
|
|
@@ -4,15 +4,19 @@ module Imap::Backup
|
|
4
4
|
include CLI::Helpers
|
5
5
|
|
6
6
|
attr_reader :account_names
|
7
|
+
attr_reader :refresh
|
7
8
|
|
8
9
|
def initialize(options)
|
9
10
|
super([])
|
10
11
|
@account_names = (options[:accounts] || "").split(",")
|
12
|
+
@refresh = options.key?(:refresh) ? !!options[:refresh] : false
|
11
13
|
end
|
12
14
|
|
13
15
|
no_commands do
|
14
16
|
def run
|
15
|
-
each_connection(account_names
|
17
|
+
each_connection(account_names) do |connection|
|
18
|
+
connection.run_backup(refresh: refresh)
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -3,6 +3,28 @@ require "imap/backup/cli/accounts"
|
|
3
3
|
|
4
4
|
module Imap::Backup
|
5
5
|
module CLI::Helpers
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
def self.verbose_option
|
9
|
+
method_option(
|
10
|
+
"verbose",
|
11
|
+
type: :boolean,
|
12
|
+
desc: "increase the amount of logging",
|
13
|
+
aliases: ["-v"]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.quiet_option
|
18
|
+
method_option(
|
19
|
+
"quiet",
|
20
|
+
type: :boolean,
|
21
|
+
desc: "silence all output",
|
22
|
+
aliases: ["-q"]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
6
28
|
def symbolized(options)
|
7
29
|
options.each.with_object({}) do |(k, v), acc|
|
8
30
|
key = k.gsub("-", "_").intern
|
@@ -39,8 +39,8 @@ module Imap::Backup
|
|
39
39
|
|
40
40
|
uids = folder_serializer.uids
|
41
41
|
|
42
|
-
folder_serializer.each_message(uids).map do |
|
43
|
-
list_message
|
42
|
+
folder_serializer.each_message(uids).map do |message|
|
43
|
+
list_message message
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -59,22 +59,22 @@ module Imap::Backup
|
|
59
59
|
raise "Folder '#{folder_name}' not found" if !folder_serializer
|
60
60
|
|
61
61
|
uid_list = uids.split(",")
|
62
|
-
folder_serializer.each_message(uid_list).each do |
|
62
|
+
folder_serializer.each_message(uid_list).each do |message|
|
63
63
|
if uid_list.count > 1
|
64
64
|
Kernel.puts <<~HEADER
|
65
65
|
#{'-' * 80}
|
66
|
-
#{format('| UID: %-71s |', uid)}
|
66
|
+
#{format('| UID: %-71s |', message.uid)}
|
67
67
|
#{'-' * 80}
|
68
68
|
HEADER
|
69
69
|
end
|
70
|
-
Kernel.puts message.
|
70
|
+
Kernel.puts message.body
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
74
|
no_commands do
|
75
|
-
def list_message(
|
75
|
+
def list_message(message)
|
76
76
|
m = {
|
77
|
-
uid: uid,
|
77
|
+
uid: message.uid,
|
78
78
|
date: message.date.to_s,
|
79
79
|
subject: message.subject || ""
|
80
80
|
}
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require "imap/backup/mirror"
|
2
|
+
|
3
|
+
module Imap::Backup
|
4
|
+
class CLI::Mirror < Thor
|
5
|
+
include Thor::Actions
|
6
|
+
|
7
|
+
attr_reader :destination_email
|
8
|
+
attr_reader :destination_prefix
|
9
|
+
attr_reader :source_email
|
10
|
+
attr_reader :source_prefix
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
source_email,
|
14
|
+
destination_email,
|
15
|
+
destination_prefix: "",
|
16
|
+
source_prefix: ""
|
17
|
+
)
|
18
|
+
super([])
|
19
|
+
@destination_email = destination_email
|
20
|
+
@destination_prefix = destination_prefix
|
21
|
+
@source_email = source_email
|
22
|
+
@source_prefix = source_prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
no_commands do
|
26
|
+
def run
|
27
|
+
check_accounts!
|
28
|
+
warn_if_source_account_is_not_in_mirror_mode
|
29
|
+
|
30
|
+
CLI::Backup.new(accounts: source_email).run
|
31
|
+
|
32
|
+
folders.each do |serializer, folder|
|
33
|
+
Mirror.new(serializer, folder).run
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_accounts!
|
38
|
+
if destination_email == source_email
|
39
|
+
raise "Source and destination accounts cannot be the same!"
|
40
|
+
end
|
41
|
+
|
42
|
+
raise "Account '#{destination_email}' does not exist" if !destination_account
|
43
|
+
|
44
|
+
raise "Account '#{source_email}' does not exist" if !source_account
|
45
|
+
end
|
46
|
+
|
47
|
+
def warn_if_source_account_is_not_in_mirror_mode
|
48
|
+
return if source_account.mirror_mode
|
49
|
+
|
50
|
+
message =
|
51
|
+
"The account '#{source_account.username}' " \
|
52
|
+
"is not set up to make mirror backups"
|
53
|
+
Logger.logger.info message
|
54
|
+
end
|
55
|
+
|
56
|
+
def config
|
57
|
+
Configuration.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def destination_account
|
61
|
+
config.accounts.find { |a| a.username == destination_email }
|
62
|
+
end
|
63
|
+
|
64
|
+
def folders
|
65
|
+
return enum_for(:folders) if !block_given?
|
66
|
+
|
67
|
+
glob = File.join(source_local_path, "**", "*.imap")
|
68
|
+
Pathname.glob(glob) do |path|
|
69
|
+
name = source_folder_name(path)
|
70
|
+
serializer = Serializer.new(source_local_path, name)
|
71
|
+
folder = folder_for(name)
|
72
|
+
yield serializer, folder
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def folder_for(source_folder)
|
77
|
+
no_source_prefix =
|
78
|
+
if source_prefix != "" && source_folder.start_with?(source_prefix)
|
79
|
+
source_folder.delete_prefix(source_prefix)
|
80
|
+
else
|
81
|
+
source_folder.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
with_destination_prefix =
|
85
|
+
if destination_prefix && destination_prefix != ""
|
86
|
+
destination_prefix + no_source_prefix
|
87
|
+
else
|
88
|
+
no_source_prefix
|
89
|
+
end
|
90
|
+
|
91
|
+
Account::Folder.new(
|
92
|
+
destination_account.connection,
|
93
|
+
with_destination_prefix
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def source_local_path
|
98
|
+
source_account.local_path
|
99
|
+
end
|
100
|
+
|
101
|
+
def source_account
|
102
|
+
config.accounts.find { |a| a.username == source_email }
|
103
|
+
end
|
104
|
+
|
105
|
+
def source_folder_name(imap_pathname)
|
106
|
+
base = Pathname.new(source_local_path)
|
107
|
+
imap_name = imap_pathname.relative_path_from(base).to_s
|
108
|
+
dir = File.dirname(imap_name)
|
109
|
+
stripped = File.basename(imap_name, ".imap")
|
110
|
+
if dir == "."
|
111
|
+
stripped
|
112
|
+
else
|
113
|
+
File.join(dir, stripped)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -8,7 +8,10 @@ module Imap::Backup
|
|
8
8
|
FAKE_EMAIL = "fake@email.com".freeze
|
9
9
|
|
10
10
|
desc "ignore-history EMAIL", "Skip downloading emails up to today for all configured folders"
|
11
|
+
verbose_option
|
12
|
+
quiet_option
|
11
13
|
def ignore_history(email)
|
14
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
12
15
|
connection = connection(email)
|
13
16
|
|
14
17
|
connection.backup_folders.each do |folder|
|
@@ -26,6 +29,8 @@ module Imap::Backup
|
|
26
29
|
A folder called 'imap-backup/EMAIL' is created under 'Local Folders'.
|
27
30
|
DOC
|
28
31
|
)
|
32
|
+
verbose_option
|
33
|
+
quiet_option
|
29
34
|
method_option(
|
30
35
|
"force",
|
31
36
|
type: :boolean,
|
@@ -40,6 +45,7 @@ module Imap::Backup
|
|
40
45
|
)
|
41
46
|
def export_to_thunderbird(email)
|
42
47
|
opts = symbolized(options)
|
48
|
+
Imap::Backup::Logger.setup_logging opts
|
43
49
|
force = opts.key?(:force) ? opts[:force] : false
|
44
50
|
profile_name = opts[:profile]
|
45
51
|
|
data/lib/imap/backup/cli.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "thor"
|
2
|
+
require "imap/backup/logger"
|
2
3
|
|
3
4
|
module Imap; end
|
4
5
|
|
@@ -10,6 +11,7 @@ module Imap::Backup
|
|
10
11
|
autoload :Folders, "imap/backup/cli/folders"
|
11
12
|
autoload :Local, "imap/backup/cli/local"
|
12
13
|
autoload :Migrate, "imap/backup/cli/migrate"
|
14
|
+
autoload :Mirror, "imap/backup/cli/mirror"
|
13
15
|
autoload :Remote, "imap/backup/cli/remote"
|
14
16
|
autoload :Restore, "imap/backup/cli/restore"
|
15
17
|
autoload :Setup, "imap/backup/cli/setup"
|
@@ -43,7 +45,16 @@ module Imap::Backup
|
|
43
45
|
The setup tool can be used to choose a specific list of folders to back up.
|
44
46
|
DESC
|
45
47
|
accounts_option
|
48
|
+
verbose_option
|
49
|
+
quiet_option
|
50
|
+
method_option(
|
51
|
+
"refresh",
|
52
|
+
type: :boolean,
|
53
|
+
desc: "in 'keep all emails' mode, update flags for messages that are already downloaded",
|
54
|
+
aliases: ["-r"]
|
55
|
+
)
|
46
56
|
def backup
|
57
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
47
58
|
Backup.new(symbolized(options)).run
|
48
59
|
end
|
49
60
|
|
@@ -85,6 +96,8 @@ module Imap::Backup
|
|
85
96
|
use the `--reset` option. In this case, all existing emails are
|
86
97
|
deleted before uploading the migrated emails.
|
87
98
|
DESC
|
99
|
+
verbose_option
|
100
|
+
quiet_option
|
88
101
|
method_option(
|
89
102
|
"destination-prefix",
|
90
103
|
type: :string,
|
@@ -104,9 +117,49 @@ module Imap::Backup
|
|
104
117
|
aliases: ["-s"]
|
105
118
|
)
|
106
119
|
def migrate(source_email, destination_email)
|
120
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
107
121
|
Migrate.new(source_email, destination_email, **symbolized(options)).run
|
108
122
|
end
|
109
123
|
|
124
|
+
desc(
|
125
|
+
"mirror SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
|
126
|
+
"Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
|
127
|
+
)
|
128
|
+
long_desc <<~DESC
|
129
|
+
This command updates the DESTINATION_EMAIL account's folders to have the same contents
|
130
|
+
as those on the SOURCE_EMAIL account.
|
131
|
+
|
132
|
+
If a folder list is configured for the SOURCE_EMAIL account,
|
133
|
+
only the folders indicated by the setting are copied.
|
134
|
+
|
135
|
+
First, runs the download of the SOURCE_EMAIL account.
|
136
|
+
If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
|
137
|
+
a warning is printed.
|
138
|
+
|
139
|
+
When the mirror command is used, for each folder that is processed,
|
140
|
+
a new file is created alongside the normal backup files (.imap and .mbox)
|
141
|
+
This file has a '.mirror' extension. This file contains a mapping of
|
142
|
+
the known UIDs on the source account to those on the destination account.
|
143
|
+
DESC
|
144
|
+
verbose_option
|
145
|
+
quiet_option
|
146
|
+
method_option(
|
147
|
+
"destination-prefix",
|
148
|
+
type: :string,
|
149
|
+
desc: "the prefix (namespace) to add to destination folder names",
|
150
|
+
aliases: ["-d"]
|
151
|
+
)
|
152
|
+
method_option(
|
153
|
+
"source-prefix",
|
154
|
+
type: :string,
|
155
|
+
desc: "the prefix (namespace) to strip from source folder names",
|
156
|
+
aliases: ["-s"]
|
157
|
+
)
|
158
|
+
def mirror(source_email, destination_email)
|
159
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
160
|
+
Mirror.new(source_email, destination_email, **symbolized(options)).run
|
161
|
+
end
|
162
|
+
|
110
163
|
desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
|
111
164
|
subcommand "remote", Remote
|
112
165
|
|
@@ -116,7 +169,10 @@ module Imap::Backup
|
|
116
169
|
their original server.
|
117
170
|
DESC
|
118
171
|
accounts_option
|
172
|
+
verbose_option
|
173
|
+
quiet_option
|
119
174
|
def restore(email = nil)
|
175
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
120
176
|
Restore.new(email, symbolized(options)).run
|
121
177
|
end
|
122
178
|
|
@@ -125,7 +181,10 @@ module Imap::Backup
|
|
125
181
|
A menu-driven command-line application used to configure imap-backup.
|
126
182
|
Configure email accounts to back up.
|
127
183
|
DESC
|
184
|
+
verbose_option
|
185
|
+
quiet_option
|
128
186
|
def setup
|
187
|
+
Imap::Backup::Logger.setup_logging symbolized(options)
|
129
188
|
Setup.new.run
|
130
189
|
end
|
131
190
|
|
@@ -20,8 +20,6 @@ module Imap::Backup
|
|
20
20
|
|
21
21
|
def initialize(pathname = self.class.default_pathname)
|
22
22
|
@pathname = pathname
|
23
|
-
@saved_debug = nil
|
24
|
-
@debug = nil
|
25
23
|
end
|
26
24
|
|
27
25
|
def path
|
@@ -36,8 +34,7 @@ module Imap::Backup
|
|
36
34
|
remove_deleted_accounts
|
37
35
|
save_data = {
|
38
36
|
version: VERSION,
|
39
|
-
accounts: accounts.map(&:to_h)
|
40
|
-
debug: debug?
|
37
|
+
accounts: accounts.map(&:to_h)
|
41
38
|
}
|
42
39
|
File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
|
43
40
|
FileUtils.chmod(0o600, pathname) if !windows?
|
@@ -53,29 +50,16 @@ module Imap::Backup
|
|
53
50
|
|
54
51
|
def modified?
|
55
52
|
ensure_loaded!
|
56
|
-
return true if @saved_debug != @debug
|
57
53
|
|
58
54
|
accounts.any? { |a| a.modified? || a.marked_for_deletion? }
|
59
55
|
end
|
60
56
|
|
61
|
-
def debug?
|
62
|
-
ensure_loaded!
|
63
|
-
@debug
|
64
|
-
end
|
65
|
-
|
66
|
-
def debug=(value)
|
67
|
-
ensure_loaded!
|
68
|
-
@debug = [true, false].include?(value) ? value : false
|
69
|
-
end
|
70
|
-
|
71
57
|
private
|
72
58
|
|
73
59
|
def ensure_loaded!
|
74
60
|
return true if @data
|
75
61
|
|
76
62
|
data
|
77
|
-
@debug = data.key?(:debug) ? data[:debug] == true : false
|
78
|
-
@saved_debug = @debug
|
79
63
|
true
|
80
64
|
end
|
81
65
|
|