lib/sup/crypto.rb
lib/sup/draft.rb
lib/sup/hook.rb
+lib/sup/horizontal-selector.rb
lib/sup/imap.rb
lib/sup/index.rb
lib/sup/keymap.rb
require "sup/draft"
require "sup/poll"
require "sup/crypto"
+require "sup/horizontal-selector"
require "sup/modes/line-cursor-mode"
require "sup/modes/help-mode"
require "sup/modes/edit-message-mode"
case bg
when Curses::COLOR_CYAN
Curses::COLOR_YELLOW
+ when Curses::COLOR_YELLOW
+ Curses::COLOR_BLUE
else
Curses::COLOR_CYAN
end
class CryptoManager
include Singleton
+ OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
+ [:sign, "Sign"],
+ [:sign_and_encrypt, "Sign and encrypt"],
+ [:encrypt, "Encrypt only"]
+ )
+
def initialize
@mutex = Mutex.new
self.class.i_am_the_instance self
@cmd =
case bin
when /\S/
+ Redwood::log "crypto: detected gpg binary in #{bin}"
"#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
else
+ Redwood::log "crypto: no gpg binary detected"
nil
end
end
- # returns a cryptosignature
+ def have_crypto?; !@cmd.nil? end
+
def verify payload, signature # both RubyMail::Message objects
return unknown_status(cant_find_binary) unless @cmd
class SendmailCommandFailed < StandardError; end
class EditMessageMode < LineCursorMode
+ DECORATION_LINES = 1
+
FORCE_HEADERS = %w(From To Cc Bcc Subject)
MULTI_HEADERS = %w(To Cc Bcc)
NON_EDITABLE_HEADERS = %w(Message-Id Date)
k.add :save_as_draft, "Save as draft", 'P'
k.add :attach_file, "Attach a file", 'a'
k.add :delete_attachment, "Delete an attachment", 'd'
+ k.add :move_cursor_right, "Move selector to the right", :right
+ k.add :move_cursor_left, "Move selector to the left", :left
end
def initialize opts={}
@attachments = []
@message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
@edited = false
- @skip_top_rows = opts[:skip_top_rows] || 0
+ @reserve_top_rows = opts[:reserve_top_rows] || 0
+ @selectors = []
+ @selector_label_width = 0
+
+ @crypto_selector =
+ if CryptoManager.have_crypto?
+ HorizontalSelector.new "Crypto:", [:none] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.keys, ["None"] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.values
+ end
+ add_selector @crypto_selector if @crypto_selector
HookManager.run "before-edit", :header => @header, :body => @body
regen_text
end
- def lines; @text.length end
- def [] i; @text[i] end
+ def lines; @text.length + (@selectors.empty? ? 0 : (@selectors.length + DECORATION_LINES)) end
+
+ def [] i
+ if @selectors.empty?
+ @text[i]
+ elsif i < @selectors.length
+ @selectors[i].line @selector_label_width
+ elsif i == @selectors.length
+ "-" * buffer.content_width
+ else
+ @text[i - @selectors.length - DECORATION_LINES]
+ end
+ end
- ## a hook
+ ## hook for subclasses. i hate this style of programming.
def handle_new_text header, body; end
def edit_message_or_field
- if (curpos - @skip_top_rows) >= @header_lines.length
+ lines = DECORATION_LINES + @selectors.size
+ if (curpos - lines) >= @header_lines.length
edit_message
else
- edit_field @header_lines[curpos - @skip_top_rows]
+ edit_field @header_lines[curpos - lines]
end
end
end
def delete_attachment
- i = (curpos - @skip_top_rows) - @attachment_lines_offset
+ i = (curpos - @reserve_top_rows) - @attachment_lines_offset
if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
@attachments.delete_at i
update
protected
+ def move_cursor_left
+ return unless curpos < @selectors.length
+ @selectors[curpos].roll_left
+ buffer.mark_dirty
+ end
+
+ def move_cursor_right
+ return unless curpos < @selectors.length
+ @selectors[curpos].roll_right
+ buffer.mark_dirty
+ end
+
+ def add_selector s
+ @selectors << s
+ @selector_label_width = [@selector_label_width, s.label.length].max
+ end
+
def update
regen_text
buffer.mark_dirty if buffer
class ReplyMode < EditMessageMode
REPLY_TYPES = [:sender, :recipient, :list, :all, :user]
TYPE_DESCRIPTIONS = {
- :sender => "Reply to sender",
- :recipient => "Reply to recipient",
- :all => "Reply to all",
- :list => "Reply to mailing list",
- :user => "Customized reply"
+ :sender => "Sender",
+ :recipient => "Recipient",
+ :all => "All",
+ :list => "Mailing list",
+ :user => "Customized"
}
- register_keymap do |k|
- k.add :move_cursor_right, "Move cursor to the right", :right
- k.add :move_cursor_left, "Move cursor to the left", :left
- end
-
def initialize message
@m = message
## next, cc:
cc = (@m.to + @m.cc - [from, to]).uniq
- Redwood::log "cc = (#{@m.to.inspect} + #{@m.cc.inspect} - #{[from, to].inspect}).uniq = #{cc.inspect}"
## one potential reply type is "reply to recipient". this only happens
## in certain cases:
}.merge v
end
- @type_labels = REPLY_TYPES.select { |t| @headers.member?(t) }
- @selected_type =
+ types = REPLY_TYPES.select { |t| @headers.member?(t) }
+ @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
+
+ @type_selector.set_to(
if @m.is_list_message?
:list
elsif @headers.member? :sender
:sender
else
:recipient
- end
+ end)
- super :header => @headers[@selected_type], :body => body,
- :skip_top_rows => 2, :twiddles => false
+ super :header => @headers[@type_selector.val], :body => body, :twiddles => false
+ add_selector @type_selector
end
- def lines; super + 2; end
- def [] i
- case i
- when 0
- @type_labels.inject([]) do |array, t|
- array + [[(t == @selected_type ? :none_highlight : :none),
- "#{TYPE_DESCRIPTIONS[t]}"], [:none, " "]]
- end + [[:none, ""]]
- when 1
- ""
- else
- super(i - 2)
+protected
+
+ def move_cursor_right
+ super
+ if @headers[@type_selector.val] != self.header
+ self.header = @headers[@type_selector.val]
+ update
end
end
-protected
+ def move_cursor_left
+ super
+ if @headers[@type_selector.val] != self.header
+ self.header = @headers[@type_selector.val]
+ update
+ end
+ end
def reply_body_lines m
lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] + m.quotable_body_lines.map { |l| "> #{l}" }
end
def handle_new_text new_header, new_body
- old_header = @headers[@selected_type]
+ old_header = @headers[@type_selector.val]
if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
- @selected_type = :user
+ @type_selector.set_to :user
self.header = @headers[:user] = new_header
update
end
def edit_field field
edited_field = super
if edited_field && edited_field != "Subject"
- @selected_type = :user
+ @type_selector.set_to :user
update
end
end
-
- def move_cursor_left
- i = @type_labels.index @selected_type
- @selected_type = @type_labels[(i - 1) % @type_labels.length]
- self.header = @headers[@selected_type]
- update
- end
-
- def move_cursor_right
- i = @type_labels.index @selected_type
- @selected_type = @type_labels[(i + 1) % @type_labels.length]
- self.header = @headers[@selected_type]
- update
- end
end
end
draw_line_from_array ln, s, opts
end
else
- raise "unknown drawable object: #{s.inspect}" # good for debugging
+ raise "unknown drawable object: #{s.inspect} in #{self} for line #{ln}" # good for debugging
end
## speed test
class OrderedHash < Hash
alias_method :store, :[]=
alias_method :each_pair, :each
+ attr_reader :keys
- def initialize
+ def initialize *a
@keys = []
+ a.each { |k, v| self[k] = v }
end
- def []=(key, val)
+ def []= key, val
@keys << key unless member?(key)
super
end
- def delete(key)
- @keys.delete(key)
+ def values; keys.map { |k| self[k] } end
+ def index key; @keys.index key end
+
+ def delete key
+ @keys.delete key
super
end
- def each
- @keys.each { |k| yield k, self[k] }
- end
+ def each; @keys.each { |k| yield k, self[k] } end
end