sup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,45 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/sup.rb'
6
+
7
+ Hoe.new('sup', Redwood::VERSION) do |p|
8
+ p.rubyforge_name = 'sup'
9
+ p.author = "William Morgan"
10
+ p.summary = 'A console-based email client with the best features of GMail, mutt, and emacs. Features full text search, labels, tagged operations, multiple buffers, recent contacts, and more.'
11
+ p.description = p.paragraphs_of('README.txt', 2..4).join("\n\n")
12
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "")
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ p.email = "wmorgan-sup@masanjin.net"
15
+ p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17']]
16
+ end
17
+
18
+ rule 'ss?.png' => 'ss?-small.png' do |t|
19
+
20
+ end
21
+
22
+
23
+ ## is there really no way to make a rule for this?
24
+ WWW_FILES = %w(www/index.html README.txt doc/Philosophy.txt doc/FAQ.txt)
25
+
26
+ SCREENSHOTS = FileList["www/ss?.png"]
27
+ SCREENSHOTS_SMALL = []
28
+ SCREENSHOTS.each do |fn|
29
+ fn =~ /ss(\d+)\.png/
30
+ sfn = "www/ss#{$1}-small.png"
31
+ file sfn => [fn] do |t|
32
+ sh "cat #{fn} | pngtopnm | pnmscale -xysize 320 240 | pnmtopng > #{sfn}"
33
+ end
34
+ SCREENSHOTS_SMALL << sfn
35
+ end
36
+
37
+ task :upload_webpage => WWW_FILES do |t|
38
+ sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
39
+ end
40
+
41
+ task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
42
+ sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
43
+ end
44
+
45
+ # vim: syntax=Ruby
data/bin/sup ADDED
@@ -0,0 +1,229 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ncurses'
5
+ require "sup"
6
+
7
+ module Redwood
8
+
9
+ $exception = nil
10
+
11
+ global_keymap = Keymap.new do |k|
12
+ k.add :quit, "Quit Redwood", 'q'
13
+ k.add :help, "Show help", 'H', '?'
14
+ k.add :roll_buffers, "Switch to next buffer", 'b'
15
+ k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
16
+ k.add :kill_buffer, "Kill the current buffer", 'x'
17
+ k.add :list_buffers, "List all buffers", 'A'
18
+ k.add :list_contacts, "List contacts", 'C'
19
+ k.add :redraw, "Redraw screen", :ctrl_l
20
+ k.add :search, "Search messages", '/'
21
+ k.add :list_labels, "List labels", 'L'
22
+ k.add :poll, "Poll for new messages", 'P'
23
+ k.add :compose, "Compose new message", 'm'
24
+ end
25
+
26
+ def start_cursing
27
+ Ncurses.initscr
28
+ Ncurses.noecho
29
+ Ncurses.cbreak
30
+ Ncurses.stdscr.keypad 1
31
+ Ncurses.curs_set 0
32
+ Ncurses.start_color
33
+ end
34
+
35
+ def stop_cursing
36
+ Ncurses.curs_set 1
37
+ Ncurses.echo
38
+ Ncurses.endwin
39
+ end
40
+ module_function :start_cursing, :stop_cursing
41
+
42
+ Redwood::SentManager.new Redwood::SENT_FN
43
+ Redwood::ContactManager.new Redwood::CONTACT_FN
44
+ Redwood::LabelManager.new Redwood::LABEL_FN
45
+ Redwood::AccountManager.new $config[:accounts]
46
+ Redwood::DraftManager.new Redwood::DRAFT_DIR
47
+ Redwood::UpdateManager.new
48
+ Redwood::PollManager.new
49
+
50
+ Index.new.load
51
+ log "loaded #{Index.size} messages from index"
52
+
53
+ if(s = Index.source_for DraftManager.source_name)
54
+ DraftManager.source = s
55
+ else
56
+ Index.add_source DraftManager.new_source
57
+ end
58
+
59
+ if(s = Index.source_for SentManager.source_name)
60
+ SentManager.source = s
61
+ else
62
+ Index.add_source SentManager.new_source
63
+ end
64
+
65
+ begin
66
+ log "starting curses"
67
+ start_cursing
68
+
69
+ log "initializing colormap"
70
+ Colormap.new do |c|
71
+ c.add :status_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE, Ncurses::A_BOLD
72
+ c.add :index_old_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
73
+ c.add :index_new_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK,
74
+ Ncurses::A_BOLD
75
+ c.add :labellist_old_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
76
+ c.add :labellist_new_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK,
77
+ Ncurses::A_BOLD
78
+ c.add :twiddle_color, Ncurses::COLOR_BLUE, Ncurses::COLOR_BLACK
79
+ c.add :label_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
80
+ c.add :message_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_GREEN
81
+ c.add :mime_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
82
+ c.add :quote_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
83
+ c.add :sig_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
84
+ c.add :quote_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
85
+ c.add :sig_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
86
+ c.add :to_me_color, Ncurses::COLOR_GREEN, Ncurses::COLOR_BLACK
87
+ c.add :starred_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
88
+ Ncurses::A_BOLD
89
+ c.add :starred_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_GREEN,
90
+ Ncurses::A_BOLD
91
+ c.add :snippet_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
92
+ c.add :option_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
93
+ c.add :tagged_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
94
+ Ncurses::A_BOLD
95
+ c.add :draft_notification_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK,
96
+ Ncurses::A_BOLD
97
+ end
98
+
99
+ log "initializing buffer manager"
100
+ bm = BufferManager.new
101
+
102
+ if Index.usual_sources.any? { |s| !s.done? }
103
+ log "polling for new mail"
104
+ pmode = PollMode.new
105
+ pbuf = bm.spawn "load new messages", pmode
106
+ pmode.poll
107
+ # sleep 1
108
+ # bm.kill_buffer pbuf
109
+ end
110
+
111
+ log "initializing mail index buffer"
112
+ imode = InboxMode.new
113
+ ibuf = bm.spawn "inbox", imode
114
+
115
+ log "ready for (inter)action!"
116
+ Logger.make_buf
117
+
118
+ bm.draw_screen
119
+ imode.load_more_threads ibuf.content_height
120
+
121
+ until $exception
122
+ bm.draw_screen
123
+ c = Ncurses.nonblocking_getch
124
+ bm.erase_flash
125
+
126
+ if c == Ncurses::KEY_RESIZE
127
+ bm.handle_resize
128
+ elsif c
129
+ unless bm.handle_input(c)
130
+ x = global_keymap.action_for c
131
+ case x
132
+ when :quit
133
+ break
134
+ when :help
135
+ curmode = bm.focus_buf.mode
136
+ bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
137
+ when :roll_buffers
138
+ bm.roll_buffers
139
+ when :roll_buffers_backwards
140
+ bm.roll_buffers_backwards
141
+ when :kill_buffer
142
+ bm.kill_buffer bm.focus_buf unless bm.focus_buf.mode.is_a? InboxMode
143
+ when :list_buffers
144
+ bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
145
+ when :list_contacts
146
+ mode = ContactListMode.new
147
+ bm.spawn "compose to contacts", mode
148
+ when :search
149
+ text = bm.ask :search, "query: "
150
+ next unless text && text !~ /^\s*$/
151
+ mode = SearchResultsMode.new text
152
+ short_text =
153
+ if text.length < 20
154
+ text
155
+ else
156
+ text[0 ... 20] + "..."
157
+ end
158
+ bm.spawn "search: \"#{short_text}\"", mode
159
+ bm.draw_screen
160
+ mode.load_more_threads mode.buffer.content_height
161
+ when :list_labels
162
+ b = BufferManager.spawn_unless_exists("all labels") do
163
+ LabelListMode.new
164
+ end
165
+ b.mode.load_in_background
166
+ when :compose
167
+ mode = ComposeMode.new
168
+ bm.spawn "new message", mode
169
+ mode.edit
170
+ when :poll
171
+ b = BufferManager.spawn_unless_exists("load new messages") do
172
+ PollMode.new
173
+ end
174
+ b.mode.poll
175
+ when :nothing
176
+ when :redraw
177
+ bm.completely_redraw_screen
178
+ else
179
+ BufferManager.flash "Unknown key press '#{c.to_character}' for #{bm.focus_buf.mode.name}."
180
+ end
181
+ end
182
+ end
183
+ end
184
+ bm.kill_all_buffers
185
+ Redwood::LabelManager.save
186
+ Redwood::ContactManager.save
187
+ rescue Exception => e
188
+ $exception ||= e
189
+ ensure
190
+ stop_cursing
191
+ end
192
+
193
+ Index.save unless $exception # TODO: think about this
194
+
195
+ if $exception
196
+ if $exception.is_a? IndexError
197
+ $stderr.puts <<EOS
198
+ An error occurred while loading a message from source "#{$exception.source}".
199
+ Typically, this means that the source has been modified in some
200
+ way which has rendered the messages invalid.
201
+
202
+ You must rebuild the index for this source. Please run:
203
+ sup-import --rebuild #{$exception.source}
204
+ to correct this error.
205
+ EOS
206
+ raise $exception
207
+ else
208
+ $stderr.puts <<EOS
209
+ -----------------------------------------------------------------
210
+ I'm very sorry, but it seems that an error occurred in Redwood.
211
+ Please accept my sincere apologies. If you don't mind, please
212
+ send the backtrace below and a brief report of the circumstances
213
+ to user wmorgan-sup at site masanjin dot net so that I might
214
+ address this problem. Thank you!
215
+
216
+ Sincerely,
217
+ William
218
+ -----------------------------------------------------------------
219
+
220
+ The problem was: #{$exception.message} (error type #{$exception.class.name})
221
+ A backtrace follows:
222
+ EOS
223
+ raise $exception
224
+ end
225
+ end
226
+
227
+
228
+ end
229
+
@@ -0,0 +1,162 @@
1
+ #!/bin/env ruby
2
+
3
+ require "sup"
4
+
5
+ class Float
6
+ def to_s; sprintf '%.2f', self; end
7
+ end
8
+
9
+ class Numeric
10
+ def to_time_s
11
+ i = to_i
12
+ sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
13
+ end
14
+ end
15
+
16
+ def time
17
+ startt = Time.now
18
+ yield
19
+ Time.now - startt
20
+ end
21
+
22
+ def educate_user
23
+ $stderr.puts <<EOS
24
+ Loads messages into the Sup index, adding sources as needed to the
25
+ source list.
26
+
27
+ Usage:
28
+ sup-import [options] <source>*
29
+ where <source>* is zero or more source descriptions (e.g., mbox
30
+ filenames on disk).
31
+
32
+ If the sources listed are not already in the Sup source list,
33
+ they will be added to it, as parameterized by the following options:
34
+ --archive: messages from these sources will not appear in the inbox
35
+ --unusual: these sources will not be polled when the flag --the-usual
36
+ is called
37
+
38
+ Regardless of whether the sources are new or not, they will be polled,
39
+ and any new messages will be added to the index, as parameterized by
40
+ the following options:
41
+ --force-archive: regardless of the source "archive" flag, any new
42
+ messages found will not appear in the inbox.
43
+ --force-read: any messages found will not be marked as new.
44
+
45
+ The following options can also be specified:
46
+ --the-usual: import new messages from all usual sources
47
+ --rebuild: rebuild the index for the specified sources rather than
48
+ just adding new messages. Useful if the sources
49
+ have changed in any way *other* than new messages
50
+ being added.
51
+ --force-rebuild: force a rebuild of all messages in the inbox, not just
52
+ ones that have changed. You probably won't need this
53
+ unless William changes the index format.
54
+ --optimize: optimize the index after adding any new messages.
55
+ --help: don't do anything, just show this message.
56
+ EOS
57
+ #' stupid ruby-mode
58
+ exit
59
+ end
60
+
61
+ educate_user if ARGV.member? '--help'
62
+
63
+ archive = ARGV.delete "--archive"
64
+ unusual = ARGV.delete "--unusual"
65
+ force_archive = ARGV.delete "--force-archive"
66
+ force_read = ARGV.delete "--force-read"
67
+ the_usual = ARGV.delete "--the-usual"
68
+ rebuild = ARGV.delete "--rebuild"
69
+ force_rebuild = ARGV.delete "--force-rebuild"
70
+ optimize = ARGV.delete "--optimize"
71
+
72
+ if(o = ARGV.find { |x| x =~ /^--/ })
73
+ $stderr.puts "error: unknown option #{o}"
74
+ educate_user
75
+ end
76
+
77
+ puts "loading index..."
78
+ index = Redwood::Index.new
79
+ index.load
80
+ pre_nm = index.size
81
+ puts "loaded index of #{index.size} messages"
82
+
83
+ sources = ARGV.map do |fn|
84
+ source = index.source_for fn
85
+ unless source
86
+ source = Redwood::MBox::Loader.new(fn, 0, !unusual, !!archive)
87
+ index.add_source source
88
+ end
89
+ source
90
+ end
91
+ sources = (sources + index.usual_sources).uniq if the_usual
92
+ sources.each { |s| s.reset! } if rebuild || force_rebuild
93
+
94
+ found = {}
95
+ start = Time.now
96
+ begin
97
+ sources.each do |source|
98
+ next if source.done?
99
+ puts "loading from #{source}... "
100
+ num = 0
101
+ start_offset = nil
102
+ source.each do |offset, labels|
103
+ start_offset ||= offset
104
+ labels -= [:inbox] if force_archive
105
+ labels -= [:unread] if force_read
106
+ begin
107
+ m = Redwood::Message.new source, offset, labels
108
+ if found[m.id]
109
+ puts "skipping duplicate message #{m.id}"
110
+ next
111
+ else
112
+ found[m.id] = true
113
+ end
114
+
115
+ m.remove_label :unread if m.mbox_status == "RO" unless force_read
116
+ if (rebuild || force_rebuild) &&
117
+ (docid, entry = index.load_entry_for_id(m.id)) && entry
118
+ if force_rebuild || entry[:source_info].to_i != offset
119
+ puts "replacing message #{m.id} labels #{entry[:label].inspect} (offset #{entry[:source_info]} => #{offset})"
120
+ m.labels = entry[:label].split.map { |l| l.intern }
121
+ num += 1 if index.update_message m, source, offset
122
+ end
123
+ else
124
+ num += 1 if index.add_message m
125
+ end
126
+ rescue Redwood::MessageFormatError => e
127
+ $stderr.puts "ignoring erroneous message at #{source}##{offset}: #{e.message}"
128
+ end
129
+ if num % 1000 == 0 && num > 0
130
+ elapsed = Time.now - start
131
+ pctdone = (offset.to_f - start_offset) / (source.total.to_f - start_offset)
132
+ remaining = (source.total.to_f - offset.to_f) * (elapsed.to_f / (offset.to_f - start_offset))
133
+ puts "## #{num} (#{(pctdone * 100.0)}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining"
134
+ end
135
+ end
136
+ puts "loaded #{num} messages" unless num == 0
137
+ end
138
+ ensure
139
+ index.save
140
+ end
141
+
142
+ if rebuild || force_rebuild
143
+ puts "deleting missing messages from the index..."
144
+ numdel = 0
145
+ sources.each do |source|
146
+ raise "no source id for #{source}" unless source.id
147
+ index.index.search_each("source_id:#{source.id}", :limit => :all) do |docid, score|
148
+ mid = index.index[docid][:message_id]
149
+ next if found[mid]
150
+ puts "deleting #{mid}"
151
+ index.index.delete docid
152
+ numdel += 1
153
+ end
154
+ end
155
+ puts "deleted #{numdel} messages"
156
+ end
157
+
158
+ if optimize
159
+ puts "optimizing index..."
160
+ optt = time { index.index.optimize }
161
+ puts "optimized index of size #{index.size} in #{optt}s."
162
+ end
@@ -0,0 +1,38 @@
1
+ Sup FAQ
2
+ -------
3
+
4
+ Q: How is Sup even possible?
5
+ A: Sup is only possible through the hard work of Dave Balmain, the
6
+ author of ferret.
7
+
8
+ I started using Ferret when it was still slightly buggy, and it
9
+ seemed like every week Dave released a bugfix or a speed
10
+ improvement that directly affected sup. Ferret has become a
11
+ first-class piece of software, and it's almost entirely due to him.
12
+ It amazes me just how much time and effort he has put into it.
13
+
14
+ Q: Why the console?
15
+ A: As the millions (ok, hundreds) of mutt users will tell you, there are
16
+ many advantages to the console:
17
+ - You don't need a bulky web browser.
18
+ - You can ssh and check your mail on another machine.
19
+ - Instantaneous interaction.
20
+ - A few keystrokes are worth a hundred mouse clicks.
21
+
22
+ Q: If you love GMail so much, why not just use it?
23
+ A: I hate using a mouse, and I hate ads, and I hate non-programmability.
24
+
25
+ Q: How does Sup deal with spam?
26
+ A: You can manually mark messages as spam, which prevents them from
27
+ showing up in future searches, but that's all that Sup does. Spam
28
+ filtering should be done by a dedicated tool like SpamAssassin.
29
+
30
+ Q: What are all these "Redwood" references I see in the code?
31
+ A: That was Sup's original name. (Think pine, elm. Although I am a
32
+ Mutt user, I couldn't think of a good progression there.) But it was
33
+ taken by another project on RubyForge, and wasn't that original,
34
+ and was too long to type anyways.
35
+
36
+ Maybe one day I'll do a huge search-and-replace on the code, but it
37
+ doesn't seem that important at this point.
38
+