sup 0.3 → 0.4

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 (52) hide show
  1. data/HACKING +31 -9
  2. data/History.txt +7 -0
  3. data/Manifest.txt +2 -0
  4. data/Rakefile +9 -5
  5. data/bin/sup +81 -57
  6. data/bin/sup-config +1 -1
  7. data/bin/sup-sync +3 -0
  8. data/bin/sup-tweak-labels +127 -0
  9. data/doc/TODO +23 -12
  10. data/lib/sup.rb +13 -11
  11. data/lib/sup/account.rb +25 -12
  12. data/lib/sup/buffer.rb +61 -41
  13. data/lib/sup/colormap.rb +2 -0
  14. data/lib/sup/contact.rb +28 -18
  15. data/lib/sup/crypto.rb +86 -31
  16. data/lib/sup/draft.rb +12 -6
  17. data/lib/sup/horizontal-selector.rb +47 -0
  18. data/lib/sup/imap.rb +50 -37
  19. data/lib/sup/index.rb +76 -13
  20. data/lib/sup/keymap.rb +27 -8
  21. data/lib/sup/maildir.rb +1 -1
  22. data/lib/sup/mbox/loader.rb +1 -1
  23. data/lib/sup/message-chunks.rb +43 -15
  24. data/lib/sup/message.rb +67 -31
  25. data/lib/sup/mode.rb +40 -9
  26. data/lib/sup/modes/completion-mode.rb +1 -1
  27. data/lib/sup/modes/compose-mode.rb +3 -3
  28. data/lib/sup/modes/contact-list-mode.rb +12 -8
  29. data/lib/sup/modes/edit-message-mode.rb +100 -36
  30. data/lib/sup/modes/file-browser-mode.rb +1 -0
  31. data/lib/sup/modes/forward-mode.rb +43 -8
  32. data/lib/sup/modes/inbox-mode.rb +8 -5
  33. data/lib/sup/modes/label-search-results-mode.rb +12 -1
  34. data/lib/sup/modes/line-cursor-mode.rb +4 -7
  35. data/lib/sup/modes/reply-mode.rb +59 -54
  36. data/lib/sup/modes/resume-mode.rb +6 -6
  37. data/lib/sup/modes/scroll-mode.rb +4 -3
  38. data/lib/sup/modes/search-results-mode.rb +8 -5
  39. data/lib/sup/modes/text-mode.rb +19 -2
  40. data/lib/sup/modes/thread-index-mode.rb +109 -40
  41. data/lib/sup/modes/thread-view-mode.rb +180 -49
  42. data/lib/sup/person.rb +3 -3
  43. data/lib/sup/poll.rb +9 -8
  44. data/lib/sup/rfc2047.rb +7 -1
  45. data/lib/sup/sent.rb +1 -1
  46. data/lib/sup/tagger.rb +10 -4
  47. data/lib/sup/textfield.rb +7 -7
  48. data/lib/sup/thread.rb +86 -49
  49. data/lib/sup/update.rb +11 -0
  50. data/lib/sup/util.rb +74 -34
  51. data/test/test_message.rb +441 -0
  52. metadata +136 -117
@@ -16,9 +16,7 @@ module Redwood
16
16
  ## in sup, completion support is implemented through BufferManager#ask
17
17
  ## and CompletionMode.
18
18
  class TextField
19
- def initialize window, y, x, width
20
- @w, @x, @y = window, x, y
21
- @width = width
19
+ def initialize
22
20
  @i = nil
23
21
  @history = []
24
22
 
@@ -31,11 +29,11 @@ class TextField
31
29
 
32
30
  def value; @value || get_cursed_value end
33
31
 
34
- def activate question, default=nil, &block
32
+ def activate window, y, x, width, question, default=nil, &block
33
+ @w, @y, @x, @width = window, y, x, width
35
34
  @question = question
36
35
  @completion_block = block
37
- @field = Ncurses::Form.new_field 1, @width - question.length,
38
- @y, @x + question.length, 0, 0
36
+ @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 0, 0
39
37
  @form = Ncurses::Form.new_form [@field]
40
38
  @value = default
41
39
  Ncurses::Form.post_form @form
@@ -47,7 +45,7 @@ class TextField
47
45
  @w.mvaddstr @y, 0, @question
48
46
  Ncurses.curs_set 1
49
47
  Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
50
- Ncurses::Form.form_driver @form, Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED!!!!
48
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED
51
49
  end
52
50
 
53
51
  def deactivate
@@ -100,6 +98,8 @@ class TextField
100
98
  Ncurses::Form::REQ_PREV_CHAR
101
99
  when Ncurses::KEY_RIGHT
102
100
  Ncurses::Form::REQ_NEXT_CHAR
101
+ when Ncurses::KEY_DC
102
+ Ncurses::Form::REQ_DEL_CHAR
103
103
  when Ncurses::KEY_BACKSPACE
104
104
  Ncurses::Form::REQ_DEL_PREV
105
105
  when 1 #ctrl-a
@@ -11,7 +11,7 @@
11
11
  ## zero or more Threads. A Thread represents all the message related
12
12
  ## to a particular subject. Each Thread has one or more Containers. A
13
13
  ## Container is a recursive structure that holds the message tree as
14
- ## determined by the references: and in-reply-to: headers. EAch
14
+ ## determined by the references: and in-reply-to: headers. Each
15
15
  ## Container holds zero or one messages. In the case of zero messages,
16
16
  ## it means we've seen a reference to the message but haven't (yet)
17
17
  ## seen the message itself.
@@ -47,8 +47,8 @@ class Thread
47
47
 
48
48
  ## unused
49
49
  def dump f=$stdout
50
- f.puts "=== start thread #{self} with #{@containers.length} trees ==="
51
- @containers.each { |c| c.dump_recursive f }
50
+ f.puts "=== start thread with #{@containers.length} trees ==="
51
+ @containers.each { |c| c.dump_recursive f; f.puts }
52
52
  f.puts "=== end thread ==="
53
53
  end
54
54
 
@@ -58,7 +58,7 @@ class Thread
58
58
  ## messages).
59
59
  def each fake_root=false
60
60
  adj = 0
61
- root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date || 0 }
61
+ root = @containers.find_all { |c| c.message && !Message.subj_is_reply?(c.message.subj) }.argmin { |c| c.date }
62
62
 
63
63
  if root
64
64
  adj = 1
@@ -185,6 +185,10 @@ class Container
185
185
  def root?; @parent.nil?; end
186
186
  def root; root? ? self : @parent.root; end
187
187
 
188
+ ## skip over any containers which are empty and have only one child. we use
189
+ ## this make the threaded display a little nicer, and only stick in the
190
+ ## "missing message" line when it's graphically necessary, i.e. when the
191
+ ## missing message has more than one descendent.
188
192
  def first_useful_descendant
189
193
  if empty? && @children.size == 1
190
194
  @children.first.first_useful_descendant
@@ -203,7 +207,7 @@ class Container
203
207
  def subj; find_attr :subj; end
204
208
  def date; find_attr :date; end
205
209
 
206
- def is_reply?; subj && Message.subject_is_reply?(subj); end
210
+ def is_reply?; subj && Message.subj_is_reply?(subj); end
207
211
 
208
212
  def to_s
209
213
  [ "<#{id}",
@@ -218,9 +222,9 @@ class Container
218
222
  f.print " " * indent
219
223
  f.print "+->"
220
224
  end
221
- line = #"[#{useful? ? 'U' : ' '}] " +
225
+ line = "[#{thread.nil? ? ' ' : '*'}] " + #"[#{useful? ? 'U' : ' '}] " +
222
226
  if @message
223
- "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}"
227
+ message.subj ##{@message.refs.inspect} / #{@message.replytos.inspect}"
224
228
  else
225
229
  "<no message>"
226
230
  end
@@ -231,13 +235,16 @@ class Container
231
235
  end
232
236
  end
233
237
 
234
- ## A set of threads (so a forest). Builds thread structures by reading
235
- ## messages from an index.
238
+ ## A set of threads, so a forest. Is integrated with the index and
239
+ ## builds thread structures by reading messages from it.
236
240
  ##
237
241
  ## If 'thread_by_subj' is true, puts messages with the same subject in
238
242
  ## one thread, even if they don't reference each other. This is
239
243
  ## helpful for crappy MUAs that don't set In-reply-to: or References:
240
244
  ## headers, but means that messages may be threaded unnecessarily.
245
+ ##
246
+ ## The following invariants are maintained: every Thread has at least one
247
+ ## Container tree, and every Container tree has at least one Message.
241
248
  class ThreadSet
242
249
  attr_reader :num_messages
243
250
  bool_reader :thread_by_subj
@@ -252,21 +259,15 @@ class ThreadSet
252
259
  @thread_by_subj = thread_by_subj
253
260
  end
254
261
 
255
- def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
256
- def thread_for m
257
- (c = @messages[m.id]) && c.root.thread
258
- end
259
-
260
- def delete_cruft
261
- @threads.each { |k, v| @threads.delete(k) if v.empty? }
262
- end
263
- private :delete_cruft
262
+ def thread_for_id mid; (c = @messages[mid]) && c.root.thread end
263
+ def contains_id? id; @messages.member?(id) && !@messages[id].empty? end
264
+ def thread_for m; thread_for_id m.id end
265
+ def contains? m; contains_id? m.id end
264
266
 
265
- def threads; delete_cruft; @threads.values; end
266
- def size; delete_cruft; @threads.size; end
267
+ def threads; @threads.values end
268
+ def size; @threads.size end
267
269
 
268
- ## unused
269
- def dump f
270
+ def dump f=$stdout
270
271
  @threads.each do |s, t|
271
272
  f.puts "**********************"
272
273
  f.puts "** for subject #{s} **"
@@ -275,32 +276,49 @@ class ThreadSet
275
276
  end
276
277
  end
277
278
 
279
+ ## link two containers
278
280
  def link p, c, overwrite=false
279
281
  if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop
280
- # puts "*** linking parent #{p} and child #{c} would create a loop"
282
+ #puts "*** linking parent #{p.id} and child #{c.id} would create a loop"
281
283
  return
282
284
  end
283
285
 
284
- if c.parent.nil? || overwrite
285
- c.parent.children.delete c if overwrite && c.parent
286
- if c.thread
287
- c.thread.drop c
288
- c.thread = nil
289
- end
290
- p.children << c
291
- c.parent = p
292
- end
286
+ #puts "in link for #{p.id} to #{c.id}, perform? #{c.parent.nil?} || #{overwrite}"
287
+
288
+ return unless c.parent.nil? || overwrite
289
+ remove_container c
290
+ p.children << c
291
+ c.parent = p
292
+
293
+ ## if the child was previously a top-level container, it now ain't,
294
+ ## so ditch our thread and kill it if necessary
295
+ prune_thread_of c
293
296
  end
294
297
  private :link
295
298
 
296
- def remove mid
299
+ def remove_container c
300
+ c.parent.children.delete c if c.parent # remove from tree
301
+ end
302
+ private :remove_container
303
+
304
+ def prune_thread_of c
305
+ return unless c.thread
306
+ c.thread.drop c
307
+ @threads.delete_if { |k, v| v == c.thread } if c.thread.empty?
308
+ c.thread = nil
309
+ end
310
+ private :prune_thread_of
311
+
312
+ def remove_id mid
297
313
  return unless(c = @messages[mid])
314
+ remove_container c
315
+ prune_thread_of c
316
+ end
298
317
 
299
- c.parent.children.delete c if c.parent
300
- if c.thread
301
- c.thread.drop c
302
- c.thread = nil
303
- end
318
+ def remove_thread_containing_id mid
319
+ c = @messages[mid] or return
320
+ t = c.root.thread
321
+ @threads.delete_if { |key, thread| t == thread }
304
322
  end
305
323
 
306
324
  ## load in (at most) num number of threads from the index
@@ -331,6 +349,32 @@ class ThreadSet
331
349
  t.each { |m, *o| add_message m }
332
350
  end
333
351
 
352
+ ## merges two threads together. both must be members of this threadset.
353
+ ## does its best, heuristically, to determine which is the parent.
354
+ def join_threads threads
355
+ return if threads.size < 2
356
+
357
+ containers = threads.map do |t|
358
+ c = @messages[t.first.id]
359
+ raise "not in threadset: #{t.first.id}" unless c && c.message
360
+ c
361
+ end
362
+
363
+ ## use subject headers heuristically
364
+ parent = containers.find { |c| !c.is_reply? }
365
+
366
+ ## no thread was rooted by a non-reply, so make a fake parent
367
+ parent ||= @messages["joining-ref-" + containers.map { |c| c.id }.join("-")]
368
+
369
+ containers.each do |c|
370
+ next if c == parent
371
+ c.message.add_ref parent.id
372
+ link parent, c
373
+ end
374
+
375
+ true
376
+ end
377
+
334
378
  def is_relevant? m
335
379
  m.refs.any? { |ref_id| @messages.member? ref_id }
336
380
  end
@@ -340,17 +384,17 @@ class ThreadSet
340
384
  el = @messages[message.id]
341
385
  return if el.message # we've seen it before
342
386
 
387
+ #puts "adding: #{message.id}, refs #{message.refs.inspect}"
388
+
343
389
  el.message = message
344
390
  oldroot = el.root
345
391
 
346
392
  ## link via references:
347
- prev = nil
348
- message.refs.each do |ref_id|
393
+ (message.refs + [el.id]).inject(nil) do |prev, ref_id|
349
394
  ref = @messages[ref_id]
350
395
  link prev, ref if prev
351
- prev = ref
396
+ ref
352
397
  end
353
- link prev, el, true if prev
354
398
 
355
399
  ## link via in-reply-to:
356
400
  message.replytos.each do |ref_id|
@@ -360,13 +404,6 @@ class ThreadSet
360
404
  end
361
405
 
362
406
  root = el.root
363
-
364
- ## new root. need to drop old one and put this one in its place
365
- if root != oldroot && oldroot.thread
366
- oldroot.thread.drop oldroot
367
- oldroot.thread = nil
368
- end
369
-
370
407
  key =
371
408
  if thread_by_subj?
372
409
  Message.normalize_subj root.subj
@@ -1,5 +1,16 @@
1
1
  module Redwood
2
2
 
3
+ ## Classic listener/broadcaster paradigm. Handles communication between various
4
+ ## parts of Sup.
5
+ ##
6
+ ## Usage note: don't pass threads around. Neither thread nor message equality is
7
+ ## defined anywhere in Sup beyond standard object equality. To communicate
8
+ ## something about a particular thread, just pass a representative message from
9
+ ## it around.
10
+ ##
11
+ ## (This assumes that no message will be a part of more than one thread within a
12
+ ## single "view". Luckily, that's true.)
13
+
3
14
  class UpdateManager
4
15
  include Singleton
5
16
 
@@ -1,3 +1,4 @@
1
+ require 'thread'
1
2
  require 'lockfile'
2
3
  require 'mime/types'
3
4
  require 'pathname'
@@ -37,16 +38,7 @@ class Pathname
37
38
  rescue SystemCallError
38
39
  return "?"
39
40
  end
40
-
41
- if s < 1024
42
- s.to_s + "b"
43
- elsif s < (1024 * 1024)
44
- (s / 1024).to_s + "k"
45
- elsif s < (1024 * 1024 * 1024)
46
- (s / 1024 / 1024).to_s + "m"
47
- else
48
- (s / 1024 / 1024 / 1024).to_s + "g"
49
- end
41
+ s.to_human_size
50
42
  end
51
43
 
52
44
  def human_time
@@ -63,33 +55,35 @@ module RMail
63
55
  class EncodingUnsupportedError < StandardError; end
64
56
 
65
57
  class Message
66
- def add_file_attachment fn
58
+ def self.make_file_attachment fn
67
59
  bfn = File.basename fn
68
- a = Message.new
69
60
  t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
61
+ make_attachment IO.read(fn), t.content_type, t.encoding, bfn.to_s
62
+ end
63
+
64
+ def charset
65
+ if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/i
66
+ $1
67
+ end
68
+ end
70
69
 
71
- a.header.add "Content-Disposition", "attachment; filename=#{bfn.to_s.inspect}"
72
- a.header.add "Content-Type", "#{t.content_type}; name=#{bfn.to_s.inspect}"
73
- a.header.add "Content-Transfer-Encoding", t.encoding
70
+ def self.make_attachment payload, mime_type, encoding, filename
71
+ a = Message.new
72
+ a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
73
+ a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
74
+ a.header.add "Content-Transfer-Encoding", encoding if encoding
74
75
  a.body =
75
- case t.encoding
76
+ case encoding
76
77
  when "base64"
77
- [IO.read(fn)].pack "m"
78
+ [payload].pack "m"
78
79
  when "quoted-printable"
79
- [IO.read(fn)].pack "M"
80
- when "7bit", "8bit"
81
- IO.read(fn)
80
+ [payload].pack "M"
81
+ when "7bit", "8bit", nil
82
+ payload
82
83
  else
83
- raise EncodingUnsupportedError, t.encoding
84
+ raise EncodingUnsupportedError, encoding.inspect
84
85
  end
85
-
86
- add_part a
87
- end
88
-
89
- def charset
90
- if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/
91
- $1
92
- end
86
+ a
93
87
  end
94
88
  end
95
89
  end
@@ -294,14 +288,21 @@ class Numeric
294
288
  end
295
289
 
296
290
  def in? range; range.member? self; end
291
+
292
+ def to_human_size
293
+ if self < 1024
294
+ to_s + "b"
295
+ elsif self < (1024 * 1024)
296
+ (self / 1024).to_s + "k"
297
+ elsif self < (1024 * 1024 * 1024)
298
+ (self / 1024 / 1024).to_s + "m"
299
+ else
300
+ (self / 1024 / 1024 / 1024).to_s + "g"
301
+ end
302
+ end
297
303
  end
298
304
 
299
305
  class Fixnum
300
- def num_digits base=10
301
- return 1 if self == 0
302
- 1 + (Math.log(self) / Math.log(10)).floor
303
- end
304
-
305
306
  def to_character
306
307
  if self < 128 && self >= 0
307
308
  chr
@@ -573,3 +574,42 @@ class SavingHash
573
574
 
574
575
  defer_all_other_method_calls_to :hash
575
576
  end
577
+
578
+ class OrderedHash < Hash
579
+ alias_method :store, :[]=
580
+ alias_method :each_pair, :each
581
+ attr_reader :keys
582
+
583
+ def initialize *a
584
+ @keys = []
585
+ a.each { |k, v| self[k] = v }
586
+ end
587
+
588
+ def []= key, val
589
+ @keys << key unless member?(key)
590
+ super
591
+ end
592
+
593
+ def values; keys.map { |k| self[k] } end
594
+ def index key; @keys.index key end
595
+
596
+ def delete key
597
+ @keys.delete key
598
+ super
599
+ end
600
+
601
+ def each; @keys.each { |k| yield k, self[k] } end
602
+ end
603
+
604
+ ## easy thread-safe class for determining who's the "winner" in a race (i.e.
605
+ ## first person to hit the finish line
606
+ class FinishLine
607
+ def initialize
608
+ @m = Mutex.new
609
+ @over = false
610
+ end
611
+
612
+ def winner?
613
+ @m.synchronize { !@over && @over = true }
614
+ end
615
+ end
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'test/unit'
4
+ require 'sup'
5
+ require 'stringio'
6
+
7
+ require 'dummy_source'
8
+
9
+ # override File.exists? to make it work with StringIO for testing.
10
+ # FIXME: do aliasing to avoid breaking this when sup moves from
11
+ # File.exists? to File.exist?
12
+
13
+ class File
14
+
15
+ def File.exists? file
16
+ # puts "fake File::exists?"
17
+
18
+ if file.is_a?(StringIO)
19
+ return false
20
+ end
21
+ # use the different function
22
+ File.exist?(file)
23
+ end
24
+
25
+ end
26
+
27
+ module Redwood
28
+
29
+ class TestMessage < Test::Unit::TestCase
30
+
31
+ def setup
32
+ person_file = StringIO.new("")
33
+ # this is a singleton
34
+ if not PersonManager.instantiated?
35
+ @person_manager = PersonManager.new(person_file)
36
+ end
37
+ end
38
+
39
+ def teardown
40
+ end
41
+
42
+ def test_simple_message
43
+
44
+ message = <<EOS
45
+ Return-path: <fake_sender@example.invalid>
46
+ Envelope-to: fake_receiver@localhost
47
+ Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
48
+ Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
49
+ (envelope-from <fake_sender@example.invalid>)
50
+ id 1J1S8R-0006lA-MJ
51
+ for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
52
+ Date: Sun, 9 Dec 2007 21:48:19 +0200
53
+ Mailing-List: contact example-help@example.invalid; run by ezmlm
54
+ Precedence: bulk
55
+ List-Id: <example.list-id.example.invalid>
56
+ List-Post: <mailto:example@example.invalid>
57
+ List-Help: <mailto:example-help@example.invalid>
58
+ List-Unsubscribe: <mailto:example-unsubscribe@example.invalid>
59
+ List-Subscribe: <mailto:example-subscribe@example.invalid>
60
+ Delivered-To: mailing list example@example.invalid
61
+ Delivered-To: moderator for example@example.invalid
62
+ From: Fake Sender <fake_sender@example.invalid>
63
+ To: Fake Receiver <fake_receiver@localhost>
64
+ Subject: Re: Test message subject
65
+ Message-ID: <20071209194819.GA25972@example.invalid>
66
+ References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
67
+ MIME-Version: 1.0
68
+ Content-Type: text/plain; charset=us-ascii
69
+ Content-Disposition: inline
70
+ In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
71
+ User-Agent: Sup/0.3
72
+
73
+ Test message!
74
+ EOS
75
+
76
+ source = DummySource.new("sup-test://test_simple_message")
77
+ source.messages = [ message ]
78
+ source_info = 0
79
+
80
+ sup_message = Message.new( {:source => source, :source_info => source_info } )
81
+
82
+ # see how well parsing the header went
83
+
84
+ to = sup_message.to
85
+ # "to" is an Array containing person items
86
+
87
+ # there should be only one item
88
+ assert_equal(1, to.length)
89
+
90
+ # sup doesn't do capitalized letters in email addresses
91
+ assert_equal("fake_receiver@localhost", to[0].email)
92
+ assert_equal("Fake Receiver", to[0].name)
93
+
94
+ from = sup_message.from
95
+ # "from" is just a simple person item
96
+
97
+ assert_equal("fake_sender@example.invalid", from.email)
98
+ assert_equal("Fake Sender", from.name)
99
+
100
+ subj = sup_message.subj
101
+ assert_equal("Re: Test message subject", subj)
102
+
103
+ list_subscribe = sup_message.list_subscribe
104
+ assert_equal("<mailto:example-subscribe@example.invalid>", list_subscribe)
105
+
106
+ list_unsubscribe = sup_message.list_unsubscribe
107
+ assert_equal("<mailto:example-unsubscribe@example.invalid>", list_unsubscribe)
108
+
109
+ list_address = sup_message.list_address
110
+ assert_equal("example@example.invalid", list_address.email)
111
+ assert_equal("example", list_address.name)
112
+
113
+ date = sup_message.date
114
+ assert_equal(Time.parse("Sun, 9 Dec 2007 21:48:19 +0200"), date)
115
+
116
+ id = sup_message.id
117
+ assert_equal("20071209194819.GA25972@example.invalid", id)
118
+
119
+ refs = sup_message.refs
120
+ assert_equal(1, refs.length)
121
+ assert_equal("E1J1Rvb-0006k2-CE@localhost.localdomain", refs[0])
122
+
123
+ replytos = sup_message.replytos
124
+ assert_equal(1, replytos.length)
125
+ assert_equal("E1J1Rvb-0006k2-CE@localhost.localdomain", replytos[0])
126
+
127
+ cc = sup_message.cc
128
+ # there are no ccs
129
+ assert_equal(0, cc.length)
130
+
131
+ bcc = sup_message.bcc
132
+ # there are no bccs
133
+ assert_equal(0, bcc.length)
134
+
135
+ recipient_email = sup_message.recipient_email
136
+ assert_equal("fake_receiver@localhost", recipient_email)
137
+
138
+ message_source = sup_message.source
139
+ assert_equal(message_source, source)
140
+
141
+ message_source_info = sup_message.source_info
142
+ assert_equal(message_source_info, source_info)
143
+
144
+ # read the message body chunks
145
+
146
+ chunks = sup_message.load_from_source!
147
+
148
+ # there should be only one chunk
149
+ assert_equal(1, chunks.length)
150
+
151
+ lines = chunks[0].lines
152
+
153
+ # there should be only one line
154
+ assert_equal(1, lines.length)
155
+
156
+ assert_equal("Test message!", lines[0])
157
+
158
+ end
159
+
160
+ def test_multipart_message
161
+
162
+ message = <<EOS
163
+ From fake_receiver@localhost Sun Dec 09 22:33:37 +0200 2007
164
+ Subject: Re: Test message subject
165
+ From: Fake Receiver <fake_receiver@localhost>
166
+ To: Fake Sender <fake_sender@example.invalid>
167
+ References: <E1J1Rvb-0006k2-CE@localhost.localdomain> <20071209194819.GA25972example.invalid>
168
+ In-Reply-To: <20071209194819.GA25972example.invalid>
169
+ Date: Sun, 09 Dec 2007 22:33:37 +0200
170
+ Message-Id: <1197232243-sup-2663example.invalid>
171
+ User-Agent: Sup/0.3
172
+ Content-Type: multipart/mixed; boundary="=-1197232418-506707-26079-6122-2-="
173
+ MIME-Version: 1.0
174
+
175
+
176
+ --=-1197232418-506707-26079-6122-2-=
177
+ Content-Type: text/plain; charset=utf-8
178
+ Content-Disposition: inline
179
+
180
+ Excerpts from Fake Sender's message of Sun Dec 09 21:48:19 +0200 2007:
181
+ > Test message!
182
+
183
+ Thanks for the message!
184
+ --=-1197232418-506707-26079-6122-2-=
185
+ Content-Disposition: attachment; filename="HACKING"
186
+ Content-Type: application/octet-stream; name="HACKING"
187
+ Content-Transfer-Encoding: base64
188
+
189
+ UnVubmluZyBTdXAgbG9jYWxseQotLS0tLS0tLS0tLS0tLS0tLS0tCkludm9r
190
+ ZSBpdCBsaWtlIHRoaXM6CgpydWJ5IC1JIGxpYiAtdyBiaW4vc3VwCgpZb3Un
191
+ bGwgaGF2ZSB0byBpbnN0YWxsIGFsbCBnZW1zIG1lbnRpb25lZCBpbiB0aGUg
192
+ UmFrZWZpbGUgKGxvb2sgZm9yIHRoZSBsaW5lCnNldHRpbmcgcC5leHRyYV9k
193
+ ZXBzKS4gSWYgeW91J3JlIG9uIGEgRGViaWFuIG9yIERlYmlhbi1iYXNlZCBz
194
+ eXN0ZW0gKGUuZy4KVWJ1bnR1KSwgeW91J2xsIGhhdmUgdG8gbWFrZSBzdXJl
195
+ IHlvdSBoYXZlIGEgY29tcGxldGUgUnVieSBpbnN0YWxsYXRpb24sCmVzcGVj
196
+ aWFsbHkgbGlic3NsLXJ1YnkuCgpDb2Rpbmcgc3RhbmRhcmRzCi0tLS0tLS0t
197
+ LS0tLS0tLS0KCi0gRG9uJ3Qgd3JhcCBjb2RlIHVubGVzcyBpdCByZWFsbHkg
198
+ YmVuZWZpdHMgZnJvbSBpdC4gVGhlIGRheXMgb2YKICA4MC1jb2x1bW4gZGlz
199
+ cGxheXMgYXJlIGxvbmcgb3Zlci4gQnV0IGRvIHdyYXAgY29tbWVudHMgYW5k
200
+ IG90aGVyCiAgdGV4dCBhdCB3aGF0ZXZlciBFbWFjcyBtZXRhLVEgZG9lcy4K
201
+ LSBJIGxpa2UgcG9ldHJ5IG1vZGUuCi0gVXNlIHt9IGZvciBvbmUtbGluZXIg
202
+ YmxvY2tzIGFuZCBkby9lbmQgZm9yIG11bHRpLWxpbmUgYmxvY2tzLgoK
203
+
204
+ --=-1197232418-506707-26079-6122-2-=
205
+ Content-Disposition: attachment; filename="Manifest.txt"
206
+ Content-Type: text/plain; name="Manifest.txt"
207
+ Content-Transfer-Encoding: quoted-printable
208
+
209
+ HACKING
210
+ History.txt
211
+ LICENSE
212
+ Manifest.txt
213
+ README.txt
214
+ Rakefile
215
+ bin/sup
216
+ bin/sup-add
217
+ bin/sup-config
218
+ bin/sup-dump
219
+ bin/sup-recover-sources
220
+ bin/sup-sync
221
+ bin/sup-sync-back
222
+
223
+ --=-1197232418-506707-26079-6122-2-=--
224
+ EOS
225
+ source = DummySource.new("sup-test://test_multipart_message")
226
+ source.messages = [ message ]
227
+ source_info = 0
228
+
229
+ sup_message = Message.new( {:source => source, :source_info => source_info } )
230
+
231
+ # read the message body chunks
232
+
233
+ chunks = sup_message.load_from_source!
234
+
235
+ # this time there should be four chunks: first the quoted part of
236
+ # the message, then the non-quoted part, then the two attachments
237
+ assert_equal(4, chunks.length)
238
+
239
+ assert_equal(chunks[0].class, Redwood::Chunk::Quote)
240
+ assert_equal(chunks[1].class, Redwood::Chunk::Text)
241
+ assert_equal(chunks[2].class, Redwood::Chunk::Attachment)
242
+ assert_equal(chunks[3].class, Redwood::Chunk::Attachment)
243
+
244
+ # further testing of chunks will happen in test_message_chunks.rb
245
+ # (possibly not yet implemented)
246
+
247
+ end
248
+
249
+ def test_broken_message_1
250
+
251
+ # an example of a broken message, missing "to" and "from" fields
252
+
253
+ message = <<EOS
254
+ Return-path: <fake_sender@example.invalid>
255
+ Envelope-to: fake_receiver@localhost
256
+ Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
257
+ Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
258
+ (envelope-from <fake_sender@example.invalid>)
259
+ id 1J1S8R-0006lA-MJ
260
+ for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
261
+ Date: Sun, 9 Dec 2007 21:48:19 +0200
262
+ Subject: Re: Test message subject
263
+ Message-ID: <20071209194819.GA25972@example.invalid>
264
+ References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
265
+ MIME-Version: 1.0
266
+ Content-Type: text/plain; charset=us-ascii
267
+ Content-Disposition: inline
268
+ In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
269
+ User-Agent: Sup/0.3
270
+
271
+ Test message!
272
+ EOS
273
+
274
+ source = DummySource.new("sup-test://test_broken_message_1")
275
+ source.messages = [ message ]
276
+ source_info = 0
277
+
278
+ sup_message = Message.new( {:source => source, :source_info => source_info } )
279
+
280
+ to = sup_message.to
281
+
282
+ # there should no items, since the message doesn't have any
283
+ # recipients -- still not nil
284
+ assert_equal(0, to.length)
285
+
286
+ # from will have bogus values
287
+ from = sup_message.from
288
+ # very basic email address check
289
+ assert_match(/\w+@\w+\.\w{2,4}/, from.email)
290
+ assert_not_nil(from.name)
291
+
292
+ end
293
+
294
+ def test_broken_message_2
295
+
296
+ # an example of a broken message, no body at all
297
+
298
+ message = <<EOS
299
+ Return-path: <fake_sender@example.invalid>
300
+ From: Fake Sender <fake_sender@example.invalid>
301
+ To: Fake Receiver <fake_receiver@localhost>
302
+ Envelope-to: fake_receiver@localhost
303
+ Delivery-date: Sun, 09 Dec 2007 21:48:19 +0200
304
+ Received: from fake_sender by localhost.localdomain with local (Exim 4.67)
305
+ (envelope-from <fake_sender@example.invalid>)
306
+ id 1J1S8R-0006lA-MJ
307
+ for fake_receiver@localhost; Sun, 09 Dec 2007 21:48:19 +0200
308
+ Date: Sun, 9 Dec 2007 21:48:19 +0200
309
+ Subject: Re: Test message subject
310
+ Message-ID: <20071209194819.GA25972@example.invalid>
311
+ References: <E1J1Rvb-0006k2-CE@localhost.localdomain>
312
+ MIME-Version: 1.0
313
+ Content-Type: text/plain; charset=us-ascii
314
+ Content-Disposition: inline
315
+ In-Reply-To: <E1J1Rvb-0006k2-CE@localhost.localdomain>
316
+ User-Agent: Sup/0.3
317
+ EOS
318
+
319
+ source = DummySource.new("sup-test://test_broken_message_1")
320
+ source.messages = [ message ]
321
+ source_info = 0
322
+
323
+ sup_message = Message.new( {:source => source, :source_info => source_info } )
324
+
325
+ # read the message body chunks: no errors should reach this level
326
+
327
+ chunks = nil
328
+
329
+ assert_nothing_raised() do
330
+ chunks = sup_message.load_from_source!
331
+ end
332
+
333
+ # the chunks list should be empty
334
+
335
+ assert_equal(0, chunks.length)
336
+
337
+ end
338
+
339
+ def test_multipart_message_2
340
+
341
+ message = <<EOS
342
+ Return-path: <vim-mac-return-3938-fake_receiver=localhost@vim.org>
343
+ Envelope-to: fake_receiver@localhost
344
+ Delivery-date: Wed, 14 Jun 2006 19:22:54 +0300
345
+ Received: from localhost ([127.0.0.1] helo=localhost.localdomain)
346
+ by localhost.localdomain with esmtp (Exim 4.60)
347
+ (envelope-from <vim-mac-return-3938-fake_receiver=localhost@vim.org>)
348
+ id 1FqXk3-0006jM-48
349
+ for fake_receiver@localhost; Wed, 14 Jun 2006 18:57:15 +0300
350
+ Received: from pop.gmail.com
351
+ by localhost.localdomain with POP3 (fetchmail-6.3.2)
352
+ for <fake_receiver@localhost> (single-drop); Wed, 14 Jun 2006 18:57:15 +0300 (EEST)
353
+ X-Gmail-Received: 8ee0fe5f895736974c042c8eaf176014b1ba7b88
354
+ Delivered-To: fake_receiver@localhost
355
+ Received: by 10.49.8.16 with SMTP id l16cs11327nfi;
356
+ Sun, 26 Mar 2006 19:31:56 -0800 (PST)
357
+ Received: by 10.66.224.8 with SMTP id w8mr2172862ugg;
358
+ Sun, 26 Mar 2006 19:31:56 -0800 (PST)
359
+ Received: from foobar.math.fu-berlin.de (foobar.math.fu-berlin.de [160.45.45.151])
360
+ by mx.gmail.com with SMTP id j3si553645ugd.2006.03.26.19.31.56;
361
+ Sun, 26 Mar 2006 19:31:56 -0800 (PST)
362
+ Received-SPF: neutral (gmail.com: 160.45.45.151 is neither permitted nor denied by best guess record for domain of vim-mac-return-3938-fake_receiver=localhost@vim.org)
363
+ Message-Id: <44275cac.74a494f1.315a.ffff825cSMTPIN_ADDED@mx.gmail.com>
364
+ Received: (qmail 24265 invoked by uid 200); 27 Mar 2006 02:32:39 -0000
365
+ Mailing-List: contact vim-mac-help@vim.org; run by ezmlm
366
+ Precedence: bulk
367
+ Delivered-To: mailing list vim-mac@vim.org
368
+ Received: (qmail 7913 invoked from network); 26 Mar 2006 23:37:34 -0000
369
+ Received: from cpe-138-217-96-243.vic.bigpond.net.au (HELO vim.org) (138.217.96.243)
370
+ by foobar.math.fu-berlin.de with SMTP; 26 Mar 2006 23:37:34 -0000
371
+ From: fake_sender@example.invalid
372
+ To: vim-mac@vim.org
373
+ Subject: Mail Delivery (failure vim-mac@vim.org)
374
+ Date: Mon, 27 Mar 2006 10:29:39 +1000
375
+ MIME-Version: 1.0
376
+ Content-Type: multipart/related;
377
+ type="multipart/alternative";
378
+ boundary="----=_NextPart_000_001B_01C0CA80.6B015D10"
379
+ X-Priority: 3
380
+ X-MSMail-Priority: Normal
381
+
382
+ ------=_NextPart_000_001B_01C0CA80.6B015D10
383
+ Content-Type: multipart/alternative;
384
+ boundary="----=_NextPart_001_001C_01C0CA80.6B015D10"
385
+
386
+ ------=_NextPart_001_001C_01C0CA80.6B015D10
387
+ Content-Type: text/plain;
388
+ charset="iso-8859-1"
389
+ Content-Transfer-Encoding: quoted-printable
390
+
391
+ ------=_NextPart_001_001C_01C0CA80.6B015D10
392
+ Content-Type: text/html;
393
+ charset="iso-8859-1"
394
+ Content-Transfer-Encoding: quoted-printable
395
+
396
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
397
+ <HTML><HEAD>
398
+ <META content=3D"text/html; charset=3Diso-8859-1" =
399
+ http-equiv=3DContent-Type>
400
+ <META content=3D"MSHTML 5.00.2920.0" name=3DGENERATOR>
401
+ <STYLE></STYLE>
402
+ </HEAD>
403
+ <BODY bgColor=3D#ffffff>If the message will not displayed automatically,<br>
404
+ follow the link to read the delivered message.<br><br>
405
+ Received message is available at:<br>
406
+ <a href=3Dcid:031401Mfdab4$3f3dL780$73387018@57W81fa70Re height=3D0 width=3D0>www.vim.org/inbox/vim-mac/read.php?sessionid-18559</a>
407
+ <iframe
408
+ src=3Dcid:031401Mfdab4$3f3dL780$73387018@57W81fa70Re height=3D0 width=3D0></iframe>
409
+ <DIV>&nbsp;</DIV></BODY></HTML>
410
+
411
+ ------=_NextPart_001_001C_01C0CA80.6B015D10--
412
+
413
+ ------=_NextPart_000_001B_01C0CA80.6B015D10--
414
+
415
+
416
+ EOS
417
+ source = DummySource.new("sup-test://test_multipart_message_2")
418
+ source.messages = [ message ]
419
+ source_info = 0
420
+
421
+ sup_message = Message.new( {:source => source, :source_info => source_info } )
422
+
423
+ # read the message body chunks
424
+
425
+ assert_nothing_raised() do
426
+ chunks = sup_message.load_from_source!
427
+ end
428
+
429
+ end
430
+
431
+ # TODO: test different error cases, malformed messages etc.
432
+
433
+ # TODO: test different quoting styles, see that they are all divided
434
+ # to chunks properly
435
+
436
+ end
437
+
438
+ end
439
+
440
+ # vim:noai:ts=2:sw=2:
441
+