sup 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,203 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
## extends ScrollMode to have a line-based cursor.
|
|
4
|
+
class LineCursorMode < ScrollMode
|
|
5
|
+
register_keymap do |k|
|
|
6
|
+
## overwrite scrollmode binding on arrow keys for cursor movement
|
|
7
|
+
## but j and k still scroll!
|
|
8
|
+
k.add :cursor_down, "Move cursor down one line", :down, 'j'
|
|
9
|
+
k.add :cursor_up, "Move cursor up one line", :up, 'k'
|
|
10
|
+
k.add :select, "Select this item", :enter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :curpos
|
|
14
|
+
|
|
15
|
+
def initialize opts={}
|
|
16
|
+
@cursor_top = @curpos = opts.delete(:skip_top_rows) || 0
|
|
17
|
+
@load_more_callbacks = []
|
|
18
|
+
@load_more_q = Queue.new
|
|
19
|
+
@load_more_thread = ::Thread.new do
|
|
20
|
+
while true
|
|
21
|
+
e = @load_more_q.pop
|
|
22
|
+
@load_more_callbacks.each { |c| c.call e }
|
|
23
|
+
sleep 0.5
|
|
24
|
+
@load_more_q.pop until @load_more_q.empty?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
super opts
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cleanup
|
|
32
|
+
@load_more_thread.kill
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def draw
|
|
37
|
+
super
|
|
38
|
+
set_status
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
## callbacks when the cursor is asked to go beyond the bottom
|
|
44
|
+
def to_load_more &b
|
|
45
|
+
@load_more_callbacks << b
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def draw_line ln, opts={}
|
|
49
|
+
if ln == @curpos
|
|
50
|
+
super ln, :highlight => true, :debug => opts[:debug], :color => :text_color
|
|
51
|
+
else
|
|
52
|
+
super ln, :color => :text_color
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ensure_mode_validity
|
|
57
|
+
super
|
|
58
|
+
raise @curpos.inspect unless @curpos.is_a?(Integer)
|
|
59
|
+
c = @curpos.clamp topline, botline - 1
|
|
60
|
+
c = @cursor_top if c < @cursor_top
|
|
61
|
+
buffer.mark_dirty unless c == @curpos
|
|
62
|
+
@curpos = c
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_cursor_pos p
|
|
66
|
+
return if @curpos == p
|
|
67
|
+
@curpos = p.clamp @cursor_top, lines
|
|
68
|
+
buffer.mark_dirty
|
|
69
|
+
set_status
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
## override search behavior to be cursor-based. this is a stupid
|
|
73
|
+
## implementation and should be made better. TODO: improve.
|
|
74
|
+
def search_goto_line line
|
|
75
|
+
page_down while line >= botline
|
|
76
|
+
page_up while line < topline
|
|
77
|
+
set_cursor_pos line
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def search_start_line; @curpos end
|
|
81
|
+
|
|
82
|
+
def line_down # overwrite scrollmode
|
|
83
|
+
super
|
|
84
|
+
call_load_more_callbacks([topline + buffer.content_height - lines, 10].max) if topline + buffer.content_height > lines
|
|
85
|
+
set_cursor_pos topline if @curpos < topline
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def line_up # overwrite scrollmode
|
|
89
|
+
super
|
|
90
|
+
set_cursor_pos botline - 1 if @curpos > botline - 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cursor_down
|
|
94
|
+
call_load_more_callbacks buffer.content_height if @curpos >= lines - [buffer.content_height/2,1].max
|
|
95
|
+
return false unless @curpos < lines - 1
|
|
96
|
+
|
|
97
|
+
if $config[:continuous_scroll] and (@curpos == botline - 3 and @curpos < lines - 3)
|
|
98
|
+
# load more lines, one at a time.
|
|
99
|
+
jump_to_line topline + 1
|
|
100
|
+
@curpos += 1
|
|
101
|
+
unless buffer.dirty?
|
|
102
|
+
draw_line @curpos - 1
|
|
103
|
+
draw_line @curpos
|
|
104
|
+
set_status
|
|
105
|
+
buffer.commit
|
|
106
|
+
end
|
|
107
|
+
elsif @curpos >= botline - 1
|
|
108
|
+
page_down
|
|
109
|
+
set_cursor_pos topline
|
|
110
|
+
else
|
|
111
|
+
@curpos += 1
|
|
112
|
+
unless buffer.dirty?
|
|
113
|
+
draw_line @curpos - 1
|
|
114
|
+
draw_line @curpos
|
|
115
|
+
set_status
|
|
116
|
+
buffer.commit
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cursor_up
|
|
123
|
+
return false unless @curpos > @cursor_top
|
|
124
|
+
|
|
125
|
+
if $config[:continuous_scroll] and (@curpos == topline + 2)
|
|
126
|
+
jump_to_line topline - 1
|
|
127
|
+
@curpos -= 1
|
|
128
|
+
unless buffer.dirty?
|
|
129
|
+
draw_line @curpos + 1
|
|
130
|
+
draw_line @curpos
|
|
131
|
+
set_status
|
|
132
|
+
buffer.commit
|
|
133
|
+
end
|
|
134
|
+
elsif @curpos == topline
|
|
135
|
+
old_topline = topline
|
|
136
|
+
page_up
|
|
137
|
+
set_cursor_pos [old_topline - 1, topline].max
|
|
138
|
+
else
|
|
139
|
+
@curpos -= 1
|
|
140
|
+
unless buffer.dirty?
|
|
141
|
+
draw_line @curpos + 1
|
|
142
|
+
draw_line @curpos
|
|
143
|
+
set_status
|
|
144
|
+
buffer.commit
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def page_up # overwrite
|
|
151
|
+
if topline <= @cursor_top
|
|
152
|
+
set_cursor_pos @cursor_top
|
|
153
|
+
else
|
|
154
|
+
relpos = @curpos - topline
|
|
155
|
+
super
|
|
156
|
+
set_cursor_pos topline + relpos
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
## more complicated than one might think. three behaviors.
|
|
161
|
+
def page_down
|
|
162
|
+
## if we're on the last page, and it's not a full page, just move
|
|
163
|
+
## the cursor down to the bottom and assume we can't load anything
|
|
164
|
+
## else via the callbacks.
|
|
165
|
+
if topline > lines - buffer.content_height
|
|
166
|
+
set_cursor_pos(lines - 1)
|
|
167
|
+
|
|
168
|
+
## if we're on the last page, and it's a full page, try and load
|
|
169
|
+
## more lines via the callbacks and then shift the page down
|
|
170
|
+
elsif topline == lines - buffer.content_height
|
|
171
|
+
call_load_more_callbacks buffer.content_height
|
|
172
|
+
super
|
|
173
|
+
|
|
174
|
+
## otherwise, just move down
|
|
175
|
+
else
|
|
176
|
+
relpos = @curpos - topline
|
|
177
|
+
super
|
|
178
|
+
set_cursor_pos [topline + relpos, lines - 1].min
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def jump_to_start
|
|
183
|
+
super
|
|
184
|
+
set_cursor_pos @cursor_top
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def jump_to_end
|
|
188
|
+
super if topline < (lines - buffer.content_height)
|
|
189
|
+
set_cursor_pos(lines - 1)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def set_status
|
|
195
|
+
l = lines
|
|
196
|
+
@status = l > 0 ? "line #{@curpos + 1} of #{l}" : ""
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def call_load_more_callbacks size
|
|
200
|
+
@load_more_q.push size if $config[:load_more_threads_when_scrolling]
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
module Redwood
|
|
3
|
+
|
|
4
|
+
## a variant of text mode that allows the user to automatically follow text,
|
|
5
|
+
## and respawns when << is called if necessary.
|
|
6
|
+
|
|
7
|
+
class LogMode < TextMode
|
|
8
|
+
register_keymap do |k|
|
|
9
|
+
k.add :toggle_follow, "Toggle follow mode", 'f'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
## if buffer_name is supplied, this mode will spawn a buffer
|
|
13
|
+
## upon receiving the << message. otherwise, it will act like
|
|
14
|
+
## a regular buffer.
|
|
15
|
+
def initialize autospawn_buffer_name=nil
|
|
16
|
+
@follow = true
|
|
17
|
+
@autospawn_buffer_name = autospawn_buffer_name
|
|
18
|
+
@on_kill = []
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
## register callbacks for when the buffer is killed
|
|
23
|
+
def on_kill &b; @on_kill << b end
|
|
24
|
+
|
|
25
|
+
def toggle_follow
|
|
26
|
+
@follow = !@follow
|
|
27
|
+
if @follow
|
|
28
|
+
jump_to_line(lines - buffer.content_height + 1) # leave an empty line at bottom
|
|
29
|
+
end
|
|
30
|
+
buffer.mark_dirty
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def << s
|
|
34
|
+
if buffer.nil? && @autospawn_buffer_name
|
|
35
|
+
BufferManager.spawn @autospawn_buffer_name, self, :hidden => true, :system => true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
s.split("\n").each { |l| super(l + "\n") } # insane. different << semantics.
|
|
39
|
+
|
|
40
|
+
if @follow
|
|
41
|
+
follow_top = lines - buffer.content_height + 1
|
|
42
|
+
jump_to_line follow_top if topline < follow_top
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def status
|
|
47
|
+
super + " (follow: #@follow)"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cleanup
|
|
51
|
+
@on_kill.each { |cb| cb.call self }
|
|
52
|
+
self.text = ""
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
class PollMode < LogMode
|
|
4
|
+
def initialize
|
|
5
|
+
@new = true
|
|
6
|
+
super "poll for new messages"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def poll
|
|
10
|
+
unless @new
|
|
11
|
+
@new = false
|
|
12
|
+
self << "\n"
|
|
13
|
+
end
|
|
14
|
+
self << "Poll started at #{Time.now}\n"
|
|
15
|
+
PollManager.do_poll { |s| self << (s + "\n") }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
class ReplyMode < EditMessageMode
|
|
4
|
+
REPLY_TYPES = [:sender, :recipient, :list, :all, :user]
|
|
5
|
+
TYPE_DESCRIPTIONS = {
|
|
6
|
+
:sender => "Sender",
|
|
7
|
+
:recipient => "Recipient",
|
|
8
|
+
:all => "All",
|
|
9
|
+
:list => "Mailing list",
|
|
10
|
+
:user => "Customized"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
HookManager.register "attribution", <<EOS
|
|
14
|
+
Generates an attribution ("Excerpts from Joe Bloggs's message of Fri Jan 11 09:54:32 -0500 2008:").
|
|
15
|
+
Variables:
|
|
16
|
+
message: a message object representing the message being replied to
|
|
17
|
+
(useful values include message.from.name and message.date)
|
|
18
|
+
Return value:
|
|
19
|
+
A string containing the text of the quote line (can be multi-line)
|
|
20
|
+
EOS
|
|
21
|
+
|
|
22
|
+
HookManager.register "reply-from", <<EOS
|
|
23
|
+
Selects a default address for the From: header of a new reply.
|
|
24
|
+
Variables:
|
|
25
|
+
message: a message object representing the message being replied to
|
|
26
|
+
(useful values include message.recipient_email, message.to, and message.cc)
|
|
27
|
+
Return value:
|
|
28
|
+
A Person to be used as the default for the From: header, or nil to use the
|
|
29
|
+
default behavior.
|
|
30
|
+
EOS
|
|
31
|
+
|
|
32
|
+
HookManager.register "reply-to", <<EOS
|
|
33
|
+
Set the default reply-to mode.
|
|
34
|
+
Variables:
|
|
35
|
+
modes: array of valid modes to choose from, which will be a subset of
|
|
36
|
+
[:#{REPLY_TYPES * ', :'}]
|
|
37
|
+
The default behavior is equivalent to
|
|
38
|
+
([:list, :sender, :recipent] & modes)[0]
|
|
39
|
+
Return value:
|
|
40
|
+
The reply mode you desire, or nil to use the default behavior.
|
|
41
|
+
EOS
|
|
42
|
+
|
|
43
|
+
def initialize message, type_arg=nil
|
|
44
|
+
@m = message
|
|
45
|
+
@edited = false
|
|
46
|
+
|
|
47
|
+
## it's important to put this early because it forces a read of
|
|
48
|
+
## the full headers (most importantly the list-post header, if
|
|
49
|
+
## any)
|
|
50
|
+
body = reply_body_lines message
|
|
51
|
+
@body_orig = body
|
|
52
|
+
|
|
53
|
+
## first, determine the address at which we received this email. this will
|
|
54
|
+
## become our From: address in the reply.
|
|
55
|
+
hook_reply_from = HookManager.run "reply-from", :message => @m
|
|
56
|
+
|
|
57
|
+
## sanity check that selection is a Person (or we'll fail below)
|
|
58
|
+
## don't check that it's an Account, though; assume they know what they're
|
|
59
|
+
## doing.
|
|
60
|
+
if hook_reply_from && !(hook_reply_from.is_a? Person)
|
|
61
|
+
info "reply-from returned non-Person, using default from."
|
|
62
|
+
hook_reply_from = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
## determine the from address of a reply.
|
|
66
|
+
## if we have a value from a hook, use it.
|
|
67
|
+
from = if hook_reply_from
|
|
68
|
+
hook_reply_from
|
|
69
|
+
## otherwise, try and find an account somewhere in the list of to's
|
|
70
|
+
## and cc's and look up the corresponding name form the list of accounts.
|
|
71
|
+
## if this does not succeed use the recipient_email (=envelope-to) instead.
|
|
72
|
+
## this is for the case where mail is received from a mailing lists (so the
|
|
73
|
+
## To: is the list id itself). if the user subscribes via a particular
|
|
74
|
+
## alias, we want to use that alias in the reply.
|
|
75
|
+
elsif(b = (@m.to.collect {|t| t.email} + @m.cc.collect {|c| c.email} + [@m.recipient_email] ).find { |p| AccountManager.is_account_email? p })
|
|
76
|
+
a = AccountManager.account_for(b)
|
|
77
|
+
Person.new a.name, b
|
|
78
|
+
## if all else fails, use the default
|
|
79
|
+
else
|
|
80
|
+
AccountManager.default_account
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
## now, determine to: and cc: addressess. we ignore reply-to for list
|
|
84
|
+
## messages because it's typically set to the list address, which we
|
|
85
|
+
## explicitly treat with reply type :list
|
|
86
|
+
to = @m.is_list_message? ? @m.from : (@m.replyto || @m.from)
|
|
87
|
+
|
|
88
|
+
## next, cc:
|
|
89
|
+
cc = (@m.to + @m.cc - [from, to]).uniq
|
|
90
|
+
|
|
91
|
+
## one potential reply type is "reply to recipient". this only happens
|
|
92
|
+
## in certain cases:
|
|
93
|
+
## if there's no cc, then the sender is the person you want to reply
|
|
94
|
+
## to. if it's a list message, then the list address is. otherwise,
|
|
95
|
+
## the cc contains a recipient.
|
|
96
|
+
useful_recipient = !(cc.empty? || @m.is_list_message?)
|
|
97
|
+
|
|
98
|
+
@headers = {}
|
|
99
|
+
@headers[:recipient] = {
|
|
100
|
+
"To" => cc.map { |p| p.full_address },
|
|
101
|
+
"Cc" => [],
|
|
102
|
+
} if useful_recipient
|
|
103
|
+
|
|
104
|
+
## typically we don't want to have a reply-to-sender option if the sender
|
|
105
|
+
## is a user account. however, if the cc is empty, it's a message to
|
|
106
|
+
## ourselves, so for the lack of any other options, we'll add it.
|
|
107
|
+
@headers[:sender] = {
|
|
108
|
+
"To" => [to.full_address],
|
|
109
|
+
"Cc" => [],
|
|
110
|
+
} if !AccountManager.is_account?(to) || !useful_recipient
|
|
111
|
+
|
|
112
|
+
@headers[:user] = {
|
|
113
|
+
"To" => [],
|
|
114
|
+
"Cc" => [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
not_me_ccs = cc.select { |p| !AccountManager.is_account?(p) }
|
|
118
|
+
@headers[:all] = {
|
|
119
|
+
"To" => [to.full_address],
|
|
120
|
+
"Cc" => not_me_ccs.map { |p| p.full_address },
|
|
121
|
+
} unless not_me_ccs.empty?
|
|
122
|
+
|
|
123
|
+
@headers[:list] = {
|
|
124
|
+
"To" => [@m.list_address.full_address],
|
|
125
|
+
"Cc" => [],
|
|
126
|
+
} if @m.is_list_message?
|
|
127
|
+
|
|
128
|
+
refs = gen_references
|
|
129
|
+
|
|
130
|
+
types = REPLY_TYPES.select { |t| @headers.member?(t) }
|
|
131
|
+
@type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
|
|
132
|
+
|
|
133
|
+
hook_reply = HookManager.run "reply-to", :modes => types
|
|
134
|
+
|
|
135
|
+
@type_selector.set_to(
|
|
136
|
+
if types.include? type_arg
|
|
137
|
+
type_arg
|
|
138
|
+
elsif types.include? hook_reply
|
|
139
|
+
hook_reply
|
|
140
|
+
elsif @m.is_list_message?
|
|
141
|
+
:list
|
|
142
|
+
elsif @headers.member? :sender
|
|
143
|
+
:sender
|
|
144
|
+
else
|
|
145
|
+
:recipient
|
|
146
|
+
end)
|
|
147
|
+
|
|
148
|
+
headers_full = {
|
|
149
|
+
"From" => from.full_address,
|
|
150
|
+
"Bcc" => [],
|
|
151
|
+
"In-reply-to" => "<#{@m.id}>",
|
|
152
|
+
"Subject" => Message.reify_subj(@m.subj),
|
|
153
|
+
"References" => refs,
|
|
154
|
+
}.merge @headers[@type_selector.val]
|
|
155
|
+
|
|
156
|
+
HookManager.run "before-edit", :header => headers_full, :body => body
|
|
157
|
+
|
|
158
|
+
super :header => headers_full, :body => body, :twiddles => false
|
|
159
|
+
add_selector @type_selector
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
protected
|
|
163
|
+
|
|
164
|
+
def move_cursor_right
|
|
165
|
+
super
|
|
166
|
+
if @headers[@type_selector.val] != self.header
|
|
167
|
+
self.header = self.header.merge @headers[@type_selector.val]
|
|
168
|
+
rerun_crypto_selector_hook
|
|
169
|
+
update
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def move_cursor_left
|
|
174
|
+
super
|
|
175
|
+
if @headers[@type_selector.val] != self.header
|
|
176
|
+
self.header = self.header.merge @headers[@type_selector.val]
|
|
177
|
+
rerun_crypto_selector_hook
|
|
178
|
+
update
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def reply_body_lines m
|
|
183
|
+
attribution = HookManager.run("attribution", :message => m) || default_attribution(m)
|
|
184
|
+
lines = attribution.split("\n") + m.quotable_body_lines.map { |l| "> #{l}" }
|
|
185
|
+
lines.pop while lines.last =~ /^\s*$/
|
|
186
|
+
lines
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def default_attribution m
|
|
190
|
+
"Excerpts from #{@m.from.name}'s message of #{@m.date}:"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_new_text new_header, new_body
|
|
194
|
+
if new_body != @body_orig
|
|
195
|
+
@body_orig = new_body
|
|
196
|
+
@edited = true
|
|
197
|
+
end
|
|
198
|
+
old_header = @headers[@type_selector.val]
|
|
199
|
+
if old_header.any? { |k, v| new_header[k] != v }
|
|
200
|
+
@type_selector.set_to :user
|
|
201
|
+
self.header["To"] = @headers[:user]["To"] = new_header["To"]
|
|
202
|
+
self.header["Cc"] = @headers[:user]["Cc"] = new_header["Cc"]
|
|
203
|
+
update
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def gen_references
|
|
208
|
+
(@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def edit_field field
|
|
212
|
+
edited_field = super
|
|
213
|
+
if edited_field and (field == "To" or field == "Cc")
|
|
214
|
+
@type_selector.set_to :user
|
|
215
|
+
@headers[:user]["To"] = self.header["To"]
|
|
216
|
+
@headers[:user]["Cc"] = self.header["Cc"]
|
|
217
|
+
update
|
|
218
|
+
end
|
|
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
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
end
|