lib/sup/tagger.rb
lib/sup/textfield.rb
lib/sup/thread.rb
+lib/sup/undo.rb
lib/sup/update.rb
lib/sup/util.rb
- Unix-centrism in MIME attachment handling and in sendmail
invocation.
-- Several obvious missing features, like undo, filters / saved
+- Several obvious missing features, like filters / saved
searches, message annotations, etc.
== SYNOPSYS:
k.add :quit_ask, "Quit Sup, but ask first", 'q'
k.add :quit_now, "Quit Sup immediately", 'Q'
k.add :help, "Show help", '?'
- k.add :roll_buffers, "Switch to next buffer", 'b'
-# k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
+ k.add_multi "(n)ext/(p)revious:", 'b' do |kk|
+ kk.add :roll_buffers, "Switch to next buffer", 'n'
+ kk.add :roll_buffers_backwards, "Switch to previous buffer", 'p'
+ end
k.add :kill_buffer, "Kill the current buffer", 'x'
k.add :list_buffers, "List all buffers", 'B'
k.add :list_contacts, "List contacts", 'C'
when :search_unread
SearchResultsMode.spawn_from_query "is:unread"
when :list_labels
- labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
+ labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
unless user_label.nil?
if user_label.empty?
next if target == :changed && entry && entry[:source_id].to_i == source.id && entry[:source_info].to_i == offset
## get the state currently in the index
- index_state =
- if entry
- entry[:label].split(/\s+/).map { |x| x.intern }
- else
- nil
- end
+ index_state = entry[:label].split(/\s+/).map { |x| x.intern } if entry
## skip if we're operating on restored messages, and this one
## ain't.
## assign message labels based on the operation we're performing
case op
when :asis
- m.labels = index_state if index_state
+ m.labels = ((m.labels - [:unread, :inbox]) + index_state).uniq if index_state
when :restore
## if the entry exists on disk
if restored_state[m.id]
component: sup
release:
reporter: William Morgan <wmorgan-sup@masanjin.net>
-status: :unstarted
-disposition:
+status: :closed
+disposition: :fixed
creation_time: 2008-05-19 23:42:25.910550 Z
references: []
- William Morgan <wmorgan-sup@masanjin.net>
- unassigned from release 0.6
- ""
+- - 2008-11-22 16:31:27.450146 Z
+ - Nicolas Pouillard <nicolas.pouillard@gmail.com>
+ - closed with disposition fixed
+ - This mapping and the PersonManager are now removed.
git_branch:
--- /dev/null
+#compdef sup sup-add sup-config sup-dump sup-sync sup-sync-back sup-tweak-labels sup-recover-sources
+# vim: set et sw=2 sts=2 ts=2 ft=zsh :
+
+# TODO: sources completion: maildir://some/dir, mbox://some/file, ...
+# for sup-add, sup-sync, sup-sync-back, sup-tweak-labels
+
+(( ${+functions[_sup_cmd]} )) ||
+_sup_cmd()
+{
+ _arguments -s : \
+ "(--list-hooks -l)"{--list-hooks,-l}"[list all hooks and descriptions, and quit]" \
+ "(--no-threads -n)"{--no-threads,-n}"[turn off threading]" \
+ "(--no-initial-poll -o)"{--no-initial-poll,-o}"[Don't poll for new messages when starting]" \
+ "(--search -s)"{--search,-s}"[search for this query upon startup]:Query: " \
+ "(--compose -c)"{--compose,-c}"[compose message to this recipient upon startup]:Email: " \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_add_cmd]} )) ||
+_sup_add_cmd()
+{
+ _arguments -s : \
+ "(--archive -a)"{--archive,-a}"[automatically archive all new messages from this source]" \
+ "(--unusual -u)"{--unusual,-u}"[do not automatically poll for new messages from this source]" \
+ "(--labels -l)"{--labels,-l}"[set of labels to apply to all messages from this source]:Labels: " \
+ "(--force-new -f)"{--force-new,-f}"[create a new account for this source, even if one already exists]" \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_config_cmd]} )) ||
+_sup_config_cmd()
+{
+ _arguments -s : \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_dump_cmd]} )) ||
+_sup_dump_cmd()
+{
+ _arguments -s : \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_recover_sources_cmd]} )) ||
+_sup_recover_sources_cmd()
+{
+ _arguments -s : \
+ "--archive[automatically archive all new messages from this source]" \
+ "--scan-num[number of messages to scan per source]:" \
+ "--unusual[do not automatically poll for new messages from this source]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_sync_cmd]} )) ||
+_sup_sync_cmd()
+{
+ # XXX Add only when --restore is given: (--restored -r)
+ # Add only when --changed or--all are given: (--start-at -s)
+ _arguments -s : \
+ "--new[operate on new messages only]" \
+ "(--changed -c)"{--changed,-c}"[scan over the entire source for messages that have been deleted, altered, or moved from another source]" \
+ "(--restored -r)"{--restored,-r}"[operate only on those messages included in a dump file as specified by --restore which have changed state]" \
+ "(--all -a)"{--all,-a}"[operate on all messages in the source, regardless of newness or changedness]" \
+ "(--start-at -s)"{--start-at,-s}"[start at a particular offset]:Offset: " \
+ "--asis[if the message is already in the index, preserve its state, otherwise, use default source state]" \
+ "--restore[restore message state from a dump file created with sup-dump]:File:_file" \
+ "--discard[discard any message state in the index and use the default source state]" \
+ "(--archive -x)"{--archive,-x}"[mark messages as archived when using the default source state]" \
+ "(--read -e)"{--read,-e}"[mark messages as read when using the default source state]" \
+ "--extra-labels[apply these labels when using the default source state]:Labels: " \
+ "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+ "(--optimize -o)"{--optimize,-o}"[as the final operation, optimize the index]" \
+ "--all-sources[scan over all sources]" \
+ "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_sync_back_cmd]} )) ||
+_sup_sync_back_cmd()
+{
+ _arguments -s : \
+ "(--drop-deleted -d)"{--drop-deleted,-d}"[drop deleted messages]" \
+ "--move-deleted[move deleted messages to a local mbox file]:File:_file" \
+ "(--drop-spam -s)"{--drop-spam,-s}"[drop spam messages]" \
+ "--move-spam[move spam messages to a local mbox file]:File:_file" \
+ "--with-dotlockfile[specific dotlockfile location (mbox files only)]:File:_file" \
+ "--dont-use-dotlockfile[don't use dotlockfile to lock mbox files]" \
+ "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+ "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_tweak_labels_cmd]} )) ||
+_sup_tweak_labels_cmd()
+{
+ _arguments -s : \
+ "(--add -a)"{--add,-a}"[which labels to add to every message from the specified sources]:Labels: " \
+ "(--remove -r)"{--remove,-r}"[which labels to remove from every message from the specified sources]:Labels: " \
+ "--all-sources[scan over all sources]" \
+ "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+ "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+ "--version[show version information]" \
+ "(--help -h)"{--help,-h}"[show help]"
+}
+
+_call_function ret _${words[1]//-/_}_cmd
+return ret
+
COLOR_FN = File.join(BASE_DIR, "colors.yaml")
SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
LABEL_FN = File.join(BASE_DIR, "labels.txt")
- PERSON_FN = File.join(BASE_DIR, "people.txt")
CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
DRAFT_DIR = File.join(BASE_DIR, "drafts")
SENT_FN = File.join(BASE_DIR, "sent.mbox")
end
def start
- Redwood::PersonManager.new Redwood::PERSON_FN
Redwood::SentManager.new Redwood::SENT_FN
Redwood::ContactManager.new Redwood::CONTACT_FN
Redwood::LabelManager.new Redwood::LABEL_FN
Redwood::PollManager.new
Redwood::SuicideManager.new Redwood::SUICIDE_FN
Redwood::CryptoManager.new
+ Redwood::UndoManager.new
end
def finish
Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
- Redwood::PersonManager.save if Redwood::PersonManager.instantiated?
Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
end
require "sup/draft"
require "sup/poll"
require "sup/crypto"
+require "sup/undo"
require "sup/horizontal-selector"
require "sup/modes/line-cursor-mode"
require "sup/modes/help-mode"
def initialize h
raise ArgumentError, "no name for account" unless h[:name]
- raise ArgumentError, "no name for email" unless h[:name]
- super h[:name], h[:email], 0, true
+ raise ArgumentError, "no email for account" unless h[:email]
+ super h[:name], h[:email]
@sendmail = h[:sendmail]
@signature = h[:signature]
end
hash[:alternates] ||= []
a = Account.new hash
- PersonManager.register a
@accounts[a] = true
if default
default = default_labels.join(" ")
default += " " unless default.empty?
- applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
+ # here I would prefer to give more control and allow all_labels instead of
+ # user_defined_labels only
+ applyable_labels = (LabelManager.user_defined_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
answer = ask_many_with_completions domain, question, applyable_labels, default
answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
if answer
- answer.split_on_commas.map { |x| ContactManager.contact_for(x) || PersonManager.person_for(x) }
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
end
end
IO.foreach(fn) do |l|
l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
aalias, addr = $1, $2
- p = PersonManager.person_for addr, :definitive => true
+ p = Person.from_address addr
@p2a[p] = aalias
@a2p[aalias] = p unless aalias.nil? || aalias.empty?
end
:snippet => snippet, # always override
:label => labels.uniq.join(" "),
:attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
- :from => (entry[:from] || (m.from ? m.from.indexable_content : "")),
- :to => (entry[:to] || (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" ")),
+
+ ## always override :from and :to.
+ ## older versions of Sup would often store the wrong thing in the index
+ ## (because they were canonicalizing email addresses, resulting in the
+ ## wrong name associated with each.) the correct address is read from
+ ## the original header when these messages are opened in thread-view-mode,
+ ## so this allows people to forcibly update the address in the index by
+ ## marking those threads for saving.
+ :from => (m.from ? m.from.indexable_content : ""),
+ :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
+
:subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
:refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
}
t = @index[docid][:to]
if AccountManager.is_account_email? f
- t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
+ t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
else
- contacts[PersonManager.person_for(f)] = true
+ contacts[Person.from_address(f)] = true
end
end
end
## add/remove these via normal label mechanisms.
RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment ]
- ## labels which it nonetheless makes sense to search for by
- LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox, :attachment ]
-
## labels that will typically be hidden from the user
HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment ]
self.class.i_am_the_instance self
end
- ## all listable (just user-defined at the moment) labels, ordered
+ ## all labels user-defined and system, ordered
## nicely and converted to pretty strings. use #label_for to recover
## the original label.
- def listable_labels
+ def all_labels
## uniq's only necessary here because of certain upgrade issues
- (LISTABLE_RESERVED_LABELS + @labels.keys).uniq
+ (RESERVED_LABELS + @labels.keys).uniq
end
- ## all apply-able (user-defined and system listable) labels, ordered
+ ## all user-defined labels, ordered
## nicely and converted to pretty strings. use #label_for to recover
## the original label.
- def applyable_labels
+ def user_defined_labels
@labels.keys
end
@from =
if header["from"]
- PersonManager.person_for header["from"]
+ Person.from_address header["from"]
else
fakename = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
- PersonManager.person_for fakename
+ Person.from_address fakename
end
Redwood::log "faking message-id for message from #@from: #{id}" if fakeid
end
@subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
- @to = PersonManager.people_for header["to"]
- @cc = PersonManager.people_for header["cc"]
- @bcc = PersonManager.people_for header["bcc"]
+ @to = Person.from_address_list header["to"]
+ @cc = Person.from_address_list header["cc"]
+ @bcc = Person.from_address_list header["bcc"]
## before loading our full header from the source, we can actually
## have some extra refs set by the UI. (this happens when the user
@refs = (@refs + refs).uniq
@replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
- @replyto = PersonManager.person_for header["reply-to"]
+ @replyto = Person.from_address header["reply-to"]
@list_address =
if header["list-post"]
- @list_address = PersonManager.person_for header["list-post"].gsub(/^<mailto:|>$/, "")
+ @list_address = Person.from_address header["list-post"].gsub(/^<mailto:|>$/, "")
else
nil
end
elsif m.header.content_type == "message/rfc822"
payload = RMail::Parser.read(m.body)
from = payload.header.from.first
- from_person = from ? PersonManager.person_for(from.format) : nil
+ from_person = from ? Person.from_address(from.format) : nil
[Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
message_to_chunks(payload, encrypted)
else
## do whatever crypto transformation is necessary
if @crypto_selector && @crypto_selector.val != :none
- from_email = PersonManager.person_for(@header["From"]).email
- to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
+ from_email = Person.from_address(@header["From"]).email
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
end
end
def sig_lines
- p = PersonManager.person_for(@header["From"])
+ p = Person.from_address(@header["From"])
from_email = p && p.email
## first run the hook
def archive
return unless cursor_thread
+ thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+ undo = lambda {
+ thread.apply_label :inbox
+ add_or_unhide thread.first
+ }
+ UndoManager.register("archiving thread #{thread.first.id}", undo)
+
cursor_thread.remove_label :inbox
hide_thread cursor_thread
regen_text
end
def multi_archive threads
+ undo = threads.map {|t|
+ lambda{
+ t.apply_label :inbox
+ add_or_unhide t.first
+ }}
+ UndoManager.register("archiving #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda {regen_text} )
+
threads.each do |t|
t.remove_label :inbox
hide_thread t
def read_and_archive
return unless cursor_thread
+ thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+ undo = lambda {
+ thread.apply_label :inbox
+ thread.apply_label :unread
+ add_or_unhide thread.first
+ }
+ UndoManager.register("reading and archiving thread ", undo)
+
cursor_thread.remove_label :unread
cursor_thread.remove_label :inbox
hide_thread cursor_thread
end
def multi_read_and_archive threads
+ undo = threads.map {|t|
+ lambda {
+ t.apply_label :inbox
+ t.apply_label :unread
+ add_or_unhide t.first
+ }
+ }
+ UndoManager.register("reading and archiving #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda {regen_text})
+
threads.each do |t|
t.remove_label :unread
t.remove_label :inbox
def regen_text
@text = []
- labels = LabelManager.listable_labels
+ labels = LabelManager.all_labels
counts = labels.map do |label|
string = LabelManager.string_for label
total = Index.num_results_for :label => label
- unread = Index.num_results_for :labels => [label, :unread]
+ unread = (label == :unread)? total : Index.num_results_for(:labels => [label, :unread])
[label, string, total, unread]
end.sort_by { |l, s, t, u| s.downcase }
hook_reply_from = HookManager.run "reply-from", :message => @m
## sanity check that selection is a Person (or we'll fail below)
- ## don't check that it's an Account, though; assume they know what they're doing.
+ ## don't check that it's an Account, though; assume they know what they're
+ ## doing.
if hook_reply_from && !(hook_reply_from.is_a? Person)
- Redwood::log "reply-from returned non-Person, using default from."
- hook_reply_from = nil
+ Redwood::log "reply-from returned non-Person, using default from."
+ hook_reply_from = nil
end
- from =
- if hook_reply_from
- hook_reply_from
- elsif @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
- PersonManager.person_for(@m.recipient_email)
- elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
- b
- else
- AccountManager.default_account
- end
+ ## determine the from address of a reply.
+ ## if we have a value from a hook, use it.
+ from = if hook_reply_from
+ hook_reply_from
+ ## otherwise, if the original email had an envelope-to header, try and use
+ ## it, and look up the corresponding name form the list of accounts.
+ ##
+ ## this is for the case where mail is received from a mailing lists (so the
+ ## To: is the list id itself). if the user subscribes via a particular
+ ## alias, we want to use that alias in the reply.
+ elsif @m.recipient_email && (a = AccountManager.account_for(@m.recipient_email))
+ Person.new a.name, @m.recipient_email
+ ## otherwise, try and find an account somewhere in the list of to's
+ ## and cc's.
+ elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
+ b
+ ## if all else fails, use the default
+ else
+ AccountManager.default_account
+ end
## now, determine to: and cc: addressess. we ignore reply-to for list
## messages because it's typically set to the list address, which we
k.add :tag_matching, "Tag matching threads", 'g'
k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
+ k.add :undo, "Undo the previous action", 'u'
end
def initialize hidden_labels=[], load_thread_opts={}
UpdateManager.register self
+ @save_thread_mutex = Mutex.new
+
@last_load_more_size = nil
to_load_more do |size|
next if @last_load_more_size == 0
def reload
drop_all_threads
+ UndoManager.clear
BufferManager.draw_screen
load_threads :num => buffer.content_height
end
add_or_unhide m
end
+ def undo
+ UndoManager.undo
+ end
+
def update
@mutex.synchronize do
## let's see you do THIS in python
end
def actually_toggle_starred t
+ thread = t # cargo cult programming
+ pos = curpos
if t.has_label? :starred # if ANY message has a star
+ undo = lambda {
+ thread.first.add_label :starred
+ update_text_for_line pos
+ UpdateManager.relay self, :starred, thread.first
+ }
t.remove_label :starred # remove from all
UpdateManager.relay self, :unstarred, t.first
else
+ undo = lambda {
+ thread.remove_label :starred
+ update_text_for_line pos
+ UpdateManager.relay self, :unstarred, thread.first
+ }
t.first.add_label :starred # add only to first
UpdateManager.relay self, :starred, t.first
end
+
+ return undo
end
def toggle_starred
t = cursor_thread or return
- actually_toggle_starred t
+ undo = actually_toggle_starred t
+ UndoManager.register("starring/unstarring thread #{t.first.id}",undo)
update_text_for_line curpos
cursor_down
end
def multi_toggle_starred threads
- threads.each { |t| actually_toggle_starred t }
+ undo = threads.map { |t| actually_toggle_starred t }
+ UndoManager.register("starring/unstarring #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo)
regen_text
end
def actually_toggle_archived t
+ thread = t
+ pos = curpos
if t.has_label? :inbox
t.remove_label :inbox
+ undo = lambda {
+ thread.apply_label :inbox
+ update_text_for_line pos
+ UpdateManager.relay self,:unarchived, thread.first
+ }
UpdateManager.relay self, :archived, t.first
else
t.apply_label :inbox
+ undo = lambda {
+ thread.remove_label :inbox
+ update_text_for_line pos
+ UpdateManager.relay self, :unarchived, thread.first
+ }
UpdateManager.relay self, :unarchived, t.first
end
+
+ return undo
end
def actually_toggle_spammed t
+ thread = t
if t.has_label? :spam
+ undo = lambda {
+ thread.apply_label :spam
+ self.hide_thread thread
+ UpdateManager.relay self,:spammed, thread.first
+ }
t.remove_label :spam
+ add_or_unhide t.first
UpdateManager.relay self, :unspammed, t.first
else
+ undo = lambda {
+ thread.remove_label :spam
+ add_or_unhide thread.first
+ UpdateManager.relay self,:unspammed, thread.first
+ }
t.apply_label :spam
+ hide_thread t
UpdateManager.relay self, :spammed, t.first
end
+
+ return undo
end
def actually_toggle_deleted t
if t.has_label? :deleted
+ undo = lambda {
+ t.apply_label :deleted
+ hide_thread t
+ UpdateManager.relay self, :deleted, t.first
+ }
t.remove_label :deleted
+ add_or_unhide t.first
UpdateManager.relay self, :undeleted, t.first
else
+ undo = lambda {
+ t.remove_label :deleted
+ add_or_unhide t.first
+ UpdateManager.relay self, :undeleted, t.first
+ }
t.apply_label :deleted
+ hide_thread t
UpdateManager.relay self, :deleted, t.first
end
+
+ return undo
end
def toggle_archived
t = cursor_thread or return
- actually_toggle_archived t
+ undo = [actually_toggle_archived(t), lambda {self.update_text_for_line curpos}]
+ UndoManager.register("deleting/undeleting thread #{t.first.id}",undo)
update_text_for_line curpos
end
def multi_toggle_archived threads
- threads.each { |t| actually_toggle_archived t }
+ undo = threads.map { |t| actually_toggle_archived t}
+ UndoManager.register("deleting/undeleting #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda {self.regen_text})
regen_text
end
## see deleted or spam emails, and when you undelete or unspam them
## you also want them to disappear immediately.
def multi_toggle_spam threads
- threads.each do |t|
- actually_toggle_spammed t
- hide_thread t
- end
+ undo = threads.map{ |t| actually_toggle_spammed t}
+ UndoManager.register("marking/unmarking #{threads.size} #{threads.size.pluralize 'thread'} as spam",
+ undo << lambda {self.regen_text})
regen_text
end
## see comment for multi_toggle_spam
def multi_toggle_deleted threads
- threads.each do |t|
- actually_toggle_deleted t
- hide_thread t
- end
+ undo = threads.map{ |t| actually_toggle_deleted t}
+ UndoManager.register("deleting/undeleting #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda {regen_text})
regen_text
end
multi_kill [t]
end
+ ## m-m-m-m-MULTI-KILL
def multi_kill threads
- threads.each do |t|
+ undo = threads.map do |t|
t.apply_label :killed
hide_thread t
+ thread = t
+ lambda { thread.remove_label :killed
+ add_or_unhide thread.first
+ }
end
+ UndoManager.register("killing #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda {regen_text})
regen_text
BufferManager.flash "#{threads.size.pluralize 'Thread'} killed."
end
- def save
- BufferManager.say("Saving contacts...") { ContactManager.instance.save }
- dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
- return if dirty_threads.empty?
+ def save background=true
+ if background
+ Redwood::reporting_thread("saving thread") { actually_save }
+ else
+ actually_save
+ end
+ end
+
+ def actually_save
+ @save_thread_mutex.synchronize do
+ BufferManager.say("Saving contacts...") { ContactManager.instance.save }
+ dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
+ next if dirty_threads.empty?
- BufferManager.say("Saving threads...") do |say_id|
- dirty_threads.each_with_index do |t, i|
- BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
- t.save Index
+ BufferManager.say("Saving threads...") do |say_id|
+ dirty_threads.each_with_index do |t, i|
+ BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
+ t.save Index
+ end
end
end
end
sleep 0.1 # TODO: necessary?
BufferManager.erase_flash
end
- save
+ save false
super
end
def edit_labels
thread = cursor_thread or return
speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
+
+ old_labels = thread.labels
+ pos = curpos
+
keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
thread.labels = keepl + user_labels
user_labels.each { |l| LabelManager << l }
update_text_for_line curpos
+
+ undo = lambda{
+ thread.labels = old_labels
+ update_text_for_line pos
+ UpdateManager.relay self, :labeled, thread.first
+ }
+
+ UndoManager.register("labeling thread #{thread.first.id}", undo)
+
UpdateManager.relay self, :labeled, thread.first
end
def multi_edit_labels threads
- user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
+ user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
return unless user_labels
-
- hl = user_labels.select { |l| @hidden_labels.member? l }
+
+ user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
+ hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
if hl.empty?
- threads.each { |t| user_labels.each { |l| t.apply_label l } }
- user_labels.each { |l| LabelManager << l }
+ undo = threads.map do |t|
+ old_labels = t.labels
+ user_labels.each do |(l, to_remove)|
+ if to_remove
+ t.remove_label l
+ else
+ t.apply_label l
+ end
+ end
+ ## UpdateManager or some other regresh mechanism?
+ UpdateManager.relay self, :labeled, t.first
+ lambda do
+ t.labels = old_labels
+ UpdateManager.relay self, :labeled, t.first
+ end
+ end
+ user_labels.each { |(l,_)| LabelManager << l }
+ UndoManager.register("labeling #{threads.size} #{threads.size.pluralize 'thread'}",
+ undo << lambda { regen_text})
else
BufferManager.flash "'#{hl}' is a reserved label!"
end
def subscribe_to_list
m = @message_lines[curpos] or return
if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
- ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
else
BufferManager.flash "Can't find List-Subscribe header for this message."
end
def unsubscribe_from_list
m = @message_lines[curpos] or return
if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
- ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
else
BufferManager.flash "Can't find List-Unsubscribe header for this message."
end
module Redwood
-class PersonManager
- include Singleton
-
- def initialize fn
- @fn = fn
- @@people = {}
-
- ## read in stored people
- IO.readlines(fn).map do |l|
- l =~ /^(.*)?:\s+(\d+)\s+(.*)$/ or next
- email, time, name = $1, $2, $3
- @@people[email] = Person.new name, email, time, false
- end if File.exists? fn
-
- self.class.i_am_the_instance self
- end
-
- def save
- File.open(@fn, "w") do |f|
- @@people.each do |email, p|
- next if p.email == p.name
- next if p.name =~ /=/ # drop rfc2047-encoded, and lots of other useless emails. definitely a heuristic.
- f.puts "#{p.email}: #{p.timestamp} #{p.name}"
- end
- end
- end
-
- def self.people_for s, opts={}
- return [] if s.nil?
- s.split_on_commas.map { |ss| self.person_for ss, opts }
- end
-
- def self.person_for s, opts={}
- p = Person.from_address(s) or return nil
- p.definitive = true if opts[:definitive]
- register p
- end
-
- def self.register p
- oldp = @@people[p.email]
-
- if oldp.nil? || p.better_than?(oldp)
- @@people[p.email] = p
- end
-
- @@people[p.email].touch!
- @@people[p.email]
- end
-end
-
-## don't create these by hand. rather, go through personmanager, to
-## ensure uniqueness and overriding.
class Person
- attr_accessor :name, :email, :timestamp
- bool_accessor :definitive
+ attr_accessor :name, :email
- def initialize name, email, timestamp=0, definitive=false
+ def initialize name, email
raise ArgumentError, "email can't be nil" unless email
if name
end
@email = email.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").downcase
- @definitive = definitive
- @timestamp = timestamp
- end
-
- ## heuristic: whether the name attached to this email is "real", i.e.
- ## we should bother to store it.
- def generic?
- @email =~ /no\-?reply/
- end
-
- def better_than? o
- return false if o.definitive? || generic?
- return true if definitive?
- o.name.nil? || (name && name.length > o.name.length && name =~ /[a-z]/)
end
def to_s; "#@name <#@email>" end
- def touch!; @timestamp = Time.now.to_i end
-
# def == o; o && o.email == email; end
# alias :eql? :==
# def hash; [name, email].hash; end
return nil if s.nil?
## try and parse an email address and name
- name, email =
- case s
+ name, email = case s
+ when /(.+?) ((\S+?)@\S+) \3/
+ ## ok, this first match cause is insane, but bear with me. email
+ ## addresses are stored in the to/from/etc fields of the index in a
+ ## weird format: "name address first-part-of-address", i.e. spaces
+ ## separating those three bits, and no <>'s. this is the output of
+ ## #indexable_content. here, we reverse-engineer that format to extract
+ ## a valid address.
+ ##
+ ## we store things this way to allow searches on a to/from/etc field to
+ ## match any of those parts. a more robust solution would be to store a
+ ## separate, non-indexed field with the proper headers. but this way we
+ ## save precious bits, and it's backwards-compatible with older indexes.
+ [$1, $2]
when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
a, b = $1, $2
[a.gsub('\"', '"'), b]
Person.new name, email
end
+ def self.from_address_list ss
+ return [] if ss.nil?
+ ss.split_on_commas.map { |s| self.from_address s }
+ end
+
+ ## see comments in self.from_address
def indexable_content
[name, email, email.split(/@/).first].join(" ")
end
numi = 0
add_messages_from source do |m, offset, entry|
## always preserve the labels on disk.
- m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
+ m.labels = ((m.labels - [:unread, :inbox]) + entry[:label].split(/\s+/).map { |x| x.intern }).uniq if entry
yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
unless entry
num += 1
--- /dev/null
+module Redwood
+
+## Implements a single undo list for the Sup instance
+##
+## The basic idea is to keep a list of lambdas to undo
+## things. When an action is called (such as 'archive'),
+## a lambda is registered with UndoManager that will
+## undo the archival action
+
+class UndoManager
+ include Singleton
+
+ def initialize
+ @@actionlist = []
+ self.class.i_am_the_instance self
+ end
+
+ def register desc, actions
+ actions = [actions] unless actions.is_a?Array
+ raise StandardError, "when would I need to undo 'nothing?'" unless actions.length > 0
+ Redwood::log "registering #{actions.length} actions: #{desc}"
+ @@actionlist.push({:desc => desc, :actions => actions})
+ end
+
+ def undo
+ unless @@actionlist.length == 0 then
+ actionset = @@actionlist.pop
+ Redwood::log "undoing #{actionset[:desc]}..."
+ actionset[:actions].each{|action|
+ action.call
+ }
+ BufferManager.flash "undid #{actionset[:desc]}"
+ else
+ BufferManager.flash "nothing more to undo"
+ end
+ end
+
+ def clear
+ @@actionlist = []
+ end
+end
+end
class Iconv
def self.easy_decode target, charset, text
- return text if charset =~ /^(x-unknown|unknown[-_]?8bit|ascii[-_]?7[-_]?bit)$/i
+ return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
charset = case charset
- when /UTF[-_]?8/i: "utf-8"
- when /(iso[-_])?latin[-_]?1$/i: "ISO-8859-1"
- when /unicode[-_]1[-_]1[-_]utf[-_]7/i: "utf-7"
+ when /UTF[-_ ]?8/i: "utf-8"
+ when /(iso[-_ ])?latin[-_ ]?1$/i: "ISO-8859-1"
+ when /iso[-_ ]?8859[-_ ]?15/i: 'ISO-8859-15'
+ when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i: "utf-7"
else charset
end
class TestMessage < Test::Unit::TestCase
def setup
- person_file = StringIO.new("")
- # this is a singleton
- if not PersonManager.instantiated?
- @person_manager = PersonManager.new(person_file)
- end
end
def teardown