larch 1.1.0.dev.20100120 → 1.1.0.dev.20100206

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
@@ -18,6 +18,9 @@ Version 1.1.0 (git)
18
18
  * Folders are now copied recursively by default.
19
19
  * Progress information is now displayed regularly while scanning large
20
20
  mailboxes.
21
+ * Added --delete option to delete messages from the source after copying them
22
+ to the destination, or if they already exist at the destination.
23
+ * Added --expunge option to expunge deleted messages from the source.
21
24
  * Added --sync-flags option to synchronize message flags (like Seen, Flagged,
22
25
  etc.) from the source server to the destination server for messages that
23
26
  already exist on the destination.
@@ -10,7 +10,7 @@ accounts.
10
10
 
11
11
  *Author*:: Ryan Grove (mailto:ryan@wonko.com)
12
12
  *Version*:: 1.1.0 (git)
13
- *Copyright*:: Copyright (c) 2009 Ryan Grove. All rights reserved.
13
+ *Copyright*:: Copyright (c) 2010 Ryan Grove. All rights reserved.
14
14
  *License*:: GPL 2.0 (http://opensource.org/licenses/gpl-2.0.php)
15
15
  *Website*:: http://github.com/rgrove/larch
16
16
 
@@ -28,7 +28,7 @@ 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
33
  --from, -f <s>: URI of the source IMAP server
34
34
  --from-folder, -F <s>: Source folder to copy from (default: INBOX)
@@ -39,13 +39,16 @@ Latest development version:
39
39
  --to-pass, -P <s>: Destination server password (default: prompt)
40
40
  --to-user, -U <s>: Destination server username (default: prompt)
41
41
 
42
- Sync Options:
42
+ Copy Options:
43
43
  --all, -a: Copy all folders recursively
44
44
  --all-subscribed, -s: Copy all subscribed folders recursively
45
+ --delete, -d: Delete messages from the source after copying
46
+ them, or if they already exist at the destination
45
47
  --exclude <s+>: List of mailbox names/patterns that shouldn't be
46
48
  copied
47
49
  --exclude-file <s>: Filename containing mailbox names/patterns that
48
50
  shouldn't be copied
51
+ --expunge, -x: Expunge deleted messages from the source
49
52
  --sync-flags, -S: Sync message flags from the source to the
50
53
  destination for messages that already exist at the
51
54
  destination
@@ -270,18 +273,16 @@ fairly well-known trick. That said, as with anything tricky, there are caveats.
270
273
 
271
274
  ==== No hierarchical folders
272
275
 
273
- Similar to Gmail, Yahoo!'s IMAP gateway doesn't allow hierarchical (nested)
274
- folders. If you try to copy a folder hierarchy to Yahoo!, it will work, but
275
- you'll end up with a set of folders named "folder" and "folder.subfolder"
276
- rather than seeing "subfolder" as an actual subfolder of "folder".
276
+ Similar to Gmail, Yahoo! Mail doesn't allow hierarchical (nested) folders. If
277
+ you try to copy a folder hierarchy to Yahoo!, it will work, but you'll end up
278
+ with a set of folders named "folder" and "folder.subfolder" rather than seeing
279
+ "subfolder" as an actual subfolder of "folder".
277
280
 
278
281
  ==== No custom flags
279
282
 
280
- Yahoo! Mail IMAP doesn't appear to support custom message flags, such as the
281
- labels and junk/not junk flags used by Thunderbird. When transferring messages
282
- with custom flags to a Yahoo! Mail IMAP account, the custom flags will be lost.
283
- Also, Larch's <tt>sync-flags</tt> option will not work correctly if you have
284
- messages with custom flags.
283
+ Yahoo! Mail IMAP doesn't support custom message flags, such as the tags and
284
+ junk/not junk flags used by Thunderbird. When transferring messages with custom
285
+ flags to a Yahoo! Mail IMAP account, the custom flags will be lost.
285
286
 
286
287
  ==== Here there be dragons
287
288
 
@@ -336,7 +337,7 @@ Gray II).
336
337
 
337
338
  == License
338
339
 
339
- Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
340
+ Copyright (c) 2010 Ryan Grove <ryan@wonko.com>
340
341
 
341
342
  Licensed under the GNU General Public License version 2.0.
342
343
 
data/bin/larch CHANGED
@@ -12,7 +12,7 @@ module Larch
12
12
  options = Trollop.options do
13
13
  version "Larch #{APP_VERSION}\n" << APP_COPYRIGHT
14
14
  banner <<-EOS
15
- Larch syncs messages from one IMAP server to another. Awesomely.
15
+ Larch copies messages from one IMAP server to another. Awesomely.
16
16
 
17
17
  Usage:
18
18
  larch [config section] [options]
@@ -29,11 +29,13 @@ EOS
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
31
31
 
32
- text "\nSync Options:"
32
+ text "\nCopy Options:"
33
33
  opt :all, "Copy all folders recursively", :short => '-a'
34
34
  opt :all_subscribed, "Copy all subscribed folders recursively", :short => '-s'
35
+ opt :delete, "Delete messages from the source after copying them, or if they already exist at the destination", :short => '-d'
35
36
  opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
36
37
  opt :exclude_file, "Filename containing mailbox names/patterns that shouldn't be copied", :short => :none, :type => :string
38
+ opt :expunge, "Expunge deleted messages from the source", :short => '-x'
37
39
  opt :sync_flags, "Sync message flags from the source to the destination for messages that already exist at the destination", :short => '-S'
38
40
 
39
41
  text "\nGeneral Options:"
@@ -38,9 +38,10 @@ module Larch
38
38
  Net::IMAP.debug = true if @log.level == :insane
39
39
 
40
40
  # Stats
41
- @copied = 0
42
- @failed = 0
43
- @total = 0
41
+ @copied = 0
42
+ @deleted = 0
43
+ @failed = 0
44
+ @total = 0
44
45
  end
45
46
 
46
47
  # Recursively copies all messages in all folders from the source to the
@@ -49,9 +50,10 @@ module Larch
49
50
  raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
50
51
  raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
51
52
 
52
- @copied = 0
53
- @failed = 0
54
- @total = 0
53
+ @copied = 0
54
+ @deleted = 0
55
+ @failed = 0
56
+ @total = 0
55
57
 
56
58
  imap_from.each_mailbox do |mailbox_from|
57
59
  next if excluded?(mailbox_from.name)
@@ -82,9 +84,10 @@ module Larch
82
84
  raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
83
85
  raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
84
86
 
85
- @copied = 0
86
- @failed = 0
87
- @total = 0
87
+ @copied = 0
88
+ @deleted = 0
89
+ @failed = 0
90
+ @total = 0
88
91
 
89
92
  mailbox_from = imap_from.mailbox(imap_from.uri_mailbox || 'INBOX')
90
93
  mailbox_to = imap_to.mailbox(imap_to.uri_mailbox || 'INBOX')
@@ -144,7 +147,7 @@ module Larch
144
147
  end
145
148
 
146
149
  def summary
147
- @log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
150
+ @log.info "#{@copied} message(s) copied, #{@failed} failed, #{@deleted} deleted out of #{@total} total"
148
151
  end
149
152
 
150
153
 
@@ -182,19 +185,26 @@ module Larch
182
185
 
183
186
  mailbox_from.each_db_message do |from_db_message|
184
187
  guid = from_db_message.guid
188
+ uid = from_db_message.uid
185
189
 
186
190
  if mailbox_to.has_guid?(guid)
187
- next unless @config['sync_flags']
188
-
189
191
  begin
190
- to_db_message = mailbox_to.fetch_db_message(guid)
192
+ if @config['sync_flags']
193
+ to_db_message = mailbox_to.fetch_db_message(guid)
194
+
195
+ if to_db_message.flags != from_db_message.flags
196
+ new_flags = from_db_message.flags_str
197
+ new_flags = '(none)' if new_flags.empty?
191
198
 
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?
199
+ @log.info "[>] syncing flags: uid #{uid}: #{new_flags}"
200
+ mailbox_to.set_flags(guid, from_db_message.flags)
201
+ end
202
+ end
195
203
 
196
- @log.info "syncing flags: UID #{to_db_message.uid}: #{new_flags}"
197
- mailbox_to.set_flags(guid, from_db_message.flags)
204
+ if @config['delete'] && !from_db_message.flags.include?(:Deleted)
205
+ @log.info "[<] deleting uid #{uid} (already exists at destination)"
206
+ mailbox_from.set_flags(guid, [:Deleted], true)
207
+ @deleted += 1
198
208
  end
199
209
  rescue Larch::IMAP::Error => e
200
210
  @log.error e.message
@@ -213,17 +223,32 @@ module Larch
213
223
  from = '?'
214
224
  end
215
225
 
216
- @log.info "copying: #{from} - #{msg.envelope.subject}"
226
+ @log.info "[>] copying uid #{uid}: #{from} - #{msg.envelope.subject}"
217
227
 
218
228
  mailbox_to << msg
219
229
  @copied += 1
220
230
 
231
+ if @config['delete']
232
+ @log.info "[<] deleting uid #{uid}"
233
+ mailbox_from.set_flags(guid, [:Deleted], true)
234
+ @deleted += 1
235
+ end
236
+
221
237
  rescue Larch::IMAP::Error => e
222
238
  @failed += 1
223
239
  @log.error e.message
224
240
  next
225
241
  end
226
242
  end
243
+
244
+ if @config['expunge']
245
+ begin
246
+ @log.debug "[<] expunging deleted messages"
247
+ mailbox_from.expunge
248
+ rescue Larch::IMAP::Error => e
249
+ @log.error e.message
250
+ end
251
+ end
227
252
  end
228
253
 
229
254
  def db_maintenance
@@ -8,9 +8,11 @@ class Config
8
8
  'all-subscribed' => false,
9
9
  'config' => File.join('~', '.larch', 'config.yaml'),
10
10
  'database' => File.join('~', '.larch', 'larch.db'),
11
+ 'delete' => false,
11
12
  'dry-run' => false,
12
13
  'exclude' => [],
13
14
  'exclude-file' => nil,
15
+ 'expunge' => false,
14
16
  'from' => nil,
15
17
  'from-folder' => nil, # actually INBOX; see validate()
16
18
  'from-pass' => nil,
@@ -198,7 +198,7 @@ class IMAP
198
198
 
199
199
  raise unless (retries += 1) <= @options[:max_retries]
200
200
 
201
- warn "#{e.class.name}: #{e.message} (reconnecting)"
201
+ warning "#{e.class.name}: #{e.message} (reconnecting)"
202
202
 
203
203
  reset
204
204
  sleep 1 * retries
@@ -211,7 +211,7 @@ class IMAP
211
211
 
212
212
  raise unless (retries += 1) <= @options[:max_retries]
213
213
 
214
- warn "#{e.class.name}: #{e.message} (will retry)"
214
+ warning "#{e.class.name}: #{e.message} (will retry)"
215
215
 
216
216
  sleep 1 * retries
217
217
  retry
@@ -292,7 +292,7 @@ class IMAP
292
292
  # verification errors.
293
293
  raise if e.is_a?(OpenSSL::SSL::SSLError) && e.message =~ /certificate verify failed/
294
294
 
295
- warn "#{e.class.name}: #{e.message} (will retry)"
295
+ warning "#{e.class.name}: #{e.message} (will retry)"
296
296
 
297
297
  reset
298
298
  sleep 1 * retries
@@ -74,21 +74,8 @@ class Mailbox
74
74
  raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
75
75
  end
76
76
 
77
- flags = message.flags.dup
78
-
79
- # Don't set any flags that aren't supported on the destination mailbox.
80
- flags.delete_if do |flag|
81
- # The \Recent flag is read-only, so we shouldn't try to set it.
82
- next true if flag == :Recent
83
-
84
- unless @flags.include?(flag) || @perm_flags.include?(:*) || @perm_flags.include?(flag)
85
- info "flag not supported on destination: #{flag}"
86
- true
87
- end
88
- end
89
-
90
77
  debug "appending message: #{message.guid}"
91
- @imap.conn.append(@name_utf7, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
78
+ @imap.conn.append(@name_utf7, message.rfc822, get_supported_flags(message.flags), message.internaldate) unless @imap.options[:dry_run]
92
79
  end
93
80
 
94
81
  true
@@ -114,6 +101,19 @@ class Mailbox
114
101
  mailboxes.each {|mb| yield mb }
115
102
  end
116
103
 
104
+ # Expunges this mailbox, permanently removing all messages with the \Deleted
105
+ # flag.
106
+ def expunge
107
+ return false unless imap_select
108
+
109
+ @imap.safely do
110
+ debug "expunging deleted messages"
111
+
112
+ @last_scan = nil
113
+ @imap.conn.expunge unless @imap.options[:dry_run]
114
+ end
115
+ end
116
+
117
117
  # Returns a Larch::IMAP::Message struct representing the message with the
118
118
  # specified Larch _guid_, or +nil+ if the specified guid was not found in this
119
119
  # mailbox.
@@ -121,7 +121,7 @@ class Mailbox
121
121
  scan
122
122
 
123
123
  unless db_message = fetch_db_message(guid)
124
- warn "message not found in local db: #{guid}"
124
+ warning "message not found in local db: #{guid}"
125
125
  return nil
126
126
  end
127
127
 
@@ -135,7 +135,7 @@ class Mailbox
135
135
  data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
136
136
  end
137
137
 
138
- warn "message not found on server: #{guid}"
138
+ warning "message not found on server: #{guid}"
139
139
  return nil
140
140
  end
141
141
  alias [] fetch
@@ -242,16 +242,28 @@ class Mailbox
242
242
  return
243
243
  end
244
244
 
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)
245
+ # Sets the IMAP flags for the message specified by _guid_. _flags_ should be
246
+ # an array of symbols for standard flags, strings for custom flags.
247
+ #
248
+ # If _merge_ is +true+, the specified flags will be merged with the message's
249
+ # existing flags. Otherwise, all existing flags will be cleared and replaced
250
+ # with the specified flags.
251
+ #
252
+ # Note that the :Recent flag cannot be manually set or removed.
253
+ #
254
+ # Returns +true+ on success, +false+ on failure.
255
+ def set_flags(guid, flags, merge = false)
249
256
  raise ArgumentError, "flags must be an Array" unless flags.is_a?(Array)
250
257
 
251
- db_message = fetch_db_message(guid)
252
- return false if db_message.nil? || !imap_select
258
+ return false unless db_message = fetch_db_message(guid)
259
+
260
+ merged_flags = merge ? (db_message.flags + flags).uniq : flags
261
+ supported_flags = get_supported_flags(merged_flags)
253
262
 
254
- @imap.safely { @imap.conn.uid_store(db_message.uid, 'FLAGS.SILENT', flags) } unless @imap.options[:dry_run]
263
+ return true if db_message.flags == supported_flags
264
+
265
+ return false if !imap_select
266
+ @imap.safely { @imap.conn.uid_store(db_message.uid, 'FLAGS.SILENT', supported_flags) } unless @imap.options[:dry_run]
255
267
 
256
268
  true
257
269
  end
@@ -317,6 +329,24 @@ class Mailbox
317
329
  end
318
330
  end
319
331
 
332
+ # Returns only the flags from the specified _flags_ array that can be set in
333
+ # this mailbox. Emits a warning message for any unsupported flags.
334
+ def get_supported_flags(flags)
335
+ supported_flags = flags.dup
336
+
337
+ supported_flags.delete_if do |flag|
338
+ # The \Recent flag is read-only, so we shouldn't try to set it.
339
+ next true if flag == :Recent
340
+
341
+ unless @flags.include?(flag) || @perm_flags.include?(:*) || @perm_flags.include?(flag)
342
+ warning "flag not supported on destination: #{flag}"
343
+ true
344
+ end
345
+ end
346
+
347
+ supported_flags
348
+ end
349
+
320
350
  # Fetches the latest flags from the server for the specified range of message
321
351
  # UIDs.
322
352
  def fetch_flags(flag_range)
@@ -563,7 +593,7 @@ class Mailbox
563
593
  raise unless e.message == 'Some messages could not be FETCHed (Failure)'
564
594
 
565
595
  # Workaround for stupid Gmail shenanigans.
566
- warn "Gmail error: '#{e.message}'; continuing anyway"
596
+ warning "Gmail error: '#{e.message}'; continuing anyway"
567
597
  end
568
598
 
569
599
  next data
@@ -4,12 +4,13 @@ class Logger
4
4
  attr_reader :level, :output
5
5
 
6
6
  LEVELS = {
7
- :fatal => 0,
8
- :error => 1,
9
- :warn => 2,
10
- :info => 3,
11
- :debug => 4,
12
- :insane => 5
7
+ :fatal => 0,
8
+ :error => 1,
9
+ :warn => 2,
10
+ :warning => 2,
11
+ :info => 3,
12
+ :debug => 4,
13
+ :insane => 5
13
14
  }
14
15
 
15
16
  def initialize(level = :info, output = $stdout)
@@ -1,9 +1,9 @@
1
1
  module Larch
2
2
  APP_NAME = 'Larch'
3
- APP_VERSION = '1.1.0.dev.20100120'
3
+ APP_VERSION = '1.1.0.dev.20100206'
4
4
  APP_AUTHOR = 'Ryan Grove'
5
5
  APP_EMAIL = 'ryan@wonko.com'
6
6
  APP_URL = 'http://github.com/rgrove/larch/'
7
- APP_COPYRIGHT = 'Copyright (c) 2009 Ryan Grove <ryan@wonko.com>. All ' <<
7
+ APP_COPYRIGHT = 'Copyright (c) 2010 Ryan Grove <ryan@wonko.com>. All ' <<
8
8
  'rights reserved.'
9
9
  end
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.20100120
4
+ version: 1.1.0.dev.20100206
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: 2010-01-20 00:00:00 -08:00
12
+ date: 2010-02-06 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -104,6 +104,6 @@ rubyforge_project:
104
104
  rubygems_version: 1.3.5
105
105
  signing_key:
106
106
  specification_version: 3
107
- summary: Larch syncs messages from one IMAP server to another. Awesomely.
107
+ summary: Larch copies messages from one IMAP server to another. Awesomely.
108
108
  test_files: []
109
109