sup 0.1 → 0.2
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.
- data/History.txt +9 -0
- data/Manifest.txt +5 -1
- data/Rakefile +2 -1
- data/bin/sup +27 -10
- data/bin/sup-add +2 -1
- data/bin/sup-sync-back +51 -23
- data/doc/FAQ.txt +29 -37
- data/doc/Hooks.txt +38 -0
- data/doc/{UserGuide.txt → NewUserGuide.txt} +27 -21
- data/doc/TODO +91 -57
- data/lib/sup.rb +17 -1
- data/lib/sup/buffer.rb +80 -16
- data/lib/sup/colormap.rb +0 -2
- data/lib/sup/contact.rb +3 -2
- data/lib/sup/crypto.rb +110 -0
- data/lib/sup/draft.rb +2 -6
- data/lib/sup/hook.rb +131 -0
- data/lib/sup/imap.rb +27 -16
- data/lib/sup/index.rb +38 -14
- data/lib/sup/keymap.rb +0 -2
- data/lib/sup/label.rb +30 -9
- data/lib/sup/logger.rb +12 -1
- data/lib/sup/maildir.rb +48 -3
- data/lib/sup/mbox.rb +1 -1
- data/lib/sup/mbox/loader.rb +22 -12
- data/lib/sup/mbox/ssh-loader.rb +1 -1
- data/lib/sup/message-chunks.rb +198 -0
- data/lib/sup/message.rb +154 -115
- data/lib/sup/modes/compose-mode.rb +18 -0
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +112 -31
- data/lib/sup/modes/file-browser-mode.rb +1 -1
- data/lib/sup/modes/inbox-mode.rb +1 -1
- data/lib/sup/modes/label-list-mode.rb +8 -6
- data/lib/sup/modes/label-search-results-mode.rb +4 -1
- data/lib/sup/modes/log-mode.rb +1 -1
- data/lib/sup/modes/reply-mode.rb +18 -16
- data/lib/sup/modes/search-results-mode.rb +1 -1
- data/lib/sup/modes/thread-index-mode.rb +61 -33
- data/lib/sup/modes/thread-view-mode.rb +111 -102
- data/lib/sup/person.rb +5 -1
- data/lib/sup/poll.rb +36 -7
- data/lib/sup/sent.rb +1 -0
- data/lib/sup/source.rb +7 -3
- data/lib/sup/textfield.rb +48 -34
- data/lib/sup/thread.rb +9 -5
- data/lib/sup/util.rb +16 -22
- metadata +7 -3
@@ -12,13 +12,26 @@ class EditMessageMode < LineCursorMode
|
|
12
12
|
MULTI_HEADERS = %w(To Cc Bcc)
|
13
13
|
NON_EDITABLE_HEADERS = %w(Message-Id Date)
|
14
14
|
|
15
|
+
HookManager.register "signature", <<EOS
|
16
|
+
Generates a signature for a message.
|
17
|
+
Variables:
|
18
|
+
header: an object that supports string-to-string hashtable-style access
|
19
|
+
to the raw headers for the message. E.g., header["From"],
|
20
|
+
header["To"], etc.
|
21
|
+
from_email: the email part of the From: line, or nil if empty
|
22
|
+
Return value:
|
23
|
+
A string (multi-line ok) containing the text of the signature, or nil to
|
24
|
+
use the default signature.
|
25
|
+
EOS
|
26
|
+
|
15
27
|
attr_reader :status
|
16
28
|
attr_accessor :body, :header
|
17
29
|
bool_reader :edited
|
18
30
|
|
19
31
|
register_keymap do |k|
|
20
32
|
k.add :send_message, "Send message", 'y'
|
21
|
-
k.add :
|
33
|
+
k.add :edit_field, "Edit field", 'e'
|
34
|
+
k.add :edit_message, "Edit message", :enter
|
22
35
|
k.add :save_as_draft, "Save as draft", 'P'
|
23
36
|
k.add :attach_file, "Attach a file", 'a'
|
24
37
|
k.add :delete_attachment, "Delete an attachment", 'd'
|
@@ -26,12 +39,15 @@ class EditMessageMode < LineCursorMode
|
|
26
39
|
|
27
40
|
def initialize opts={}
|
28
41
|
@header = opts.delete(:header) || {}
|
42
|
+
@header_lines = []
|
43
|
+
|
29
44
|
@body = opts.delete(:body) || []
|
30
45
|
@body += sig_lines if $config[:edit_signature]
|
46
|
+
|
31
47
|
@attachments = []
|
32
|
-
@attachment_lines = {}
|
33
48
|
@message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
|
34
49
|
@edited = false
|
50
|
+
@skip_top_rows = opts[:skip_top_rows] || 0
|
35
51
|
|
36
52
|
super opts
|
37
53
|
regen_text
|
@@ -43,9 +59,36 @@ class EditMessageMode < LineCursorMode
|
|
43
59
|
## a hook
|
44
60
|
def handle_new_text header, body; end
|
45
61
|
|
46
|
-
def
|
62
|
+
def edit_field
|
63
|
+
if (curpos - @skip_top_rows) >= @header_lines.length
|
64
|
+
edit_message
|
65
|
+
else
|
66
|
+
case(field = @header_lines[curpos - @skip_top_rows])
|
67
|
+
when "Subject"
|
68
|
+
text = BufferManager.ask :subject, "Subject: ", @header[field]
|
69
|
+
@header[field] = parse_header field, text if text
|
70
|
+
else
|
71
|
+
default =
|
72
|
+
case field
|
73
|
+
when *MULTI_HEADERS
|
74
|
+
@header[field].join(", ")
|
75
|
+
else
|
76
|
+
@header[field]
|
77
|
+
end
|
78
|
+
|
79
|
+
contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
|
80
|
+
if contacts
|
81
|
+
text = contacts.map { |s| s.longname }.join(", ")
|
82
|
+
@header[field] = parse_header field, text
|
83
|
+
end
|
84
|
+
end
|
85
|
+
update
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def edit_message
|
47
90
|
@file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
|
48
|
-
@file.puts
|
91
|
+
@file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
|
49
92
|
@file.puts
|
50
93
|
@file.puts @body
|
51
94
|
@file.close
|
@@ -56,12 +99,14 @@ class EditMessageMode < LineCursorMode
|
|
56
99
|
BufferManager.shell_out "#{editor} #{@file.path}"
|
57
100
|
@edited = true if File.mtime(@file.path) > mtime
|
58
101
|
|
59
|
-
|
102
|
+
return @edited unless @edited
|
60
103
|
|
61
104
|
header, @body = parse_file @file.path
|
62
105
|
@header = header - NON_EDITABLE_HEADERS
|
63
106
|
handle_new_text @header, @body
|
64
107
|
update
|
108
|
+
|
109
|
+
@edited
|
65
110
|
end
|
66
111
|
|
67
112
|
def killable?
|
@@ -69,13 +114,14 @@ class EditMessageMode < LineCursorMode
|
|
69
114
|
end
|
70
115
|
|
71
116
|
def attach_file
|
72
|
-
fn = BufferManager.
|
73
|
-
|
117
|
+
fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
|
118
|
+
return unless fn
|
119
|
+
@attachments << Pathname.new(fn)
|
74
120
|
update
|
75
121
|
end
|
76
122
|
|
77
123
|
def delete_attachment
|
78
|
-
i = curpos - @attachment_lines_offset
|
124
|
+
i = (curpos - @skip_top_rows) - @attachment_lines_offset
|
79
125
|
if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
|
80
126
|
@attachments.delete_at i
|
81
127
|
update
|
@@ -90,8 +136,8 @@ protected
|
|
90
136
|
end
|
91
137
|
|
92
138
|
def regen_text
|
93
|
-
|
94
|
-
@text =
|
139
|
+
header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""]
|
140
|
+
@text = header + [""] + @body
|
95
141
|
@text += sig_lines unless $config[:edit_signature]
|
96
142
|
|
97
143
|
unless @attachments.empty?
|
@@ -107,24 +153,30 @@ protected
|
|
107
153
|
body = f.readlines
|
108
154
|
|
109
155
|
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
|
110
|
-
header.each
|
111
|
-
next unless MULTI_HEADERS.include?(k) && !v.empty?
|
112
|
-
header[k] = v.split_on_commas.map do |name|
|
113
|
-
(p = ContactManager.person_with(name)) && p.full_address || name
|
114
|
-
end
|
115
|
-
end
|
156
|
+
header.each { |k, v| header[k] = parse_header k, v }
|
116
157
|
|
117
158
|
[header, body]
|
118
159
|
end
|
119
160
|
end
|
120
161
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
|
162
|
+
def parse_header k, v
|
163
|
+
if MULTI_HEADERS.include?(k)
|
164
|
+
v.split_on_commas.map do |name|
|
165
|
+
(p = ContactManager.contact_for(name)) && p.full_address || name
|
166
|
+
end
|
167
|
+
else
|
168
|
+
v
|
125
169
|
end
|
170
|
+
end
|
126
171
|
|
127
|
-
|
172
|
+
def format_headers header
|
173
|
+
header_lines = []
|
174
|
+
headers = (FORCE_HEADERS + (header.keys - FORCE_HEADERS)).map do |h|
|
175
|
+
lines = make_lines "#{h}:", header[h]
|
176
|
+
lines.length.times { header_lines << h }
|
177
|
+
lines
|
178
|
+
end.flatten.compact
|
179
|
+
[headers, header_lines]
|
128
180
|
end
|
129
181
|
|
130
182
|
def make_lines header, things
|
@@ -150,7 +202,9 @@ protected
|
|
150
202
|
end
|
151
203
|
|
152
204
|
def send_message
|
153
|
-
return
|
205
|
+
return false if !edited? && !BufferManager.ask_yes_or_no("Message unedited. Really send?")
|
206
|
+
return false if $config[:confirm_no_attachments] && mentions_attachments? && @attachments.size == 0 && !BufferManager.ask_yes_or_no("You haven't added any attachments. Really send?")#" stupid ruby-mode
|
207
|
+
return false if $config[:confirm_top_posting] && top_posting? && !BufferManager.ask_yes_or_no("You're top-posting. That makes you a bad person. Really send?") #" stupid ruby-mode
|
154
208
|
|
155
209
|
date = Time.now
|
156
210
|
from_email =
|
@@ -164,14 +218,16 @@ protected
|
|
164
218
|
BufferManager.flash "Sending..."
|
165
219
|
|
166
220
|
begin
|
167
|
-
IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date }
|
221
|
+
IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date, false }
|
168
222
|
raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
|
169
|
-
SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date }
|
223
|
+
SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date, true }
|
170
224
|
BufferManager.kill_buffer buffer
|
171
225
|
BufferManager.flash "Message sent!"
|
226
|
+
true
|
172
227
|
rescue SystemCallError, SendmailCommandFailed => e
|
173
228
|
Redwood::log "Problem sending mail: #{e.message}"
|
174
229
|
BufferManager.flash "Problem sending mail: #{e.message}"
|
230
|
+
false
|
175
231
|
end
|
176
232
|
end
|
177
233
|
|
@@ -181,7 +237,7 @@ protected
|
|
181
237
|
BufferManager.flash "Saved for later editing."
|
182
238
|
end
|
183
239
|
|
184
|
-
def write_full_message_to f, date=Time.now
|
240
|
+
def write_full_message_to f, date=Time.now, escape=false
|
185
241
|
m = RMail::Message.new
|
186
242
|
@header.each do |k, v|
|
187
243
|
next if v.nil? || v.empty?
|
@@ -199,26 +255,31 @@ protected
|
|
199
255
|
m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
|
200
256
|
|
201
257
|
if @attachments.empty?
|
202
|
-
m.header["Content-Disposition"] = "inline"
|
203
258
|
m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
|
204
259
|
m.body = @body.join
|
260
|
+
m.body = sanitize_body m.body if escape
|
205
261
|
m.body += sig_lines.join("\n") unless $config[:edit_signature]
|
206
262
|
else
|
207
263
|
body_m = RMail::Message.new
|
208
264
|
body_m.body = @body.join
|
265
|
+
body_m.body = sanitize_body body_m.body if escape
|
209
266
|
body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
|
267
|
+
body_m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
|
268
|
+
body_m.header["Content-Disposition"] = "inline"
|
210
269
|
|
211
270
|
m.add_part body_m
|
212
|
-
@attachments.each { |fn| m.
|
271
|
+
@attachments.each { |fn| m.add_file_attachment fn.to_s }
|
213
272
|
end
|
214
273
|
f.puts m.to_s
|
215
274
|
end
|
216
275
|
|
276
|
+
## TODO: remove this. redundant with write_full_message_to.
|
277
|
+
##
|
217
278
|
## this is going to change soon: draft messages (currently written
|
218
279
|
## with full=false) will be output as yaml.
|
219
280
|
def write_message f, full=true, date=Time.now
|
220
281
|
raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"]
|
221
|
-
f.puts
|
282
|
+
f.puts format_headers(@header).first
|
222
283
|
f.puts <<EOS
|
223
284
|
Date: #{date.rfc2822}
|
224
285
|
Message-Id: #{@message_id}
|
@@ -233,15 +294,35 @@ EOS
|
|
233
294
|
end
|
234
295
|
|
235
296
|
f.puts
|
236
|
-
f.puts @body.
|
297
|
+
f.puts sanitize_body(@body.join)
|
237
298
|
f.puts sig_lines if full unless $config[:edit_signature]
|
238
299
|
end
|
239
300
|
|
240
301
|
private
|
241
302
|
|
303
|
+
def sanitize_body body
|
304
|
+
body.gsub(/^From /, ">From ")
|
305
|
+
end
|
306
|
+
|
307
|
+
def mentions_attachments?
|
308
|
+
@body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
|
309
|
+
end
|
310
|
+
|
311
|
+
def top_posting?
|
312
|
+
@body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
|
313
|
+
end
|
314
|
+
|
242
315
|
def sig_lines
|
243
|
-
p = PersonManager.person_for
|
244
|
-
|
316
|
+
p = PersonManager.person_for(@header["From"])
|
317
|
+
from_email = p && p.email
|
318
|
+
|
319
|
+
## first run the hook
|
320
|
+
hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
|
321
|
+
return ["", "-- "] + hook_sig.split("\n") if hook_sig
|
322
|
+
|
323
|
+
## no hook, do default signature generation based on config.yaml
|
324
|
+
return [] unless from_email
|
325
|
+
sigfn = (AccountManager.account_for(from_email) ||
|
245
326
|
AccountManager.default_account).signature
|
246
327
|
|
247
328
|
if sigfn && File.exists?(sigfn)
|
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -9,7 +9,7 @@ class InboxMode < ThreadIndexMode
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize
|
12
|
-
super [:inbox, :sent], { :label => :inbox }
|
12
|
+
super [:inbox, :sent], { :label => :inbox, :skip_killed => true }
|
13
13
|
raise "can't have more than one!" if defined? @@instance
|
14
14
|
@@instance = self
|
15
15
|
end
|
@@ -3,7 +3,7 @@ module Redwood
|
|
3
3
|
class LabelListMode < LineCursorMode
|
4
4
|
register_keymap do |k|
|
5
5
|
k.add :select_label, "Select label", :enter
|
6
|
-
k.add :reload, "Discard label list and reload", '
|
6
|
+
k.add :reload, "Discard label list and reload", '@'
|
7
7
|
end
|
8
8
|
|
9
9
|
bool_reader :done
|
@@ -30,17 +30,18 @@ protected
|
|
30
30
|
|
31
31
|
def regen_text
|
32
32
|
@text = []
|
33
|
-
|
33
|
+
labels = LabelManager.listable_labels
|
34
34
|
|
35
|
-
counts =
|
36
|
-
|
35
|
+
counts = labels.map do |label|
|
36
|
+
string = LabelManager.string_for label
|
37
37
|
total = Index.num_results_for :label => label
|
38
38
|
unread = Index.num_results_for :labels => [label, :unread]
|
39
39
|
[label, string, total, unread]
|
40
|
-
end
|
40
|
+
end.sort_by { |l, s, t, u| s.downcase }
|
41
41
|
|
42
|
-
width =
|
42
|
+
width = counts.max_of { |l, s, t, u| s.length }
|
43
43
|
|
44
|
+
@labels = []
|
44
45
|
counts.map do |label, string, total, unread|
|
45
46
|
if total == 0 && !LabelManager::RESERVED_LABELS.include?(label)
|
46
47
|
Redwood::log "no hits for label #{label}, deleting"
|
@@ -50,6 +51,7 @@ protected
|
|
50
51
|
|
51
52
|
@text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
|
52
53
|
sprintf("%#{width + 1}s %5d %s, %5d unread", string, total, total == 1 ? " message" : "messages", unread)]]
|
54
|
+
@labels << label
|
53
55
|
yield i if block_given?
|
54
56
|
end.compact
|
55
57
|
end
|
@@ -3,7 +3,10 @@ module Redwood
|
|
3
3
|
class LabelSearchResultsMode < ThreadIndexMode
|
4
4
|
def initialize labels
|
5
5
|
@labels = labels
|
6
|
-
|
6
|
+
opts = { :labels => @labels }
|
7
|
+
opts[:load_deleted] = true if labels.include? :deleted
|
8
|
+
opts[:load_spam] = true if labels.include? :spam
|
9
|
+
super [], opts
|
7
10
|
end
|
8
11
|
|
9
12
|
def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
|
data/lib/sup/modes/log-mode.rb
CHANGED
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -24,13 +24,13 @@ class ReplyMode < EditMessageMode
|
|
24
24
|
body = reply_body_lines message
|
25
25
|
|
26
26
|
from =
|
27
|
-
if @m.recipient_email
|
28
|
-
|
27
|
+
if @m.recipient_email && (a = AccountManager.account_for(@m.recipient_email))
|
28
|
+
a
|
29
|
+
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
|
30
|
+
b
|
29
31
|
else
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
from_email = from.email
|
32
|
+
AccountManager.default_account
|
33
|
+
end
|
34
34
|
|
35
35
|
## ignore reply-to for list messages because it's typically set to
|
36
36
|
## the list address, which we explicitly treat with :list
|
@@ -39,36 +39,31 @@ class ReplyMode < EditMessageMode
|
|
39
39
|
|
40
40
|
@headers = {}
|
41
41
|
@headers[:sender] = {
|
42
|
-
"From" => "#{from.name} <#{from_email}>",
|
43
42
|
"To" => [to.full_address],
|
44
43
|
} unless AccountManager.is_account? to
|
45
44
|
|
46
45
|
@headers[:recipient] = {
|
47
|
-
"From" => "#{from.name} <#{from_email}>",
|
48
46
|
"To" => cc.map { |p| p.full_address },
|
49
47
|
} unless cc.empty? || @m.is_list_message?
|
50
48
|
|
51
|
-
@headers[:user] = {
|
52
|
-
"From" => "#{from.name} <#{from_email}>",
|
53
|
-
}
|
49
|
+
@headers[:user] = {}
|
54
50
|
|
55
51
|
@headers[:all] = {
|
56
|
-
"From" => "#{from.name} <#{from_email}>",
|
57
52
|
"To" => [to.full_address],
|
58
53
|
"Cc" => cc.select { |p| !AccountManager.is_account?(p) }.map { |p| p.full_address },
|
59
54
|
} unless cc.empty?
|
60
55
|
|
61
56
|
@headers[:list] = {
|
62
|
-
"From" => "#{from.name} <#{from_email}>",
|
63
57
|
"To" => [@m.list_address.full_address],
|
64
58
|
} if @m.is_list_message?
|
65
59
|
|
66
60
|
refs = gen_references
|
67
61
|
@headers.each do |k, v|
|
68
62
|
@headers[k] = {
|
69
|
-
"
|
70
|
-
"
|
71
|
-
"
|
63
|
+
"From" => "#{from.name} <#{from.email}>",
|
64
|
+
"To" => [],
|
65
|
+
"Cc" => [],
|
66
|
+
"Bcc" => [],
|
72
67
|
"In-Reply-To" => "<#{@m.id}>",
|
73
68
|
"Subject" => Message.reify_subj(@m.subj),
|
74
69
|
"References" => refs,
|
@@ -125,6 +120,13 @@ protected
|
|
125
120
|
def gen_references
|
126
121
|
(@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
|
127
122
|
end
|
123
|
+
|
124
|
+
def edit_field
|
125
|
+
@selected_type = :user
|
126
|
+
self.header = @headers[:user]
|
127
|
+
update
|
128
|
+
super
|
129
|
+
end
|
128
130
|
|
129
131
|
def move_cursor_left
|
130
132
|
i = @type_labels.index @selected_type
|
@@ -3,7 +3,7 @@ module Redwood
|
|
3
3
|
class SearchResultsMode < ThreadIndexMode
|
4
4
|
def initialize qobj
|
5
5
|
@qobj = qobj
|
6
|
-
super [], { :qobj => @qobj
|
6
|
+
super [], { :qobj => @qobj }
|
7
7
|
end
|
8
8
|
|
9
9
|
## a proper is_relevant? method requires some way of asking ferret
|
@@ -10,19 +10,19 @@ class ThreadIndexMode < LineCursorMode
|
|
10
10
|
|
11
11
|
register_keymap do |k|
|
12
12
|
k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
|
13
|
-
k.add :reload, "
|
13
|
+
k.add :reload, "Refresh view", '@'
|
14
14
|
k.add :toggle_archived, "Toggle archived status", 'a'
|
15
15
|
k.add :toggle_starred, "Star or unstar all messages in thread", '*'
|
16
16
|
k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
|
17
17
|
k.add :edit_labels, "Edit or add labels for a thread", 'l'
|
18
18
|
k.add :edit_message, "Edit message (drafts only)", 'e'
|
19
|
-
k.add :
|
20
|
-
k.add :
|
19
|
+
k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
|
20
|
+
k.add :toggle_deleted, "Delete/undelete thread", 'd'
|
21
21
|
k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
|
22
22
|
k.add :save, "Save changes now", '$'
|
23
23
|
k.add :jump_to_next_new, "Jump to next new thread", :tab
|
24
|
-
k.add :reply, "Reply to a thread", 'r'
|
25
|
-
k.add :forward, "Forward a thread", 'f'
|
24
|
+
k.add :reply, "Reply to latest message in a thread", 'r'
|
25
|
+
k.add :forward, "Forward latest message in a thread", 'f'
|
26
26
|
k.add :toggle_tagged, "Tag/untag current line", 't'
|
27
27
|
k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
|
28
28
|
end
|
@@ -80,7 +80,6 @@ class ThreadIndexMode < LineCursorMode
|
|
80
80
|
## the first draw_screen is needed before topline and botline
|
81
81
|
## are set, and the second to show the cursor having moved
|
82
82
|
|
83
|
-
t.remove_label :unread
|
84
83
|
update_text_for_line curpos
|
85
84
|
UpdateManager.relay self, :read, t
|
86
85
|
end
|
@@ -90,7 +89,7 @@ class ThreadIndexMode < LineCursorMode
|
|
90
89
|
threads.each { |t| select t }
|
91
90
|
end
|
92
91
|
|
93
|
-
def
|
92
|
+
def handle_label_update sender, m
|
94
93
|
t = @ts.thread_for(m) or return
|
95
94
|
l = @lines[t] or return
|
96
95
|
update_text_for_line l
|
@@ -105,6 +104,12 @@ class ThreadIndexMode < LineCursorMode
|
|
105
104
|
|
106
105
|
def handle_archived_update *a; handle_read_update(*a); end
|
107
106
|
|
107
|
+
def handle_deleted_update sender, t
|
108
|
+
handle_read_update sender, t
|
109
|
+
hide_thread t
|
110
|
+
regen_text
|
111
|
+
end
|
112
|
+
|
108
113
|
## overwrite me!
|
109
114
|
def is_relevant? m; false; end
|
110
115
|
|
@@ -145,8 +150,10 @@ class ThreadIndexMode < LineCursorMode
|
|
145
150
|
def actually_toggle_starred t
|
146
151
|
if t.has_label? :starred # if ANY message has a star
|
147
152
|
t.remove_label :starred # remove from all
|
153
|
+
UpdateManager.relay self, :unstarred, t
|
148
154
|
else
|
149
155
|
t.first.add_label :starred # add only to first
|
156
|
+
UpdateManager.relay self, :starred, t
|
150
157
|
end
|
151
158
|
end
|
152
159
|
|
@@ -172,6 +179,26 @@ class ThreadIndexMode < LineCursorMode
|
|
172
179
|
end
|
173
180
|
end
|
174
181
|
|
182
|
+
def actually_toggle_spammed t
|
183
|
+
if t.has_label? :spam
|
184
|
+
t.remove_label :spam
|
185
|
+
UpdateManager.relay self, :unspammed, t
|
186
|
+
else
|
187
|
+
t.apply_label :spam
|
188
|
+
UpdateManager.relay self, :spammed, t
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def actually_toggle_deleted t
|
193
|
+
if t.has_label? :deleted
|
194
|
+
t.remove_label :deleted
|
195
|
+
UpdateManager.relay self, :undeleted, t
|
196
|
+
else
|
197
|
+
t.apply_label :deleted
|
198
|
+
UpdateManager.relay self, :deleted, t
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
175
202
|
def toggle_archived
|
176
203
|
t = @threads[curpos] or return
|
177
204
|
actually_toggle_archived t
|
@@ -211,28 +238,36 @@ class ThreadIndexMode < LineCursorMode
|
|
211
238
|
end
|
212
239
|
end
|
213
240
|
|
214
|
-
def
|
241
|
+
def toggle_spam
|
215
242
|
t = @threads[curpos] or return
|
216
|
-
|
243
|
+
multi_toggle_spam [t]
|
217
244
|
end
|
218
245
|
|
219
|
-
|
246
|
+
## both spam and deleted have the curious characteristic that you
|
247
|
+
## always want to hide the thread after either applying or removing
|
248
|
+
## that label. in all thread-index-views except for
|
249
|
+
## label-search-results-mode, when you mark a message as spam or
|
250
|
+
## deleted, you want it to disappear immediately; in LSRM, you only
|
251
|
+
## see deleted or spam emails, and when you undelete or unspam them
|
252
|
+
## you also want them to disappear immediately.
|
253
|
+
def multi_toggle_spam threads
|
220
254
|
threads.each do |t|
|
221
|
-
t
|
222
|
-
hide_thread t
|
255
|
+
actually_toggle_spammed t
|
256
|
+
hide_thread t
|
223
257
|
end
|
224
258
|
regen_text
|
225
259
|
end
|
226
260
|
|
227
|
-
def
|
261
|
+
def toggle_deleted
|
228
262
|
t = @threads[curpos] or return
|
229
|
-
|
263
|
+
multi_toggle_deleted [t]
|
230
264
|
end
|
231
265
|
|
232
|
-
|
266
|
+
## see comment for multi_toggle_spam
|
267
|
+
def multi_toggle_deleted threads
|
233
268
|
threads.each do |t|
|
234
|
-
t
|
235
|
-
hide_thread t
|
269
|
+
actually_toggle_deleted t
|
270
|
+
hide_thread t
|
236
271
|
end
|
237
272
|
regen_text
|
238
273
|
end
|
@@ -248,6 +283,7 @@ class ThreadIndexMode < LineCursorMode
|
|
248
283
|
hide_thread t
|
249
284
|
end
|
250
285
|
regen_text
|
286
|
+
BufferManager.flash "Thread#{threads.size == 1 ? '' : 's'} killed."
|
251
287
|
end
|
252
288
|
|
253
289
|
def save
|
@@ -288,20 +324,12 @@ class ThreadIndexMode < LineCursorMode
|
|
288
324
|
thread = @threads[curpos] or return
|
289
325
|
speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
|
290
326
|
keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
|
291
|
-
label_string = modifyl.join(" ")
|
292
|
-
label_string += " " unless label_string.empty?
|
293
327
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
if hl.empty?
|
300
|
-
thread.labels = keepl + user_labels
|
301
|
-
user_labels.each { |l| LabelManager << l }
|
302
|
-
else
|
303
|
-
BufferManager.flash "'#{hl}' is a reserved label!"
|
304
|
-
end
|
328
|
+
user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
|
329
|
+
|
330
|
+
return unless user_labels
|
331
|
+
thread.labels = keepl + user_labels
|
332
|
+
user_labels.each { |l| LabelManager << l }
|
305
333
|
update_text_for_line curpos
|
306
334
|
end
|
307
335
|
|
@@ -336,7 +364,7 @@ class ThreadIndexMode < LineCursorMode
|
|
336
364
|
m.load_from_source!
|
337
365
|
mode = ForwardMode.new m
|
338
366
|
BufferManager.spawn "Forward of #{m.subj}", mode
|
339
|
-
mode.
|
367
|
+
mode.edit_message
|
340
368
|
end
|
341
369
|
|
342
370
|
def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
|
@@ -384,9 +412,9 @@ class ThreadIndexMode < LineCursorMode
|
|
384
412
|
myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
|
385
413
|
opts[:when_done].call(num) if opts[:when_done]
|
386
414
|
if num > 0
|
387
|
-
BufferManager.flash "Found #{num} threads"
|
415
|
+
BufferManager.flash "Found #{num} threads."
|
388
416
|
else
|
389
|
-
BufferManager.flash "No matches"
|
417
|
+
BufferManager.flash "No matches."
|
390
418
|
end
|
391
419
|
end)})
|
392
420
|
|