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 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
- --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)
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
- --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
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
- --config, -c <s>: Specify a non-default config file to use (default:
52
- ~/.larch/config.yaml)
53
- --database <s>: Specify a non-default message database to use
54
- (default: ~/.larch/larch.db)
55
- --dry-run, -n: Don't actually make any changes
56
- --max-retries <i>: Maximum number of times to retry after a recoverable
57
- error (default: 3)
58
- --no-create-folder: Don't create destination folders that don't already
59
- exist
60
- --ssl-certs <s>: Path to a trusted certificate bundle to use to
61
- verify server SSL certificates
62
- --ssl-verify: Verify server SSL certificates
63
- --verbosity, -V <s>: Output verbosity: debug, info, warn, error, or fatal
64
- (default: info)
65
- --version, -v: Print version and exit
66
- --help, -h: Show this message
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.", :short => '-f', :type => :string
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.", :short => '-t', :type => :string
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
@@ -19,6 +19,7 @@ class Config
19
19
  'no-create-folder' => false,
20
20
  'ssl-certs' => nil,
21
21
  'ssl-verify' => false,
22
+ 'sync-flags' => false,
22
23
  'to' => nil,
23
24
  'to-folder' => nil, # actually INBOX; see validate()
24
25
  'to-pass' => nil,
@@ -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
@@ -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
@@ -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.username}@#{@imap.host}: #{@name}: #{msg}")
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
- scan
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 = @db_mailbox.messages_dataset.filter(:guid => guid).first
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
- # Open the mailbox for read-only access.
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
- start = @db_mailbox.messages_dataset.count + 1
272
- total = status['MESSAGES']
273
- fetched = 0
274
- progress = 0
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
- @db_mailbox.update(:uidnext => last_good_uid + 1)
304
- end
241
+ @db_mailbox.update(:uidnext => status['UIDNEXT'])
242
+ return
243
+ end
305
244
 
306
- if show_progress
307
- fetched += fetch_data.length
308
- last_progress = progress
309
- progress = ((100 / (total - start).to_f) * fetched).round
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
- info "#{progress}% complete" if progress > last_progress
312
- end
313
- end
251
+ db_message = fetch_db_message(guid)
252
+ return false if db_message.nil? || !imap_select
314
253
 
315
- rescue => e
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
- @db_mailbox.update(:uidnext => status['UIDNEXT'])
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, "#{username}@#{host}: #{msg}")
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 = name.gsub(delim, self.delim)
130
- name = 'INBOX' if name.downcase == 'inbox'
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.mailboxes.each do |db_mailbox|
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('%b %d %H:%M:%S')}] [#{level}] #{msg}"
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
@@ -1,6 +1,6 @@
1
1
  module Larch
2
2
  APP_NAME = 'Larch'
3
- APP_VERSION = '1.1.0.dev.20091203'
3
+ APP_VERSION = '1.1.0.dev.20091206'
4
4
  APP_AUTHOR = 'Ryan Grove'
5
5
  APP_EMAIL = 'ryan@wonko.com'
6
6
  APP_URL = 'http://github.com/rgrove/larch/'
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 "copying messages from #{imap_from.host}/#{mailbox_from.name} to #{imap_to.host}/#{mailbox_to.name}"
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.each_guid do |guid|
180
- next if mailbox_to.has_guid?(guid)
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 message: #{from} - #{msg.envelope.subject}"
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.20091203
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-03 00:00:00 -08:00
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