bin/sup-tweak-labels (4253B) - raw
1 #!/usr/bin/env ruby
2
3 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
5 require 'optimist'
6 require "sup"
7
8 class Float
9 def to_s; sprintf '%.2f', self; end
10 def to_time_s
11 infinite? ? "unknown" : super
12 end
13 end
14
15 class Numeric
16 def to_time_s
17 i = to_i
18 sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
19 end
20 end
21
22 def time
23 startt = Time.now
24 yield
25 Time.now - startt
26 end
27
28 opts = Optimist::options do
29 version "sup-tweak-labels (sup #{Redwood::VERSION})"
30 banner <<EOS
31 Batch modification of message state for messages already in the index.
32
33 Usage:
34 sup-tweak-labels [options] <source>*
35
36 where <source>* is zero or more source URIs. Supported source URI schemes can
37 be seen by running "sup-add --help".
38
39 Options:
40 EOS
41 opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
42 opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
43 opt :query, "A Sup search query", :type => String
44
45 text <<EOS
46
47 Other options:
48 EOS
49 opt :verbose, "Print message ids as they're processed."
50 opt :very_verbose, "Print message names and subjects as they're processed."
51 opt :all_sources, "Scan over all sources.", :short => :none
52 opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
53 opt :no_sync_back, "Do not sync back to the original Maildir."
54 opt :version, "Show version information", :short => :none
55 end
56 opts[:verbose] = true if opts[:very_verbose]
57
58 add_labels = opts[:add].to_set_of_symbols ","
59 remove_labels = opts[:remove].to_set_of_symbols ","
60
61 Optimist::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
62
63 Redwood::start
64 index = Redwood::Index.init
65 index.lock_interactively or exit
66
67 begin
68 index.load
69
70 source_ids = if opts[:all_sources]
71 Redwood::SourceManager.sources
72 else
73 ARGV.map do |uri|
74 Redwood::SourceManager.source_for uri or Optimist::die "Unknown source: #{uri}. Did you add it with sup-add first?"
75 end
76 end.map { |s| s.id }
77 Optimist::die "nothing to do: no sources" if source_ids.empty?
78
79 query = "(" + source_ids.map { |id| "source_id:#{id}" }.join(" OR ") + ")"
80 if add_labels.empty?
81 ## if all we're doing is removing labels, we can further restrict the
82 ## query to only messages with those labels
83 query += " (" + remove_labels.map { |l| "label:#{l}" }.join(" OR ") + ")"
84 end
85 query += ' AND ' + opts[:query] if opts[:query]
86
87 parsed_query = index.parse_query query
88 parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
89 ids = index.to_enum(:each_id, parsed_query)
90 num_total = index.num_results_for parsed_query
91
92 $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
93
94 num_changed = num_scanned = 0
95 last_info_time = start_time = Time.now
96 ids.each do |id|
97 num_scanned += 1
98
99 m = index.build_message id
100 old_labels = m.labels.dup
101
102 m.labels += add_labels
103 m.labels -= remove_labels
104
105 unless m.labels == old_labels
106 num_changed += 1
107 puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
108 puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
109 puts if opts[:very_verbose]
110 unless opts[:dry_run]
111 index.update_message_state [m, false]
112 m.sync_back unless opts[:no_sync_back]
113 end
114 end
115
116 if Time.now - last_info_time > 60
117 last_info_time = Time.now
118 elapsed = last_info_time - start_time
119 pctdone = 100.0 * num_scanned.to_f / num_total.to_f
120 remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
121 $stderr.puts "## #{num_scanned} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
122 end
123 end
124 $stderr.puts "Scanned #{num_scanned} / #{num_total} messages and changed #{num_changed}."
125
126 unless num_changed == 0
127 $stderr.puts "Optimizing index..."
128 index.optimize unless opts[:dry_run]
129 end
130
131 rescue Exception => e
132 File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
133 raise
134 ensure
135 index.save
136 Redwood::finish
137 index.unlock
138 end
139