EOS
opt :archive, "Automatically archive all new messages from these sources."
opt :unusual, "Do not automatically poll these sources for new messages."
+ opt :labels, "A comma-separated set of labels to apply to all messages from this source", :type => String
opt :force_new, "Create a new account for this source, even if one already exists."
end
index.load
ARGV.each do |uri|
+ labels = $opts[:labels] ? $opts[:labels].split(/\s*,\s*/).uniq : []
+
if !$opts[:force_new] && index.source_for(uri)
say "Already know about #{uri}; skipping."
next
say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
say ""
username, password = get_login_info uri, index.sources
- Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive]
+ Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
when "imap", "imaps"
username, password = get_login_info uri, index.sources
- Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive]
+ Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
when "maildir"
- Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive]
+ Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
when "mbox"
- Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive]
+ Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
else
Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"
end
def axe q, default=nil
ans =
- if default && !default.empty?
- ask "#{q} (enter for \"#{default}\"): "
- else
- ask "#{q}: "
- end
+ if default && !default.empty?
+ ask "#{q} (enter for \"#{default}\"): "
+ else
+ ask "#{q}: "
+ end
ans.empty? ? default : ans
end
say "Ok, adding a new source."
choose do |menu|
- menu.prompt = "What type of mail source is it?"
+ menu.prompt = "What type of mail source is it? "
menu.choice("mbox file") { type = :mbox }
menu.choice("maildir directory") { type = :maildir }
menu.choice("remote mbox file (accessible via ssh)") { type = :mboxssh }
end
while true do
- say "Now for the details."
+ say "Ok, now for the details."
- components =
+ default_labels, components =
case type
when :mbox
- fn = axe "What's the full path to the mbox file?", ENV["MAIL"] #"srm
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the mbox file?", $last_fn #"srm
return if fn.nil? || fn.empty?
- { :scheme => "mbox", :path => fn }
+
+ $last_fn = fn
+ [Redwood::MBox::Loader.suggest_labels_for(fn),
+ { :scheme => "mbox", :path => fn }]
when :maildir
- fn = axe "What's the full path to the maildir directory?", ENV["MAIL"] #"srm
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the maildir directory?", $last_fn #"srm
return if fn.nil? || fn.empty?
- { :scheme => "maildir", :path => fn }
+
+ $last_fn = fn
+ [Redwood::Maildir.suggest_labels_for(fn),
+ { :scheme => "maildir", :path => fn }]
when :mboxssh
+ $last_server ||= "localhost"
srv = axe "What machine is the mbox file located on?", $last_server
return if srv.nil? || srv.empty?
$last_server = srv
- fn = axe "What's the full path to the mbox file?", ENV["MAIL"] #"srm
+
+ fn = axe "What's the full path to the mbox file?", $last_fn #" stupid ruby-mode
return if fn.nil? || fn.empty?
+ $last_fn = fn
fn = "/#{fn}" # lame
- { :scheme => "mbox+ssh", :host => srv, :path => fn }
+ [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
+ { :scheme => "mbox+ssh", :host => srv, :path => fn }]
when :imap, :imaps
+ $last_server ||= "localhost"
srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
return if srv.nil? || srv.empty?
$last_server = srv
- fn = axe "What's the folder path?", "INBOX" #"srm
+
+ $last_folder ||= "INBOX"
+ fn = axe "What's the folder path?", $last_folder #"srm
return if fn.nil? || fn.empty?
- fn = "/#{fn}" # lame
+ $last_folder = fn
+ fn = "/#{fn}" # lame
if srv =~ /^(\w+):(\d+)$/
host, port = $1, $2.to_i
else
host, port = srv, nil
end
- { :scheme => type.to_s, :host => host, :port => port, :path => fn }
+ [Redwood::IMAP.suggest_labels_for(fn),
+ { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
end
uri =
if axe_yes("Try again?") then next else return end
end
- say "I'm going to add this source: #{uri}."
+ say "I'm going to add this source: #{uri}"
unless axe("Does that look right?", "y") =~ /^y|yes$/i
if axe_yes("Try again?") then next else return end
end
usual = axe_yes "Does this source ever receive new messages?", "y"
- archive = usual ? axe_yes("Should those new messages be automatically archived?") : false
+ archive = usual ? axe_yes("Should new messages be automatically archived? (I.e. not appear in your inbox, though still be accessible via search.)") : false
+
+ labels_str = axe("Enter any labels to be automatically added to all messages from this source, separated by spaces (or 'none')", default_labels.join(","))
+
+ labels =
+ if labels_str =~ /^\s*none\s*$/i
+ nil
+ else
+ labels_str.split(/\s+/)
+ end
cmd = build_cmd "sup-add"
cmd += " --unusual" unless usual
cmd += " --archive" if archive
+ cmd += " --labels=#{labels.join(',')}" if labels
cmd += " #{uri}"
puts "Ok, trying to run \"#{cmd}\"..."
with your amazing keyboarding skills! Jump from email to email with
nary a click of the mouse!
-Just answer these simple questions and you'll be on your way! Press
-enter at any point to accept the default answer.
+Just answer these simple questions and you'll be on your way.
EOS
#' stupid ruby-mode
account = $config[:accounts][:default]
name = axe "What's your name?", account[:name]
-email = axe "What's your email address?", account[:email] #'srm
+email = axe "What's your (primary) email address?", account[:email] #'srm
say "Ok, your header will look like this:"
say " From: #{name} <#{email}>"
say "\n"
choose do |menu|
- menu.prompt = "Your wish?"
+ menu.prompt = "Your wish? "
menu.choice("Add a new source.") { add_source }
menu.choice("Done adding sources!") { done = true }
end
Depending on how many messages are in the sources, this could take
quite a while.
-IMPORTANT NOTE: this import will archive messages if the source is
-marked archival, and won't otherwise. It will preserve read/unread
-status as given by the source, and it will automatically add one label
-per source. All of this behavior can be controlled on per-source
-basis by running sup-sync manually.
-
EOS
#'
if axe_yes "Run sup-sync to import all messages now?"
Just one last command:
- sup
+ #{build_cmd "sup"}
Have fun!
EOS
elapsed = last_info_time - start_time
pctdone = source.respond_to?(:pct_done) ? source.pct_done : 100.0 * (source.cur_offset.to_f - source.start_offset).to_f / (source.end_offset - source.start_offset).to_f
remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
- $stderr.puts "## #{num_added + num_updated} (#{pctdone}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining (for this source)"
+ $stderr.puts "## #{num_added + num_updated} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
end
if index_state.nil?
for 0.0.9
---------
-_ add arbitrary labels to sources
+_ bugfix: when one new message comes into an imap folder, we don't
+ catch it until a reload (sometimes?)
+ message indicating they're loaded to inbox (imap only?)
+_ rss feed reading
_ detect other sup instances and do something intelligent (because ferret crashes violently with more than one index writer open)
_ bugfix: need a way to force an address to a particular name, for things like evite addresses
_ bugfix: read before thread-index has finished loading then hides the thread?!? wtf. (on jamie)
_ bugfix: ferret flakiness: just added message but can't find it.
-_ bugfix: when one new message comes into an imap folder, we don't
- catch it until a reload (sometimes?)
_ bugfix: add new message counts until keypress
-_ bugfix: deadlock
-_ split out threading & message chunk parsing to a separate library
+_ bugfix: deadlock (on rubyforge)
_ decode RFC 2047 ("encoded word") headers
- see: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949, http://dev.rubyonrails.org/ticket/6807
_ refactor all the *-search-results-mode classes into one.
+x add arbitrary labels to sources
+x improve sup-config
x autoload more threads when you go down
x add a sync-back tool that at least works for mboxes
x thread by subject configurable in config.yaml
maybe: de-archived messages auto-added to inbox
prune old entries from contacts.txt so that it doesn't arbitrarily
+maybe
+_ split out threading & message chunk parsing to a separate library
+
+
done
----
x nice little startup config program
if File.exists? Redwood::CONFIG_FN
$config = Redwood::load_yaml_obj Redwood::CONFIG_FN
else
+ require 'etc'
+ require 'socket'
+ name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first
+ email = ENV["USER"] + "@" +
+ begin
+ Socket.gethostbyname(Socket.gethostname).first
+ rescue SocketError
+ Socket.gethostname
+ end
+
$config = {
:accounts => {
:default => {
- :name => "Sup Rocks",
- :email => "sup-rocks@reading-my-emails",
+ :name => name,
+ :email => email,
:alternates => [],
:sendmail => "/usr/sbin/sendmail -oem -ti",
:signature => File.join(ENV["HOME"], ".signature")
attr_accessor :username, :password
yaml_properties :uri, :username, :password, :cur_offset, :usual,
- :archived, :id
+ :archived, :id, :labels
- def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil
+ def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil, labels=[]
raise ArgumentError, "username and password must be specified" unless username && password
raise ArgumentError, "not an imap uri" unless uri =~ %r!imaps?://!
@imap_ids = {}
@ids = []
@last_scan = nil
- @labels = [:unread]
- @labels << mailbox.intern unless mailbox =~ /inbox/i
+ @labels = (labels || []).freeze
+ @say_id = nil
@mutex = Mutex.new
end
+ def self.suggest_labels_for path
+ if path =~ /inbox/i
+ [path.intern]
+ else
+ []
+ end
+ end
+
def host; @parsed_uri.host; end
def port; @parsed_uri.port || (ssl? ? 993 : 143); end
def mailbox
def raw_header id
unsynchronized_scan_mailbox
header, flags = get_imap_fields id, 'RFC822.HEADER', 'FLAGS'
+ ## very bad. this is very very bad. very bad bad bad.
header = header + "Status: RO\n" if flags.include? :Seen # fake an mbox-style read header # TODO: improve source-marked-as-read reporting system
header.gsub(/\r\n/, "\n")
end
start.upto(ids.length - 1) do |i|
id = ids[i]
self.cur_offset = id
- yield id, @labels.clone
+ yield id, @labels
end
end
class Maildir < Source
SCAN_INTERVAL = 30 # seconds
- yaml_properties :uri, :cur_offset, :usual, :archived, :id
- def initialize uri, last_date=nil, usual=true, archived=false, id=nil
- super
+ yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+ def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
+ super uri, last_date, usual, archived, id
uri = URI(uri)
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
@dir = uri.path
+ @labels = (labels || []).freeze
@ids = []
@ids_to_fns = {}
@last_scan = nil
@mutex = Mutex.new
end
+ def self.suggest_labels_for path; [] end
+
def check
scan_mailbox
start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
start.upto(@ids.length - 1) do |i|
id = @ids[i]
self.cur_offset = id
- yield id, (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
+ yield id, @labels + (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
end
end
module MBox
class Loader < Source
- yaml_properties :uri, :cur_offset, :usual, :archived, :id
- def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil
- super
+ yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+ def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
+ super uri_or_fp, start_offset, usual, archived, id
@mutex = Mutex.new
- @labels = [:unread]
+ @labels = (labels || []).freeze
case uri_or_fp
when String
uri = URI(uri_or_fp)
raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
raise ArgumentError, "mbox uri ('#{uri}') cannot have a host: #{uri.host}" if uri.host
- ## heuristic: use the filename as a label, unless the file
- ## has a path that probably represents an inbox.
- @labels << File.basename(uri.path).intern unless File.dirname(uri.path) =~ /\b(var|usr|spool)\b/
@f = File.open uri.path
else
@f = uri_or_fp
end
end
+ def self.suggest_labels_for path
+ ## heuristic: use the filename as a label, unless the file
+ ## has a path that probably represents an inbox.
+ if File.dirname(path) =~ /\b(var|usr|spool)\b/
+ []
+ else
+ [File.basename(path).intern]
+ end
+ end
+
def check
if (cur_offset ||= start_offset) > end_offset
raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
end
self.cur_offset = next_offset
- [returned_offset, @labels.clone]
+ [returned_offset, @labels]
end
end
attr_accessor :username, :password
yaml_properties :uri, :username, :password, :cur_offset, :usual,
- :archived, :id
+ :archived, :id, :labels
- def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil
+ def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
raise ArgumentError, "not an mbox+ssh uri: #{uri.inspect}" unless uri =~ %r!^mbox\+ssh://!
super uri, start_offset, usual, archived, id
@password = password
@uri = uri
@cur_offset = start_offset
+ @labels = (labels || []).freeze
opts = {}
opts[:username] = @username if @username
## heuristic: use the filename as a label, unless the file
## has a path that probably represents an inbox.
- @labels = [:unread]
- @labels << File.basename(filename).intern unless File.dirname(filename) =~ /\b(var|usr|spool)\b/
end
+ def self.suggest_labels_for path; Loader.suggest_labels_for(path) end
+
def connect; safely { @f.connect }; end
def host; @parsed_uri.host; end
def filename; @parsed_uri.path[1..-1] end
if m.source_marked_read?
m.remove_label :unread
labels.delete :unread
+ else
+ m.add_label :unread
+ labels << :unread
end
docid, entry = Index.load_entry_for_id m.id
attr_accessor :id
def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
+ raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
+
@uri = uri
@cur_offset = initial_offset
@usual = usual