imap-backup 7.0.0.rc1 → 7.0.2
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/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
|
|