larch 1.1.0.dev.20091203 → 1.1.0.dev.20091206
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/HISTORY +4 -0
- data/README.rdoc +34 -31
- data/bin/larch +5 -2
- data/lib/larch/config.rb +1 -0
- data/lib/larch/db/account.rb +19 -0
- data/lib/larch/db/message.rb +14 -0
- data/lib/larch/db/migrate/002_add_timestamps.rb +15 -0
- data/lib/larch/imap/mailbox.rb +146 -107
- data/lib/larch/imap.rb +18 -6
- data/lib/larch/logger.rb +1 -1
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +39 -4
- metadata +3 -2
data/HISTORY
CHANGED
@@ -15,11 +15,15 @@ Version 1.1.0 (git)
|
|
15
15
|
* Folders are now copied recursively by default.
|
16
16
|
* Progress information is now displayed regularly while scanning large
|
17
17
|
mailboxes.
|
18
|
+
* Added --sync-flags option to synchronize message flags (like Seen, Flagged,
|
19
|
+
etc.) from the source server to the destination server for messages that
|
20
|
+
already exist on the destination.
|
18
21
|
* Added short versions of common command-line options.
|
19
22
|
* The --fast-scan option has been removed.
|
20
23
|
* The --to-folder option can now be used in conjunction with --all or
|
21
24
|
--all-subscribed to copy messages from multiple source folders to a single
|
22
25
|
destination folder.
|
26
|
+
* More concise log messages to reduce visual clutter in the log.
|
23
27
|
* Fixed encoding issues when creating mailboxes and getting mailbox lists.
|
24
28
|
* Fixed incorrect case-sensitive treatment of the 'INBOX' folder name.
|
25
29
|
* Fixed a bug in which Larch would try to copy flags that weren't supported on
|
data/README.rdoc
CHANGED
@@ -28,42 +28,45 @@ Latest development version:
|
|
28
28
|
|
29
29
|
larch [config section] [options]
|
30
30
|
larch --from <uri> --to <uri> [options]
|
31
|
-
|
31
|
+
|
32
32
|
Server Options:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
--from, -f <s>: URI of the source IMAP server
|
34
|
+
--from-folder, -F <s>: Source folder to copy from (default: INBOX)
|
35
|
+
--from-pass, -p <s>: Source server password (default: prompt)
|
36
|
+
--from-user, -u <s>: Source server username (default: prompt)
|
37
|
+
--to, -t <s>: URI of the destination IMAP server
|
38
|
+
--to-folder, -T <s>: Destination folder to copy to (default: INBOX)
|
39
|
+
--to-pass, -P <s>: Destination server password (default: prompt)
|
40
|
+
--to-user, -U <s>: Destination server username (default: prompt)
|
41
41
|
|
42
42
|
Sync Options:
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
--all, -a: Copy all folders recursively
|
44
|
+
--all-subscribed, -s: Copy all subscribed folders recursively
|
45
|
+
--exclude <s+>: List of mailbox names/patterns that shouldn't be
|
46
|
+
copied
|
47
|
+
--exclude-file <s>: Filename containing mailbox names/patterns that
|
48
|
+
shouldn't be copied
|
49
|
+
--sync-flags, -S: Sync message flags from the source to the
|
50
|
+
destination for messages that already exist at the
|
51
|
+
destination
|
49
52
|
|
50
53
|
General Options:
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
54
|
+
--config, -c <s>: Specify a non-default config file to use (default:
|
55
|
+
~/.larch/config.yaml)
|
56
|
+
--database <s>: Specify a non-default message database to use
|
57
|
+
(default: ~/.larch/larch.db)
|
58
|
+
--dry-run, -n: Don't actually make any changes
|
59
|
+
--max-retries <i>: Maximum number of times to retry after a
|
60
|
+
recoverable error (default: 3)
|
61
|
+
--no-create-folder: Don't create destination folders that don't
|
62
|
+
already exist
|
63
|
+
--ssl-certs <s>: Path to a trusted certificate bundle to use to
|
64
|
+
verify server SSL certificates
|
65
|
+
--ssl-verify: Verify server SSL certificates
|
66
|
+
--verbosity, -V <s>: Output verbosity: debug, info, warn, error, or
|
67
|
+
fatal (default: info)
|
68
|
+
--version, -v: Print version and exit
|
69
|
+
--help, -h: Show this message
|
67
70
|
|
68
71
|
== Usage Examples
|
69
72
|
|
data/bin/larch
CHANGED
@@ -20,11 +20,11 @@ Usage:
|
|
20
20
|
|
21
21
|
Server Options:
|
22
22
|
EOS
|
23
|
-
opt :from, "URI of the source IMAP server
|
23
|
+
opt :from, "URI of the source IMAP server", :short => '-f', :type => :string
|
24
24
|
opt :from_folder, "Source folder to copy from (default: INBOX)", :short => '-F', :default => Config::DEFAULT['from-folder'], :type => :string
|
25
25
|
opt :from_pass, "Source server password (default: prompt)", :short => '-p', :type => :string
|
26
26
|
opt :from_user, "Source server username (default: prompt)", :short => '-u', :type => :string
|
27
|
-
opt :to, "URI of the destination IMAP server
|
27
|
+
opt :to, "URI of the destination IMAP server", :short => '-t', :type => :string
|
28
28
|
opt :to_folder, "Destination folder to copy to (default: INBOX)", :short => '-T', :default => Config::DEFAULT['to-folder'], :type => :string
|
29
29
|
opt :to_pass, "Destination server password (default: prompt)", :short => '-P', :type => :string
|
30
30
|
opt :to_user, "Destination server username (default: prompt)", :short => '-U', :type => :string
|
@@ -34,6 +34,7 @@ EOS
|
|
34
34
|
opt :all_subscribed, "Copy all subscribed folders recursively", :short => '-s'
|
35
35
|
opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
|
36
36
|
opt :exclude_file, "Filename containing mailbox names/patterns that shouldn't be copied", :short => :none, :type => :string
|
37
|
+
opt :sync_flags, "Sync message flags from the source to the destination for messages that already exist at the destination", :short => '-S'
|
37
38
|
|
38
39
|
text "\nGeneral Options:"
|
39
40
|
opt :config, "Specify a non-default config file to use", :short => '-c', :default => Config::DEFAULT['config']
|
@@ -87,6 +88,7 @@ EOS
|
|
87
88
|
|
88
89
|
imap_from = Larch::IMAP.new(uri_from,
|
89
90
|
:dry_run => config[:dry_run],
|
91
|
+
:log_label => '[<]',
|
90
92
|
:max_retries => config[:max_retries],
|
91
93
|
:ssl_certs => config[:ssl_certs] || nil,
|
92
94
|
:ssl_verify => config[:ssl_verify]
|
@@ -95,6 +97,7 @@ EOS
|
|
95
97
|
imap_to = Larch::IMAP.new(uri_to,
|
96
98
|
:create_mailbox => !config[:no_create_folder] && !config[:dry_run],
|
97
99
|
:dry_run => config[:dry_run],
|
100
|
+
:log_label => '[>]',
|
98
101
|
:max_retries => config[:max_retries],
|
99
102
|
:ssl_certs => config[:ssl_certs] || nil,
|
100
103
|
:ssl_verify => config[:ssl_verify]
|
data/lib/larch/config.rb
CHANGED
data/lib/larch/db/account.rb
CHANGED
@@ -2,11 +2,30 @@ module Larch; module Database
|
|
2
2
|
|
3
3
|
class Account < Sequel::Model(:accounts)
|
4
4
|
plugin :hook_class_methods
|
5
|
+
|
5
6
|
one_to_many :mailboxes, :class => Larch::Database::Mailbox
|
6
7
|
|
8
|
+
before_create do
|
9
|
+
now = Time.now.to_i
|
10
|
+
|
11
|
+
self.created_at = now
|
12
|
+
self.updated_at = now
|
13
|
+
end
|
14
|
+
|
7
15
|
before_destroy do
|
8
16
|
Mailbox.filter(:account_id => id).destroy
|
9
17
|
end
|
18
|
+
|
19
|
+
before_save do
|
20
|
+
now = Time.now.to_i
|
21
|
+
|
22
|
+
self.created_at = now if self.created_at.nil?
|
23
|
+
self.updated_at = now
|
24
|
+
end
|
25
|
+
|
26
|
+
def touch
|
27
|
+
update(:updated_at => Time.now.to_i)
|
28
|
+
end
|
10
29
|
end
|
11
30
|
|
12
31
|
end; end
|
data/lib/larch/db/message.rb
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
module Larch; module Database
|
2
2
|
|
3
3
|
class Message < Sequel::Model(:messages)
|
4
|
+
def flags
|
5
|
+
self[:flags].split(',').sort.map do |f|
|
6
|
+
# Flags beginning with $ should be strings; all others should be symbols.
|
7
|
+
f[0,1] == '$' ? f : f.to_sym
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def flags_str
|
12
|
+
self[:flags]
|
13
|
+
end
|
14
|
+
|
15
|
+
def flags=(flags)
|
16
|
+
self[:flags] = flags.map{|f| f.to_s }.join(',')
|
17
|
+
end
|
4
18
|
end
|
5
19
|
|
6
20
|
end; end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddTimestamps < Sequel::Migration
|
2
|
+
def down
|
3
|
+
alter_table :accounts do
|
4
|
+
drop_column :created_at
|
5
|
+
drop_column :updated_at
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def up
|
10
|
+
alter_table :accounts do
|
11
|
+
add_column :created_at, :integer
|
12
|
+
add_column :updated_at, :integer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/larch/imap/mailbox.rb
CHANGED
@@ -54,7 +54,7 @@ class Mailbox
|
|
54
54
|
|
55
55
|
Mailbox.class_eval do
|
56
56
|
define_method(level) do |msg|
|
57
|
-
Larch.log.log(level, "#{@imap.
|
57
|
+
Larch.log.log(level, "#{@imap.options[:log_label]} #{@name}: #{msg}")
|
58
58
|
end
|
59
59
|
|
60
60
|
private level
|
@@ -95,11 +95,17 @@ class Mailbox
|
|
95
95
|
end
|
96
96
|
alias << append
|
97
97
|
|
98
|
+
# Iterates through messages in this mailbox, yielding a
|
99
|
+
# Larch::Database::Message object for each to the provided block.
|
100
|
+
def each_db_message # :yields: db_message
|
101
|
+
scan
|
102
|
+
@db_mailbox.messages_dataset.all {|db_message| yield db_message }
|
103
|
+
end
|
104
|
+
|
98
105
|
# Iterates through messages in this mailbox, yielding the Larch message guid
|
99
106
|
# of each to the provided block.
|
100
107
|
def each_guid # :yields: guid
|
101
|
-
|
102
|
-
@db_mailbox.messages_dataset.each {|db_message| yield db_message.guid }
|
108
|
+
each_db_message {|db_message| yield db_message.guid }
|
103
109
|
end
|
104
110
|
|
105
111
|
# Iterates through mailboxes that are first-level children of this mailbox,
|
@@ -114,7 +120,7 @@ class Mailbox
|
|
114
120
|
def fetch(guid, peek = false)
|
115
121
|
scan
|
116
122
|
|
117
|
-
unless db_message =
|
123
|
+
unless db_message = fetch_db_message(guid)
|
118
124
|
warn "message not found in local db: #{guid}"
|
119
125
|
return nil
|
120
126
|
end
|
@@ -134,6 +140,14 @@ class Mailbox
|
|
134
140
|
end
|
135
141
|
alias [] fetch
|
136
142
|
|
143
|
+
# Returns a Larch::Database::Message object representing the message with the
|
144
|
+
# specified Larch _guid_, or +nil+ if the specified guide was not found in
|
145
|
+
# this mailbox.
|
146
|
+
def fetch_db_message(guid)
|
147
|
+
scan
|
148
|
+
@db_mailbox.messages_dataset.filter(:guid => guid).first
|
149
|
+
end
|
150
|
+
|
137
151
|
# Returns +true+ if a message with the specified Larch guid exists in this
|
138
152
|
# mailbox, +false+ otherwise.
|
139
153
|
def has_guid?(guid)
|
@@ -174,6 +188,7 @@ class Mailbox
|
|
174
188
|
def scan
|
175
189
|
now = Time.now.to_i
|
176
190
|
return if @last_scan && (now - @last_scan) < SCAN_INTERVAL
|
191
|
+
|
177
192
|
first_scan = @last_scan.nil?
|
178
193
|
@last_scan = now
|
179
194
|
|
@@ -212,118 +227,33 @@ class Mailbox
|
|
212
227
|
end
|
213
228
|
|
214
229
|
@db_mailbox.update(:uidvalidity => status['UIDVALIDITY'])
|
215
|
-
|
216
230
|
return unless flag_range || full_range.last - full_range.first > 0
|
217
231
|
|
218
|
-
|
219
|
-
return unless imap_examine
|
220
|
-
|
221
|
-
if flag_range && flag_range.last - flag_range.first > 0
|
222
|
-
info "fetching latest message flags..."
|
223
|
-
|
224
|
-
# Load the expected UIDs and their flags into a Hash for quicker lookups.
|
225
|
-
expected_uids = {}
|
226
|
-
@db_mailbox.messages_dataset.each do |db_message|
|
227
|
-
expected_uids[db_message.uid] = db_message.flags.split(',').map{|f| f.to_sym }
|
228
|
-
end
|
229
|
-
|
230
|
-
imap_uid_fetch(flag_range, "(UID FLAGS)", 16384) do |fetch_data|
|
231
|
-
# Check the fields in the first response to ensure that everything we
|
232
|
-
# asked for is there.
|
233
|
-
check_response_fields(fetch_data.first, 'UID', 'FLAGS') unless fetch_data.empty?
|
234
|
-
|
235
|
-
Larch.db.transaction do
|
236
|
-
fetch_data.each do |data|
|
237
|
-
uid = data.attr['UID']
|
238
|
-
flags = data.attr['FLAGS']
|
239
|
-
local_flags = expected_uids[uid]
|
240
|
-
|
241
|
-
# If we haven't seen this message before, or if its flags have
|
242
|
-
# changed, update the database.
|
243
|
-
unless local_flags && local_flags == flags
|
244
|
-
@db_mailbox.messages_dataset.filter(:uid => uid).update(
|
245
|
-
:flags => flags.map{|f| f.to_s }.join(','))
|
246
|
-
end
|
247
|
-
|
248
|
-
expected_uids.delete(uid)
|
249
|
-
end
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
# Any UIDs that are in the database but weren't in the response have been
|
254
|
-
# deleted from the server, so we need to delete them from the database as
|
255
|
-
# well.
|
256
|
-
unless expected_uids.empty?
|
257
|
-
debug "removing #{expected_uids.length} deleted messages from the database..."
|
258
|
-
|
259
|
-
Larch.db.transaction do
|
260
|
-
expected_uids.each_key do |uid|
|
261
|
-
@db_mailbox.messages_dataset.filter(:uid => uid).destroy
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
expected_uids = nil
|
267
|
-
fetch_data = nil
|
268
|
-
end
|
232
|
+
fetch_flags(flag_range) if flag_range && flag_range.last - flag_range.first > 0
|
269
233
|
|
270
234
|
if full_range && full_range.last - full_range.first > 0
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
show_progress = total - start > FETCH_BLOCK_SIZE * 4
|
277
|
-
|
278
|
-
info "fetching message headers #{start} through #{total}..."
|
279
|
-
|
280
|
-
begin
|
281
|
-
last_good_uid = nil
|
282
|
-
|
283
|
-
imap_uid_fetch(full_range, "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE FLAGS)") do |fetch_data|
|
284
|
-
check_response_fields(fetch_data, 'UID', 'RFC822.SIZE', 'INTERNALDATE', 'FLAGS')
|
285
|
-
|
286
|
-
Larch.db.transaction do
|
287
|
-
fetch_data.each do |data|
|
288
|
-
uid = data.attr['UID']
|
289
|
-
|
290
|
-
Database::Message.create(
|
291
|
-
:mailbox_id => @db_mailbox.id,
|
292
|
-
:guid => create_guid(data),
|
293
|
-
:uid => uid,
|
294
|
-
:message_id => parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']),
|
295
|
-
:rfc822_size => data.attr['RFC822.SIZE'].to_i,
|
296
|
-
:internaldate => Time.parse(data.attr['INTERNALDATE']).to_i,
|
297
|
-
:flags => data.attr['FLAGS'].map{|f| f.to_s }.join(',')
|
298
|
-
)
|
299
|
-
|
300
|
-
last_good_uid = uid
|
301
|
-
end
|
235
|
+
fetch_headers(full_range, {
|
236
|
+
:progress_start => @db_mailbox.messages_dataset.count + 1,
|
237
|
+
:progress_total => status['MESSAGES']
|
238
|
+
})
|
239
|
+
end
|
302
240
|
|
303
|
-
|
304
|
-
|
241
|
+
@db_mailbox.update(:uidnext => status['UIDNEXT'])
|
242
|
+
return
|
243
|
+
end
|
305
244
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
245
|
+
# Sets the IMAP flags for the message specified by _guid_, replacing any
|
246
|
+
# existing flags (except <code>:Recent</code>). _flags_ should be an array of
|
247
|
+
# symbols. Returns +true+ on success, +false+ on failure.
|
248
|
+
def set_flags(guid, flags)
|
249
|
+
raise ArgumentError, "flags must be an Array" unless flags.is_a?(Array)
|
310
250
|
|
311
|
-
|
312
|
-
|
313
|
-
end
|
251
|
+
db_message = fetch_db_message(guid)
|
252
|
+
return false if db_message.nil? || !imap_select
|
314
253
|
|
315
|
-
|
316
|
-
# Set this mailbox's uidnext value to the last known good UID that was
|
317
|
-
# stored in the database, plus 1. This will allow Larch to resume where
|
318
|
-
# the error occurred on the next attempt rather than having to start
|
319
|
-
# over.
|
320
|
-
@db_mailbox.update(:uidnext => last_good_uid + 1) if last_good_uid
|
321
|
-
raise
|
322
|
-
end
|
323
|
-
end
|
254
|
+
@imap.safely { @imap.conn.uid_store(db_message.uid, 'FLAGS.SILENT', flags) } unless @imap.options[:dry_run]
|
324
255
|
|
325
|
-
|
326
|
-
return
|
256
|
+
true
|
327
257
|
end
|
328
258
|
|
329
259
|
# Subscribes to this mailbox.
|
@@ -387,6 +317,115 @@ class Mailbox
|
|
387
317
|
end
|
388
318
|
end
|
389
319
|
|
320
|
+
# Fetches the latest flags from the server for the specified range of message
|
321
|
+
# UIDs.
|
322
|
+
def fetch_flags(flag_range)
|
323
|
+
return unless imap_examine
|
324
|
+
|
325
|
+
info "fetching latest message flags..."
|
326
|
+
|
327
|
+
# Load the expected UIDs and their flags into a Hash for quicker lookups.
|
328
|
+
expected_uids = {}
|
329
|
+
@db_mailbox.messages_dataset.all do |db_message|
|
330
|
+
expected_uids[db_message.uid] = db_message.flags
|
331
|
+
end
|
332
|
+
|
333
|
+
imap_uid_fetch(flag_range, "(UID FLAGS)", 16384) do |fetch_data|
|
334
|
+
# Check the fields in the first response to ensure that everything we
|
335
|
+
# asked for is there.
|
336
|
+
check_response_fields(fetch_data.first, 'UID', 'FLAGS') unless fetch_data.empty?
|
337
|
+
|
338
|
+
Larch.db.transaction do
|
339
|
+
fetch_data.each do |data|
|
340
|
+
uid = data.attr['UID']
|
341
|
+
flags = data.attr['FLAGS']
|
342
|
+
local_flags = expected_uids[uid]
|
343
|
+
|
344
|
+
# If we haven't seen this message before, or if its flags have
|
345
|
+
# changed, update the database.
|
346
|
+
unless local_flags && local_flags == flags
|
347
|
+
@db_mailbox.messages_dataset.filter(:uid => uid).update(:flags => flags.map{|f| f.to_s }.join(','))
|
348
|
+
end
|
349
|
+
|
350
|
+
expected_uids.delete(uid)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Any UIDs that are in the database but weren't in the response have been
|
356
|
+
# deleted from the server, so we need to delete them from the database as
|
357
|
+
# well.
|
358
|
+
unless expected_uids.empty?
|
359
|
+
debug "removing #{expected_uids.length} deleted messages from the database..."
|
360
|
+
|
361
|
+
Larch.db.transaction do
|
362
|
+
expected_uids.each_key do |uid|
|
363
|
+
@db_mailbox.messages_dataset.filter(:uid => uid).destroy
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
expected_uids = nil
|
369
|
+
fetch_data = nil
|
370
|
+
end
|
371
|
+
|
372
|
+
# Fetches the latest headers from the server for the specified range of
|
373
|
+
# message UIDs.
|
374
|
+
def fetch_headers(header_range, options = {})
|
375
|
+
return unless imap_examine
|
376
|
+
|
377
|
+
options = {
|
378
|
+
:progress_start => 0,
|
379
|
+
:progress_total => 0
|
380
|
+
}.merge(options)
|
381
|
+
|
382
|
+
fetched = 0
|
383
|
+
progress = 0
|
384
|
+
show_progress = options[:progress_total] - options[:progress_start] > FETCH_BLOCK_SIZE * 4
|
385
|
+
|
386
|
+
info "fetching message headers #{options[:progress_start]} through #{options[:progress_total]}..."
|
387
|
+
|
388
|
+
last_good_uid = nil
|
389
|
+
|
390
|
+
imap_uid_fetch(header_range, "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE FLAGS)") do |fetch_data|
|
391
|
+
# Check the fields in the first response to ensure that everything we
|
392
|
+
# asked for is there.
|
393
|
+
check_response_fields(fetch_data, 'UID', 'RFC822.SIZE', 'INTERNALDATE', 'FLAGS')
|
394
|
+
|
395
|
+
Larch.db.transaction do
|
396
|
+
fetch_data.each do |data|
|
397
|
+
uid = data.attr['UID']
|
398
|
+
|
399
|
+
Database::Message.create(
|
400
|
+
:mailbox_id => @db_mailbox.id,
|
401
|
+
:guid => create_guid(data),
|
402
|
+
:uid => uid,
|
403
|
+
:message_id => parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']),
|
404
|
+
:rfc822_size => data.attr['RFC822.SIZE'].to_i,
|
405
|
+
:internaldate => Time.parse(data.attr['INTERNALDATE']).to_i,
|
406
|
+
:flags => data.attr['FLAGS']
|
407
|
+
)
|
408
|
+
|
409
|
+
last_good_uid = uid
|
410
|
+
end
|
411
|
+
|
412
|
+
# Set this mailbox's uidnext value to the last known good UID that
|
413
|
+
# was stored in the database, plus 1. This will allow Larch to
|
414
|
+
# resume where the error occurred on the next attempt rather than
|
415
|
+
# having to start over.
|
416
|
+
@db_mailbox.update(:uidnext => last_good_uid + 1)
|
417
|
+
end
|
418
|
+
|
419
|
+
if show_progress
|
420
|
+
fetched += fetch_data.length
|
421
|
+
last_progress = progress
|
422
|
+
progress = ((100 / (options[:progress_total] - options[:progress_start]).to_f) * fetched).round
|
423
|
+
|
424
|
+
info "#{progress}% complete" if progress > last_progress
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
390
429
|
# Examines this mailbox. If _force_ is true, the mailbox will be examined even
|
391
430
|
# if it is already selected (which isn't necessary unless you want to ensure
|
392
431
|
# that it's in a read-only state).
|
data/lib/larch/imap.rb
CHANGED
@@ -30,6 +30,10 @@ class IMAP
|
|
30
30
|
# that it's not actually possible to simulate mailbox creation, so
|
31
31
|
# +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
|
32
32
|
#
|
33
|
+
# [:log_label]
|
34
|
+
# Label to use for this connection in log output. If not specified, the
|
35
|
+
# default label is "[username@host]".
|
36
|
+
#
|
33
37
|
# [:max_retries]
|
34
38
|
# After a recoverable error occurs, retry the operation up to this many
|
35
39
|
# times. Default is 3.
|
@@ -49,8 +53,12 @@ class IMAP
|
|
49
53
|
raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
|
50
54
|
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
51
55
|
|
52
|
-
@options = {:max_retries => 3, :ssl_verify => false}.merge(options)
|
53
56
|
@uri = uri.is_a?(URI) ? uri : URI(uri)
|
57
|
+
@options = {
|
58
|
+
:log_label => "[#{username}@#{host}]",
|
59
|
+
:max_retries => 3,
|
60
|
+
:ssl_verify => false
|
61
|
+
}.merge(options)
|
54
62
|
|
55
63
|
raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
|
56
64
|
|
@@ -65,6 +73,8 @@ class IMAP
|
|
65
73
|
:username => username
|
66
74
|
)
|
67
75
|
|
76
|
+
@db_account.touch
|
77
|
+
|
68
78
|
# Create private convenience methods (debug, info, warn, etc.) to make
|
69
79
|
# logging easier.
|
70
80
|
Logger::LEVELS.each_key do |level|
|
@@ -72,7 +82,7 @@ class IMAP
|
|
72
82
|
|
73
83
|
IMAP.class_eval do
|
74
84
|
define_method(level) do |msg|
|
75
|
-
Larch.log.log(level, "#{
|
85
|
+
Larch.log.log(level, "#{@options[:log_label]} #{msg}")
|
76
86
|
end
|
77
87
|
|
78
88
|
private level
|
@@ -126,8 +136,8 @@ class IMAP
|
|
126
136
|
def mailbox(name, delim = '/')
|
127
137
|
retries = 0
|
128
138
|
|
129
|
-
name
|
130
|
-
name
|
139
|
+
name.gsub!(/^(inbox\/?)/i){ $1.upcase }
|
140
|
+
name.gsub!(delim, self.delim)
|
131
141
|
|
132
142
|
# Gmail doesn't allow folders with leading or trailing whitespace.
|
133
143
|
name.strip! if @quirks[:gmail]
|
@@ -298,7 +308,7 @@ class IMAP
|
|
298
308
|
ssl? && @options[:ssl_verify] ? @options[:ssl_certs] : nil,
|
299
309
|
@options[:ssl_verify])
|
300
310
|
|
301
|
-
info "connected on port #{port}" << (ssl? ? ' using SSL' : '')
|
311
|
+
info "connected to #{host} on port #{port}" << (ssl? ? ' using SSL' : '')
|
302
312
|
|
303
313
|
check_quirks
|
304
314
|
|
@@ -339,6 +349,8 @@ class IMAP
|
|
339
349
|
end
|
340
350
|
|
341
351
|
def update_mailboxes
|
352
|
+
debug "updating mailboxes"
|
353
|
+
|
342
354
|
all = safely { @conn.list('', '*') } || []
|
343
355
|
subscribed = safely { @conn.lsub('', '*') } || []
|
344
356
|
|
@@ -355,7 +367,7 @@ class IMAP
|
|
355
367
|
end
|
356
368
|
|
357
369
|
# Remove mailboxes that no longer exist from the database.
|
358
|
-
@db_account.
|
370
|
+
@db_account.mailboxes_dataset.all do |db_mailbox|
|
359
371
|
db_mailbox.destroy unless @mailboxes.has_key?(db_mailbox.name)
|
360
372
|
end
|
361
373
|
end
|
data/lib/larch/logger.rb
CHANGED
@@ -34,7 +34,7 @@ class Logger
|
|
34
34
|
|
35
35
|
def log(level, msg)
|
36
36
|
return true if LEVELS[level] > LEVELS[@level] || msg.nil? || msg.empty?
|
37
|
-
@output.puts "[#{Time.new.strftime('%
|
37
|
+
@output.puts "[#{Time.new.strftime('%H:%M:%S')}] [#{level}] #{msg}"
|
38
38
|
true
|
39
39
|
|
40
40
|
rescue => e
|
data/lib/larch/version.rb
CHANGED
data/lib/larch.rb
CHANGED
@@ -73,6 +73,7 @@ module Larch
|
|
73
73
|
|
74
74
|
ensure
|
75
75
|
summary
|
76
|
+
db_maintenance
|
76
77
|
end
|
77
78
|
|
78
79
|
# Copies the messages in a single IMAP folder and all its subfolders
|
@@ -98,6 +99,7 @@ module Larch
|
|
98
99
|
|
99
100
|
ensure
|
100
101
|
summary
|
102
|
+
db_maintenance
|
101
103
|
end
|
102
104
|
|
103
105
|
# Opens a connection to the Larch message database, creating it if
|
@@ -145,8 +147,10 @@ module Larch
|
|
145
147
|
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
|
146
148
|
end
|
147
149
|
|
150
|
+
|
148
151
|
private
|
149
152
|
|
153
|
+
|
150
154
|
def copy_mailbox(mailbox_from, mailbox_to)
|
151
155
|
raise ArgumentError, "mailbox_from must be a Larch::IMAP::Mailbox instance" unless mailbox_from.is_a?(Larch::IMAP::Mailbox)
|
152
156
|
raise ArgumentError, "mailbox_to must be a Larch::IMAP::Mailbox instance" unless mailbox_to.is_a?(Larch::IMAP::Mailbox)
|
@@ -172,12 +176,32 @@ module Larch
|
|
172
176
|
imap_from = mailbox_from.imap
|
173
177
|
imap_to = mailbox_to.imap
|
174
178
|
|
175
|
-
@log.info "
|
179
|
+
@log.info "#{imap_from.host}/#{mailbox_from.name} -> #{imap_to.host}/#{mailbox_to.name}"
|
176
180
|
|
177
181
|
@total += mailbox_from.length
|
178
182
|
|
179
|
-
mailbox_from.
|
180
|
-
|
183
|
+
mailbox_from.each_db_message do |from_db_message|
|
184
|
+
guid = from_db_message.guid
|
185
|
+
|
186
|
+
if mailbox_to.has_guid?(guid)
|
187
|
+
next unless @config['sync_flags']
|
188
|
+
|
189
|
+
begin
|
190
|
+
to_db_message = mailbox_to.fetch_db_message(guid)
|
191
|
+
|
192
|
+
if to_db_message.flags != from_db_message.flags
|
193
|
+
new_flags = from_db_message.flags_str
|
194
|
+
new_flags = '(none)' if new_flags.empty?
|
195
|
+
|
196
|
+
@log.info "syncing flags: UID #{to_db_message.uid}: #{new_flags}"
|
197
|
+
mailbox_to.set_flags(guid, from_db_message.flags)
|
198
|
+
end
|
199
|
+
rescue Larch::IMAP::Error => e
|
200
|
+
@log.error e.message
|
201
|
+
end
|
202
|
+
|
203
|
+
next
|
204
|
+
end
|
181
205
|
|
182
206
|
begin
|
183
207
|
next unless msg = mailbox_from.peek(guid)
|
@@ -189,7 +213,7 @@ module Larch
|
|
189
213
|
from = '?'
|
190
214
|
end
|
191
215
|
|
192
|
-
@log.info "copying
|
216
|
+
@log.info "copying: #{from} - #{msg.envelope.subject}"
|
193
217
|
|
194
218
|
mailbox_to << msg
|
195
219
|
@copied += 1
|
@@ -202,6 +226,17 @@ module Larch
|
|
202
226
|
end
|
203
227
|
end
|
204
228
|
|
229
|
+
def db_maintenance
|
230
|
+
@log.debug 'performing database maintenance'
|
231
|
+
|
232
|
+
# Remove accounts that haven't been used in over 30 days.
|
233
|
+
Database::Account.filter(:updated_at => nil).destroy
|
234
|
+
Database::Account.filter('? - updated_at >= 2592000', Time.now.to_i).destroy
|
235
|
+
|
236
|
+
# Release unused disk space and defragment the database.
|
237
|
+
@db.run('VACUUM')
|
238
|
+
end
|
239
|
+
|
205
240
|
def excluded?(name)
|
206
241
|
name = name.downcase
|
207
242
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.0.dev.
|
4
|
+
version: 1.1.0.dev.20091206
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Grove
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-12-
|
12
|
+
date: 2009-12-06 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/larch/db/mailbox.rb
|
71
71
|
- lib/larch/db/message.rb
|
72
72
|
- lib/larch/db/migrate/001_create_schema.rb
|
73
|
+
- lib/larch/db/migrate/002_add_timestamps.rb
|
73
74
|
- lib/larch/errors.rb
|
74
75
|
- lib/larch/imap/mailbox.rb
|
75
76
|
- lib/larch/imap.rb
|