sup 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
@@ -0,0 +1,941 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class ThreadViewMode < LineCursorMode
|
4
|
+
## this holds all info we need to lay out a message
|
5
|
+
class MessageLayout
|
6
|
+
attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new, :toggled_state
|
7
|
+
end
|
8
|
+
|
9
|
+
class ChunkLayout
|
10
|
+
attr_accessor :state
|
11
|
+
end
|
12
|
+
|
13
|
+
INDENT_SPACES = 2 # how many spaces to indent child messages
|
14
|
+
|
15
|
+
HookManager.register "detailed-headers", <<EOS
|
16
|
+
Add or remove headers from the detailed header display of a message.
|
17
|
+
Variables:
|
18
|
+
message: The message whose headers are to be formatted.
|
19
|
+
headers: A hash of header (name, value) pairs, initialized to the default
|
20
|
+
headers.
|
21
|
+
Return value:
|
22
|
+
None. The variable 'headers' should be modified in place.
|
23
|
+
EOS
|
24
|
+
|
25
|
+
HookManager.register "bounce-command", <<EOS
|
26
|
+
Determines the command used to bounce a message.
|
27
|
+
Variables:
|
28
|
+
from: The From header of the message being bounced
|
29
|
+
(eg: likely _not_ your address).
|
30
|
+
to: The addresses you asked the message to be bounced to as an array.
|
31
|
+
Return value:
|
32
|
+
A string representing the command to pipe the mail into. This
|
33
|
+
should include the entire command except for the destination addresses,
|
34
|
+
which will be appended by sup.
|
35
|
+
EOS
|
36
|
+
|
37
|
+
HookManager.register "publish", <<EOS
|
38
|
+
Executed when a message or a chunk is requested to be published.
|
39
|
+
Variables:
|
40
|
+
chunk: Redwood::Message or Redwood::Chunk::* to be published.
|
41
|
+
Return value:
|
42
|
+
None.
|
43
|
+
EOS
|
44
|
+
|
45
|
+
register_keymap do |k|
|
46
|
+
k.add :toggle_detailed_header, "Toggle detailed header", 'h'
|
47
|
+
k.add :show_header, "Show full message header", 'H'
|
48
|
+
k.add :show_message, "Show full message (raw form)", 'V'
|
49
|
+
k.add :activate_chunk, "Expand/collapse or activate item", :enter
|
50
|
+
k.add :expand_all_messages, "Expand/collapse all messages", 'E'
|
51
|
+
k.add :edit_draft, "Edit draft", 'e'
|
52
|
+
k.add :send_draft, "Send draft", 'y'
|
53
|
+
k.add :edit_labels, "Edit or add labels for a thread", 'l'
|
54
|
+
k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
|
55
|
+
k.add :jump_to_next_open, "Jump to next open message", 'n'
|
56
|
+
k.add :jump_to_next_and_open, "Jump to next message and open", "\C-n"
|
57
|
+
k.add :jump_to_prev_open, "Jump to previous open message", 'p'
|
58
|
+
k.add :jump_to_prev_and_open, "Jump to previous message and open", "\C-p"
|
59
|
+
k.add :align_current_message, "Align current message in buffer", 'z'
|
60
|
+
k.add :toggle_starred, "Star or unstar message", '*'
|
61
|
+
k.add :toggle_new, "Toggle unread/read status of message", 'N'
|
62
|
+
# k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
|
63
|
+
k.add :reply, "Reply to a message", 'r'
|
64
|
+
k.add :reply_all, "Reply to all participants of this message", 'G'
|
65
|
+
k.add :forward, "Forward a message or attachment", 'f'
|
66
|
+
k.add :bounce, "Bounce message to other recipient(s)", '!'
|
67
|
+
k.add :alias, "Edit alias/nickname for a person", 'i'
|
68
|
+
k.add :edit_as_new, "Edit message as new", 'D'
|
69
|
+
k.add :save_to_disk, "Save message/attachment to disk", 's'
|
70
|
+
k.add :save_all_to_disk, "Save all attachments to disk", 'A'
|
71
|
+
k.add :publish, "Publish message/attachment using publish-hook", 'P'
|
72
|
+
k.add :search, "Search for messages from particular people", 'S'
|
73
|
+
k.add :compose, "Compose message to person", 'm'
|
74
|
+
k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
|
75
|
+
k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")"
|
76
|
+
k.add :pipe_message, "Pipe message or attachment to a shell command", '|'
|
77
|
+
|
78
|
+
k.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
|
79
|
+
k.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
|
80
|
+
k.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
|
81
|
+
k.add :toggle_wrap, "Toggle wrapping of text", 'w'
|
82
|
+
|
83
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
|
84
|
+
kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
|
85
|
+
kk.add :delete_and_kill, "Delete this thread and kill buffer", 'd'
|
86
|
+
kk.add :kill_and_kill, "Kill this thread and kill buffer", '&'
|
87
|
+
kk.add :spam_and_kill, "Mark this thread as spam and kill buffer", 's'
|
88
|
+
kk.add :unread_and_kill, "Mark this thread as unread and kill buffer", 'N'
|
89
|
+
kk.add :do_nothing_and_kill, "Just kill this buffer", '.'
|
90
|
+
end
|
91
|
+
|
92
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ',' do |kk|
|
93
|
+
kk.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
|
94
|
+
kk.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
|
95
|
+
kk.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
|
96
|
+
kk.add :spam_and_next, "Mark this thread as spam, kill buffer, and view next", 's'
|
97
|
+
kk.add :unread_and_next, "Mark this thread as unread, kill buffer, and view next", 'N'
|
98
|
+
kk.add :do_nothing_and_next, "Kill buffer, and view next", 'n', ','
|
99
|
+
end
|
100
|
+
|
101
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ']' do |kk|
|
102
|
+
kk.add :archive_and_prev, "Archive this thread, kill buffer, and view previous", 'a'
|
103
|
+
kk.add :delete_and_prev, "Delete this thread, kill buffer, and view previous", 'd'
|
104
|
+
kk.add :kill_and_prev, "Kill this thread, kill buffer, and view previous", '&'
|
105
|
+
kk.add :spam_and_prev, "Mark this thread as spam, kill buffer, and view previous", 's'
|
106
|
+
kk.add :unread_and_prev, "Mark this thread as unread, kill buffer, and view previous", 'N'
|
107
|
+
kk.add :do_nothing_and_prev, "Kill buffer, and view previous", 'n', ']'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
## there are a couple important instance variables we hold to format
|
112
|
+
## the thread and to provide line-based functionality. @layout is a
|
113
|
+
## map from Messages to MessageLayouts, and @chunk_layout from
|
114
|
+
## Chunks to ChunkLayouts. @message_lines is a map from row #s to
|
115
|
+
## Message objects. @chunk_lines is a map from row #s to Chunk
|
116
|
+
## objects. @person_lines is a map from row #s to Person objects.
|
117
|
+
|
118
|
+
def initialize thread, hidden_labels=[], index_mode=nil
|
119
|
+
super :slip_rows => $config[:slip_rows]
|
120
|
+
@thread = thread
|
121
|
+
@hidden_labels = hidden_labels
|
122
|
+
|
123
|
+
## used for dispatch-and-next
|
124
|
+
@index_mode = index_mode
|
125
|
+
@dying = false
|
126
|
+
|
127
|
+
@layout = SavingHash.new { MessageLayout.new }
|
128
|
+
@chunk_layout = SavingHash.new { ChunkLayout.new }
|
129
|
+
earliest, latest = nil, nil
|
130
|
+
latest_date = nil
|
131
|
+
altcolor = false
|
132
|
+
|
133
|
+
@thread.each do |m, d, p|
|
134
|
+
next unless m
|
135
|
+
earliest ||= m
|
136
|
+
@layout[m].state = initial_state_for m
|
137
|
+
@layout[m].toggled_state = false
|
138
|
+
@layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
|
139
|
+
@layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
|
140
|
+
@layout[m].orig_new = m.has_label? :read
|
141
|
+
altcolor = !altcolor
|
142
|
+
if latest_date.nil? || m.date > latest_date
|
143
|
+
latest_date = m.date
|
144
|
+
latest = m
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
@wrap = true
|
149
|
+
|
150
|
+
@layout[latest].state = :open if @layout[latest].state == :closed
|
151
|
+
@layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
|
152
|
+
end
|
153
|
+
|
154
|
+
def toggle_wrap
|
155
|
+
@wrap = !@wrap
|
156
|
+
regen_text
|
157
|
+
buffer.mark_dirty if buffer
|
158
|
+
end
|
159
|
+
|
160
|
+
def draw_line ln, opts={}
|
161
|
+
if ln == curpos
|
162
|
+
super ln, :highlight => true
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
166
|
+
end
|
167
|
+
def lines; @text.length; end
|
168
|
+
def [] i; @text[i]; end
|
169
|
+
|
170
|
+
## a little hacky---since regen_text can depend on buffer features like the
|
171
|
+
## content_width, we don't call it in the constructor, and instead call it
|
172
|
+
## here, which is set before we're responsible for drawing ourself.
|
173
|
+
def buffer= b
|
174
|
+
super
|
175
|
+
regen_text
|
176
|
+
end
|
177
|
+
|
178
|
+
def show_header
|
179
|
+
m = @message_lines[curpos] or return
|
180
|
+
BufferManager.spawn_unless_exists("Full header for #{m.id}") do
|
181
|
+
TextMode.new m.raw_header.ascii
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def show_message
|
186
|
+
m = @message_lines[curpos] or return
|
187
|
+
BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
|
188
|
+
TextMode.new m.raw_message.ascii
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def toggle_detailed_header
|
193
|
+
m = @message_lines[curpos] or return
|
194
|
+
@layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
|
195
|
+
update
|
196
|
+
end
|
197
|
+
|
198
|
+
def reply type_arg=nil
|
199
|
+
m = @message_lines[curpos] or return
|
200
|
+
mode = ReplyMode.new m, type_arg
|
201
|
+
BufferManager.spawn "Reply to #{m.subj}", mode
|
202
|
+
end
|
203
|
+
|
204
|
+
def reply_all; reply :all; end
|
205
|
+
|
206
|
+
def subscribe_to_list
|
207
|
+
m = @message_lines[curpos] or return
|
208
|
+
if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
209
|
+
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "subscribe")
|
210
|
+
else
|
211
|
+
BufferManager.flash "Can't find List-Subscribe header for this message."
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def unsubscribe_from_list
|
216
|
+
m = @message_lines[curpos] or return
|
217
|
+
if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
218
|
+
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
|
219
|
+
else
|
220
|
+
BufferManager.flash "Can't find List-Unsubscribe header for this message."
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def forward
|
225
|
+
if(chunk = @chunk_lines[curpos]) && chunk.is_a?(Chunk::Attachment)
|
226
|
+
ForwardMode.spawn_nicely :attachments => [chunk]
|
227
|
+
elsif(m = @message_lines[curpos])
|
228
|
+
ForwardMode.spawn_nicely :message => m
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def bounce
|
233
|
+
m = @message_lines[curpos] or return
|
234
|
+
to = BufferManager.ask_for_contacts(:people, "Bounce To: ") or return
|
235
|
+
|
236
|
+
defcmd = AccountManager.default_account.bounce_sendmail
|
237
|
+
|
238
|
+
cmd = case (hookcmd = HookManager.run "bounce-command", :from => m.from, :to => to)
|
239
|
+
when nil, /^$/ then defcmd
|
240
|
+
else hookcmd
|
241
|
+
end + ' ' + to.map { |t| t.email }.join(' ')
|
242
|
+
|
243
|
+
bt = to.size > 1 ? "#{to.size} recipients" : to[0].to_s
|
244
|
+
|
245
|
+
if BufferManager.ask_yes_or_no "Really bounce to #{bt}?"
|
246
|
+
debug "bounce command: #{cmd}"
|
247
|
+
begin
|
248
|
+
IO.popen(cmd, 'w') do |sm|
|
249
|
+
sm.puts m.raw_message
|
250
|
+
end
|
251
|
+
raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
|
252
|
+
m.add_label :forwarded
|
253
|
+
Index.save_message m
|
254
|
+
rescue SystemCallError, SendmailCommandFailed => e
|
255
|
+
warn "problem sending mail: #{e.message}"
|
256
|
+
BufferManager.flash "Problem sending mail: #{e.message}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
include CanAliasContacts
|
262
|
+
def alias
|
263
|
+
p = @person_lines[curpos] or return
|
264
|
+
alias_contact p
|
265
|
+
update
|
266
|
+
end
|
267
|
+
|
268
|
+
def search
|
269
|
+
p = @person_lines[curpos] or return
|
270
|
+
mode = PersonSearchResultsMode.new [p]
|
271
|
+
BufferManager.spawn "Search for #{p.name}", mode
|
272
|
+
mode.load_threads :num => mode.buffer.content_height
|
273
|
+
end
|
274
|
+
|
275
|
+
def compose
|
276
|
+
p = @person_lines[curpos]
|
277
|
+
if p
|
278
|
+
ComposeMode.spawn_nicely :to_default => p
|
279
|
+
else
|
280
|
+
ComposeMode.spawn_nicely
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def edit_labels
|
285
|
+
old_labels = @thread.labels
|
286
|
+
reserved_labels = old_labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
|
287
|
+
new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels.sort_by {|x| x.to_s}
|
288
|
+
|
289
|
+
return unless new_labels
|
290
|
+
@thread.labels = Set.new(reserved_labels) + new_labels
|
291
|
+
new_labels.each { |l| LabelManager << l }
|
292
|
+
update
|
293
|
+
UpdateManager.relay self, :labeled, @thread.first
|
294
|
+
Index.save_thread @thread
|
295
|
+
UndoManager.register "labeling thread" do
|
296
|
+
@thread.labels = old_labels
|
297
|
+
Index.save_thread @thread
|
298
|
+
UpdateManager.relay self, :labeled, @thread.first
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def toggle_starred
|
303
|
+
m = @message_lines[curpos] or return
|
304
|
+
toggle_label m, :starred
|
305
|
+
end
|
306
|
+
|
307
|
+
def toggle_new
|
308
|
+
m = @message_lines[curpos] or return
|
309
|
+
toggle_label m, :unread
|
310
|
+
end
|
311
|
+
|
312
|
+
def toggle_label m, label
|
313
|
+
if m.has_label? label
|
314
|
+
m.remove_label label
|
315
|
+
else
|
316
|
+
m.add_label label
|
317
|
+
end
|
318
|
+
## TODO: don't recalculate EVERYTHING just to add a stupid little
|
319
|
+
## star to the display
|
320
|
+
update
|
321
|
+
UpdateManager.relay self, :single_message_labeled, m
|
322
|
+
Index.save_thread @thread
|
323
|
+
end
|
324
|
+
|
325
|
+
## called when someone presses enter when the cursor is highlighting
|
326
|
+
## a chunk. for expandable chunks (including messages) we toggle
|
327
|
+
## open/closed state; for viewable chunks (like attachments) we
|
328
|
+
## view.
|
329
|
+
def activate_chunk
|
330
|
+
chunk = @chunk_lines[curpos] or return
|
331
|
+
if chunk.is_a? Chunk::Text
|
332
|
+
## if the cursor is over a text region, expand/collapse the
|
333
|
+
## entire message
|
334
|
+
chunk = @message_lines[curpos]
|
335
|
+
end
|
336
|
+
layout = if chunk.is_a?(Message)
|
337
|
+
@layout[chunk]
|
338
|
+
elsif chunk.expandable?
|
339
|
+
@chunk_layout[chunk]
|
340
|
+
end
|
341
|
+
if layout
|
342
|
+
layout.state = (layout.state != :closed ? :closed : :open)
|
343
|
+
#cursor_down if layout.state == :closed # too annoying
|
344
|
+
update
|
345
|
+
elsif chunk.viewable?
|
346
|
+
view chunk
|
347
|
+
end
|
348
|
+
if chunk.is_a?(Message) && $config[:jump_to_open_message]
|
349
|
+
jump_to_message chunk
|
350
|
+
jump_to_next_open if layout.state == :closed
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def edit_as_new
|
355
|
+
m = @message_lines[curpos] or return
|
356
|
+
mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
|
357
|
+
BufferManager.spawn "edit as new", mode
|
358
|
+
mode.default_edit_message
|
359
|
+
end
|
360
|
+
|
361
|
+
def save_to_disk
|
362
|
+
chunk = @chunk_lines[curpos] or return
|
363
|
+
case chunk
|
364
|
+
when Chunk::Attachment
|
365
|
+
default_dir = $config[:default_attachment_save_dir]
|
366
|
+
default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
|
367
|
+
default_fn = File.expand_path File.join(default_dir, chunk.filename)
|
368
|
+
fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
|
369
|
+
|
370
|
+
# if user selects directory use file name from message
|
371
|
+
if fn and File.directory? fn
|
372
|
+
fn = File.join(fn, chunk.filename)
|
373
|
+
end
|
374
|
+
|
375
|
+
save_to_file(fn) { |f| f.print chunk.raw_content } if fn
|
376
|
+
else
|
377
|
+
m = @message_lines[curpos]
|
378
|
+
fn = BufferManager.ask_for_filename :filename, "Save message to file: "
|
379
|
+
return unless fn
|
380
|
+
save_to_file(fn) do |f|
|
381
|
+
m.each_raw_message_line { |l| f.print l }
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def save_all_to_disk
|
387
|
+
m = @message_lines[curpos] or return
|
388
|
+
default_dir = ($config[:default_attachment_save_dir] || ".")
|
389
|
+
folder = BufferManager.ask_for_filename :filename, "Save all attachments to folder: ", default_dir, true
|
390
|
+
return unless folder
|
391
|
+
|
392
|
+
num = 0
|
393
|
+
num_errors = 0
|
394
|
+
m.chunks.each do |chunk|
|
395
|
+
next unless chunk.is_a?(Chunk::Attachment)
|
396
|
+
fn = File.join(folder, chunk.filename)
|
397
|
+
num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
|
398
|
+
num += 1
|
399
|
+
end
|
400
|
+
|
401
|
+
if num == 0
|
402
|
+
BufferManager.flash "Didn't find any attachments!"
|
403
|
+
else
|
404
|
+
if num_errors == 0
|
405
|
+
BufferManager.flash "Wrote #{num.pluralize 'attachment'} to #{folder}."
|
406
|
+
else
|
407
|
+
BufferManager.flash "Wrote #{(num - num_errors).pluralize 'attachment'} to #{folder}; couldn't write #{num_errors} of them (see log)."
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def publish
|
413
|
+
chunk = @chunk_lines[curpos] or return
|
414
|
+
if HookManager.enabled? "publish"
|
415
|
+
HookManager.run "publish", :chunk => chunk
|
416
|
+
else
|
417
|
+
BufferManager.flash "Publishing hook not defined."
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def edit_draft
|
422
|
+
m = @message_lines[curpos] or return
|
423
|
+
if m.is_draft?
|
424
|
+
mode = ResumeMode.new m
|
425
|
+
BufferManager.spawn "Edit message", mode
|
426
|
+
BufferManager.kill_buffer self.buffer
|
427
|
+
mode.default_edit_message
|
428
|
+
else
|
429
|
+
BufferManager.flash "Not a draft message!"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def send_draft
|
434
|
+
m = @message_lines[curpos] or return
|
435
|
+
if m.is_draft?
|
436
|
+
mode = ResumeMode.new m
|
437
|
+
BufferManager.spawn "Send message", mode
|
438
|
+
BufferManager.kill_buffer self.buffer
|
439
|
+
mode.send_message
|
440
|
+
else
|
441
|
+
BufferManager.flash "Not a draft message!"
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def jump_to_first_open
|
446
|
+
m = @message_lines[0] or return
|
447
|
+
if @layout[m].state != :closed
|
448
|
+
jump_to_message m#, true
|
449
|
+
else
|
450
|
+
jump_to_next_open #true
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def jump_to_next_and_open
|
455
|
+
return continue_search_in_buffer if in_search? # err.. don't know why im doing this
|
456
|
+
|
457
|
+
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
458
|
+
return unless m
|
459
|
+
|
460
|
+
nextm = @layout[m].next
|
461
|
+
return unless nextm
|
462
|
+
|
463
|
+
if @layout[m].toggled_state == true
|
464
|
+
@layout[m].state = :closed
|
465
|
+
@layout[m].toggled_state = false
|
466
|
+
update
|
467
|
+
end
|
468
|
+
|
469
|
+
if @layout[nextm].state == :closed
|
470
|
+
@layout[nextm].state = :open
|
471
|
+
@layout[nextm].toggled_state = true
|
472
|
+
end
|
473
|
+
|
474
|
+
jump_to_message nextm if nextm
|
475
|
+
|
476
|
+
update if @layout[nextm].toggled_state
|
477
|
+
end
|
478
|
+
|
479
|
+
def jump_to_next_open force_alignment=nil
|
480
|
+
return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
|
481
|
+
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
482
|
+
return unless m
|
483
|
+
while nextm = @layout[m].next
|
484
|
+
break if @layout[nextm].state != :closed
|
485
|
+
m = nextm
|
486
|
+
end
|
487
|
+
jump_to_message nextm, force_alignment if nextm
|
488
|
+
end
|
489
|
+
|
490
|
+
def align_current_message
|
491
|
+
m = @message_lines[curpos] or return
|
492
|
+
jump_to_message m, true
|
493
|
+
end
|
494
|
+
|
495
|
+
def jump_to_prev_and_open force_alignment=nil
|
496
|
+
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
|
497
|
+
return unless m
|
498
|
+
|
499
|
+
nextm = @layout[m].prev
|
500
|
+
return unless nextm
|
501
|
+
|
502
|
+
if @layout[m].toggled_state == true
|
503
|
+
@layout[m].state = :closed
|
504
|
+
@layout[m].toggled_state = false
|
505
|
+
update
|
506
|
+
end
|
507
|
+
|
508
|
+
if @layout[nextm].state == :closed
|
509
|
+
@layout[nextm].state = :open
|
510
|
+
@layout[nextm].toggled_state = true
|
511
|
+
end
|
512
|
+
|
513
|
+
jump_to_message nextm if nextm
|
514
|
+
update if @layout[nextm].toggled_state
|
515
|
+
end
|
516
|
+
|
517
|
+
def jump_to_prev_open
|
518
|
+
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
|
519
|
+
return unless m
|
520
|
+
## jump to the top of the current message if we're in the body;
|
521
|
+
## otherwise, to the previous message
|
522
|
+
|
523
|
+
top = @layout[m].top
|
524
|
+
if curpos == top
|
525
|
+
while(prevm = @layout[m].prev)
|
526
|
+
break if @layout[prevm].state != :closed
|
527
|
+
m = prevm
|
528
|
+
end
|
529
|
+
jump_to_message prevm if prevm
|
530
|
+
else
|
531
|
+
jump_to_message m
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def jump_to_message m, force_alignment=false
|
536
|
+
l = @layout[m]
|
537
|
+
|
538
|
+
## boundaries of the message
|
539
|
+
message_left = l.depth * INDENT_SPACES
|
540
|
+
message_right = message_left + l.width
|
541
|
+
|
542
|
+
## calculate leftmost colum
|
543
|
+
left = if force_alignment # force mode: align exactly
|
544
|
+
message_left
|
545
|
+
else # regular: minimize cursor movement
|
546
|
+
## leftmost and rightmost are boundaries of all valid left-column
|
547
|
+
## alignments.
|
548
|
+
leftmost = [message_left, message_right - buffer.content_width + 1].min
|
549
|
+
rightmost = message_left
|
550
|
+
leftcol.clamp(leftmost, rightmost)
|
551
|
+
end
|
552
|
+
|
553
|
+
jump_to_line l.top # move vertically
|
554
|
+
jump_to_col left # move horizontally
|
555
|
+
set_cursor_pos l.top # set cursor pos
|
556
|
+
end
|
557
|
+
|
558
|
+
def expand_all_messages
|
559
|
+
@global_message_state ||= :closed
|
560
|
+
@global_message_state = (@global_message_state == :closed ? :open : :closed)
|
561
|
+
@layout.each { |m, l| l.state = @global_message_state }
|
562
|
+
update
|
563
|
+
end
|
564
|
+
|
565
|
+
def collapse_non_new_messages
|
566
|
+
@layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
|
567
|
+
update
|
568
|
+
end
|
569
|
+
|
570
|
+
def expand_all_quotes
|
571
|
+
if(m = @message_lines[curpos])
|
572
|
+
quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
|
573
|
+
numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
|
574
|
+
newstate = numopen > quotes.length / 2 ? :closed : :open
|
575
|
+
quotes.each { |c| @chunk_layout[c].state = newstate }
|
576
|
+
update
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def cleanup
|
581
|
+
@layout = @chunk_layout = @text = nil # for good luck
|
582
|
+
end
|
583
|
+
|
584
|
+
def archive_and_kill; archive_and_then :kill end
|
585
|
+
def spam_and_kill; spam_and_then :kill end
|
586
|
+
def delete_and_kill; delete_and_then :kill end
|
587
|
+
def kill_and_kill; kill_and_then :kill end
|
588
|
+
def unread_and_kill; unread_and_then :kill end
|
589
|
+
def do_nothing_and_kill; do_nothing_and_then :kill end
|
590
|
+
|
591
|
+
def archive_and_next; archive_and_then :next end
|
592
|
+
def spam_and_next; spam_and_then :next end
|
593
|
+
def delete_and_next; delete_and_then :next end
|
594
|
+
def kill_and_next; kill_and_then :next end
|
595
|
+
def unread_and_next; unread_and_then :next end
|
596
|
+
def do_nothing_and_next; do_nothing_and_then :next end
|
597
|
+
|
598
|
+
def archive_and_prev; archive_and_then :prev end
|
599
|
+
def spam_and_prev; spam_and_then :prev end
|
600
|
+
def delete_and_prev; delete_and_then :prev end
|
601
|
+
def kill_and_prev; kill_and_then :prev end
|
602
|
+
def unread_and_prev; unread_and_then :prev end
|
603
|
+
def do_nothing_and_prev; do_nothing_and_then :prev end
|
604
|
+
|
605
|
+
def archive_and_then op
|
606
|
+
dispatch op do
|
607
|
+
@thread.remove_label :inbox
|
608
|
+
UpdateManager.relay self, :archived, @thread.first
|
609
|
+
Index.save_thread @thread
|
610
|
+
UndoManager.register "archiving 1 thread" do
|
611
|
+
@thread.apply_label :inbox
|
612
|
+
Index.save_thread @thread
|
613
|
+
UpdateManager.relay self, :unarchived, @thread.first
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
def spam_and_then op
|
619
|
+
dispatch op do
|
620
|
+
@thread.apply_label :spam
|
621
|
+
UpdateManager.relay self, :spammed, @thread.first
|
622
|
+
Index.save_thread @thread
|
623
|
+
UndoManager.register "marking 1 thread as spam" do
|
624
|
+
@thread.remove_label :spam
|
625
|
+
Index.save_thread @thread
|
626
|
+
UpdateManager.relay self, :unspammed, @thread.first
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
def delete_and_then op
|
632
|
+
dispatch op do
|
633
|
+
@thread.apply_label :deleted
|
634
|
+
UpdateManager.relay self, :deleted, @thread.first
|
635
|
+
Index.save_thread @thread
|
636
|
+
UndoManager.register "deleting 1 thread" do
|
637
|
+
@thread.remove_label :deleted
|
638
|
+
Index.save_thread @thread
|
639
|
+
UpdateManager.relay self, :undeleted, @thread.first
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
def kill_and_then op
|
645
|
+
dispatch op do
|
646
|
+
@thread.apply_label :killed
|
647
|
+
UpdateManager.relay self, :killed, @thread.first
|
648
|
+
Index.save_thread @thread
|
649
|
+
UndoManager.register "killed 1 thread" do
|
650
|
+
@thread.remove_label :killed
|
651
|
+
Index.save_thread @thread
|
652
|
+
UpdateManager.relay self, :unkilled, @thread.first
|
653
|
+
end
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
def unread_and_then op
|
658
|
+
dispatch op do
|
659
|
+
@thread.apply_label :unread
|
660
|
+
UpdateManager.relay self, :unread, @thread.first
|
661
|
+
Index.save_thread @thread
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
def do_nothing_and_then op
|
666
|
+
dispatch op
|
667
|
+
end
|
668
|
+
|
669
|
+
def dispatch op
|
670
|
+
return if @dying
|
671
|
+
@dying = true
|
672
|
+
|
673
|
+
l = lambda do
|
674
|
+
yield if block_given?
|
675
|
+
BufferManager.kill_buffer_safely buffer
|
676
|
+
end
|
677
|
+
|
678
|
+
case op
|
679
|
+
when :next
|
680
|
+
@index_mode.launch_next_thread_after @thread, &l
|
681
|
+
when :prev
|
682
|
+
@index_mode.launch_prev_thread_before @thread, &l
|
683
|
+
when :kill
|
684
|
+
l.call
|
685
|
+
else
|
686
|
+
raise ArgumentError, "unknown thread dispatch operation #{op.inspect}"
|
687
|
+
end
|
688
|
+
end
|
689
|
+
private :dispatch
|
690
|
+
|
691
|
+
def pipe_message
|
692
|
+
chunk = @chunk_lines[curpos]
|
693
|
+
chunk = nil unless chunk.is_a?(Chunk::Attachment)
|
694
|
+
message = @message_lines[curpos] unless chunk
|
695
|
+
|
696
|
+
return unless chunk || message
|
697
|
+
|
698
|
+
command = BufferManager.ask(:shell, "pipe command: ")
|
699
|
+
return if command.nil? || command.empty?
|
700
|
+
|
701
|
+
output = pipe_to_process(command) do |stream|
|
702
|
+
if chunk
|
703
|
+
stream.print chunk.raw_content
|
704
|
+
else
|
705
|
+
message.each_raw_message_line { |l| stream.print l }
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
if output
|
710
|
+
BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
|
711
|
+
else
|
712
|
+
BufferManager.flash "'#{command}' done!"
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
|
717
|
+
def status
|
718
|
+
user_labels = @thread.labels.to_a.map do |l|
|
719
|
+
l.to_s if LabelManager.user_defined_labels.member?(l)
|
720
|
+
end.compact.join(",")
|
721
|
+
user_labels = (user_labels.empty? and "" or "<#{user_labels}>")
|
722
|
+
[user_labels, super].join(" -- ")
|
723
|
+
end
|
724
|
+
|
725
|
+
private
|
726
|
+
|
727
|
+
def initial_state_for m
|
728
|
+
if m.has_label?(:starred) || m.has_label?(:unread)
|
729
|
+
:open
|
730
|
+
else
|
731
|
+
:closed
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
def update
|
736
|
+
regen_text
|
737
|
+
buffer.mark_dirty if buffer
|
738
|
+
end
|
739
|
+
|
740
|
+
## here we generate the actual content lines. we accumulate
|
741
|
+
## everything into @text, and we set @chunk_lines and
|
742
|
+
## @message_lines, and we update @layout.
|
743
|
+
def regen_text
|
744
|
+
@text = []
|
745
|
+
@chunk_lines = []
|
746
|
+
@message_lines = []
|
747
|
+
@person_lines = []
|
748
|
+
|
749
|
+
prevm = nil
|
750
|
+
@thread.each do |m, depth, parent|
|
751
|
+
unless m.is_a? Message # handle nil and :fake_root
|
752
|
+
@text += chunk_to_lines m, nil, @text.length, depth, parent
|
753
|
+
next
|
754
|
+
end
|
755
|
+
l = @layout[m]
|
756
|
+
|
757
|
+
## is this still necessary?
|
758
|
+
next unless @layout[m].state # skip discarded drafts
|
759
|
+
|
760
|
+
## build the patina
|
761
|
+
text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color
|
762
|
+
|
763
|
+
l.top = @text.length
|
764
|
+
l.bot = @text.length + text.length # updated below
|
765
|
+
l.prev = prevm
|
766
|
+
l.next = nil
|
767
|
+
l.depth = depth
|
768
|
+
# l.state we preserve
|
769
|
+
l.width = 0 # updated below
|
770
|
+
@layout[l.prev].next = m if l.prev
|
771
|
+
|
772
|
+
(0 ... text.length).each do |i|
|
773
|
+
@chunk_lines[@text.length + i] = m
|
774
|
+
@message_lines[@text.length + i] = m
|
775
|
+
lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum
|
776
|
+
end
|
777
|
+
|
778
|
+
@text += text
|
779
|
+
prevm = m
|
780
|
+
if l.state != :closed
|
781
|
+
m.chunks.each do |c|
|
782
|
+
cl = @chunk_layout[c]
|
783
|
+
|
784
|
+
## set the default state for chunks
|
785
|
+
cl.state ||=
|
786
|
+
if c.expandable? && c.respond_to?(:initial_state)
|
787
|
+
c.initial_state
|
788
|
+
else
|
789
|
+
:closed
|
790
|
+
end
|
791
|
+
|
792
|
+
text = chunk_to_lines c, cl.state, @text.length, depth
|
793
|
+
(0 ... text.length).each do |i|
|
794
|
+
@chunk_lines[@text.length + i] = c
|
795
|
+
@message_lines[@text.length + i] = m
|
796
|
+
lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
|
797
|
+
l.width = lw if lw > l.width
|
798
|
+
end
|
799
|
+
@text += text
|
800
|
+
end
|
801
|
+
@layout[m].bot = @text.length
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
|
806
|
+
def message_patina_lines m, state, start, parent, prefix, color, star_color
|
807
|
+
prefix_widget = [color, prefix]
|
808
|
+
|
809
|
+
open_widget = [color, (state == :closed ? "+ " : "- ")]
|
810
|
+
new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
|
811
|
+
starred_widget = if m.has_label?(:starred)
|
812
|
+
[star_color, "*"]
|
813
|
+
else
|
814
|
+
[color, " "]
|
815
|
+
end
|
816
|
+
attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
|
817
|
+
|
818
|
+
case state
|
819
|
+
when :open
|
820
|
+
@person_lines[start] = m.from
|
821
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
822
|
+
[color,
|
823
|
+
"#{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!})"]]]
|
824
|
+
|
825
|
+
when :closed
|
826
|
+
@person_lines[start] = m.from
|
827
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
828
|
+
[color,
|
829
|
+
"#{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! : ''}"]]]
|
830
|
+
|
831
|
+
when :detailed
|
832
|
+
@person_lines[start] = m.from
|
833
|
+
from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
834
|
+
[color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
|
835
|
+
|
836
|
+
addressee_lines = []
|
837
|
+
unless m.to.empty?
|
838
|
+
m.to.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
839
|
+
addressee_lines += format_person_list " To: ", m.to
|
840
|
+
end
|
841
|
+
unless m.cc.empty?
|
842
|
+
m.cc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
843
|
+
addressee_lines += format_person_list " Cc: ", m.cc
|
844
|
+
end
|
845
|
+
unless m.bcc.empty?
|
846
|
+
m.bcc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
847
|
+
addressee_lines += format_person_list " Bcc: ", m.bcc
|
848
|
+
end
|
849
|
+
|
850
|
+
headers = OrderedHash.new
|
851
|
+
headers["Date"] = "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})"
|
852
|
+
headers["Subject"] = m.subj
|
853
|
+
|
854
|
+
show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
|
855
|
+
unless show_labels.empty?
|
856
|
+
headers["Labels"] = show_labels.map { |x| x.to_s }.sort.join(', ')
|
857
|
+
end
|
858
|
+
if parent
|
859
|
+
headers["In reply to"] = "#{parent.from.mediumname}'s message of #{parent.date.to_message_nice_s}"
|
860
|
+
end
|
861
|
+
|
862
|
+
HookManager.run "detailed-headers", :message => m, :headers => headers
|
863
|
+
|
864
|
+
from_line + (addressee_lines + headers.map { |k, v| " #{k}: #{v}" }).map { |l| [[color, prefix + " " + l]] }
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
def format_person_list prefix, people
|
869
|
+
ptext = people.map { |p| format_person p }
|
870
|
+
pad = " " * prefix.display_length
|
871
|
+
[prefix + ptext.first + (ptext.length > 1 ? "," : "")] +
|
872
|
+
ptext[1 .. -1].map_with_index do |e, i|
|
873
|
+
pad + e + (i == ptext.length - 1 ? "" : ",")
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
def format_person p
|
878
|
+
p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
|
879
|
+
end
|
880
|
+
|
881
|
+
def maybe_wrap_text lines
|
882
|
+
if @wrap
|
883
|
+
config_width = $config[:wrap_width]
|
884
|
+
if config_width and config_width != 0
|
885
|
+
width = [config_width, buffer.content_width].min
|
886
|
+
else
|
887
|
+
width = buffer.content_width
|
888
|
+
end
|
889
|
+
# lines can apparently be both String and Array, convert to Array for map.
|
890
|
+
if lines.kind_of? String
|
891
|
+
lines = lines.lines.to_a
|
892
|
+
end
|
893
|
+
lines = lines.map { |l| l.chomp.wrap width if l }.flatten
|
894
|
+
end
|
895
|
+
return lines
|
896
|
+
end
|
897
|
+
|
898
|
+
## todo: check arguments on this overly complex function
|
899
|
+
def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
|
900
|
+
prefix = " " * INDENT_SPACES * depth
|
901
|
+
case chunk
|
902
|
+
when :fake_root
|
903
|
+
[[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
|
904
|
+
when nil
|
905
|
+
[[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
|
906
|
+
when Message
|
907
|
+
message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
|
908
|
+
(chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. Hit 'e' to edit, 'y' to send. <<<"]]] : [])
|
909
|
+
|
910
|
+
else
|
911
|
+
raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
|
912
|
+
if chunk.inlineable?
|
913
|
+
lines = maybe_wrap_text(chunk.lines)
|
914
|
+
lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
|
915
|
+
elsif chunk.expandable?
|
916
|
+
case state
|
917
|
+
when :closed
|
918
|
+
[[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
|
919
|
+
when :open
|
920
|
+
lines = maybe_wrap_text(chunk.lines)
|
921
|
+
[[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
|
922
|
+
end
|
923
|
+
else
|
924
|
+
[[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
|
925
|
+
end
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
929
|
+
def view chunk
|
930
|
+
BufferManager.flash "viewing #{chunk.content_type} attachment..."
|
931
|
+
success = chunk.view!
|
932
|
+
BufferManager.erase_flash
|
933
|
+
BufferManager.completely_redraw_screen
|
934
|
+
unless success
|
935
|
+
BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s.ascii, chunk.filename)
|
936
|
+
BufferManager.flash "Couldn't execute view command, viewing as text."
|
937
|
+
end
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
end
|