sup 0.14.1.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -197,7 +197,15 @@ EOS
197
197
  @file = Tempfile.new ["sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}", ".eml"]
198
198
  @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
199
199
  @file.puts
200
- @file.puts @body.join("\n")
200
+
201
+ begin
202
+ text = @body.join("\n")
203
+ rescue Encoding::CompatibilityError
204
+ text = @body.map { |x| x.fix_encoding! }.join("\n")
205
+ debug "encoding problem while writing message, trying to rescue, but expect errors: #{text}"
206
+ end
207
+
208
+ @file.puts text
201
209
  @file.puts sig if ($config[:edit_signature] and !@sig_edited)
202
210
  @file.close
203
211
  end
@@ -205,13 +213,13 @@ EOS
205
213
  def set_sig_edit_flag
206
214
  sig = sig_lines.join("\n")
207
215
  if $config[:edit_signature]
208
- pbody = @body.join("\n")
216
+ pbody = @body.map { |x| x.fix_encoding! }.join("\n").fix_encoding!
209
217
  blen = pbody.length
210
218
  slen = sig.length
211
219
 
212
220
  if blen > slen and pbody[blen-slen..blen] == sig
213
221
  @sig_edited = false
214
- @body = pbody[0..blen-slen].split("\n")
222
+ @body = pbody[0..blen-slen].fix_encoding!.split("\n")
215
223
  else
216
224
  @sig_edited = true
217
225
  end
@@ -319,7 +327,7 @@ EOS
319
327
  end
320
328
 
321
329
  def delete_attachment
322
- i = curpos - @attachment_lines_offset - DECORATION_LINES - 2
330
+ i = curpos - @attachment_lines_offset - (@selectors.empty? ? 0 : DECORATION_LINES) - @selectors.size
323
331
  if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachment_names[i]}?")
324
332
  @attachments.delete_at i
325
333
  @attachment_names.delete_at i
@@ -551,9 +559,9 @@ protected
551
559
  m.header[k] =
552
560
  case v
553
561
  when String
554
- (k.match(/subject/i) ? mime_encode_subject(v) : mime_encode_address(v)).fix_encoding!
562
+ (k.match(/subject/i) ? mime_encode_subject(v).dup.fix_encoding! : mime_encode_address(v)).dup.fix_encoding!
555
563
  when Array
556
- (v.map { |v| mime_encode_address v }.join ", ").fix_encoding!
564
+ (v.map { |v| mime_encode_address v }.join ", ").dup.fix_encoding!
557
565
  end
558
566
  end
559
567
 
@@ -635,12 +643,12 @@ private
635
643
  if HookManager.enabled? "mentions-attachments"
636
644
  HookManager.run "mentions-attachments", :header => @header, :body => @body
637
645
  else
638
- @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
646
+ @body.any? { |l| l.fix_encoding! =~ /^[^>]/ && l.fix_encoding! =~ /\battach(ment|ed|ing|)\b/i }
639
647
  end
640
648
  end
641
649
 
642
650
  def top_posting?
643
- @body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
651
+ @body.map { |x| x.fix_encoding! }.join("\n").fix_encoding! =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
644
652
  end
645
653
 
646
654
  def sig_lines
@@ -7,9 +7,10 @@ class ForwardMode < EditMessageMode
7
7
  "From" => AccountManager.default_account.full_address,
8
8
  }
9
9
 
10
+ @m = opts[:message]
10
11
  header["Subject"] =
11
- if opts[:message]
12
- "Fwd: " + opts[:message].subj
12
+ if @m
13
+ "Fwd: " + @m.subj
13
14
  elsif opts[:attachments]
14
15
  "Fwd: " + opts[:attachments].keys.join(", ")
15
16
  end
@@ -19,8 +20,8 @@ class ForwardMode < EditMessageMode
19
20
  header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
20
21
 
21
22
  body =
22
- if opts[:message]
23
- forward_body_lines(opts[:message])
23
+ if @m
24
+ forward_body_lines @m
24
25
  elsif opts[:attachments]
25
26
  ["Note: #{opts[:attachments].size.pluralize 'attachment'}."]
26
27
  end
@@ -68,6 +69,14 @@ protected
68
69
  m.quotable_header_lines + [""] + m.quotable_body_lines +
69
70
  ["--- End forwarded message ---"]
70
71
  end
72
+
73
+ def send_message
74
+ return unless super # super returns true if the mail has been sent
75
+ if @m
76
+ @m.add_label :forwarded
77
+ Index.save_message @m
78
+ end
79
+ end
71
80
  end
72
81
 
73
82
  end
@@ -217,6 +217,12 @@ protected
217
217
  update
218
218
  end
219
219
  end
220
+
221
+ def send_message
222
+ return unless super # super returns true if the mail has been sent
223
+ @m.add_label :replied
224
+ Index.save_message @m
225
+ end
220
226
  end
221
227
 
222
228
  end
@@ -145,6 +145,12 @@ protected
145
145
  def rename_selected_search
146
146
  old_name, num_unread = @searches[curpos]
147
147
  return unless old_name
148
+
149
+ if SearchManager.predefined_searches.has_key? old_name
150
+ BufferManager.flash "Cannot be edited: predefined search."
151
+ return
152
+ end
153
+
148
154
  new_name = BufferManager.ask :save_search, "Rename this saved search: ", old_name
149
155
  return unless new_name && new_name !~ /^\s*$/ && new_name != old_name
150
156
  new_name.strip!
@@ -163,6 +169,12 @@ protected
163
169
  def edit_selected_search
164
170
  name, num_unread = @searches[curpos]
165
171
  return unless name
172
+
173
+ if SearchManager.predefined_searches.has_key? name
174
+ BufferManager.flash "Cannot be edited: predefined search."
175
+ return
176
+ end
177
+
166
178
  old_search_string = SearchManager.search_string_for name
167
179
  new_search_string = BufferManager.ask :search, "Edit this saved search: ", (old_search_string + " ")
168
180
  return unless new_search_string && new_search_string !~ /^\s*$/ && new_search_string != old_search_string
@@ -200,6 +200,26 @@ EOS
200
200
  BufferManager.draw_screen
201
201
  end
202
202
 
203
+ def handle_updated_update sender, m
204
+ t = thread_containing(m) or return
205
+ l = @lines[t] or return
206
+ @ts_mutex.synchronize do
207
+ @ts.delete_message m
208
+ @ts.add_message m
209
+ end
210
+ Index.save_thread t
211
+ update_text_for_line l
212
+ end
213
+
214
+ def handle_location_deleted_update sender, m
215
+ t = thread_containing(m)
216
+ delete_thread t if t and t.first.id == m.id
217
+ @ts_mutex.synchronize do
218
+ @ts.delete_message m if t
219
+ end
220
+ update
221
+ end
222
+
203
223
  def handle_single_message_deleted_update sender, m
204
224
  @ts_mutex.synchronize do
205
225
  return unless @ts.contains? m
@@ -755,6 +775,16 @@ protected
755
775
  update
756
776
  end
757
777
 
778
+ def delete_thread t
779
+ @mutex.synchronize do
780
+ i = @threads.index(t) or return
781
+ @threads.delete_at i
782
+ @size_widgets.delete_at i
783
+ @date_widgets.delete_at i
784
+ @tags.drop_tag_for t
785
+ end
786
+ end
787
+
758
788
  def hide_thread t
759
789
  @mutex.synchronize do
760
790
  i = @threads.index(t) or return
@@ -1,3 +1,5 @@
1
+ require 'shellwords'
2
+
1
3
  module Redwood
2
4
 
3
5
  class ThreadViewMode < LineCursorMode
@@ -246,6 +248,8 @@ EOS
246
248
  sm.puts m.raw_message
247
249
  end
248
250
  raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
251
+ m.add_label :forwarded
252
+ Index.save_message m
249
253
  rescue SystemCallError, SendmailCommandFailed => e
250
254
  warn "problem sending mail: #{e.message}"
251
255
  BufferManager.flash "Problem sending mail: #{e.message}"
@@ -359,8 +363,14 @@ EOS
359
363
  when Chunk::Attachment
360
364
  default_dir = $config[:default_attachment_save_dir]
361
365
  default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
362
- default_fn = File.expand_path File.join(default_dir, chunk.filename)
363
- fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", default_fn
366
+ default_fn = File.expand_path File.join(default_dir, Shellwords.escape(chunk.filename))
367
+ fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
368
+
369
+ # if user selects directory use file name from message
370
+ if fn and File.directory? fn
371
+ fn = File.join(fn, Shellwords.escape(chunk.filename))
372
+ end
373
+
364
374
  save_to_file(fn) { |f| f.print chunk.raw_content } if fn
365
375
  else
366
376
  m = @message_lines[curpos]
@@ -382,7 +392,7 @@ EOS
382
392
  num_errors = 0
383
393
  m.chunks.each do |chunk|
384
394
  next unless chunk.is_a?(Chunk::Attachment)
385
- fn = File.join(folder, chunk.filename)
395
+ fn = File.join(folder, Shellwords.escape(chunk.filename))
386
396
  num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
387
397
  num += 1
388
398
  end
@@ -780,13 +790,13 @@ private
780
790
  @person_lines[start] = m.from
781
791
  [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
782
792
  [color,
783
- "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
793
+ "#{m.from ? m.from.mediumname.fix_encoding! : '?'} to #{m.recipients.map { |l| l.shortname.fix_encoding! }.join(', ')} #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!})"]]]
784
794
 
785
795
  when :closed
786
796
  @person_lines[start] = m.from
787
797
  [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
788
798
  [color,
789
- "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
799
+ "#{m.from ? m.from.mediumname.fix_encoding! : '?'}, #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!}) #{m.snippet ? m.snippet.fix_encoding! : ''}"]]]
790
800
 
791
801
  when :detailed
792
802
  @person_lines[start] = m.from
@@ -25,6 +25,9 @@ Variables:
25
25
  num_total: the total number of messages
26
26
  num_inbox_total: the total number of new messages in the inbox.
27
27
  num_inbox_total_unread: the total number of unread messages in the inbox
28
+ num_updated: the total number of updated messages
29
+ num_deleted: the total number of deleted messages
30
+ labels: the labels that were applied
28
31
  from_and_subj: an array of (from email address, subject) pairs
29
32
  from_and_subj_inbox: an array of (from email address, subject) pairs for
30
33
  only those messages appearing in the inbox
@@ -52,23 +55,32 @@ EOS
52
55
  BufferManager.flash "Polling for new messages..."
53
56
  end
54
57
 
55
- num, numi, from_and_subj, from_and_subj_inbox, loaded_labels = @mode.poll
58
+ num, numi, numu, numd, from_and_subj, from_and_subj_inbox, loaded_labels = @mode.poll
56
59
  clear_running_totals if @should_clear_running_totals
57
60
  @running_totals[:num] += num
58
61
  @running_totals[:numi] += numi
62
+ @running_totals[:numu] += numu
63
+ @running_totals[:numd] += numd
59
64
  @running_totals[:loaded_labels] += loaded_labels || []
60
65
 
61
66
 
62
67
  if HookManager.enabled? "after-poll"
63
68
  hook_args = { :num => num, :num_inbox => numi,
64
69
  :num_total => @running_totals[:num], :num_inbox_total => @running_totals[:numi],
70
+ :num_updated => @running_totals[:numu],
71
+ :num_deleted => @running_totals[:numd],
72
+ :labels => @running_totals[:loaded_labels],
65
73
  :from_and_subj => from_and_subj, :from_and_subj_inbox => from_and_subj_inbox,
66
74
  :num_inbox_total_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] } }
67
75
 
68
76
  HookManager.run("after-poll", hook_args)
69
77
  else
70
78
  if @running_totals[:num] > 0
71
- BufferManager.flash "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}"
79
+ flash_msg = "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. " if @running_totals[:num] > 0
80
+ flash_msg += "Updated #{@running_totals[:numu].pluralize 'message'}. " if @running_totals[:numu] > 0
81
+ flash_msg += "Deleted #{@running_totals[:numd].pluralize 'message'}. " if @running_totals[:numd] > 0
82
+ flash_msg += "Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}." if @running_totals[:loaded_labels].size > 0
83
+ BufferManager.flash flash_msg
72
84
  else
73
85
  BufferManager.flash "No new messages."
74
86
  end
@@ -115,7 +127,7 @@ EOS
115
127
  end
116
128
 
117
129
  def do_poll
118
- total_num = total_numi = 0
130
+ total_num = total_numi = total_numu = total_numd = 0
119
131
  from_and_subj = []
120
132
  from_and_subj_inbox = []
121
133
  loaded_labels = Set.new
@@ -129,16 +141,23 @@ EOS
129
141
  next
130
142
  end
131
143
 
132
- num = 0
133
- numi = 0
144
+ msg = ""
145
+ num = numi = numu = numd = 0
134
146
  poll_from source do |action,m,old_m,progress|
135
147
  if action == :delete
136
148
  yield "Deleting #{m.id}"
149
+ loaded_labels.merge m.labels
150
+ numd += 1
151
+ elsif action == :update
152
+ yield "Message at #{m.source_info} is an update of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
153
+ loaded_labels.merge m.labels
154
+ numu += 1
137
155
  elsif action == :add
138
156
  if old_m
139
157
  new_locations = (m.locations - old_m.locations)
140
158
  if not new_locations.empty?
141
- yield "Message at #{new_locations[0].info} is an update of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
159
+ yield "Message at #{new_locations[0].info} has changed its source location. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
160
+ numu += 1
142
161
  else
143
162
  yield "Skipping already-imported message at #{m.locations[-1].info}"
144
163
  end
@@ -155,25 +174,29 @@ EOS
155
174
  else fail
156
175
  end
157
176
  end
158
- yield "Found #{num} messages, #{numi} to inbox." unless num == 0
177
+ msg += "Found #{num} messages, #{numi} to inbox. " unless num == 0
178
+ msg += "Updated #{numu} messages. " unless numu == 0
179
+ msg += "Deleted #{numd} messages." unless numd == 0
180
+ yield msg unless msg == ""
159
181
  total_num += num
160
182
  total_numi += numi
183
+ total_numu += numu
184
+ total_numd += numd
161
185
  end
162
186
 
163
187
  loaded_labels = loaded_labels - LabelManager::HIDDEN_RESERVED_LABELS - [:inbox, :killed]
164
188
  yield "Done polling; loaded #{total_num} new messages total"
165
189
  @last_poll = Time.now
166
190
  end
167
- [total_num, total_numi, from_and_subj, from_and_subj_inbox, loaded_labels]
191
+ [total_num, total_numi, total_numu, total_numd, from_and_subj, from_and_subj_inbox, loaded_labels]
168
192
  end
169
193
 
170
194
  ## like Source#poll, but yields successive Message objects, which have their
171
195
  ## labels and locations set correctly. The Messages are saved to or removed
172
196
  ## from the index after being yielded.
173
197
  def poll_from source, opts={}
174
- debug "trying to acquire poll lock for: #{source}.."
175
- if source.poll_lock.try_lock
176
- debug "lock acquired for: #{source}."
198
+ debug "trying to acquire poll lock for: #{source}..."
199
+ if source.try_lock
177
200
  begin
178
201
  source.poll do |sym, args|
179
202
  case sym
@@ -191,18 +214,40 @@ EOS
191
214
  yield :add, m, old_m, args[:progress] if block_given?
192
215
  Index.sync_message m, true
193
216
 
217
+ if Index.message_joining_killed? m
218
+ m.labels += [:killed]
219
+ Index.sync_message m, true
220
+ end
221
+
194
222
  ## We need to add or unhide the message when it either did not exist
195
223
  ## before at all or when it was updated. We do *not* add/unhide when
196
224
  ## the same message was found at a different location
197
- if !old_m or not old_m.locations.member? m.location
225
+ if old_m
226
+ UpdateManager.relay self, :updated, m
227
+ elsif !old_m or not old_m.locations.member? m.location
198
228
  UpdateManager.relay self, :added, m
199
229
  end
200
230
  when :delete
201
- Index.each_message :location => [source.id, args[:info]] do |m|
231
+ Index.each_message({:location => [source.id, args[:info]]}, false) do |m|
202
232
  m.locations.delete Location.new(source, args[:info])
203
- yield :delete, m, [source,args[:info]], args[:progress] if block_given?
204
233
  Index.sync_message m, false
205
- #UpdateManager.relay self, :deleted, m
234
+ if m.locations.size == 0
235
+ yield :delete, m, [source,args[:info]], args[:progress] if block_given?
236
+ Index.delete m.id
237
+ UpdateManager.relay self, :location_deleted, m
238
+ end
239
+ end
240
+ when :update
241
+ Index.each_message({:location => [source.id, args[:old_info]]}, false) do |m|
242
+ old_m = Index.build_message m.id
243
+ m.locations.delete Location.new(source, args[:old_info])
244
+ m.locations.push Location.new(source, args[:new_info])
245
+ ## Update labels that might have been modified remotely
246
+ m.labels -= source.supported_labels?
247
+ m.labels += args[:labels]
248
+ yield :update, m, old_m if block_given?
249
+ Index.sync_message m, true
250
+ UpdateManager.relay self, :updated, m
206
251
  end
207
252
  end
208
253
  end
@@ -212,7 +257,7 @@ EOS
212
257
 
213
258
  ensure
214
259
  source.go_idle
215
- source.poll_lock.unlock
260
+ source.unlock
216
261
  end
217
262
  else
218
263
  debug "source #{source} is already being polled."
@@ -221,7 +266,7 @@ EOS
221
266
 
222
267
  def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
223
268
  def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
224
- def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :loaded_labels => Set.new}; end
269
+ def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :numu => 0, :numd => 0, :loaded_labels => Set.new}; end
225
270
  end
226
271
 
227
272
  end
@@ -7,6 +7,8 @@ class SearchManager
7
7
 
8
8
  class ExpansionError < StandardError; end
9
9
 
10
+ attr_reader :predefined_searches
11
+
10
12
  def initialize fn
11
13
  @fn = fn
12
14
  @searches = {}
@@ -43,24 +45,40 @@ class SearchManager
43
45
 
44
46
  def add name, search_string
45
47
  return unless valid_name? name
48
+ if @predefined_searches.has_key? name
49
+ warn "cannot add search: #{name} is already taken by a predefined search"
50
+ return
51
+ end
46
52
  @searches[name] = search_string
47
53
  @modified = true
48
54
  end
49
55
 
50
56
  def rename old, new
51
57
  return unless @searches.has_key? old
58
+ if [old, new].any? { |x| @predefined_searches.has_key? x }
59
+ warn "cannot rename search: #{old} or #{new} is already taken by a predefined search"
60
+ return
61
+ end
52
62
  search_string = @searches[old]
53
63
  delete old if add new, search_string
54
64
  end
55
65
 
56
66
  def edit name, search_string
57
67
  return unless @searches.has_key? name
68
+ if @predefined_searches.has_key? name
69
+ warn "cannot edit predefined search: #{name}."
70
+ return
71
+ end
58
72
  @searches[name] = search_string
59
73
  @modified = true
60
74
  end
61
75
 
62
76
  def delete name
63
77
  return unless @searches.has_key? name
78
+ if @predefined_searches.has_key? name
79
+ warn "cannot delete predefined search: #{name}."
80
+ return
81
+ end
64
82
  @searches.delete name
65
83
  @modified = true
66
84
  end