sup 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

@@ -1,3 +1,11 @@
1
+ == 0.5 / 2008-04-22
2
+ * new hooks: extra-contact-addresses, startup
3
+ * '!!' now loads all threads in current search
4
+ * general state saving speedup
5
+ * threads with unsent draft messages are now shown in red
6
+ * --compose spawns a compose-message buffer on startup
7
+ * Many bugfixes and UI improvements
8
+
1
9
  == 0.4 / 2008-01-23
2
10
  * GPG support for signing and encrypting outgoing mail
3
11
  * New hooks: mime attachment, attribution line
@@ -4,6 +4,7 @@ LICENSE
4
4
  Manifest.txt
5
5
  README.txt
6
6
  Rakefile
7
+ ReleaseNotes
7
8
  bin/sup
8
9
  bin/sup-add
9
10
  bin/sup-config
data/Rakefile CHANGED
@@ -9,9 +9,13 @@ end # thanks to "Mike H"
9
9
 
10
10
  ## allow people who use development versions by running "rake gem"
11
11
  ## and installing the resulting gem it to be able to do this. (gem
12
- ## versions must be in dotted-digit notation only).
13
- version = Redwood::VERSION == "git" ? "999" : Redwood::VERSION
14
-
12
+ ## versions must be in dotted-digit notation only and can be passed
13
+ ## with the REL environment variable to "rake gem").
14
+ if ENV['REL']
15
+ version = ENV['REL']
16
+ else
17
+ version = Redwood::VERSION == "git" ? "999" : Redwood::VERSION
18
+ end
15
19
  Hoe.new('sup', version) do |p|
16
20
  p.rubyforge_name = 'sup'
17
21
  p.author = "William Morgan"
@@ -50,3 +54,7 @@ end
50
54
 
51
55
  # vim: syntax=ruby
52
56
  # -*- ruby -*-
57
+ task :upload_report do |t|
58
+ sh "ditz html ditz"
59
+ sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
60
+ end
@@ -0,0 +1,6 @@
1
+ Release 0.5:
2
+
3
+ Saving message state (pressing "$") has been sped up. However, this is only
4
+ automatically in effect for new messages. To make it effective for older
5
+ messages (i.e. messages indexed with versions of Sup before 0.5), you must
6
+ reindex them, e.g. by running sup-sync --all on a source.
data/bin/sup CHANGED
@@ -7,7 +7,7 @@ require 'fileutils'
7
7
  require 'trollop'
8
8
  require "sup"
9
9
 
10
- BIN_VERSION = "0.4"
10
+ BIN_VERSION = "0.5"
11
11
 
12
12
  unless Redwood::VERSION == BIN_VERSION
13
13
  $stderr.puts <<EOS
@@ -32,12 +32,19 @@ Usage:
32
32
 
33
33
  Options are:
34
34
  EOS
35
- opt :list_hooks, "List all hooks and descriptions thereof, and quit."
36
- opt :no_threads, "Turn of threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
35
+ opt :list_hooks, "List all hooks and descriptions, and quit."
36
+ opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
37
37
  opt :no_initial_poll, "Don't poll for new messages when starting."
38
- opt :search, "Search for threads ", :type => String
38
+ opt :search, "Search for this query upon startup", :type => String
39
+ opt :compose, "Compose message to this recipient upon startup", :type => String
39
40
  end
40
41
 
42
+ Redwood::HookManager.register "startup", <<EOS
43
+ Executes at startup
44
+ No variables.
45
+ No return value.
46
+ EOS
47
+
41
48
  if $opts[:list_hooks]
42
49
  Redwood::HookManager.print_hooks
43
50
  exit
@@ -94,7 +101,7 @@ rescue Index::LockError => e
94
101
 
95
102
  case h.ask("Should I ask that process to kill itself? ")
96
103
  when /^\s*y\s*$/i
97
- h.say "Ok, suggesting sepuku..."
104
+ h.say "Ok, suggesting seppuku..."
98
105
  FileUtils.touch Redwood::SUICIDE_FN
99
106
  sleep SuicideManager::DELAY * 2
100
107
  FileUtils.rm_f Redwood::SUICIDE_FN
@@ -127,6 +134,8 @@ begin
127
134
  Index.add_source SentManager.new_source
128
135
  end
129
136
 
137
+ HookManager.run "startup"
138
+
130
139
  log "starting curses"
131
140
  start_cursing
132
141
 
@@ -137,6 +146,8 @@ begin
137
146
  Ncurses::A_BOLD
138
147
  c.add :index_starred_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
139
148
  Ncurses::A_BOLD
149
+ c.add :index_draft_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK,
150
+ Ncurses::A_BOLD
140
151
  c.add :labellist_old_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
141
152
  c.add :labellist_new_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK,
142
153
  Ncurses::A_BOLD
@@ -198,6 +209,10 @@ begin
198
209
 
199
210
  imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
200
211
 
212
+ if $opts[:compose]
213
+ ComposeMode.spawn_nicely :to_default => $opts[:compose]
214
+ end
215
+
201
216
  unless $opts[:no_threads]
202
217
  PollManager.start
203
218
  SuicideManager.start
@@ -298,7 +313,7 @@ ensure
298
313
  Redwood::log "stopped cursing"
299
314
 
300
315
  if SuicideManager.instantiated? && SuicideManager.die?
301
- Redwood::log "I've been ordered to commit sepuku. I obey!"
316
+ Redwood::log "I've been ordered to commit seppuku. I obey!"
302
317
  end
303
318
 
304
319
  if $exceptions.empty?
@@ -133,7 +133,7 @@ begin
133
133
  num_added = num_updated = num_scanned = num_restored = 0
134
134
  last_info_time = start_time = Time.now
135
135
 
136
- Redwood::PollManager.add_messages_from source do |m, offset, entry|
136
+ Redwood::PollManager.add_messages_from source, :force_overwrite => true do |m, offset, entry|
137
137
  num_scanned += 1
138
138
  seen[m.id] = true
139
139
 
@@ -227,6 +227,8 @@ begin
227
227
  $stderr.puts "Deleted #{num_del} / #{num_scanned} messages"
228
228
  end
229
229
 
230
+ index.save
231
+
230
232
  if opts[:optimize]
231
233
  $stderr.puts "Optimizing index..."
232
234
  optt = time { index.index.optimize unless opts[:dry_run] }
@@ -238,7 +240,6 @@ rescue Exception => e
238
240
  File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
239
241
  raise
240
242
  ensure
241
- index.save
242
243
  Redwood::finish
243
244
  index.unlock
244
245
  end
@@ -51,7 +51,11 @@ EOS
51
51
  end
52
52
 
53
53
  unless opts[:drop_deleted] || opts[:move_deleted] || opts[:drop_spam] || opts[:move_spam]
54
- puts "Nothing to do."
54
+ puts <<EOS
55
+ Nothing to do. Please specify at least one of --drop-deleted, --move-deleted,
56
+ --drop-spam, or --move-spam.
57
+ EOS
58
+
55
59
  exit
56
60
  end
57
61
 
@@ -111,7 +111,7 @@ S: The current solution is to directly modify RubyMail. Change line 159 of
111
111
  multipart.rb to:
112
112
  chunk = chunk[0..start]
113
113
  This is because RubyMail hasn't been updated since like Ruby 1.8.2.
114
- Please bug Matt Lickey.
114
+ Please bug Matt Armstrong.
115
115
 
116
116
  P: I see this error:
117
117
  /usr/local/lib/ruby/1.8/yaml.rb:133:in `transfer': allocator undefined for Bignum (TypeError)
data/lib/sup.rb CHANGED
@@ -33,7 +33,7 @@ class Module
33
33
  end
34
34
 
35
35
  module Redwood
36
- VERSION = "0.4"
36
+ VERSION = "0.5"
37
37
 
38
38
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
39
39
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -72,7 +72,7 @@ module Redwood
72
72
  def save_yaml_obj object, fn, safe=false
73
73
  if safe
74
74
  safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
75
- mode = File.stat(fn) if File.exists? fn
75
+ mode = File.stat(fn).mode if File.exists? fn
76
76
  File.open(safe_fn, "w", mode) { |f| f.puts object.to_yaml }
77
77
  FileUtils.mv safe_fn, fn
78
78
  else
@@ -169,7 +169,8 @@ if File.exists? Redwood::CONFIG_FN
169
169
  else
170
170
  require 'etc'
171
171
  require 'socket'
172
- name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first
172
+ name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
173
+ name ||= ENV["USER"]
173
174
  email = ENV["USER"] + "@" +
174
175
  begin
175
176
  Socket.gethostbyname(Socket.gethostname).first
@@ -165,6 +165,15 @@ called at least once per keystroke, so excessive computation is discouraged.
165
165
 
166
166
  Variables: the same as status-bar-text hook.
167
167
  Return value: a string to be used as the terminal title.
168
+ EOS
169
+
170
+ HookManager.register "extra-contact-addresses", <<EOS
171
+ A list of extra addresses to propose for tab completion, etc. when the
172
+ user is entering an email address. Can be plain email addresses or can
173
+ be full "User Name <email@domain.tld>" entries.
174
+
175
+ Variables: none
176
+ Return value: an array of email address strings.
168
177
  EOS
169
178
 
170
179
  def initialize
@@ -263,7 +272,8 @@ EOS
263
272
  get_status_and_title @focus_buf # must be called outside of the ncurses lock
264
273
  end
265
274
 
266
- print "\033]2;#{title}\07" if title && @in_x
275
+ ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
276
+ print "\033]0;#{title}\07" if title && @in_x
267
277
 
268
278
  Ncurses.mutex.lock unless opts[:sync] == false
269
279
 
@@ -455,7 +465,7 @@ EOS
455
465
  elsif File.directory?(answer)
456
466
  spawn_modal "file browser", FileBrowserMode.new(answer)
457
467
  else
458
- answer
468
+ File.expand_path answer
459
469
  end
460
470
  end
461
471
 
@@ -492,6 +502,7 @@ EOS
492
502
  contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
493
503
 
494
504
  completions = (recent + contacts).flatten.uniq.sort
505
+ completions += HookManager.run("extra-contact-addresses") || []
495
506
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
496
507
 
497
508
  if answer
@@ -557,7 +568,6 @@ EOS
557
568
 
558
569
  def ask_getch question, accept=nil
559
570
  raise "impossible!" if @asking
560
- @asking = true
561
571
 
562
572
  accept = accept.split(//).map { |x| x[0] } if accept
563
573
 
@@ -570,6 +580,7 @@ EOS
570
580
  Ncurses.refresh
571
581
  end
572
582
 
583
+ @asking = true
573
584
  ret = nil
574
585
  done = false
575
586
  until done
@@ -51,7 +51,7 @@ class ContactManager
51
51
 
52
52
  def save
53
53
  File.open(@fn, "w") do |f|
54
- @p2a.each do |p, a|
54
+ @p2a.sort_by { |(p, a)| [p.full_address, a] }.each do |(p, a)|
55
55
  f.puts "#{a || ''}: #{p.full_address}"
56
56
  end
57
57
  end
@@ -40,7 +40,7 @@ class CryptoManager
40
40
  raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
41
41
 
42
42
  envelope = RMail::Message.new
43
- envelope.header["Content-Type"] = 'multipart/signed; protocol="application/pgp-signature"; micalg=pgp-sha1'
43
+ envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
44
44
 
45
45
  envelope.add_part payload
46
46
  signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
@@ -86,7 +86,7 @@ class HookManager
86
86
  log "error running hook: #{e.message}"
87
87
  log e.backtrace.join("\n")
88
88
  @hooks[name] = nil # disable it
89
- BufferManager.flash "Error running hook: #{e.message}"
89
+ BufferManager.flash "Error running hook: #{e.message}" if BufferManager.instantiated?
90
90
  end
91
91
  context.__cleanup
92
92
  result
@@ -176,7 +176,7 @@ class IMAP < Source
176
176
 
177
177
  def end_offset
178
178
  unsynchronized_scan_mailbox
179
- @ids.last
179
+ @ids.last + 1
180
180
  end
181
181
  synchronized :end_offset
182
182
 
@@ -125,9 +125,13 @@ EOS
125
125
  @sources[source.id] = source
126
126
  end
127
127
 
128
- def source_for uri; @sources.values.find { |s| s.is_source_for? uri }; end
129
- def usual_sources; @sources.values.find_all { |s| s.usual? }; end
130
- def sources; @sources.values; end
128
+ def sources
129
+ ## favour the inbox by listing non-archived sources first
130
+ @sources.values.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
131
+ end
132
+
133
+ def source_for uri; sources.find { |s| s.is_source_for? uri }; end
134
+ def usual_sources; sources.find_all { |s| s.usual? }; end
131
135
 
132
136
  def load_index dir=File.join(@dir, "ferret")
133
137
  if File.exists? dir
@@ -137,11 +141,11 @@ EOS
137
141
  else
138
142
  Redwood::log "creating index..."
139
143
  field_infos = Ferret::Index::FieldInfos.new :store => :yes
140
- field_infos.add_field :message_id
144
+ field_infos.add_field :message_id, :index => :untokenized
141
145
  field_infos.add_field :source_id
142
146
  field_infos.add_field :source_info
143
147
  field_infos.add_field :date, :index => :untokenized
144
- field_infos.add_field :body, :store => :no
148
+ field_infos.add_field :body
145
149
  field_infos.add_field :label
146
150
  field_infos.add_field :subject
147
151
  field_infos.add_field :from
@@ -157,7 +161,7 @@ EOS
157
161
  ## and adding either way. Index state will be determined by m.labels.
158
162
  ##
159
163
  ## docid and entry can be specified if they're already known.
160
- def sync_message m, docid=nil, entry=nil
164
+ def sync_message m, docid=nil, entry=nil, opts={}
161
165
  docid, entry = load_entry_for_id m.id unless docid && entry
162
166
 
163
167
  raise "no source info for message #{m.id}" unless m.source && m.source_info
@@ -170,7 +174,6 @@ EOS
170
174
  m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
171
175
  end
172
176
 
173
- to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
174
177
  snippet =
175
178
  if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
176
179
  ""
@@ -178,18 +181,52 @@ EOS
178
181
  m.snippet
179
182
  end
180
183
 
184
+ ## write the new document to the index. if the entry already exists in the
185
+ ## index, reuse it (which avoids having to reload the entry from the source,
186
+ ## which can be quite expensive for e.g. large threads of IMAP actions.)
187
+ ##
188
+ ## exception: if the index entry belongs to an earlier version of the
189
+ ## message, use everything from the new message instead, but union the
190
+ ## flags. this allows messages sent to mailing lists to have their header
191
+ ## updated and to have flags set properly.
192
+ ##
193
+ ## minor hack: messages in sources with lower ids have priority over
194
+ ## messages in sources with higher ids. so messages in the inbox will
195
+ ## override everyone, and messages in the sent box will be overridden
196
+ ## by everyone else.
197
+ ##
198
+ ## written in this manner to support previous versions of the index which
199
+ ## did not keep around the entry body. upgrading is thus seamless.
200
+ entry ||= {}
201
+ labels = m.labels.uniq # override because this is the new state, unless...
202
+
203
+ ## if we are a later version of a message, ignore what's in the index,
204
+ ## but merge in the labels.
205
+ if entry[:source_id] && entry[:source_info] && entry[:label] &&
206
+ ((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
207
+ labels = (entry[:label].split(/\s+/).map { |l| l.intern } + m.labels).uniq
208
+ #Redwood::log "found updated version of message #{m.id}: #{m.subj}"
209
+ #Redwood::log "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
210
+ #Redwood::log "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
211
+ entry = {}
212
+ end
213
+
214
+ ## if force_overwite is true, ignore what's in the index. this is used
215
+ ## primarily by sup-sync to force index updates.
216
+ entry = {} if opts[:force_overwrite]
217
+
181
218
  d = {
182
219
  :message_id => m.id,
183
220
  :source_id => source_id,
184
221
  :source_info => m.source_info,
185
- :date => m.date.to_indexable_s,
186
- :body => m.content,
187
- :snippet => snippet,
188
- :label => m.labels.uniq.join(" "),
189
- :from => m.from ? m.from.email : "",
190
- :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
191
- :subject => wrap_subj(m.subj),
192
- :refs => (m.refs + m.replytos).uniq.join(" "),
222
+ :date => (entry[:date] || m.date.to_indexable_s),
223
+ :body => (entry[:body] || m.indexable_content),
224
+ :snippet => snippet, # always override
225
+ :label => labels.uniq.join(" "),
226
+ :from => (entry[:from] || (m.from ? m.from.indexable_content : "")),
227
+ :to => (entry[:to] || (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" ")),
228
+ :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
229
+ :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
193
230
  }
194
231
 
195
232
  @index.delete docid if docid
@@ -250,6 +287,7 @@ EOS
250
287
  searched = {}
251
288
  num_queries = 0
252
289
 
290
+ pending = [m.id]
253
291
  if $config[:thread_by_subject] # do subject queries
254
292
  date_min = m.date - (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
255
293
  date_max = m.date + (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
@@ -264,10 +302,13 @@ EOS
264
302
 
265
303
  q = build_query :qobj => q
266
304
 
267
- pending = @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] }
268
- Redwood::log "found #{pending.size} results for subject query #{q}"
269
- else
270
- pending = [m.id]
305
+ p1 = @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] }
306
+ Redwood::log "found #{p1.size} results for subject query #{q}"
307
+
308
+ p2 = @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] }
309
+ Redwood::log "found #{p2.size} results in string form"
310
+
311
+ pending = (pending + p1 + p2).uniq
271
312
  end
272
313
 
273
314
  until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
@@ -396,18 +437,7 @@ protected
396
437
  def parse_user_query_string s
397
438
  extraopts = {}
398
439
 
399
- ## this is a little hacky, but it works, at least until ferret changes
400
- ## its api. we parse the user query string with ferret twice: the first
401
- ## time we just turn the resulting object back into a string, which has
402
- ## the next effect of transforming the original string into a nice
403
- ## normalized form with + and - instead of AND, OR, etc. then we do some
404
- ## string substitutions which depend on this normalized form, re-parse
405
- ## the string with Ferret, and return the resulting query object.
406
-
407
- norms = @qparser.parse(s).to_s
408
- Redwood::log "normalized #{s.inspect} to #{norms.inspect}" unless s == norms
409
-
410
- subs = norms.gsub(/\b(to|from):(\S+)\b/) do
440
+ subs = s.gsub(/\b(to|from):(\S+)\b/) do
411
441
  field, name = $1, $2
412
442
  if(p = ContactManager.contact_for(name))
413
443
  [field, p.email]
@@ -477,7 +507,6 @@ protected
477
507
  subs = nil if chronic_failure
478
508
  end
479
509
 
480
- Redwood::log "translated #{norms.inspect} to #{subs.inspect}" unless subs == norms
481
510
  if subs
482
511
  [@qparser.parse(subs), extraopts]
483
512
  else
@@ -512,7 +541,7 @@ protected
512
541
  File.chmod 0600, fn
513
542
  FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
514
543
  end
515
- Redwood::save_yaml_obj @sources.values.sort_by { |s| s.id.to_i }, fn, true
544
+ Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
516
545
  File.chmod 0600, fn
517
546
  end
518
547
  @sources_dirty = false