larch 1.1.0.dev.20091203 → 1.1.0.dev.20091206

Sign up to get free protection for your applications and to get access to all the features.
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