sup 0.0.1 → 0.0.2

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.

data/doc/TODO CHANGED
@@ -1,3 +1,4 @@
1
+ search for other messages from author in thread-view-mode
1
2
  forward attachments
2
3
  tab completion on labels, contacts
3
4
  within-buffer search
@@ -8,7 +9,12 @@ maybe: rangefilter on the initial inbox to only consider the most recent 1000 me
8
9
  select all, starred, to me, etc
9
10
  editing of arbitrary messages
10
11
  annotations on messages
12
+ gmail
13
+ pop
14
+ move sup-import argument handling to getopt or something
11
15
 
16
+ x handle broken sources better
17
+ x imap
12
18
  x word wrap
13
19
  x background indexing
14
20
  x auto-insertion of draft messages
data/lib/sup.rb CHANGED
@@ -3,18 +3,19 @@ require 'yaml'
3
3
  require 'zlib'
4
4
  require 'thread'
5
5
  require 'fileutils'
6
+
6
7
  Thread.abort_on_exception = true # make debugging possible
7
8
 
8
9
  class Object
9
- ## this is for debugging purposes because i keep calling nil.id and
10
- ## i want it to throw an exception
11
- def id
10
+ ## this is for debugging purposes because i keep calling #id on the
11
+ ## wrong object and i want it to throw an exception
12
+ def id
12
13
  raise "wrong id called"
13
14
  end
14
15
  end
15
16
 
16
17
  module Redwood
17
- VERSION = "0.0.1"
18
+ VERSION = "0.0.2"
18
19
 
19
20
  BASE_DIR = File.join(ENV["HOME"], ".sup")
20
21
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -28,7 +29,6 @@ module Redwood
28
29
  YAML_DATE = "2006-10-01"
29
30
 
30
31
  ## one-stop shop for yamliciousness
31
-
32
32
  def register_yaml klass, props
33
33
  vars = props.map { |p| "@#{p}" }
34
34
  path = klass.name.gsub(/::/, "/")
@@ -65,7 +65,6 @@ module Redwood
65
65
  end
66
66
 
67
67
  ## set up default configuration file
68
-
69
68
  if File.exists? Redwood::CONFIG_FN
70
69
  $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
71
70
  else
@@ -92,7 +91,9 @@ end
92
91
  require "sup/util"
93
92
  require "sup/update"
94
93
  require "sup/message"
94
+ require "sup/source"
95
95
  require "sup/mbox"
96
+ require "sup/imap"
96
97
  require "sup/person"
97
98
  require "sup/account"
98
99
  require "sup/thread"
data/lib/sup/buffer.rb CHANGED
@@ -132,7 +132,7 @@ class BufferManager
132
132
  @minibuf_stack = []
133
133
  @textfields = {}
134
134
  @flash = nil
135
- @shelled_out = false
135
+ @freeze = false
136
136
 
137
137
  self.class.i_am_the_instance self
138
138
  end
@@ -178,14 +178,14 @@ class BufferManager
178
178
  end
179
179
 
180
180
  def completely_redraw_screen
181
- return if @shelled_out
181
+ return if @freeze
182
182
  Ncurses.clear
183
183
  @dirty = true
184
184
  draw_screen
185
185
  end
186
186
 
187
187
  def handle_resize
188
- return if @shelled_out
188
+ return if @freeze
189
189
  rows, cols = Ncurses.rows, Ncurses.cols
190
190
  @buffers.each { |b| b.resize rows - 1, cols }
191
191
  completely_redraw_screen
@@ -193,7 +193,7 @@ class BufferManager
193
193
  end
194
194
 
195
195
  def draw_screen skip_minibuf=false
196
- return if @shelled_out
196
+ return if @freeze
197
197
 
198
198
  ## disabling this for the time being, to help with debugging
199
199
  ## (currently we only have one buffer visible at a time).
@@ -216,7 +216,7 @@ class BufferManager
216
216
  def spawn_unless_exists title, opts={}
217
217
  if @name_map.member? title
218
218
  Redwood::log "buffer '#{title}' already exists, raising to front"
219
- raise_to_front @name_map[title]
219
+ raise_to_front @name_map[title] unless opts[:hidden]
220
220
  else
221
221
  mode = yield
222
222
  spawn title, mode, opts
@@ -291,11 +291,13 @@ class BufferManager
291
291
  tf.activate question, default
292
292
  @dirty = true
293
293
  draw_screen true
294
- tf.position_cursor
295
- Ncurses.refresh
296
294
 
297
295
  ret = nil
296
+ @freeze = true
297
+ tf.position_cursor
298
+ Ncurses.refresh
298
299
  while tf.handle_input(Ncurses.nonblocking_getch); end
300
+ @freeze = false
299
301
 
300
302
  ret = tf.value
301
303
  tf.deactivate
@@ -315,6 +317,7 @@ class BufferManager
315
317
 
316
318
  ret = nil
317
319
  done = false
320
+ @freeze = true
318
321
  until done
319
322
  key = Ncurses.nonblocking_getch
320
323
  if key == Ncurses::KEY_CANCEL
@@ -324,7 +327,7 @@ class BufferManager
324
327
  done = true
325
328
  end
326
329
  end
327
-
330
+ @freeze = false
328
331
  Ncurses.curs_set 0
329
332
  erase_flash
330
333
  draw_screen
@@ -334,7 +337,15 @@ class BufferManager
334
337
  end
335
338
 
336
339
  def ask_yes_or_no question
337
- [?y, ?Y].member? ask_getch(question, "ynYN")
340
+ r = ask_getch(question, "ynYN")
341
+ case r
342
+ when ?y, ?Y
343
+ true
344
+ when nil
345
+ nil
346
+ else
347
+ false
348
+ end
338
349
  end
339
350
 
340
351
  def draw_minibuf
@@ -348,8 +359,8 @@ class BufferManager
348
359
  def say s, id=nil
349
360
  id ||= @minibuf_stack.length
350
361
  @minibuf_stack[id] = s
351
- unless @shelled_out
352
- draw_minibuf
362
+ unless @freeze
363
+ draw_screen
353
364
  Ncurses.refresh
354
365
  end
355
366
  id
@@ -359,8 +370,8 @@ class BufferManager
359
370
 
360
371
  def flash s
361
372
  @flash = s
362
- unless @shelled_out
363
- draw_minibuf
373
+ unless @freeze
374
+ draw_screen
364
375
  Ncurses.refresh
365
376
  end
366
377
  end
@@ -373,19 +384,19 @@ class BufferManager
373
384
  @minibuf_stack.delete_at i
374
385
  end
375
386
  end
376
- unless @shelled_out
377
- draw_minibuf
387
+ unless @freeze
388
+ draw_screen
378
389
  Ncurses.refresh
379
390
  end
380
391
  end
381
392
 
382
393
  def shell_out command
383
- @shelled_out = true
394
+ @freeze = true
384
395
  Ncurses.endwin
385
396
  system command
386
397
  Ncurses.refresh
387
398
  Ncurses.curs_set 0
388
- @shelled_out = false
399
+ @freeze = false
389
400
  end
390
401
  end
391
402
  end
data/lib/sup/draft.rb CHANGED
@@ -10,9 +10,9 @@ class DraftManager
10
10
  self.class.i_am_the_instance self
11
11
  end
12
12
 
13
- def self.source_name; "drafts"; end
13
+ def self.source_name; "drafts://"; end
14
14
  def self.source_id; 9999; end
15
- def new_source; @source = DraftLoader.new @dir; end
15
+ def new_source; @source = DraftLoader.new; end
16
16
 
17
17
  def write_draft
18
18
  offset = @source.gen_offset
@@ -20,7 +20,7 @@ class DraftManager
20
20
  File.open(fn, "w") { |f| yield f }
21
21
 
22
22
  @source.each do |offset, labels|
23
- m = Message.new @source, offset, labels
23
+ m = Message.new :source => @source, :source_info => offset, :labels => labels
24
24
  Index.add_message m
25
25
  UpdateManager.relay :add, m
26
26
  end
@@ -36,25 +36,30 @@ class DraftManager
36
36
  end
37
37
  end
38
38
 
39
- class DraftLoader
40
- attr_accessor :dir, :end_offset
41
- bool_reader :dirty
39
+ class DraftLoader < Source
40
+ attr_accessor :dir
42
41
 
43
- def initialize dir, end_offset=0
42
+ def initialize cur_offset=0
43
+ dir = Redwood::DRAFT_DIR
44
44
  Dir.mkdir dir unless File.exists? dir
45
+ super "draft://#{dir}", cur_offset, true, false
45
46
  @dir = dir
46
- @end_offset = end_offset
47
- @dirty = false
48
47
  end
49
48
 
50
- def done?; !File.exists? fn_for_offset(@end_offset); end
51
- def usual?; true; end
52
49
  def id; DraftManager.source_id; end
53
50
  def to_s; DraftManager.source_name; end
54
- def is_source_for? x; x == DraftManager.source_name; end
51
+
52
+ def next
53
+ ret = nil
54
+ begin
55
+ ret = cur_offset
56
+ self.cur_offset = cur_offset + 1
57
+ end until File.exists? fn_for_offset(ret)
58
+ [ret, [:draft]]
59
+ end
55
60
 
56
61
  def gen_offset
57
- i = @end_offset
62
+ i = cur_offset
58
63
  while File.exists? fn_for_offset(i)
59
64
  i += 1
60
65
  end
@@ -77,8 +82,7 @@ class DraftLoader
77
82
  end
78
83
  end
79
84
 
80
- ## load the full header text
81
- def load_header_text offset
85
+ def raw_header offset
82
86
  ret = ""
83
87
  File.open fn_for_offset(offset) do |f|
84
88
  until f.eof? || (l = f.gets) =~ /^$/
@@ -88,18 +92,18 @@ class DraftLoader
88
92
  ret
89
93
  end
90
94
 
91
- def each
92
- while File.exists?(fn = File.join(@dir, @end_offset.to_s))
93
- yield @end_offset, [:draft, :inbox]
94
- @end_offset += 1
95
- @dirty = true
95
+ def raw_full_message offset
96
+ ret = ""
97
+ File.open fn_for_offset(offset) do |f|
98
+ ret += l until f.eof?
96
99
  end
100
+ ret
97
101
  end
98
102
 
99
- def total; Dir[File.join(@dir, "*")].sort.last.to_i; end
100
- def reset!; @end_offset = 0; @dirty = true; end
103
+ def start_offset; 0; end
104
+ def end_offset; Dir.new(@dir).entries.sort.last.to_i + 1; end
101
105
  end
102
106
 
103
- Redwood::register_yaml(DraftLoader, %w(dir end_offset))
107
+ Redwood::register_yaml(DraftLoader, %w(cur_offset))
104
108
 
105
109
  end
data/lib/sup/imap.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'uri'
2
+ require 'net/imap'
3
+ require 'stringio'
4
+
5
+ module Redwood
6
+
7
+ class IMAP < Source
8
+ attr_reader :labels
9
+
10
+ def initialize uri, username, password, last_uid=nil, usual=true, archived=false, id=nil
11
+ raise ArgumentError, "username and password must be specified" unless username && password
12
+
13
+ super uri, last_uid, usual, archived, id
14
+
15
+ @parsed_uri = URI(uri)
16
+ @username = username
17
+ @password = password
18
+ @imap = nil
19
+ @labels = [:unread]
20
+ @labels << mailbox.intern unless mailbox =~ /inbox/i || mailbox.empty?
21
+ @labels << :inbox unless archived?
22
+
23
+ connect
24
+ end
25
+
26
+ def connect
27
+ return false if broken?
28
+ return true if @imap
29
+ Redwood::log "connecting to #{@parsed_uri.host} port #{ssl? ? 993 : 143}, ssl=#{ssl?} ..."
30
+
31
+ ## ok, this is FUCKING ANNOYING.
32
+ ##
33
+ ## what imap.rb likes to do is, if an exception occurs, catch it
34
+ ## and re-raise it on the calling thread. seems reasonable. but
35
+ ## what that REALLY means is that the only way to reasonably
36
+ ## initialize imap is in its own thread, because otherwise, you
37
+ ## will never be able to catch the exception it raises on the
38
+ ## calling thread, and the backtrace will not make any sense at
39
+ ## all, and you will waste HOURS of your life on this fucking
40
+ ## problem.
41
+ ##
42
+ ## FUCK!!!!!!!!!
43
+ ::Thread.new do
44
+ begin
45
+ #raise Net::IMAP::ByeResponseError, "simulated imap failure"
46
+ @imap = Net::IMAP.new @parsed_uri.host, ssl? ? 993 : 143, ssl?
47
+ @imap.authenticate 'LOGIN', @username, @password
48
+ @imap.examine mailbox
49
+ Redwood::log "successfully connected to #{@parsed_uri}, mailbox #{mailbox}"
50
+ rescue Exception => e
51
+ self.broken_msg = e.message.chomp # fucking chomp! fuck!!!
52
+ @imap = nil
53
+ Redwood::log "error connecting to IMAP server: #{self.broken_msg}"
54
+ end
55
+ end.join
56
+
57
+ !!@imap
58
+ end
59
+ private :connect
60
+
61
+ def mailbox; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
62
+ def ssl?; @parsed_uri.scheme == 'imaps' end
63
+
64
+ def load_header uid=nil
65
+ MBox::read_header StringIO.new(raw_header(uid))
66
+ end
67
+
68
+ def load_message uid
69
+ RMail::Parser.read raw_full_message(uid)
70
+ end
71
+
72
+ ## load the full header text
73
+ def raw_header uid
74
+ connect or return broken_msg
75
+ begin
76
+ connect or return broken_msg
77
+ rescue Exception => e
78
+ raise "wtf: #{e.inspect}"
79
+ end
80
+ @imap.uid_fetch(uid, 'RFC822.HEADER')[0].attr['RFC822.HEADER'].gsub(/\r\n/, "\n")
81
+ end
82
+
83
+ def raw_full_message uid
84
+ connect or return broken_msg
85
+ @imap.uid_fetch(uid, 'RFC822')[0].attr['RFC822'].gsub(/\r\n/, "\n")
86
+ end
87
+
88
+ def each
89
+ connect or return broken_msg
90
+ uids = @imap.uid_search ['UID', "#{cur_offset}:#{end_offset}"]
91
+ uids.each do |uid|
92
+ @last_uid = uid
93
+ @dirty = true
94
+ yield uid, labels
95
+ end
96
+ end
97
+
98
+ def start_offset; 1; end
99
+ def end_offset
100
+ connect or return start_offset
101
+ @imap.uid_search(['ALL']).last
102
+ end
103
+ end
104
+
105
+ Redwood::register_yaml(IMAP, %w(uri username password offset usual archived id))
106
+
107
+ end
data/lib/sup/index.rb CHANGED
@@ -18,17 +18,11 @@ end
18
18
  class Index
19
19
  include Singleton
20
20
 
21
- LOAD_THREAD_PETIT_DELAY = 0.1
22
- LOAD_THREAD_GRAND_DELAY = 5
23
-
24
- MESSAGES_AT_A_TIME = 10
25
-
26
21
  attr_reader :index # debugging only
27
22
 
28
23
  def initialize dir=BASE_DIR
29
24
  @dir = dir
30
25
  @mutex = Mutex.new
31
- @load_thread = nil # loads new messages
32
26
  @sources = {}
33
27
  @sources_dirty = false
34
28
 
@@ -50,7 +44,8 @@ class Index
50
44
  raise "duplicate source!" if @sources.include? source
51
45
  @sources_dirty = true
52
46
  source.id ||= @sources.size
53
- source.id += 1 while @sources.member? source.id
47
+ ##TODO: why was this necessary?
48
+ ##source.id += 1 while @sources.member? source.id
54
49
  @sources[source.id] = source
55
50
  end
56
51
 
@@ -71,7 +66,7 @@ class Index
71
66
  field_infos = Ferret::Index::FieldInfos.new :store => :yes
72
67
  field_infos.add_field :message_id
73
68
  field_infos.add_field :source_id
74
- field_infos.add_field :source_info, :index => :no, :term_vector => :no
69
+ field_infos.add_field :source_info
75
70
  field_infos.add_field :date, :index => :untokenized
76
71
  field_infos.add_field :body, :store => :no
77
72
  field_infos.add_field :label
@@ -190,31 +185,50 @@ class Index
190
185
  source = @sources[doc[:source_id].to_i]
191
186
  #puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
192
187
  raise "invalid source #{doc[:source_id]}" unless source
193
- begin
194
- raise "no snippet" unless doc[:snippet]
195
- Message.new source, doc[:source_info].to_i,
196
- doc[:label].split(" ").map { |s| s.intern },
197
- doc[:snippet]
198
- rescue MessageFormatError => e
199
- raise IndexError.new(source, "error building message #{doc[:message_id]} at #{source}/#{doc[:source_info]}: #{e.message}")
200
- nil
201
- end
202
- end
203
188
 
204
- def start_load_thread
205
- return if @load_thread
206
- @load_thread = true
207
- @load_thread = ::Thread.new do
208
- while @load_thread
209
- load_some_entries ENTRIES_AT_A_TIME, LOAD_THREAD_PETIT_DELAY, LOAD_THREAD_GRAND_DELAY
189
+ m =
190
+ if source.broken?
191
+ nil
192
+ else
193
+ begin
194
+ Message.new :source => source, :source_info => doc[:source_info].to_i,
195
+ :labels => doc[:label].split(" ").map { |s| s.intern },
196
+ :snippet => doc[:snippet]
197
+ rescue MessageFormatError => e
198
+ raise IndexError.new(source, "error building message #{doc[:message_id]} at #{source}/#{doc[:source_info]}: #{e.message}")
199
+ rescue SourceError => e
200
+ nil
201
+ end
210
202
  end
203
+
204
+ unless m
205
+ fake_header = {
206
+ "date" => Time.at(doc[:date].to_i),
207
+ "subject" => unwrap_subj(doc[:subject]),
208
+ "from" => doc[:from],
209
+ "to" => doc[:to],
210
+ "message-id" => doc[:message_id],
211
+ "references" => doc[:refs],
212
+ }
213
+
214
+ m = Message.new :labels => doc[:label].split(" ").map { |s| s.intern },
215
+ :snippet => doc[:snippet], :header => fake_header,
216
+ :body => <<EOS
217
+ #{doc[:snippet]}...
218
+
219
+ An error occurred while loading this message. It is possible that the source
220
+ has changed, or (in the case of remote sources) is down.
221
+
222
+ The error message was:
223
+ #{source.broken_msg}
224
+ EOS
211
225
  end
226
+ m
212
227
  end
213
228
 
214
- def end_load_thread; @load_thread = nil; end
215
229
  def fresh_thread_id; @next_thread_id += 1; end
216
-
217
230
  def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
231
+ def unwrap_subj subj; subj =~ /__START_SUBJECT__ (.*?) __END_SUBJECT__/ && $1; end
218
232
 
219
233
  def add_message m
220
234
  return false if contains? m
@@ -238,7 +252,7 @@ class Index
238
252
  :from => m.from ? m.from.email : "",
239
253
  :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
240
254
  :subject => wrap_subj(Message.normalize_subj(m.subj)),
241
- :refs => (m.refs + m.replytos).join(" "),
255
+ :refs => (m.refs + m.replytos).uniq.join(" "),
242
256
  }
243
257
 
244
258
  @index.add_document d
@@ -316,38 +330,16 @@ protected
316
330
 
317
331
  def save_sources fn=Redwood::SOURCE_FN
318
332
  if @sources_dirty || @sources.any? { |id, s| s.dirty? }
319
- FileUtils.mv fn, fn + ".bak", :force => true if File.exists? fn
333
+ bakfn = fn + ".bak"
334
+ if File.exists? fn
335
+ File.chmod 0600, fn
336
+ FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(bakfn) > File.size(fn)
337
+ end
320
338
  Redwood::save_yaml_obj @sources.values, fn
339
+ File.chmod 0600, fn
321
340
  end
322
341
  @sources_dirty = false
323
342
  end
324
-
325
- def load_some_entries max=ENTRIES_AT_A_TIME, delay1=nil, delay2=nil
326
- num = 0
327
- begin
328
- @sources.each_with_index do |source, source_id|
329
- next if source.done? || num >= max
330
- source.each do |source_info, label|
331
- begin
332
- m = Message.new(source, source_info, label + [:inbox])
333
- add_message m unless contains_id? m.id
334
- puts m.content.inspect
335
- num += 1
336
- rescue MessageFormatError => e
337
- $stderr.puts "ignoring erroneous message at #{source}##{source_info}: #{e.message}"
338
- end
339
- break if num >= max
340
- sleep delay1 if delay1
341
- end
342
- Redwood::log "loaded #{num} entries from #{source}"
343
- sleep delay2 if delay2
344
- end
345
- ensure
346
- save_sources
347
- save_index
348
- end
349
- num
350
- end
351
343
  end
352
344
 
353
345
  end