sup 0.0.6 → 0.0.7

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/FAQ.txt CHANGED
@@ -1,27 +1,55 @@
1
1
  Sup FAQ
2
2
  -------
3
3
  Q: What does Sup stand for?
4
- A: It stands for "what's up?", which is more or less the question I'm
5
- asking when I read my mail.
4
+ A: It stands for "what's up?", which is more or less the question in
5
+ mind when I fire up my mail client.
6
6
 
7
7
  Q: If you love GMail so much, why not just use it?
8
- A: I hate using a mouse, and I hate ads, and I hate non-programmability
8
+ A: I hate ads, I hate using a mouse, and I hate non-programmability
9
9
  and non-extensibility.
10
10
 
11
+ Also, GMail encourages top-posting in a variety of ways. THIS
12
+ CANNOT BE TOLERATED!
13
+
11
14
  Q: Why the console?
12
- A: There are many advantages to the console. As any Unix user knows, a
13
- few keystrokes can accomplish the work of a hundred mouse clicks.
14
- Also, you don't need web browser, and you get instantaneous response
15
- and a simple interface.
16
15
 
17
- That said, a good Ajax programmer could probably replicate GMail
18
- pretty easily using Sup as the backend.
16
+ A: Because a keystroke is with a hundred mouse clicks (as any Unix
17
+ user knows). Because you don't need web browser. Because you get
18
+ instantaneous response and a simple interface.
19
19
 
20
20
  Q: How does Sup deal with spam?
21
21
  A: You can manually mark messages as spam, which prevents them from
22
- showing up in future searches, but that's all that Sup does. Spam
22
+ showing up in future searches, but that's as far as Sup goes. Spam
23
23
  filtering should be done by a dedicated tool like SpamAssassin.
24
24
 
25
+ Q: How do I delete a message?
26
+ A: Press the 'd' key.
27
+
28
+ Q: But I want to delete it for real, not just add a 'deleted' flag in
29
+ the index. I want it gone from disk!
30
+ A: Deleting a message is an old-fashioned concept. In the modern
31
+ world, disk space is cheap enough that you should never have to
32
+ delete a message. If it's spam, save it for future analysis.
33
+
34
+ Q: C'mon, really now!
35
+ A: Ok, at some point I plan to have a batch deletion tool that will
36
+ run through a source and delete all messages that have a 'spam' or
37
+ 'deleted' tags (and, for mbox sources, will update the offsets of
38
+ all later messages). But that doesn't exist yet.
39
+
40
+ Q: I got some error message about needing to run sup-import --rescan
41
+ when I tried to read a message. What's that about?
42
+ A: If messages have been moved, deleted, or altered in a source, Sup
43
+ may have to rebuild its index for that source. For example, for
44
+ mbox files, even reading a message changes the offsets of every
45
+ file on disk. Rather than rescanning every time, Sup assumes
46
+ sources don't change except by having new messages added. If that
47
+ assumption is violated, you'll have to run sup-import --rescan.
48
+
49
+ The alternative is to rescan every source when Sup starts
50
+ up. Because Sup is designed to work with arbitrarily large mbox
51
+ files, this would not be a good idea.
52
+
25
53
  Q: What are all these "Redwood" references I see in the code?
26
54
  A: That was Sup's original name. (Think pine, elm. Although I am a
27
55
  Mutt user, I couldn't think of a good progression there.) But it was
@@ -31,9 +59,18 @@ A: That was Sup's original name. (Think pine, elm. Although I am a
31
59
  Maybe one day I'll do a huge search-and-replace on the code, but it
32
60
  doesn't seem that important at this point.
33
61
 
62
+ Q: I want to move messages from one source to another. (E.g., my
63
+ primary inbox is an IMAP server with a quota, and I want to move
64
+ some of those messages to local mbox files.) How do I do that while
65
+ preserving message state?
66
+ A: Move the messages from the source to the target using whatever tool
67
+ you'd like. Then (and this is the important part), sup-import
68
+ --rebuild both sources at once. If you do it one at a time, you may
69
+ lose message state. (Depending, actually, on which order you do it
70
+ in. But just do them both at once.)
71
+
34
72
  Q: How is Sup possible?
35
73
  A: Sup is only possible through the hard work of Dave Balmain, the
36
74
  author of ferret, which is the search engine behind Sup. Ferret is
37
75
  really a first-class piece of software, and it's due to the
38
76
  tremendous amount of time and effort he's put in to it.
39
-
data/doc/TODO CHANGED
@@ -1,25 +1,58 @@
1
- fix bug: when returning from a shelling out, ncurses is crazy
2
- batch deletion
1
+ for 0.0.8
2
+ ---------
3
+ create attachments
4
+ forward attachments
5
+ warnings: top-posting, missing attachment
3
6
  maildir
4
- on startup, multi-threadedly call #connect on all sources
5
- support for message-content modules such as ruby-talk:XXXXX detection
7
+
8
+ for 0.0.9
9
+ ---------
10
+ select all, starred, to me, etc
11
+ undo
6
12
  use Net::SMTP
7
- search for other messages from author in thread-view-mode
8
- forward attachments
9
- tab completion on labels, contacts
13
+ gmail
14
+
15
+ future
16
+ ------
17
+ decode RFC 2047 ("encoded word") headers
18
+ - see: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949, http://dev.rubyonrails.org/ticket/6807
19
+ swappable keymappings
10
20
  within-buffer search
21
+ bugfix: when returning from a shelling out, ncurses is crazy
22
+ bugfix: miscellaneous weirdnesses in buffer line editing
23
+ wide character support
24
+ i18n support
25
+ batch deletion
26
+ support for message-content modules such as ruby-talk:XXXXX detection
27
+ tab completion on labels, contacts
11
28
  contact selector in edit-message-mode
12
- undo
13
29
  maybe: filters
14
30
  maybe: rangefilter on the initial inbox to only consider the most recent 1000 messages
15
- select all, starred, to me, etc
16
- editing of arbitrary messages
17
31
  annotations on messages
18
- gmail
19
32
  pop
20
- move sup-import argument handling to getopt
21
33
  be able to mark individual messages as spam in thread-view-mode
34
+ toggle wrapping
35
+ maybe: de-archived messages auto-added to inbox
22
36
 
37
+ done
38
+ ----
39
+ x make 'A' archive in thread-view-mode
40
+ x remove stupid percent_done source methods (still useful; made it optional)
41
+ x don't quit while writing thread index state to disk or with unsaved drafts/messages
42
+ x bugfix: deleted threads are showing up (i don't see this any more)
43
+ x bugfix: changing IMAP ids
44
+ x bugfix: STILL new messages, drafts sometimes not showing up in inbox
45
+ x bugfix: killed threads
46
+ x bugfix: resuming a draft asks before discard
47
+ x add a flag to sup-import to force the creation of a new source (see http://rubyforge.org/forum/forum.php?thread_id=10973&forum_id=10340)
48
+ x use trollop to handle sup-devel args
49
+ x clean up import code and share between poll.rb and sup-import
50
+ x on startup, multi-threadedly call #connect on all sources
51
+ x bugfix: first time viewing a message only gets the first to:; subsequent views get them all (wtf)
52
+ x search for other messages from author in thread-view-mode
53
+ x resuming of arbitrary messages
54
+ x alias authors in thread-view-mode
55
+ x fix up contact list mode: should display while loading, and when you add an alias, should move everything else to the right
23
56
  x fix bug: envelope-to thing still not working
24
57
  x fix snippet repetitions with small snippets
25
58
  x fix next and previous in thread-view-mode with <unreceived messages>
data/doc/UserGuide.txt CHANGED
@@ -1,6 +1,6 @@
1
- Welcome to Sup! Here's how to actually use it.
1
+ Welcome to Sup! Here's how to get started.
2
2
 
3
- First, try running 'sup'. Assuming this is your first time, you'll be
3
+ First, try running 'sup'. Since this is your first time, you'll be
4
4
  confronted with a mostly blank screen, and a notice at the bottom that
5
5
  you have no new messages. That's because Sup doesn't have any messages
6
6
  in its index yet, and has no idea where to look for them anyways.
@@ -10,55 +10,67 @@ to cycle between buffers and 'x' to kill a buffer. There's probably
10
10
  not too much interesting there, but there's a log buffer with some
11
11
  cryptic messages. You can also press '?' at any point to get a list of
12
12
  keyboard commands, but in the absense of any email, these will be
13
- mostly useless. When you're bored, press 'q' to quit.
14
-
15
- We need to add some messages to Sup. In order for Sup to know about a
16
- message, it must be in the index. In order for Sup to load a message
17
- into the index, it must know about the "source" in which it
18
- resides. Sup stores all information necessary for search, and all
19
- state we have about a message, in the index, but doesn't duplicate the
20
- actual contents of the message itself. So when you search for messages
21
- or view your inbox, Sup just talks to the index (stored locally on
22
- disk), but when you view a message, Sup must request it from the
23
- source.
24
-
25
- So let's tell Sup about some sources. Run 'sup-import' with a URI
26
- pointing to an email source. The URI should be of the form:
13
+ mostly useless. When you get bored, press 'q' to quit.
14
+
15
+ I mentioned that Sup's index was empty. The index is where Sup stores
16
+ all message state (e.g. read or unread, any message labels), and all
17
+ information necessary for searching and for threading messages. Sup
18
+ only knows about messages in its index, so we've got to add some to
19
+ it.
20
+
21
+ We add messages to the index by telling Sup about the source where the
22
+ messages reside. Sources are things like imap folders, mbox folders,
23
+ maildir directories, and gmail accounts (in the future). Sup doesn't
24
+ duplicate the actual message content in the index. So when you search
25
+ for messages or view your inbox, Sup talks only to the index (stored
26
+ locally on disk). When you view a thread, Sup requests the full
27
+ content of all the messages from the source.
28
+
29
+ So let's add some sources to Sup's source list. Run 'sup-add' with
30
+ a URI pointing to an email source. The URI should be of the form:
27
31
  - mbox://path/to/a/filename, for an mbox file on disk. (You can also
28
32
  just provide the filename).
29
33
  - imap://imap.server/folder or imaps://secure.imap.server/folder for
30
34
  an IMAP folder. (Leave the folder blank for INBOX.)
31
35
  - mbox+ssh://remote.machine/path/to/a/filename for a remote mbox file.
32
36
 
33
- Before you actually import messages, you need to decide whether you
34
- want them to be archived or not. An archived message will not show up
35
- in your inbox. (Your inbox in Sup is, by definition, the set of all
36
- all non-archived messages). If you specify --force-archive, messages
37
- imported at *this* time will be archived, but new messages will go to
38
- your inbox. If you specify --archive, messages imported at *any* time
39
- will be automatically archived. (This is useful for sources that
40
- contain high-traffic mailing lists that you don't want "polluting"
41
- your inbox.)
42
-
43
- Typically you'll want to specify --archive for every source you import
44
- except your actual inbox. You can also force all messages to be marked
45
- as read; run sup-import --help for details. The default is to preserve
46
- the read/unread status from the source.
47
-
48
- Now run sup-import to add the source. If it requires a username and
49
- password for the source, it will prompt you. Either way, it will add
50
- the sources to Sup, and immediately start loading messages from them
51
- into the index. Depending on the size of the source, this may take a
37
+ Before you add the source, you need make two decisions. The first is
38
+ whether you want Sup to regularly poll this source for new
39
+ messages. By default it will, but if this is a source that will never
40
+ have new messages, you can specify --unusual. Sup polls only "usual"
41
+ sources.
42
+
43
+ The second is whether you want messages from the source to be
44
+ automatically archived. An archived message will not show up in your
45
+ inbox, but will be found when you search. (Your inbox in Sup is, by
46
+ definition, the set of all all non-archived messages). Specify
47
+ --archive to automatically archive all messages from the source. This
48
+ is useful for sources that contain, for example, high-traffic mailing
49
+ lists that you don't want "polluting" your inbox.
50
+
51
+ If Sup requires account information, e.g. for IMAP servers and remote
52
+ mbox files, sup-add will ask for it.
53
+
54
+ Now that you've added the source, let's import all the current
55
+ messages from it, by running sup-import with the source URI. You can
56
+ specify --archive to automatically archive all messages in this
57
+ import; typically you'll want to specify this for every source you
58
+ import except your actual inbox. You can also specify --read to mark
59
+ all imported messages as read; the default is to preserve the
60
+ read/unread status from the source.
61
+
62
+ sup-import will now load all the messages from the source into the
63
+ index. Depending on the size of the source, this may take a
52
64
  while. Don't panic! It's a one-time process.
53
65
 
54
- Now, before we run 'sup' again, take a moment to edit your
55
- ~/.sup/config.yaml file. Replace "Your Name Here" with your name,
56
- "your.email.here@domain.tld" with your email address, and fill in your
57
- .signature file if you choose. You can also set the default editor.
66
+ We're almost ready. But before we run 'sup' again, take a moment to
67
+ edit your ~/.sup/config.yaml file. Replace "Your Name Here" with your
68
+ name, "your.email.here@domain.tld" with your email address, and fill
69
+ in your .signature file if you choose. You can also set the default
70
+ editor.
58
71
 
59
- We're finally ready to run 'sup'. You should see the most recent
60
- loaded messages appear in your inbox. Congratulations, you've got Sup
61
- working!
72
+ Ok, now run 'sup'. You should see the most recent unarchived messages
73
+ appear in your inbox. Congratulations, you've got Sup working!
62
74
 
63
75
  If you're coming from the world of traditional email, there are a
64
76
  couple differences you should be aware of at this point. First, Sup
data/lib/sup.rb CHANGED
@@ -13,13 +13,13 @@ class Object
13
13
  end
14
14
 
15
15
  module Redwood
16
- VERSION = "0.0.6"
16
+ VERSION = "0.0.7"
17
17
 
18
18
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
19
19
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
20
20
  SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
21
21
  LABEL_FN = File.join(BASE_DIR, "labels.txt")
22
- PERSON_FN = File.join(BASE_DIR, "people.txt")
22
+ PERSON_FN = File.join(BASE_DIR, "people.txt")
23
23
  CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
24
24
  DRAFT_DIR = File.join(BASE_DIR, "drafts")
25
25
  SENT_FN = File.join(BASE_DIR, "sent.mbox")
@@ -34,6 +34,10 @@ module Redwood
34
34
  begin
35
35
  yield
36
36
  rescue Exception => e
37
+ File.open("sup-exception-log.txt", "w") do |f|
38
+ f.puts "--- #{e.class.name} at #{Time.now}"
39
+ f.puts e.message, e.backtrace
40
+ end
37
41
  $exception ||= e
38
42
  raise
39
43
  end
data/lib/sup/buffer.rb CHANGED
@@ -14,7 +14,7 @@ module Ncurses
14
14
  end
15
15
 
16
16
  def mutex; @mutex ||= Mutex.new; end
17
- def sync &b; mutex.synchronize &b; end
17
+ def sync &b; mutex.synchronize(&b); end
18
18
 
19
19
  ## aaahhh, user input. who would have though that such a simple
20
20
  ## idea would be SO FUCKING COMPLICATED?! because apparently
@@ -139,7 +139,7 @@ class BufferManager
139
139
  @minibuf_mutex = Mutex.new
140
140
  @textfields = {}
141
141
  @flash = nil
142
- @shelled = false
142
+ @shelled = @asking = false
143
143
 
144
144
  self.class.i_am_the_instance self
145
145
  end
@@ -271,6 +271,21 @@ class BufferManager
271
271
  b
272
272
  end
273
273
 
274
+ def kill_all_buffers_safely
275
+ until @buffers.empty?
276
+ ## inbox mode always claims it's unkillable. we'll ignore it.
277
+ return false unless @buffers.first.mode.is_a?(InboxMode) || @buffers.first.mode.killable?
278
+ kill_buffer @buffers.first
279
+ end
280
+ true
281
+ end
282
+
283
+ def kill_buffer_safely buf
284
+ return false unless buf.mode.killable?
285
+ kill_buffer buf
286
+ true
287
+ end
288
+
274
289
  def kill_all_buffers
275
290
  kill_buffer @buffers.first until @buffers.empty?
276
291
  end
@@ -359,9 +374,9 @@ class BufferManager
359
374
  ret
360
375
  end
361
376
 
377
+ ## returns true (y), false (n), or nil (ctrl-g / cancel)
362
378
  def ask_yes_or_no question
363
- r = ask_getch(question, "ynYN")
364
- case r
379
+ case(r = ask_getch question, "ynYN")
365
380
  when ?y, ?Y
366
381
  true
367
382
  when nil
@@ -399,6 +414,7 @@ class BufferManager
399
414
 
400
415
  def say s, id=nil
401
416
  new_id = nil
417
+
402
418
  @minibuf_mutex.synchronize do
403
419
  new_id = id.nil?
404
420
  id ||= @minibuf_stack.length
data/lib/sup/contact.rb CHANGED
@@ -5,33 +5,43 @@ class ContactManager
5
5
 
6
6
  def initialize fn
7
7
  @fn = fn
8
- @people = {}
8
+ @p2a = {} # person to alias map
9
+ @a2p = {} # alias to person map
9
10
 
10
11
  if File.exists? fn
11
12
  IO.foreach(fn) do |l|
12
13
  l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
13
14
  aalias, addr = $1, $2
14
- @people[aalias] = Person.for addr
15
+ p = Person.for addr
16
+ @p2a[p] = aalias
17
+ @a2p[aalias] = p
15
18
  end
16
19
  end
17
20
 
18
21
  self.class.i_am_the_instance self
19
22
  end
20
23
 
21
- def contacts; @people; end
24
+ def contacts; @p2a.keys; end
22
25
  def set_contact person, aalias
23
- oldentry = @people.find { |a, p| p == person }
24
- @people.delete oldentry.first if oldentry
25
- @people[aalias] = person
26
+ if(pold = @a2p[aalias]) && (pold != person)
27
+ drop_contact pold
28
+ end
29
+ @p2a[person] = aalias
30
+ @a2p[aalias] = person
26
31
  end
27
- def drop_contact person; @people.delete person; end
28
- def delete t; @people.delete t; end
29
- def resolve aalias; @people[aalias]; end
30
-
32
+ def drop_contact person
33
+ if(aalias = @p2a[person])
34
+ @p2a.delete person
35
+ @a2p.delete aalias
36
+ end
37
+ end
38
+ def person_with aalias; @a2p[aalias]; end
39
+ def alias_for person; @p2a[person]; end
40
+ def is_contact? person; @p2a.member? person; end
31
41
  def save
32
42
  File.open(@fn, "w") do |f|
33
- @people.keys.sort.each do |aalias|
34
- f.puts "#{aalias}: #{@people[aalias].full_address}"
43
+ @p2a.each do |p, a|
44
+ f.puts "#{a}: #{p.full_address}"
35
45
  end
36
46
  end
37
47
  end
data/lib/sup/draft.rb CHANGED
@@ -10,7 +10,7 @@ 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; "sup://drafts"; end
14
14
  def self.source_id; 9999; end
15
15
  def new_source; @source = DraftLoader.new; end
16
16
 
@@ -19,11 +19,15 @@ class DraftManager
19
19
  fn = @source.fn_for_offset offset
20
20
  File.open(fn, "w") { |f| yield f }
21
21
 
22
- @source.each do |offset, labels|
23
- m = Message.new :source => @source, :source_info => offset, :labels => labels
22
+ my_message = nil
23
+ @source.each do |thisoffset, theselabels|
24
+ m = Message.new :source => @source, :source_info => thisoffset, :labels => theselabels
24
25
  Index.add_message m
25
- UpdateManager.relay :add, m
26
+ UpdateManager.relay self, :add, m
27
+ my_message = m if thisoffset == offset
26
28
  end
29
+
30
+ my_message
27
31
  end
28
32
 
29
33
  def discard mid
@@ -32,7 +36,7 @@ class DraftManager
32
36
  raise ArgumentError, "not a draft: source id #{entry[:source_id].inspect}, should be #{DraftManager.source_id.inspect} for #{mid.inspect} / docno #{docid}" unless entry[:source_id].to_i == DraftManager.source_id
33
37
  Index.drop_entry docid
34
38
  File.delete @source.fn_for_offset(entry[:source_info])
35
- UpdateManager.relay :delete, mid
39
+ UpdateManager.relay self, :delete, mid
36
40
  end
37
41
  end
38
42
 
@@ -48,9 +52,16 @@ class DraftLoader < Source
48
52
 
49
53
  def id; DraftManager.source_id; end
50
54
  def to_s; DraftManager.source_name; end
55
+ def uri; DraftManager.source_name; end
51
56
 
52
57
  def each
53
- Dir.entries(@dir).select { |x| x =~ /^\d+$/ }.sort_by { |x| x.to_i }.each { |id| yield [id, [:draft]] }
58
+ ids = get_ids
59
+ ids.each do |id|
60
+ if id >= cur_offset
61
+ self.cur_offset = id + 1
62
+ yield [id, [:draft, :inbox]]
63
+ end
64
+ end
54
65
  end
55
66
 
56
67
  def gen_offset
@@ -96,7 +107,16 @@ class DraftLoader < Source
96
107
  end
97
108
 
98
109
  def start_offset; 0; end
99
- def end_offset; Dir.new(@dir).entries.sort.last.to_i; end
110
+ def end_offset
111
+ ids = get_ids
112
+ ids.empty? ? 0 : (ids.last + 1)
113
+ end
114
+
115
+ private
116
+
117
+ def get_ids
118
+ Dir.entries(@dir).select { |x| x =~ /^\d+$/ }.map { |x| x.to_i }.sort
119
+ end
100
120
  end
101
121
 
102
122
  Redwood::register_yaml(DraftLoader, %w(cur_offset))