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.

@@ -75,9 +75,11 @@ class Maildir < Source
75
75
  with_file_for(id) { |f| f.readlines.join }
76
76
  end
77
77
 
78
- def scan_mailbox
78
+ def scan_mailbox opts={}
79
+ return unless @ids.empty? || opts[:rescan]
79
80
  return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
80
81
 
82
+ Redwood::log "scanning maildir..."
81
83
  cdir = File.join(@dir, 'cur')
82
84
  ndir = File.join(@dir, 'new')
83
85
 
@@ -85,21 +87,21 @@ class Maildir < Source
85
87
  raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
86
88
 
87
89
  begin
88
- @ids, @ids_to_fns = @mutex.synchronize do
89
- ids, ids_to_fns = [], {}
90
- (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
91
- id = make_id fn
92
- ids << id
93
- ids_to_fns[id] = fn
94
- end
95
- [ids.sort, ids_to_fns]
90
+ @ids, @ids_to_fns = [], {}
91
+ (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
92
+ id = make_id fn
93
+ @ids << id
94
+ @ids_to_fns[id] = fn
96
95
  end
96
+ @ids.sort!
97
97
  rescue SystemCallError, IOError => e
98
98
  raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
99
99
  end
100
100
 
101
+ Redwood::log "done scanning maildir"
101
102
  @last_scan = Time.now
102
103
  end
104
+ synchronized :scan_mailbox
103
105
 
104
106
  def each
105
107
  scan_mailbox
@@ -120,8 +122,8 @@ class Maildir < Source
120
122
  end
121
123
 
122
124
  def end_offset
123
- scan_mailbox
124
- @ids.last
125
+ scan_mailbox :rescan => true
126
+ @ids.last + 1
125
127
  end
126
128
 
127
129
  def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
@@ -20,29 +20,29 @@ module MBox
20
20
  ## when scanning over large mbox files.
21
21
  while(line = f.gets)
22
22
  case line
23
- when /^(From):\s+(.*?)\s*$/i,
24
- /^(To):\s+(.*?)\s*$/i,
25
- /^(Cc):\s+(.*?)\s*$/i,
26
- /^(Bcc):\s+(.*?)\s*$/i,
27
- /^(Subject):\s+(.*?)\s*$/i,
28
- /^(Date):\s+(.*?)\s*$/i,
29
- /^(References):\s+(.*?)\s*$/i,
30
- /^(In-Reply-To):\s+(.*?)\s*$/i,
31
- /^(Reply-To):\s+(.*?)\s*$/i,
32
- /^(List-Post):\s+(.*?)\s*$/i,
33
- /^(List-Subscribe):\s+(.*?)\s*$/i,
34
- /^(List-Unsubscribe):\s+(.*?)\s*$/i,
35
- /^(Status):\s+(.*?)\s*$/i: header[last = $1] = $2
36
- when /^(Message-Id):\s+(.*?)\s*$/i: header[mid_field = last = $1] = $2
23
+ when /^(From):\s*(.*?)\s*$/i,
24
+ /^(To):\s*(.*?)\s*$/i,
25
+ /^(Cc):\s*(.*?)\s*$/i,
26
+ /^(Bcc):\s*(.*?)\s*$/i,
27
+ /^(Subject):\s*(.*?)\s*$/i,
28
+ /^(Date):\s*(.*?)\s*$/i,
29
+ /^(References):\s*(.*?)\s*$/i,
30
+ /^(In-Reply-To):\s*(.*?)\s*$/i,
31
+ /^(Reply-To):\s*(.*?)\s*$/i,
32
+ /^(List-Post):\s*(.*?)\s*$/i,
33
+ /^(List-Subscribe):\s*(.*?)\s*$/i,
34
+ /^(List-Unsubscribe):\s*(.*?)\s*$/i,
35
+ /^(Status):\s*(.*?)\s*$/i: header[last = $1] = $2
36
+ when /^(Message-Id):\s*(.*?)\s*$/i: header[mid_field = last = $1] = $2
37
37
 
38
38
  ## these next three can occur multiple times, and we want the
39
39
  ## first one
40
- when /^(Delivered-To):\s+(.*)$/i,
41
- /^(X-Original-To):\s+(.*)$/i,
42
- /^(Envelope-To):\s+(.*)$/i: header[last = $1] ||= $2
40
+ when /^(Delivered-To):\s*(.*)$/i,
41
+ /^(X-Original-To):\s*(.*)$/i,
42
+ /^(Envelope-To):\s*(.*)$/i: header[last = $1] ||= $2
43
43
 
44
- when /^$/: break
45
- when /^\S+: /: last = nil # some other header we don't care about
44
+ when /^\r*$/: break
45
+ when /^\S+:/: last = nil # some other header we don't care about
46
46
  else
47
47
  header[last] += " " + line.chomp.gsub(/^\s+/, "") if last
48
48
  end
@@ -65,6 +65,7 @@ module MBox
65
65
  header
66
66
  end
67
67
 
68
+ ## never actually called
68
69
  def read_body f
69
70
  body = []
70
71
  f.each_line do |l|
@@ -6,6 +6,7 @@ module MBox
6
6
 
7
7
  class Loader < Source
8
8
  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
9
+ attr_accessor :labels
9
10
 
10
11
  ## uri_or_fp is horrific. need to refactor.
11
12
  def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
@@ -84,7 +85,7 @@ class Loader < Source
84
85
  ret = ""
85
86
  @mutex.synchronize do
86
87
  @f.seek offset
87
- until @f.eof? || (l = @f.gets) =~ /^$/
88
+ until @f.eof? || (l = @f.gets) =~ /^\r*$/
88
89
  ret += l
89
90
  end
90
91
  end
@@ -147,7 +148,7 @@ class Loader < Source
147
148
  end
148
149
 
149
150
  self.cur_offset = next_offset
150
- [returned_offset, (@labels + [:unread]).uniq]
151
+ [returned_offset, (self.labels + [:unread]).uniq]
151
152
  end
152
153
  end
153
154
 
@@ -122,6 +122,7 @@ class SSHFile
122
122
  def seek loc; @offset = loc; end
123
123
  def tell; @offset; end
124
124
  def total; size; end
125
+ def path; @fn end
125
126
 
126
127
  def size
127
128
  if @file_size.nil? || (Time.now - @last_size_check) > SIZE_CHECK_INTERVAL
@@ -116,7 +116,9 @@ EOS
116
116
  def initial_state; :open end
117
117
  def viewable?; @lines.nil? end
118
118
  def view_default! path
119
- system "/usr/bin/run-mailcap --action=view #{@content_type}:#{path} > /dev/null 2> /dev/null"
119
+ cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}' > /dev/null 2> /dev/null"
120
+ Redwood::log "running: #{cmd.inspect}"
121
+ system cmd
120
122
  $? == 0
121
123
  end
122
124
 
@@ -124,7 +126,7 @@ EOS
124
126
  path = write_to_disk
125
127
  ret = HookManager.run "mime-view", :content_type => @content_type,
126
128
  :filename => path
127
- view_default! path unless ret
129
+ ret || view_default!(path)
128
130
  end
129
131
 
130
132
  def write_to_disk
@@ -95,7 +95,8 @@ class Message
95
95
  begin
96
96
  Time.parse date
97
97
  rescue ArgumentError => e
98
- raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
98
+ Redwood::log "faking date header for #{@id} due to error parsing date #{header['date'].inspect}: #{e.message}"
99
+ Time.now
99
100
  end
100
101
  else
101
102
  Redwood::log "faking date header for #{@id}"
@@ -135,6 +136,10 @@ class Message
135
136
  @dirty = true
136
137
  end
137
138
 
139
+ def remove_ref ref
140
+ @dirty = true if @refs.delete ref
141
+ end
142
+
138
143
  def snippet; @snippet || (chunks && @snippet); end
139
144
  def is_list_message?; !@list_address.nil?; end
140
145
  def is_draft?; @source.is_a? DraftLoader; end
@@ -143,11 +148,13 @@ class Message
143
148
  @source.fn_for_offset @source_info
144
149
  end
145
150
 
146
- def sanitize_message_id mid; mid.gsub(/\s/, "") end
151
+ def sanitize_message_id mid; mid.gsub(/\s+/, "")[0..254] end
147
152
 
148
153
  def save index
149
- index.sync_message self if @dirty
154
+ return unless @dirty
155
+ index.sync_message self
150
156
  @dirty = false
157
+ true
151
158
  end
152
159
 
153
160
  def has_label? t; @labels.member? t; end
@@ -248,13 +255,14 @@ EOS
248
255
  with_source_errors_handled { @source.each_raw_message_line(@source_info, &b) }
249
256
  end
250
257
 
251
- def content
258
+ ## returns all the content from a message that will be indexed
259
+ def indexable_content
252
260
  load_from_source!
253
261
  [
254
- from && "#{from.name} #{from.email}",
255
- to.map { |p| "#{p.name} #{p.email}" },
256
- cc.map { |p| "#{p.name} #{p.email}" },
257
- bcc.map { |p| "#{p.name} #{p.email}" },
262
+ from && from.indexable_content,
263
+ to.map { |p| p.indexable_content },
264
+ cc.map { |p| p.indexable_content },
265
+ bcc.map { |p| p.indexable_content },
258
266
  chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
259
267
  Message.normalize_subj(subj),
260
268
  ].flatten.compact.join " "
@@ -20,7 +20,7 @@ class ComposeMode < EditMessageMode
20
20
  end
21
21
 
22
22
  def self.spawn_nicely opts={}
23
- to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ") or return
23
+ to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ", [opts[:to_default]]) or return
24
24
  cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
25
25
  bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
26
26
  subj = opts[:subj] || (BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject])
@@ -29,7 +29,7 @@ class ContactListMode < LineCursorMode
29
29
 
30
30
  def initialize mode=:regular
31
31
  @mode = mode
32
- @tags = Tagger.new self
32
+ @tags = Tagger.new self, "contact"
33
33
  @num = nil
34
34
  @text = []
35
35
  super()
@@ -148,9 +148,13 @@ EOS
148
148
  def attach_file
149
149
  fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
150
150
  return unless fn
151
- @attachments << RMail::Message.make_file_attachment(fn)
152
- @attachment_names << fn
153
- update
151
+ begin
152
+ @attachments << RMail::Message.make_file_attachment(fn)
153
+ @attachment_names << fn
154
+ update
155
+ rescue SystemCallError => e
156
+ BufferManager.flash "Can't read #{fn}: #{e.message}"
157
+ end
154
158
  end
155
159
 
156
160
  def delete_attachment
@@ -9,7 +9,7 @@ class InboxMode < ThreadIndexMode
9
9
  end
10
10
 
11
11
  def initialize
12
- super [:inbox, :sent], { :label => :inbox, :skip_killed => true }
12
+ super [:inbox, :sent, :draft], { :label => :inbox, :skip_killed => true }
13
13
  raise "can't have more than one!" if defined? @@instance
14
14
  @@instance = self
15
15
  end
@@ -10,12 +10,12 @@ class LabelSearchResultsMode < ThreadIndexMode
10
10
  end
11
11
 
12
12
  register_keymap do |k|
13
- k.add :refine_search, "Refine search", '.'
13
+ k.add :refine_search, "Refine search", '|'
14
14
  end
15
15
 
16
16
  def refine_search
17
17
  label_query = @labels.size > 1 ? "(#{@labels.join('||')})" : @labels.first
18
- query = BufferManager.ask :search, "query: ", "+label:#{label_query} "
18
+ query = BufferManager.ask :search, "refine query: ", "+label:#{label_query} "
19
19
  return unless query && query !~ /^\s*$/
20
20
  SearchResultsMode.spawn_from_query query
21
21
  end
@@ -101,6 +101,10 @@ EOS
101
101
  :recipient
102
102
  end)
103
103
 
104
+ @headers.each do |k, v|
105
+ HookManager.run "before-edit", :header => v, :body => body
106
+ end
107
+
104
108
  super :header => @headers[@type_selector.val], :body => body, :twiddles => false
105
109
  add_selector @type_selector
106
110
  end
@@ -12,7 +12,7 @@ class ScrollMode < Mode
12
12
 
13
13
  attr_reader :status, :topline, :botline, :leftcol
14
14
 
15
- COL_JUMP = 4
15
+ COL_JUMP = 2
16
16
 
17
17
  register_keymap do |k|
18
18
  k.add :line_down, "Down one line", :down, 'j', 'J'
@@ -64,14 +64,14 @@ class ScrollMode < Mode
64
64
  end
65
65
 
66
66
  start = @search_line || search_start_line
67
- line = find_text @search_query, start
67
+ line, col = find_text @search_query, start
68
68
  if line.nil? && (start > 0)
69
- line = find_text @search_query, 0
69
+ line, col = find_text @search_query, 0
70
70
  BufferManager.flash "Search wrapped to top!" if line
71
71
  end
72
72
  if line
73
73
  @search_line = line + 1
74
- search_goto_line line
74
+ search_goto_pos line, col, col + @search_query.length
75
75
  buffer.mark_dirty
76
76
  else
77
77
  BufferManager.flash "Not found!"
@@ -86,7 +86,13 @@ class ScrollMode < Mode
86
86
  end
87
87
 
88
88
  ## subclasses can override these two!
89
- def search_goto_line line; jump_to_line line end
89
+ def search_goto_pos line, leftcol, rightcol
90
+ jump_to_line line
91
+
92
+ if rightcol > self.rightcol # if it's occluded...
93
+ jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
94
+ end
95
+ end
90
96
  def search_start_line; @topline end
91
97
 
92
98
  def col_left
@@ -144,9 +150,18 @@ protected
144
150
  (start_line ... lines).each do |i|
145
151
  case(s = self[i])
146
152
  when String
147
- return i if s =~ regex
153
+ match = s =~ regex
154
+ return [i, match] if match
148
155
  when Array
149
- return i if s.any? { |color, string| string =~ regex }
156
+ offset = 0
157
+ s.each do |color, string|
158
+ match = string =~ regex
159
+ if match
160
+ return [i, offset + match]
161
+ else
162
+ offset += string.length
163
+ end
164
+ end
150
165
  end
151
166
  end
152
167
  nil
@@ -9,13 +9,13 @@ class SearchResultsMode < ThreadIndexMode
9
9
  end
10
10
 
11
11
  register_keymap do |k|
12
- k.add :refine_search, "Refine search", '.'
12
+ k.add :refine_search, "Refine search", '|'
13
13
  end
14
14
 
15
15
  def refine_search
16
- query = BufferManager.ask :search, "query: ", (@qobj.to_s + " ")
16
+ query = BufferManager.ask :search, "refine query: ", (@qobj.to_s + " ")
17
17
  return unless query && query !~ /^\s*$/
18
- SearchResultsMode.spawn_from_query query, @qopts
18
+ SearchResultsMode.spawn_from_query query
19
19
  end
20
20
 
21
21
  ## a proper is_relevant? method requires some way of asking ferret
@@ -16,6 +16,10 @@ EOS
16
16
 
17
17
  register_keymap do |k|
18
18
  k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
19
+ k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
20
+ kk.add :load_all_threads, "Load all threads (may list a _lot_ of threads)", '!'
21
+ end
22
+ k.add :cancel_search, "Cancel current search", :ctrl_g
19
23
  k.add :reload, "Refresh view", '@'
20
24
  k.add :toggle_archived, "Toggle archived status", 'a'
21
25
  k.add :toggle_starred, "Star or unstar all messages in thread", '*'
@@ -50,6 +54,8 @@ EOS
50
54
  @load_thread_opts = load_thread_opts
51
55
  @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
52
56
  @date_width = DATE_WIDTH
57
+
58
+ @interrupt_search = false
53
59
 
54
60
  initialize_threads # defines @ts and @ts_mutex
55
61
  update # defines @text and @lines
@@ -107,19 +113,32 @@ EOS
107
113
  threads.each { |t| select t }
108
114
  end
109
115
 
110
- ## this is called by thread-view-modes when the user wants to view
111
- ## the next thread without going to index-mode. we update the cursor
112
- ## as a convenience.
116
+ ## these two methods are called by thread-view-modes when the user
117
+ ## wants to view the previous/next thread without going back to
118
+ ## index-mode. we update the cursor as a convenience.
113
119
  def launch_next_thread_after thread, &b
120
+ launch_another_thread thread, 1, &b
121
+ end
122
+
123
+ def launch_prev_thread_before thread, &b
124
+ launch_another_thread thread, -1, &b
125
+ end
126
+
127
+ def launch_another_thread thread, direction, &b
114
128
  l = @lines[thread] or return
129
+ target_l = l + direction
115
130
  t = @mutex.synchronize do
116
- if l < @threads.length - 1
117
- set_cursor_pos l + 1 # move out of mutex?
118
- @threads[l + 1]
131
+ if target_l >= 0 && target_l < @threads.length
132
+ @threads[target_l]
119
133
  end
120
- end or return
134
+ end
121
135
 
122
- select t, b
136
+ if t # there's a next thread
137
+ set_cursor_pos target_l # move out of mutex?
138
+ select t, b
139
+ elsif b # no next thread. call the block anyways
140
+ b.call
141
+ end
123
142
  end
124
143
 
125
144
  def handle_single_message_labeled_update sender, m
@@ -166,10 +185,16 @@ EOS
166
185
  end
167
186
 
168
187
  def handle_deleted_update sender, m
169
- @ts_mutex.synchronize do
170
- return unless @ts.contains? m
171
- @ts.remove_thread_containing_id m.id
172
- end
188
+ t = @ts_mutex.synchronize { @ts.thread_for m }
189
+ return unless t
190
+ hide_thread t
191
+ update
192
+ end
193
+
194
+ def handle_spammed_update sender, m
195
+ t = @ts_mutex.synchronize { @ts.thread_for m }
196
+ return unless t
197
+ hide_thread t
173
198
  update
174
199
  end
175
200
 
@@ -410,6 +435,7 @@ EOS
410
435
  return unless user_labels
411
436
  thread.labels = keepl + user_labels
412
437
  user_labels.each { |l| LabelManager << l }
438
+ update_text_for_line curpos
413
439
  UpdateManager.relay self, :labeled, thread.first
414
440
  end
415
441
 
@@ -456,16 +482,22 @@ EOS
456
482
 
457
483
  ## TODO: figure out @ts_mutex in this method
458
484
  def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
485
+ @interrupt_search = false
459
486
  @mbid = BufferManager.say "Searching for threads..."
487
+
488
+ ts_to_load = n
489
+ ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads
490
+
460
491
  orig_size = @ts.size
461
492
  last_update = Time.now
462
- @ts.load_n_threads(@ts.size + n, opts) do |i|
493
+ @ts.load_n_threads(ts_to_load, opts) do |i|
463
494
  if (Time.now - last_update) >= 0.25
464
495
  BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
465
496
  update
466
497
  BufferManager.draw_screen
467
498
  last_update = Time.now
468
499
  end
500
+ break if @interrupt_search
469
501
  end
470
502
  @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
471
503
 
@@ -485,11 +517,24 @@ EOS
485
517
  end
486
518
  end
487
519
 
520
+ def cancel_search
521
+ @interrupt_search = true
522
+ end
523
+
524
+ def load_all_threads
525
+ load_threads :num => -1
526
+ end
527
+
488
528
  def load_threads opts={}
489
- n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
529
+ if opts[:num].nil?
530
+ n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
531
+ else
532
+ n = opts[:num]
533
+ end
490
534
 
491
535
  myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
492
536
  opts[:when_done].call(num) if opts[:when_done]
537
+
493
538
  if num > 0
494
539
  BufferManager.flash "Found #{num.pluralize 'thread'}."
495
540
  else
@@ -513,14 +558,12 @@ EOS
513
558
  protected
514
559
 
515
560
  def add_or_unhide m
516
- if @hidden_threads[m]
517
- @hidden_threads.delete m
518
- ## now it will re-appear when #update is called
519
- else
520
- @ts_mutex.synchronize do
521
- return unless is_relevant?(m) || @ts.is_relevant?(m)
561
+ @ts_mutex.synchronize do
562
+ if (is_relevant?(m) || @ts.is_relevant?(m)) && !@ts.contains?(m)
522
563
  @ts.load_thread_for_message m
523
564
  end
565
+
566
+ @hidden_threads.delete @ts.thread_for(m)
524
567
  end
525
568
 
526
569
  update
@@ -612,7 +655,6 @@ protected
612
655
 
613
656
  date = t.date.to_nice_s
614
657
 
615
- new = t.has_label?(:unread)
616
658
  starred = t.has_label?(:starred)
617
659
 
618
660
  ## format the from column
@@ -649,7 +691,9 @@ protected
649
691
  p = dp || t.participants.any? { |p| AccountManager.is_account? p }
650
692
 
651
693
  subj_color =
652
- if new
694
+ if t.has_label?(:draft)
695
+ :index_draft_color
696
+ elsif t.has_label?(:unread)
653
697
  :index_new_color
654
698
  elsif starred
655
699
  :index_starred_color