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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. 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,12 @@
1
+ module Redwood
2
+
3
+ class PersonSearchResultsMode < ThreadIndexMode
4
+ def initialize people
5
+ @people = people
6
+ super [], { :participants => @people }
7
+ end
8
+
9
+ def is_relevant? m; @people.any? { |p| m.from == p }; end
10
+ end
11
+
12
+ 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