rumbster 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +515 -0
- data/README +56 -0
- data/Rakefile +12 -0
- data/lib/message_observers.rb +40 -0
- data/lib/rumbster.rb +24 -0
- data/lib/smtp_protocol.rb +42 -0
- data/lib/smtp_states.rb +159 -0
- data/test/message_observers_test.rb +102 -0
- data/test/rumbster_test.rb +69 -0
- data/test/smtp_protocol_test.rb +64 -0
- data/test/smtp_states_test.rb +217 -0
- data/vendor/tmail.rb +4 -0
- data/vendor/tmail/.cvsignore +3 -0
- data/vendor/tmail/Makefile +19 -0
- data/vendor/tmail/address.rb +222 -0
- data/vendor/tmail/base64.rb +52 -0
- data/vendor/tmail/compat.rb +39 -0
- data/vendor/tmail/config.rb +50 -0
- data/vendor/tmail/encode.rb +447 -0
- data/vendor/tmail/header.rb +895 -0
- data/vendor/tmail/info.rb +14 -0
- data/vendor/tmail/loader.rb +1 -0
- data/vendor/tmail/mail.rb +869 -0
- data/vendor/tmail/mailbox.rb +386 -0
- data/vendor/tmail/mbox.rb +1 -0
- data/vendor/tmail/net.rb +260 -0
- data/vendor/tmail/obsolete.rb +122 -0
- data/vendor/tmail/parser.rb +1475 -0
- data/vendor/tmail/parser.y +372 -0
- data/vendor/tmail/port.rb +356 -0
- data/vendor/tmail/scanner.rb +22 -0
- data/vendor/tmail/scanner_r.rb +243 -0
- data/vendor/tmail/stringio.rb +256 -0
- data/vendor/tmail/textutils.rb +197 -0
- data/vendor/tmail/tmail.rb +1 -0
- data/vendor/tmail/utils.rb +23 -0
- metadata +88 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
#
|
2
|
+
# info.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2004 Minero Aoki
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2.1.
|
9
|
+
#
|
10
|
+
|
11
|
+
module TMail
|
12
|
+
Version = '0.10.8'
|
13
|
+
Copyright = 'Copyright (c) 1998-2004 Minero Aoki'
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'tmail/mailbox'
|
@@ -0,0 +1,869 @@
|
|
1
|
+
#
|
2
|
+
# mail.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2004 Minero Aoki
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2.1.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'tmail/encode'
|
12
|
+
require 'tmail/header'
|
13
|
+
require 'tmail/port'
|
14
|
+
require 'tmail/config'
|
15
|
+
require 'tmail/textutils'
|
16
|
+
|
17
|
+
module TMail
|
18
|
+
|
19
|
+
class BadMessage < StandardError; end
|
20
|
+
|
21
|
+
|
22
|
+
class Mail
|
23
|
+
|
24
|
+
def Mail.load(fname)
|
25
|
+
new(FilePort.new(fname))
|
26
|
+
end
|
27
|
+
|
28
|
+
def Mail.parse(str)
|
29
|
+
new(StringPort.new(str))
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(port = nil, conf = DEFAULT_CONFIG)
|
33
|
+
@port = port || StringPort.new
|
34
|
+
@config = Config.to_config(conf)
|
35
|
+
|
36
|
+
@header = {}
|
37
|
+
@body_port = nil
|
38
|
+
@body_parsed = false
|
39
|
+
@epilogue = ''
|
40
|
+
@parts = []
|
41
|
+
|
42
|
+
@port.ropen {|f|
|
43
|
+
parse_header f
|
44
|
+
parse_body f unless @port.reproducible?
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :port
|
49
|
+
|
50
|
+
def inspect
|
51
|
+
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# to_s interfaces
|
56
|
+
#
|
57
|
+
|
58
|
+
public
|
59
|
+
|
60
|
+
include StrategyInterface
|
61
|
+
|
62
|
+
def write_back(eol = "\n", charset = 'e')
|
63
|
+
parse_body
|
64
|
+
@port.wopen {|stream|
|
65
|
+
encoded eol, charset, stream
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def accept(strategy)
|
70
|
+
with_multipart_encoding(strategy) {
|
71
|
+
ordered_each do |name, field|
|
72
|
+
next if field.empty?
|
73
|
+
strategy.header_name canonical(name)
|
74
|
+
field.accept strategy
|
75
|
+
strategy.puts
|
76
|
+
end
|
77
|
+
strategy.puts
|
78
|
+
body_port().ropen {|r|
|
79
|
+
strategy.write r.read
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def canonical(name)
|
87
|
+
name.split(/-/).map {|s| s.capitalize }.join('-')
|
88
|
+
end
|
89
|
+
|
90
|
+
def with_multipart_encoding(strategy)
|
91
|
+
if parts().empty? # DO NOT USE @parts
|
92
|
+
yield
|
93
|
+
|
94
|
+
else
|
95
|
+
bound = (type_param('boundary') || ::TMail.new_boundary)
|
96
|
+
if @header.key?('content-type')
|
97
|
+
@header['content-type'].params['boundary'] = bound
|
98
|
+
else
|
99
|
+
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
|
100
|
+
end
|
101
|
+
|
102
|
+
yield
|
103
|
+
|
104
|
+
parts().each do |m|
|
105
|
+
strategy.puts
|
106
|
+
strategy.puts '--' + bound
|
107
|
+
m.accept strategy
|
108
|
+
end
|
109
|
+
strategy.puts
|
110
|
+
strategy.puts '--' + bound + '--'
|
111
|
+
strategy.write epilogue()
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
###
|
116
|
+
### High level utilities
|
117
|
+
###
|
118
|
+
|
119
|
+
public
|
120
|
+
|
121
|
+
def friendly_from(default = nil)
|
122
|
+
h = @header['from']
|
123
|
+
a, = h.addrs
|
124
|
+
return default unless a
|
125
|
+
return a.phrase if a.phrase
|
126
|
+
return h.comments.join(' ') unless h.comments.empty?
|
127
|
+
a.spec
|
128
|
+
end
|
129
|
+
|
130
|
+
def from_address(default = nil)
|
131
|
+
from([]).first || default
|
132
|
+
end
|
133
|
+
|
134
|
+
def destinations(default = nil)
|
135
|
+
result = to([]) + cc([]) + bcc([])
|
136
|
+
return default if result.empty?
|
137
|
+
result
|
138
|
+
end
|
139
|
+
|
140
|
+
def each_destination(&block)
|
141
|
+
destinations([]).each(&block)
|
142
|
+
end
|
143
|
+
|
144
|
+
alias each_dest each_destination
|
145
|
+
|
146
|
+
def reply_addresses(default = nil)
|
147
|
+
reply_to_addrs(nil) or from_addrs(nil) or default
|
148
|
+
end
|
149
|
+
|
150
|
+
def error_reply_addresses(default = nil)
|
151
|
+
if s = sender(nil)
|
152
|
+
[s]
|
153
|
+
else
|
154
|
+
from_addrs(default)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def base64_encode
|
159
|
+
store 'Content-Transfer-Encoding', 'Base64'
|
160
|
+
self.body = Base64.folding_encode(self.body)
|
161
|
+
end
|
162
|
+
|
163
|
+
def base64_decode
|
164
|
+
if /base64/i =~ self.transfer_encoding('')
|
165
|
+
store 'Content-Transfer-Encoding', '8bit'
|
166
|
+
self.body = Base64.decode(self.body, @config.strict_base64decode?)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def multipart?
|
171
|
+
main_type('').downcase == 'multipart'
|
172
|
+
end
|
173
|
+
|
174
|
+
def create_reply
|
175
|
+
mail = TMail::Mail.new
|
176
|
+
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
|
177
|
+
mail.to_addrs = reply_addresses([])
|
178
|
+
mail.in_reply_to = [message_id(nil)].compact
|
179
|
+
mail.references = references([]) + [message_id(nil)].compact
|
180
|
+
mail.mime_version = '1.0'
|
181
|
+
mail
|
182
|
+
end
|
183
|
+
|
184
|
+
###
|
185
|
+
### Header access facades
|
186
|
+
###
|
187
|
+
|
188
|
+
include TextUtils
|
189
|
+
|
190
|
+
public
|
191
|
+
|
192
|
+
def header_string(name, default = nil)
|
193
|
+
h = @header[name.downcase] or return default
|
194
|
+
h.to_s
|
195
|
+
end
|
196
|
+
|
197
|
+
#
|
198
|
+
# date time
|
199
|
+
#
|
200
|
+
|
201
|
+
def date(default = nil)
|
202
|
+
h = @header['date'] or return default
|
203
|
+
h.date
|
204
|
+
end
|
205
|
+
|
206
|
+
def date=(time)
|
207
|
+
if time
|
208
|
+
store 'Date', time2str(time)
|
209
|
+
else
|
210
|
+
@header.delete 'date'
|
211
|
+
end
|
212
|
+
time
|
213
|
+
end
|
214
|
+
|
215
|
+
def strftime(fmt, default = nil)
|
216
|
+
t = date or return default
|
217
|
+
t.strftime(fmt)
|
218
|
+
end
|
219
|
+
|
220
|
+
#
|
221
|
+
# destination
|
222
|
+
#
|
223
|
+
|
224
|
+
def to_addrs(default = nil)
|
225
|
+
h = @header['to'] or return default
|
226
|
+
h.addrs
|
227
|
+
end
|
228
|
+
|
229
|
+
def cc_addrs(default = nil)
|
230
|
+
h = @header['cc'] or return default
|
231
|
+
h.addrs
|
232
|
+
end
|
233
|
+
|
234
|
+
def bcc_addrs(default = nil)
|
235
|
+
h = @header['bcc'] or return default
|
236
|
+
h.addrs
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_addrs=(arg)
|
240
|
+
set_addrfield 'to', arg
|
241
|
+
end
|
242
|
+
|
243
|
+
def cc_addrs=(arg)
|
244
|
+
set_addrfield 'cc', arg
|
245
|
+
end
|
246
|
+
|
247
|
+
def bcc_addrs=(arg)
|
248
|
+
set_addrfield 'bcc', arg
|
249
|
+
end
|
250
|
+
|
251
|
+
def to(default = nil)
|
252
|
+
addrs2specs(to_addrs(nil)) || default
|
253
|
+
end
|
254
|
+
|
255
|
+
def cc(default = nil)
|
256
|
+
addrs2specs(cc_addrs(nil)) || default
|
257
|
+
end
|
258
|
+
|
259
|
+
def bcc(default = nil)
|
260
|
+
addrs2specs(bcc_addrs(nil)) || default
|
261
|
+
end
|
262
|
+
|
263
|
+
def to=(*strs)
|
264
|
+
set_string_array_attr 'To', strs
|
265
|
+
end
|
266
|
+
|
267
|
+
def cc=(*strs)
|
268
|
+
set_string_array_attr 'Cc', strs
|
269
|
+
end
|
270
|
+
|
271
|
+
def bcc=(*strs)
|
272
|
+
set_string_array_attr 'Bcc', strs
|
273
|
+
end
|
274
|
+
|
275
|
+
#
|
276
|
+
# originator
|
277
|
+
#
|
278
|
+
|
279
|
+
def from_addrs(default = nil)
|
280
|
+
if h = @header['from']
|
281
|
+
h.addrs
|
282
|
+
else
|
283
|
+
default
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def from_addrs=(arg)
|
288
|
+
set_addrfield 'from', arg
|
289
|
+
end
|
290
|
+
|
291
|
+
def from(default = nil)
|
292
|
+
addrs2specs(from_addrs(nil)) || default
|
293
|
+
end
|
294
|
+
|
295
|
+
def from=(*strs)
|
296
|
+
set_string_array_attr 'From', strs
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
def reply_to_addrs(default = nil)
|
301
|
+
h = @header['reply-to'] or return default
|
302
|
+
h.addrs
|
303
|
+
end
|
304
|
+
|
305
|
+
def reply_to_addrs=(arg)
|
306
|
+
set_addrfield 'reply-to', arg
|
307
|
+
end
|
308
|
+
|
309
|
+
def reply_to(default = nil)
|
310
|
+
addrs2specs(reply_to_addrs(nil)) || default
|
311
|
+
end
|
312
|
+
|
313
|
+
def reply_to=(*strs)
|
314
|
+
set_string_array_attr 'Reply-To', strs
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
def sender_addr(default = nil)
|
319
|
+
f = @header['sender'] or return default
|
320
|
+
f.addr || default
|
321
|
+
end
|
322
|
+
|
323
|
+
def sender_addr=(addr)
|
324
|
+
if addr
|
325
|
+
h = HeaderField.internal_new('sender', @config)
|
326
|
+
h.addr = addr
|
327
|
+
@header['sender'] = h
|
328
|
+
else
|
329
|
+
@header.delete 'sender'
|
330
|
+
end
|
331
|
+
addr
|
332
|
+
end
|
333
|
+
|
334
|
+
def sender(default)
|
335
|
+
f = @header['sender'] or return default
|
336
|
+
a = f.addr or return default
|
337
|
+
a.spec
|
338
|
+
end
|
339
|
+
|
340
|
+
def sender=(str)
|
341
|
+
set_string_attr 'Sender', str
|
342
|
+
end
|
343
|
+
|
344
|
+
#
|
345
|
+
# subject
|
346
|
+
#
|
347
|
+
|
348
|
+
def subject(default = nil)
|
349
|
+
h = @header['subject'] or return default
|
350
|
+
h.body
|
351
|
+
end
|
352
|
+
|
353
|
+
def subject=(str)
|
354
|
+
set_string_attr 'Subject', str
|
355
|
+
end
|
356
|
+
|
357
|
+
#
|
358
|
+
# identity & threading
|
359
|
+
#
|
360
|
+
|
361
|
+
def message_id(default = nil)
|
362
|
+
h = @header['message-id'] or return default
|
363
|
+
h.id || default
|
364
|
+
end
|
365
|
+
|
366
|
+
def message_id=(str)
|
367
|
+
set_string_attr 'Message-Id', str
|
368
|
+
end
|
369
|
+
|
370
|
+
def in_reply_to(default = nil)
|
371
|
+
h = @header['in-reply-to'] or return default
|
372
|
+
h.ids
|
373
|
+
end
|
374
|
+
|
375
|
+
def in_reply_to=(*idstrs)
|
376
|
+
set_string_array_attr 'In-Reply-To', idstrs
|
377
|
+
end
|
378
|
+
|
379
|
+
def references(default = nil)
|
380
|
+
h = @header['references'] or return default
|
381
|
+
h.refs
|
382
|
+
end
|
383
|
+
|
384
|
+
def references=(*strs)
|
385
|
+
set_string_array_attr 'References', strs
|
386
|
+
end
|
387
|
+
|
388
|
+
#
|
389
|
+
# MIME headers
|
390
|
+
#
|
391
|
+
|
392
|
+
def mime_version(default = nil)
|
393
|
+
h = @header['mime-version'] or return default
|
394
|
+
h.version || default
|
395
|
+
end
|
396
|
+
|
397
|
+
def mime_version=(m, opt = nil)
|
398
|
+
if opt
|
399
|
+
if h = @header['mime-version']
|
400
|
+
h.major = m
|
401
|
+
h.minor = opt
|
402
|
+
else
|
403
|
+
store 'Mime-Version', "#{m}.#{opt}"
|
404
|
+
end
|
405
|
+
else
|
406
|
+
store 'Mime-Version', m
|
407
|
+
end
|
408
|
+
m
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
def content_type(default = nil)
|
413
|
+
h = @header['content-type'] or return default
|
414
|
+
h.content_type || default
|
415
|
+
end
|
416
|
+
|
417
|
+
def main_type(default = nil)
|
418
|
+
h = @header['content-type'] or return default
|
419
|
+
h.main_type || default
|
420
|
+
end
|
421
|
+
|
422
|
+
def sub_type(default = nil)
|
423
|
+
h = @header['content-type'] or return default
|
424
|
+
h.sub_type || default
|
425
|
+
end
|
426
|
+
|
427
|
+
def set_content_type(str, sub = nil, param = nil)
|
428
|
+
if sub
|
429
|
+
main, sub = str, sub
|
430
|
+
else
|
431
|
+
main, sub = str.split(%r</>, 2)
|
432
|
+
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
|
433
|
+
end
|
434
|
+
if h = @header['content-type']
|
435
|
+
h.main_type = main
|
436
|
+
h.sub_type = sub
|
437
|
+
h.params.clear
|
438
|
+
else
|
439
|
+
store 'Content-Type', "#{main}/#{sub}"
|
440
|
+
end
|
441
|
+
@header['content-type'].params.replace param if param
|
442
|
+
|
443
|
+
str
|
444
|
+
end
|
445
|
+
|
446
|
+
alias content_type= set_content_type
|
447
|
+
|
448
|
+
def type_param(name, default = nil)
|
449
|
+
h = @header['content-type'] or return default
|
450
|
+
h[name] || default
|
451
|
+
end
|
452
|
+
|
453
|
+
def charset(default = nil)
|
454
|
+
h = @header['content-type'] or return default
|
455
|
+
h['charset'] || default
|
456
|
+
end
|
457
|
+
|
458
|
+
def charset=(str)
|
459
|
+
if str
|
460
|
+
if h = @header[ 'content-type' ]
|
461
|
+
h['charset'] = str
|
462
|
+
else
|
463
|
+
store 'Content-Type', "text/plain; charset=#{str}"
|
464
|
+
end
|
465
|
+
end
|
466
|
+
str
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
def transfer_encoding(default = nil)
|
471
|
+
if h = @header['content-transfer-encoding']
|
472
|
+
h.encoding || default
|
473
|
+
else
|
474
|
+
default
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def transfer_encoding=(str)
|
479
|
+
set_string_attr 'Content-Transfer-Encoding', str
|
480
|
+
end
|
481
|
+
|
482
|
+
alias encoding transfer_encoding
|
483
|
+
alias encoding= transfer_encoding=
|
484
|
+
alias content_transfer_encoding transfer_encoding
|
485
|
+
alias content_transfer_encoding= transfer_encoding=
|
486
|
+
|
487
|
+
|
488
|
+
def disposition(default = nil)
|
489
|
+
if h = @header['content-disposition']
|
490
|
+
h.disposition || default
|
491
|
+
else
|
492
|
+
default
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
alias content_disposition disposition
|
497
|
+
|
498
|
+
def set_disposition(pos, params = nil)
|
499
|
+
@header.delete 'content-disposition'
|
500
|
+
return pos unless pos
|
501
|
+
store('Content-Disposition', pos)
|
502
|
+
@header['content-disposition'].params.replace params if params
|
503
|
+
pos
|
504
|
+
end
|
505
|
+
|
506
|
+
alias disposition= set_disposition
|
507
|
+
alias set_content_disposition set_disposition
|
508
|
+
alias content_disposition= set_disposition
|
509
|
+
|
510
|
+
def disposition_param(name, default = nil)
|
511
|
+
if h = @header['content-disposition']
|
512
|
+
h[name] || default
|
513
|
+
else
|
514
|
+
default
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
#
|
519
|
+
# sub routines
|
520
|
+
#
|
521
|
+
|
522
|
+
def set_string_array_attr(key, strs)
|
523
|
+
strs.flatten!
|
524
|
+
if strs.empty?
|
525
|
+
@header.delete key.downcase
|
526
|
+
else
|
527
|
+
store key, strs.join(', ')
|
528
|
+
end
|
529
|
+
strs
|
530
|
+
end
|
531
|
+
private :set_string_array_attr
|
532
|
+
|
533
|
+
def set_string_attr(key, str)
|
534
|
+
if str
|
535
|
+
store key, str
|
536
|
+
else
|
537
|
+
@header.delete key.downcase
|
538
|
+
end
|
539
|
+
str
|
540
|
+
end
|
541
|
+
private :set_string_attr
|
542
|
+
|
543
|
+
def set_addrfield(name, arg)
|
544
|
+
if arg
|
545
|
+
h = HeaderField.internal_new(name, @config)
|
546
|
+
h.addrs.replace [arg].flatten
|
547
|
+
@header[name] = h
|
548
|
+
else
|
549
|
+
@header.delete name
|
550
|
+
end
|
551
|
+
arg
|
552
|
+
end
|
553
|
+
private :set_addrfield
|
554
|
+
|
555
|
+
def addrs2specs(addrs)
|
556
|
+
return nil unless addrs
|
557
|
+
list = addrs.map {|addr|
|
558
|
+
if addr.address_group?
|
559
|
+
then addr.map {|a| a.spec }
|
560
|
+
else addr.spec
|
561
|
+
end
|
562
|
+
}.flatten
|
563
|
+
return nil if list.empty?
|
564
|
+
list
|
565
|
+
end
|
566
|
+
private :addrs2specs
|
567
|
+
|
568
|
+
###
|
569
|
+
### Direct Header Access
|
570
|
+
###
|
571
|
+
|
572
|
+
public
|
573
|
+
|
574
|
+
ALLOW_MULTIPLE = {
|
575
|
+
'received' => true,
|
576
|
+
'resent-date' => true,
|
577
|
+
'resent-from' => true,
|
578
|
+
'resent-sender' => true,
|
579
|
+
'resent-to' => true,
|
580
|
+
'resent-cc' => true,
|
581
|
+
'resent-bcc' => true,
|
582
|
+
'resent-message-id' => true,
|
583
|
+
'comments' => true,
|
584
|
+
'keywords' => true
|
585
|
+
}
|
586
|
+
USE_ARRAY = ALLOW_MULTIPLE
|
587
|
+
|
588
|
+
def header
|
589
|
+
@header.dup
|
590
|
+
end
|
591
|
+
|
592
|
+
def [](key)
|
593
|
+
@header[key.downcase]
|
594
|
+
end
|
595
|
+
|
596
|
+
alias fetch []
|
597
|
+
|
598
|
+
def []=(key, val)
|
599
|
+
dkey = key.downcase
|
600
|
+
if val.nil?
|
601
|
+
@header.delete dkey
|
602
|
+
return nil
|
603
|
+
end
|
604
|
+
case val
|
605
|
+
when String
|
606
|
+
header = new_hf(key, val)
|
607
|
+
when HeaderField
|
608
|
+
;
|
609
|
+
when Array
|
610
|
+
raise BadMessage, "multiple #{key}: header fields exist"\
|
611
|
+
unless ALLOW_MULTIPLE.include?(dkey)
|
612
|
+
@header[dkey] = val
|
613
|
+
return val
|
614
|
+
else
|
615
|
+
header = new_hf(key, val.to_s)
|
616
|
+
end
|
617
|
+
if ALLOW_MULTIPLE.include? dkey
|
618
|
+
(@header[dkey] ||= []).push header
|
619
|
+
else
|
620
|
+
@header[dkey] = header
|
621
|
+
end
|
622
|
+
|
623
|
+
val
|
624
|
+
end
|
625
|
+
|
626
|
+
alias store []=
|
627
|
+
|
628
|
+
def each_header
|
629
|
+
@header.each do |key, val|
|
630
|
+
[val].flatten.each {|v| yield key, v }
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
alias each_pair each_header
|
635
|
+
|
636
|
+
def each_header_name(&block)
|
637
|
+
@header.each_key(&block)
|
638
|
+
end
|
639
|
+
|
640
|
+
alias each_key each_header_name
|
641
|
+
|
642
|
+
def each_field(&block)
|
643
|
+
@header.values.flatten.each(&block)
|
644
|
+
end
|
645
|
+
|
646
|
+
alias each_value each_field
|
647
|
+
|
648
|
+
FIELD_ORDER = %w(
|
649
|
+
return-path received
|
650
|
+
resent-date resent-from resent-sender resent-to
|
651
|
+
resent-cc resent-bcc resent-message-id
|
652
|
+
date from sender reply-to to cc bcc
|
653
|
+
message-id in-reply-to references
|
654
|
+
subject comments keywords
|
655
|
+
mime-version content-type content-transfer-encoding
|
656
|
+
content-disposition content-description
|
657
|
+
)
|
658
|
+
|
659
|
+
def ordered_each
|
660
|
+
list = @header.keys
|
661
|
+
FIELD_ORDER.each do |name|
|
662
|
+
if list.delete(name)
|
663
|
+
[@header[name]].flatten.each {|v| yield name, v }
|
664
|
+
end
|
665
|
+
end
|
666
|
+
list.each do |name|
|
667
|
+
[@header[name]].flatten.each {|v| yield name, v }
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def clear
|
672
|
+
@header.clear
|
673
|
+
end
|
674
|
+
|
675
|
+
def delete(key)
|
676
|
+
@header.delete key.downcase
|
677
|
+
end
|
678
|
+
|
679
|
+
def delete_if
|
680
|
+
@header.delete_if {|key, val|
|
681
|
+
if val.is_a?(Array)
|
682
|
+
val.delete_if {|v| yield key, v }
|
683
|
+
val.empty?
|
684
|
+
else
|
685
|
+
yield key, val
|
686
|
+
end
|
687
|
+
}
|
688
|
+
end
|
689
|
+
|
690
|
+
def keys
|
691
|
+
@header.keys
|
692
|
+
end
|
693
|
+
|
694
|
+
def key?(key)
|
695
|
+
@header.key?(key.downcase)
|
696
|
+
end
|
697
|
+
|
698
|
+
def values_at(*args)
|
699
|
+
args.map {|k| @header[k.downcase] }.flatten
|
700
|
+
end
|
701
|
+
|
702
|
+
alias indexes values_at
|
703
|
+
alias indices values_at
|
704
|
+
|
705
|
+
private
|
706
|
+
|
707
|
+
def parse_header(f)
|
708
|
+
name = field = nil
|
709
|
+
unixfrom = nil
|
710
|
+
|
711
|
+
while line = f.gets
|
712
|
+
case line
|
713
|
+
when /\A[ \t]/ # continue from prev line
|
714
|
+
raise SyntaxError, 'mail is began by space' unless field
|
715
|
+
field << ' ' << line.strip
|
716
|
+
when /\A([^\: \t]+):\s*/ # new header line
|
717
|
+
add_hf name, field if field
|
718
|
+
name = $1
|
719
|
+
field = $' #.strip
|
720
|
+
when /\A\-*\s*\z/ # end of header
|
721
|
+
add_hf name, field if field
|
722
|
+
name = field = nil
|
723
|
+
break
|
724
|
+
when /\AFrom (\S+)/
|
725
|
+
unixfrom = $1
|
726
|
+
else
|
727
|
+
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
|
728
|
+
end
|
729
|
+
end
|
730
|
+
add_hf name, field if name
|
731
|
+
|
732
|
+
if unixfrom
|
733
|
+
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
def add_hf(name, field)
|
738
|
+
key = name.downcase
|
739
|
+
field = new_hf(name, field)
|
740
|
+
|
741
|
+
if ALLOW_MULTIPLE.include? key
|
742
|
+
(@header[key] ||= []).push field
|
743
|
+
else
|
744
|
+
@header[key] = field
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
def new_hf(name, field)
|
749
|
+
HeaderField.new(name, field, @config)
|
750
|
+
end
|
751
|
+
|
752
|
+
###
|
753
|
+
### Message Body
|
754
|
+
###
|
755
|
+
|
756
|
+
public
|
757
|
+
|
758
|
+
def body_port
|
759
|
+
parse_body
|
760
|
+
@body_port
|
761
|
+
end
|
762
|
+
|
763
|
+
def each(&block)
|
764
|
+
body_port().ropen {|f| f.each(&block) }
|
765
|
+
end
|
766
|
+
|
767
|
+
def body
|
768
|
+
parse_body
|
769
|
+
@body_port.ropen {|f|
|
770
|
+
return f.read
|
771
|
+
}
|
772
|
+
end
|
773
|
+
|
774
|
+
def body=(str)
|
775
|
+
parse_body
|
776
|
+
@body_port.wopen {|f| f.write str }
|
777
|
+
str
|
778
|
+
end
|
779
|
+
|
780
|
+
alias preamble body
|
781
|
+
alias preamble= body=
|
782
|
+
|
783
|
+
def epilogue
|
784
|
+
parse_body
|
785
|
+
@epilogue.dup
|
786
|
+
end
|
787
|
+
|
788
|
+
def epilogue=(str)
|
789
|
+
parse_body
|
790
|
+
@epilogue = str
|
791
|
+
str
|
792
|
+
end
|
793
|
+
|
794
|
+
def parts
|
795
|
+
parse_body
|
796
|
+
@parts
|
797
|
+
end
|
798
|
+
|
799
|
+
def each_part(&block)
|
800
|
+
parts().each(&block)
|
801
|
+
end
|
802
|
+
|
803
|
+
private
|
804
|
+
|
805
|
+
def parse_body(f = nil)
|
806
|
+
return if @body_parsed
|
807
|
+
if f
|
808
|
+
parse_body_0 f
|
809
|
+
else
|
810
|
+
@port.ropen {|f|
|
811
|
+
skip_header f
|
812
|
+
parse_body_0 f
|
813
|
+
}
|
814
|
+
end
|
815
|
+
@body_parsed = true
|
816
|
+
end
|
817
|
+
|
818
|
+
def skip_header(f)
|
819
|
+
while line = f.gets
|
820
|
+
return if /\A[\r\n]*\z/ =~ line
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
def parse_body_0(f)
|
825
|
+
if multipart?
|
826
|
+
read_multipart f
|
827
|
+
else
|
828
|
+
read_singlepart f
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
def read_singlepart(f)
|
833
|
+
@body_port = @config.new_body_port(self)
|
834
|
+
@body_port.wopen {|w|
|
835
|
+
w.write f.read
|
836
|
+
}
|
837
|
+
end
|
838
|
+
|
839
|
+
def read_multipart(src)
|
840
|
+
bound = type_param('boundary')
|
841
|
+
return read_singlepart(src) unless bound
|
842
|
+
is_sep = /\A--#{Regexp.quote(bound)}(?:--)?[ \t]*(?:\n|\r\n|\r)/
|
843
|
+
lastbound = "--#{bound}--"
|
844
|
+
|
845
|
+
ports = [ @config.new_preamble_port(self) ]
|
846
|
+
begin
|
847
|
+
f = ports.last.wopen
|
848
|
+
while line = src.gets
|
849
|
+
if is_sep =~ line
|
850
|
+
f.close
|
851
|
+
break if line.strip == lastbound
|
852
|
+
ports.push @config.new_part_port(self)
|
853
|
+
f = ports.last.wopen
|
854
|
+
else
|
855
|
+
f << line
|
856
|
+
end
|
857
|
+
end
|
858
|
+
@epilogue = (src.read || '')
|
859
|
+
ensure
|
860
|
+
f.close if f and not f.closed?
|
861
|
+
end
|
862
|
+
|
863
|
+
@body_port = ports.shift
|
864
|
+
@parts = ports.map {|p| self.class.new(p, @config) }
|
865
|
+
end
|
866
|
+
|
867
|
+
end # class Mail
|
868
|
+
|
869
|
+
end # module TMail
|