sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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