sup 0.5 → 0.6

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/lib/sup/message.rb CHANGED
@@ -37,7 +37,7 @@ class Message
37
37
  DEFAULT_SENDER = "(missing sender)"
38
38
 
39
39
  attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
40
- :cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
40
+ :cc, :bcc, :labels, :attachments, :list_address, :recipient_email, :replyto,
41
41
  :source_info, :list_subscribe, :list_unsubscribe
42
42
 
43
43
  bool_reader :dirty, :source_marked_read, :snippet_contains_encrypted_content
@@ -54,6 +54,7 @@ class Message
54
54
  @dirty = false
55
55
  @encrypted = false
56
56
  @chunks = nil
57
+ @attachments = []
57
58
 
58
59
  ## we need to initialize this. see comments in parse_header as to
59
60
  ## why.
@@ -148,7 +149,18 @@ class Message
148
149
  @source.fn_for_offset @source_info
149
150
  end
150
151
 
151
- def sanitize_message_id mid; mid.gsub(/\s+/, "")[0..254] end
152
+ ## sanitize message ids by removing spaces and non-ascii characters.
153
+ ## also, truncate to 255 characters. all these steps are necessary
154
+ ## to make ferret happy. of course, we probably fuck up a couple
155
+ ## valid message ids as well. as long as we're consistent, this
156
+ ## should be fine, though.
157
+ ##
158
+ ## also, mostly the message ids that are changed by this belong to
159
+ ## spam email.
160
+ ##
161
+ ## an alternative would be to SHA1 or MD5 all message ids on a regular basis.
162
+ ## don't tempt me.
163
+ def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
152
164
 
153
165
  def save index
154
166
  return unless @dirty
@@ -405,6 +417,11 @@ private
405
417
 
406
418
  ## if there's a filename, we'll treat it as an attachment.
407
419
  if filename
420
+ # add this to the attachments list if its not a generated html
421
+ # attachment (should we allow images with generated names?).
422
+ # Lowercase the filename because searches are easier that way
423
+ @attachments.push filename.downcase unless filename =~ /^sup-attachment-/
424
+ add_label :attachment unless filename =~ /^sup-attachment-/
408
425
  [Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
409
426
 
410
427
  ## otherwise, it's body text
@@ -423,7 +440,7 @@ private
423
440
  Iconv.iconv($encoding + "//IGNORE", charset, body + " ").join[0 .. -2]
424
441
  rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence, MessageFormatError => e
425
442
  Redwood::log "warning: error (#{e.class.name}) decoding message body from #{charset}: #{e.message}"
426
- File.open("sup-unable-to-decode.txt", "w") { |f| f.write body }
443
+ File.open(File.join(BASE_DIR,"unable-to-decode.txt"), "w") { |f| f.write body }
427
444
  body
428
445
  end
429
446
  end
@@ -9,6 +9,8 @@ class ComposeMode < EditMessageMode
9
9
  header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
10
10
  header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
11
11
  header["Subject"] = opts[:subj] if opts[:subj]
12
+ header["References"] = opts[:refs].map { |r| "<#{r}>" }.join(" ") if opts[:refs]
13
+ header["In-Reply-To"] = opts[:replytos].map { |r| "<#{r}>" }.join(" ") if opts[:replytos]
12
14
 
13
15
  super :header => header, :body => (opts[:body] || [])
14
16
  end
@@ -23,7 +23,7 @@ Variables:
23
23
  from_email: the email part of the From: line, or nil if empty
24
24
  Return value:
25
25
  A string (multi-line ok) containing the text of the signature, or nil to
26
- use the default signature.
26
+ use the default signature, or :none for no signature.
27
27
  EOS
28
28
 
29
29
  HookManager.register "before-edit", <<EOS
@@ -122,7 +122,7 @@ EOS
122
122
  @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
123
123
  @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
124
124
  @file.puts
125
- @file.puts @body
125
+ @file.puts @body.join("\n")
126
126
  @file.close
127
127
 
128
128
  editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
@@ -213,7 +213,7 @@ protected
213
213
  def parse_file fn
214
214
  File.open(fn) do |f|
215
215
  header = MBox::read_header f
216
- body = f.readlines
216
+ body = f.readlines.map { |l| l.chomp }
217
217
 
218
218
  header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
219
219
  header.each { |k, v| header[k] = parse_header k, v }
@@ -304,9 +304,10 @@ protected
304
304
  def build_message date
305
305
  m = RMail::Message.new
306
306
  m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
307
- m.body = @body.join
308
- m.body = m.body
307
+ m.body = @body.join("\n")
309
308
  m.body += sig_lines.join("\n") unless $config[:edit_signature]
309
+ ## body must end in a newline or GPG signatures will be WRONG!
310
+ m.body += "\n" unless m.body =~ /\n\Z/
310
311
 
311
312
  ## there are attachments, so wrap body in an attachment of its own
312
313
  unless @attachments.empty?
@@ -321,7 +322,7 @@ protected
321
322
  ## do whatever crypto transformation is necessary
322
323
  if @crypto_selector && @crypto_selector.val != :none
323
324
  from_email = PersonManager.person_for(@header["From"]).email
324
- to_email = (@header["To"] + @header["Cc"] + @header["Bcc"]).map { |p| PersonManager.person_for(p).email }
325
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
325
326
 
326
327
  m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
327
328
  end
@@ -364,7 +365,7 @@ EOS
364
365
  end
365
366
 
366
367
  f.puts
367
- f.puts sanitize_body(@body.join)
368
+ f.puts sanitize_body(@body.join("\n"))
368
369
  f.puts sig_lines if full unless $config[:edit_signature]
369
370
  end
370
371
 
@@ -377,12 +378,11 @@ protected
377
378
  if text
378
379
  @header[field] = parse_header field, text
379
380
  update
380
- field
381
381
  end
382
382
  else
383
- default =
384
- case field
383
+ default = case field
385
384
  when *MULTI_HEADERS
385
+ @header[field] ||= []
386
386
  @header[field].join(", ")
387
387
  else
388
388
  @header[field]
@@ -393,7 +393,6 @@ protected
393
393
  text = contacts.map { |s| s.longname }.join(", ")
394
394
  @header[field] = parse_header field, text
395
395
  update
396
- field
397
396
  end
398
397
  end
399
398
  end
@@ -409,7 +408,7 @@ private
409
408
  end
410
409
 
411
410
  def top_posting?
412
- @body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
411
+ @body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
413
412
  end
414
413
 
415
414
  def sig_lines
@@ -418,6 +417,8 @@ private
418
417
 
419
418
  ## first run the hook
420
419
  hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
420
+
421
+ return [] if hook_sig == :none
421
422
  return ["", "-- "] + hook_sig.split("\n") if hook_sig
422
423
 
423
424
  ## no hook, do default signature generation based on config.yaml
@@ -47,7 +47,7 @@ protected
47
47
  return unless f && f.file?
48
48
 
49
49
  begin
50
- BufferManager.spawn f.to_s, TextMode.new(f.readlines.join)
50
+ BufferManager.spawn f.to_s, TextMode.new(f.read)
51
51
  rescue SystemCallError => e
52
52
  BufferManager.flash e.message
53
53
  end
@@ -42,7 +42,7 @@ class ForwardMode < EditMessageMode
42
42
  end
43
43
 
44
44
  attachments.each do |c|
45
- mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"]
45
+ mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"].first
46
46
  attachment_hash[c.filename] = RMail::Message.make_attachment c.raw_content, mime_type.content_type, mime_type.encoding, c.filename
47
47
  end
48
48
 
@@ -6,6 +6,7 @@ class InboxMode < ThreadIndexMode
6
6
  register_keymap do |k|
7
7
  ## overwrite toggle_archived with archive
8
8
  k.add :archive, "Archive thread (remove from inbox)", 'a'
9
+ k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
9
10
  end
10
11
 
11
12
  def initialize
@@ -38,6 +39,23 @@ class InboxMode < ThreadIndexMode
38
39
  regen_text
39
40
  end
40
41
 
42
+ def read_and_archive
43
+ return unless cursor_thread
44
+ cursor_thread.remove_label :unread
45
+ cursor_thread.remove_label :inbox
46
+ hide_thread cursor_thread
47
+ regen_text
48
+ end
49
+
50
+ def multi_read_and_archive threads
51
+ threads.each do |t|
52
+ t.remove_label :unread
53
+ t.remove_label :inbox
54
+ hide_thread t
55
+ end
56
+ regen_text
57
+ end
58
+
41
59
  def handle_unarchived_update sender, m
42
60
  add_or_unhide m
43
61
  end
@@ -29,6 +29,11 @@ class LabelListMode < LineCursorMode
29
29
  BufferManager.flash "No labels messages with unread messages."
30
30
  end
31
31
  end
32
+
33
+ def focus
34
+ reload # make sure unread message counts are up-to-date
35
+ end
36
+
32
37
  protected
33
38
 
34
39
  def toggle_show_unread_only
@@ -19,6 +19,27 @@ Return value:
19
19
  A string containing the text of the quote line (can be multi-line)
20
20
  EOS
21
21
 
22
+ HookManager.register "reply-from", <<EOS
23
+ Selects a default address for the From: header of a new reply.
24
+ Variables:
25
+ message: a message object representing the message being replied to
26
+ (useful values include message.recipient_email, message.to, and message.cc)
27
+ Return value:
28
+ A Person to be used as the default for the From: header, or nil to use the
29
+ default behavior.
30
+ EOS
31
+
32
+ HookManager.register "reply-to", <<EOS
33
+ Set the default reply-to mode.
34
+ Variables:
35
+ modes: array of valid modes to choose from, which will be a subset of
36
+ [:#{REPLY_TYPES * ', :'}]
37
+ The default behavior is equivalent to
38
+ ([:list, :sender, :recipent] & modes)[0]
39
+ Return value:
40
+ The reply mode you desire, or nil to use the default behavior.
41
+ EOS
42
+
22
43
  def initialize message
23
44
  @m = message
24
45
 
@@ -29,8 +50,19 @@ EOS
29
50
 
30
51
  ## first, determine the address at which we received this email. this will
31
52
  ## become our From: address in the reply.
53
+ hook_reply_from = HookManager.run "reply-from", :message => @m
54
+
55
+ ## sanity check that selection is a Person (or we'll fail below)
56
+ ## don't check that it's an Account, though; assume they know what they're doing.
57
+ if hook_reply_from && !(hook_reply_from.is_a? Person)
58
+ Redwood::log "reply-from returned non-Person, using default from."
59
+ hook_reply_from = nil
60
+ end
61
+
32
62
  from =
33
- if @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
63
+ if hook_reply_from
64
+ hook_reply_from
65
+ elsif @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
34
66
  PersonManager.person_for(@m.recipient_email)
35
67
  elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
36
68
  b
@@ -92,8 +124,12 @@ EOS
92
124
  types = REPLY_TYPES.select { |t| @headers.member?(t) }
93
125
  @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
94
126
 
127
+ hook_reply = HookManager.run "reply-to", :modes => types
128
+
95
129
  @type_selector.set_to(
96
- if @m.is_list_message?
130
+ if types.include? hook_reply
131
+ hook_reply
132
+ elsif @m.is_list_message?
97
133
  :list
98
134
  elsif @headers.member? :sender
99
135
  :sender
@@ -15,12 +15,14 @@ class ScrollMode < Mode
15
15
  COL_JUMP = 2
16
16
 
17
17
  register_keymap do |k|
18
- k.add :line_down, "Down one line", :down, 'j', 'J'
19
- k.add :line_up, "Up one line", :up, 'k', 'K'
18
+ k.add :line_down, "Down one line", :down, 'j', 'J', "\C-e"
19
+ k.add :line_up, "Up one line", :up, 'k', 'K', "\C-y"
20
20
  k.add :col_left, "Left one column", :left, 'h'
21
21
  k.add :col_right, "Right one column", :right, 'l'
22
- k.add :page_down, "Down one page", :page_down, ' '
23
- k.add :page_up, "Up one page", :page_up, 'p', :backspace
22
+ k.add :page_down, "Down one page", :page_down, ' ', "\C-f"
23
+ k.add :page_up, "Up one page", :page_up, 'p', :backspace, "\C-b"
24
+ k.add :half_page_down, "Down one half page", "\C-d"
25
+ k.add :half_page_up, "Up one half page", "\C-u"
24
26
  k.add :jump_to_start, "Jump to top", :home, '^', '1'
25
27
  k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
26
28
  k.add :jump_to_left, "Jump to the left", '['
@@ -85,15 +87,16 @@ class ScrollMode < Mode
85
87
  continue_search_in_buffer
86
88
  end
87
89
 
88
- ## subclasses can override these two!
90
+ ## subclasses can override these three!
89
91
  def search_goto_pos line, leftcol, rightcol
90
- jump_to_line line
92
+ search_goto_line line
91
93
 
92
94
  if rightcol > self.rightcol # if it's occluded...
93
95
  jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
94
96
  end
95
97
  end
96
98
  def search_start_line; @topline end
99
+ def search_goto_line line; jump_to_line line end
97
100
 
98
101
  def col_left
99
102
  return unless @leftcol > 0
@@ -130,6 +133,8 @@ class ScrollMode < Mode
130
133
  def line_up; jump_to_line @topline - 1; end
131
134
  def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
132
135
  def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
136
+ def half_page_down; jump_to_line @topline + buffer.content_height / 2; end
137
+ def half_page_up; jump_to_line @topline - buffer.content_height / 2; end
133
138
  def jump_to_start; jump_to_line 0; end
134
139
  def jump_to_end; jump_to_line lines - buffer.content_height; end
135
140
 
@@ -14,6 +14,12 @@ Variables:
14
14
  thread: The message thread to be formatted.
15
15
  EOS
16
16
 
17
+ HookManager.register "mark-as-spam", <<EOS
18
+ This hook is run when a thread is marked as spam
19
+ Variables:
20
+ thread: The message thread being marked as spam.
21
+ EOS
22
+
17
23
  register_keymap do |k|
18
24
  k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
19
25
  k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
@@ -333,6 +339,7 @@ EOS
333
339
  def toggle_spam
334
340
  t = cursor_thread or return
335
341
  multi_toggle_spam [t]
342
+ HookManager.run("mark-as-spam", :thread => t)
336
343
  end
337
344
 
338
345
  ## both spam and deleted have the curious characteristic that you
@@ -440,9 +447,8 @@ EOS
440
447
  end
441
448
 
442
449
  def multi_edit_labels threads
443
- answer = BufferManager.ask :add_labels, "add labels: "
444
- return unless answer
445
- user_labels = answer.split(/\s+/).map { |l| l.intern }
450
+ user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
451
+ return unless user_labels
446
452
 
447
453
  hl = user_labels.select { |l| @hidden_labels.member? l }
448
454
  if hl.empty?
@@ -713,7 +719,8 @@ protected
713
719
  from +
714
720
  [
715
721
  [subj_color, size_widget_text],
716
- [:to_me_color, dp ? " >" : (p ? ' +' : " ")],
722
+ [:to_me_color, t.labels.member?(:attachment) ? "@" : " "],
723
+ [:to_me_color, dp ? ">" : (p ? '+' : " ")],
717
724
  [subj_color, t.subj + (t.subj.empty? ? "" : " ")],
718
725
  ] +
719
726
  (t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
@@ -188,7 +188,7 @@ EOS
188
188
  def compose
189
189
  p = @person_lines[curpos]
190
190
  if p
191
- ComposeMode.spawn_nicely :to => [p]
191
+ ComposeMode.spawn_nicely :to_default => p
192
192
  else
193
193
  ComposeMode.spawn_nicely
194
194
  end
@@ -250,7 +250,7 @@ EOS
250
250
 
251
251
  def edit_as_new
252
252
  m = @message_lines[curpos] or return
253
- mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
253
+ mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
254
254
  BufferManager.spawn "edit as new", mode
255
255
  mode.edit_message
256
256
  end
@@ -562,29 +562,29 @@ private
562
562
 
563
563
  open_widget = [color, (state == :closed ? "+ " : "- ")]
564
564
  new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
565
- starred_widget =
566
- if m.has_label?(:starred)
567
- [star_color, "* "]
565
+ starred_widget = if m.has_label?(:starred)
566
+ [star_color, "*"]
568
567
  else
569
- [color, " "]
568
+ [color, " "]
570
569
  end
570
+ attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
571
571
 
572
572
  case state
573
573
  when :open
574
574
  @person_lines[start] = m.from
575
- [[prefix_widget, open_widget, new_widget, starred_widget,
575
+ [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
576
576
  [color,
577
577
  "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
578
578
 
579
579
  when :closed
580
580
  @person_lines[start] = m.from
581
- [[prefix_widget, open_widget, new_widget, starred_widget,
581
+ [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
582
582
  [color,
583
583
  "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
584
584
 
585
585
  when :detailed
586
586
  @person_lines[start] = m.from
587
- from_line = [[prefix_widget, open_widget, new_widget, starred_widget,
587
+ from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
588
588
  [color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
589
589
 
590
590
  addressee_lines = []
data/lib/sup/util.rb CHANGED
@@ -108,7 +108,9 @@ class Module
108
108
  def defer_all_other_method_calls_to obj
109
109
  class_eval %{
110
110
  def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
111
- def respond_to? meth; @#{obj}.respond_to?(meth); end
111
+ def respond_to?(m, include_private = false)
112
+ @#{obj}.respond_to?(m, include_private)
113
+ end
112
114
  }
113
115
  end
114
116
  end
@@ -527,7 +529,9 @@ class Recoverable
527
529
  def to_yaml x; __pass :to_yaml, x; end
528
530
  def is_a? c; @o.is_a? c; end
529
531
 
530
- def respond_to? m; @o.respond_to? m end
532
+ def respond_to?(m, include_private=false)
533
+ @o.respond_to?(m, include_private)
534
+ end
531
535
 
532
536
  def __pass m, *a, &b
533
537
  begin
data/lib/sup.rb CHANGED
@@ -6,6 +6,19 @@ require 'fileutils'
6
6
  require 'gettext'
7
7
  require 'curses'
8
8
 
9
+ ## the following magic enables wide characters when used with a ruby
10
+ ## ncurses.so that's been compiled against libncursesw. (note the w.) why
11
+ ## this works, i have no idea. much like pretty much every aspect of
12
+ ## dealing with curses. cargo cult programming at its best.
13
+
14
+ require 'dl/import'
15
+ module LibC
16
+ extend DL::Importable
17
+ dlload Config::CONFIG['arch'] =~ /darwin/ ? "libc.dylib" : "libc.so.6"
18
+ extern "void setlocale(int, const char *)"
19
+ end
20
+ LibC.setlocale(6, "") # LC_ALL == 6
21
+
9
22
  class Object
10
23
  ## this is for debugging purposes because i keep calling #id on the
11
24
  ## wrong object and i want it to throw an exception
@@ -33,10 +46,11 @@ class Module
33
46
  end
34
47
 
35
48
  module Redwood
36
- VERSION = "0.5"
49
+ VERSION = "0.6"
37
50
 
38
51
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
39
52
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
53
+ COLOR_FN = File.join(BASE_DIR, "colors.yaml")
40
54
  SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
41
55
  LABEL_FN = File.join(BASE_DIR, "labels.txt")
42
56
  PERSON_FN = File.join(BASE_DIR, "people.txt")
@@ -50,7 +64,18 @@ module Redwood
50
64
  YAML_DOMAIN = "masanjin.net"
51
65
  YAML_DATE = "2006-10-01"
52
66
 
53
- ## record exceptions thrown in threads nicely
67
+ ## record exceptions thrown in threads nicely
68
+ @exceptions = []
69
+ @exception_mutex = Mutex.new
70
+
71
+ attr_reader :exceptions
72
+ def record_exception e, name
73
+ @exception_mutex.synchronize do
74
+ @exceptions ||= []
75
+ @exceptions << [e, name]
76
+ end
77
+ end
78
+
54
79
  def reporting_thread name
55
80
  if $opts[:no_threads]
56
81
  yield
@@ -59,14 +84,13 @@ module Redwood
59
84
  begin
60
85
  yield
61
86
  rescue Exception => e
62
- $exceptions ||= []
63
- $exceptions << [e, name]
64
- raise
87
+ record_exception e, name
65
88
  end
66
89
  end
67
90
  end
68
91
  end
69
- module_function :reporting_thread
92
+
93
+ module_function :reporting_thread, :record_exception, :exceptions
70
94
 
71
95
  ## one-stop shop for yamliciousness
72
96
  def save_yaml_obj object, fn, safe=false
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'test/unit'
4
+ require 'sup'
5
+ require 'stringio'
6
+
7
+ include Redwood
8
+
9
+ class TestMessage < Test::Unit::TestCase
10
+ def setup
11
+ end
12
+
13
+ def teardown
14
+ end
15
+
16
+ def test_normal_headers
17
+ h = MBox.read_header StringIO.new(<<EOS)
18
+ From: Bob <bob@bob.com>
19
+ To: Sally <sally@sally.com>
20
+ EOS
21
+
22
+ assert_equal "Bob <bob@bob.com>", h["From"]
23
+ assert_equal "Sally <sally@sally.com>", h["To"]
24
+ assert_nil h["Message-Id"]
25
+ end
26
+
27
+ ## this is shitty behavior in retrospect, but it's built in now.
28
+ def test_message_id_stripping
29
+ h = MBox.read_header StringIO.new("Message-Id: <one@bob.com>\n")
30
+ assert_equal "one@bob.com", h["Message-Id"]
31
+
32
+ h = MBox.read_header StringIO.new("Message-Id: one@bob.com\n")
33
+ assert_equal "one@bob.com", h["Message-Id"]
34
+ end
35
+
36
+ def test_multiline
37
+ h = MBox.read_header StringIO.new(<<EOS)
38
+ From: Bob <bob@bob.com>
39
+ Subject: one two three
40
+ four five six
41
+ To: Sally <sally@sally.com>
42
+ References: seven
43
+ eight
44
+ Seven: Eight
45
+ EOS
46
+
47
+ assert_equal "one two three four five six", h["Subject"]
48
+ assert_equal "Sally <sally@sally.com>", h["To"]
49
+ assert_equal "seven eight", h["References"]
50
+ end
51
+
52
+ def test_ignore_spacing
53
+ variants = [
54
+ "Subject:one two three end\n",
55
+ "Subject: one two three end\n",
56
+ "Subject: one two three end \n",
57
+ ]
58
+ variants.each do |s|
59
+ h = MBox.read_header StringIO.new(s)
60
+ assert_equal "one two three end", h["Subject"]
61
+ end
62
+ end
63
+
64
+ def test_message_id_ignore_spacing
65
+ variants = [
66
+ "Message-Id: <one@bob.com> \n",
67
+ "Message-Id: one@bob.com \n",
68
+ "Message-Id:<one@bob.com> \n",
69
+ "Message-Id:one@bob.com \n",
70
+ ]
71
+ variants.each do |s|
72
+ h = MBox.read_header StringIO.new(s)
73
+ assert_equal "one@bob.com", h["Message-Id"]
74
+ end
75
+ end
76
+
77
+ def test_ignore_empty_lines
78
+ variants = [
79
+ "",
80
+ "Message-Id: \n",
81
+ "Message-Id:\n",
82
+ ]
83
+ variants.each do |s|
84
+ h = MBox.read_header StringIO.new(s)
85
+ assert_nil h["Message-Id"]
86
+ end
87
+ end
88
+
89
+ def test_detect_end_of_headers
90
+ h = MBox.read_header StringIO.new(<<EOS)
91
+ From: Bob <bob@bob.com>
92
+
93
+ To: a dear friend
94
+ EOS
95
+ assert_equal "Bob <bob@bob.com>", h["From"]
96
+ assert_nil h["To"]
97
+
98
+ h = MBox.read_header StringIO.new(<<EOS)
99
+ From: Bob <bob@bob.com>
100
+ \r
101
+ To: a dear friend
102
+ EOS
103
+ assert_equal "Bob <bob@bob.com>", h["From"]
104
+ assert_nil h["To"]
105
+
106
+ h = MBox.read_header StringIO.new(<<EOS)
107
+ From: Bob <bob@bob.com>
108
+ \r\n\r
109
+ To: a dear friend
110
+ EOS
111
+ assert_equal "Bob <bob@bob.com>", h["From"]
112
+ assert_nil h["To"]
113
+ end
114
+ end