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

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