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.
- data/HACKING +31 -9
- data/History.txt +7 -0
- data/Manifest.txt +2 -0
- data/Rakefile +9 -5
- data/bin/sup +81 -57
- data/bin/sup-config +1 -1
- data/bin/sup-sync +3 -0
- data/bin/sup-tweak-labels +127 -0
- data/doc/TODO +23 -12
- data/lib/sup.rb +13 -11
- data/lib/sup/account.rb +25 -12
- data/lib/sup/buffer.rb +61 -41
- data/lib/sup/colormap.rb +2 -0
- data/lib/sup/contact.rb +28 -18
- data/lib/sup/crypto.rb +86 -31
- data/lib/sup/draft.rb +12 -6
- data/lib/sup/horizontal-selector.rb +47 -0
- data/lib/sup/imap.rb +50 -37
- data/lib/sup/index.rb +76 -13
- data/lib/sup/keymap.rb +27 -8
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox/loader.rb +1 -1
- data/lib/sup/message-chunks.rb +43 -15
- data/lib/sup/message.rb +67 -31
- data/lib/sup/mode.rb +40 -9
- data/lib/sup/modes/completion-mode.rb +1 -1
- data/lib/sup/modes/compose-mode.rb +3 -3
- data/lib/sup/modes/contact-list-mode.rb +12 -8
- data/lib/sup/modes/edit-message-mode.rb +100 -36
- data/lib/sup/modes/file-browser-mode.rb +1 -0
- data/lib/sup/modes/forward-mode.rb +43 -8
- data/lib/sup/modes/inbox-mode.rb +8 -5
- data/lib/sup/modes/label-search-results-mode.rb +12 -1
- data/lib/sup/modes/line-cursor-mode.rb +4 -7
- data/lib/sup/modes/reply-mode.rb +59 -54
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +4 -3
- data/lib/sup/modes/search-results-mode.rb +8 -5
- data/lib/sup/modes/text-mode.rb +19 -2
- data/lib/sup/modes/thread-index-mode.rb +109 -40
- data/lib/sup/modes/thread-view-mode.rb +180 -49
- data/lib/sup/person.rb +3 -3
- data/lib/sup/poll.rb +9 -8
- data/lib/sup/rfc2047.rb +7 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/tagger.rb +10 -4
- data/lib/sup/textfield.rb +7 -7
- data/lib/sup/thread.rb +86 -49
- data/lib/sup/update.rb +11 -0
- data/lib/sup/util.rb +74 -34
- data/test/test_message.rb +441 -0
- metadata +136 -117
data/lib/sup/textfield.rb
CHANGED
@@ -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
|
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
|
data/lib/sup/thread.rb
CHANGED
@@ -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.
|
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
|
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
|
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.
|
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
|
-
|
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
|
235
|
-
## messages from
|
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
|
256
|
-
def
|
257
|
-
|
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;
|
266
|
-
def size;
|
267
|
+
def threads; @threads.values end
|
268
|
+
def size; @threads.size end
|
267
269
|
|
268
|
-
|
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
|
-
#
|
282
|
+
#puts "*** linking parent #{p.id} and child #{c.id} would create a loop"
|
281
283
|
return
|
282
284
|
end
|
283
285
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
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
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/sup/update.rb
CHANGED
@@ -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
|
|
data/lib/sup/util.rb
CHANGED
@@ -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
|
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
|
-
|
72
|
-
a
|
73
|
-
a.header.add "Content-
|
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
|
76
|
+
case encoding
|
76
77
|
when "base64"
|
77
|
-
[
|
78
|
+
[payload].pack "m"
|
78
79
|
when "quoted-printable"
|
79
|
-
[
|
80
|
-
when "7bit", "8bit"
|
81
|
-
|
80
|
+
[payload].pack "M"
|
81
|
+
when "7bit", "8bit", nil
|
82
|
+
payload
|
82
83
|
else
|
83
|
-
raise EncodingUnsupportedError,
|
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> </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
|
+
|