tmail 1.1.1

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.
Files changed (102) hide show
  1. data/LICENSE +21 -0
  2. data/README +157 -0
  3. data/bat/changelog +19 -0
  4. data/bat/clobber/package +10 -0
  5. data/bat/compile +42 -0
  6. data/bat/config.yaml +8 -0
  7. data/bat/prepare +8 -0
  8. data/bat/publish +51 -0
  9. data/bat/rdoc +42 -0
  10. data/bat/release +12 -0
  11. data/bat/setup +1616 -0
  12. data/bat/stats +138 -0
  13. data/bat/tag +25 -0
  14. data/bat/test +25 -0
  15. data/ext/tmail/Makefile +25 -0
  16. data/ext/tmail/base64/MANIFEST +4 -0
  17. data/ext/tmail/base64/base64.c +264 -0
  18. data/ext/tmail/base64/depend +1 -0
  19. data/ext/tmail/base64/extconf.rb +38 -0
  20. data/ext/tmail/scanner_c/MANIFEST +4 -0
  21. data/ext/tmail/scanner_c/depend +1 -0
  22. data/ext/tmail/scanner_c/extconf.rb +38 -0
  23. data/ext/tmail/scanner_c/scanner_c.c +582 -0
  24. data/lib/tmail.rb +4 -0
  25. data/lib/tmail/Makefile +19 -0
  26. data/lib/tmail/address.rb +245 -0
  27. data/lib/tmail/attachments.rb +47 -0
  28. data/lib/tmail/base64.rb +75 -0
  29. data/lib/tmail/compat.rb +39 -0
  30. data/lib/tmail/config.rb +71 -0
  31. data/lib/tmail/core_extensions.rb +67 -0
  32. data/lib/tmail/encode.rb +524 -0
  33. data/lib/tmail/header.rb +931 -0
  34. data/lib/tmail/index.rb +8 -0
  35. data/lib/tmail/interface.rb +540 -0
  36. data/lib/tmail/loader.rb +1 -0
  37. data/lib/tmail/mail.rb +507 -0
  38. data/lib/tmail/mailbox.rb +435 -0
  39. data/lib/tmail/mbox.rb +1 -0
  40. data/lib/tmail/net.rb +282 -0
  41. data/lib/tmail/obsolete.rb +137 -0
  42. data/lib/tmail/parser.rb +1475 -0
  43. data/lib/tmail/parser.y +381 -0
  44. data/lib/tmail/port.rb +379 -0
  45. data/lib/tmail/quoting.rb +142 -0
  46. data/lib/tmail/require_arch.rb +56 -0
  47. data/lib/tmail/scanner.rb +44 -0
  48. data/lib/tmail/scanner_r.rb +263 -0
  49. data/lib/tmail/stringio.rb +279 -0
  50. data/lib/tmail/tmail.rb +1 -0
  51. data/lib/tmail/utils.rb +281 -0
  52. data/lib/tmail/version.rb +38 -0
  53. data/meta/icli.yaml +16 -0
  54. data/meta/tmail-1.1.1.roll +24 -0
  55. data/sample/data/multipart +23 -0
  56. data/sample/data/normal +29 -0
  57. data/sample/data/sendtest +5 -0
  58. data/sample/data/simple +14 -0
  59. data/sample/data/test +27 -0
  60. data/sample/extract-attachements.rb +33 -0
  61. data/sample/from-check.rb +26 -0
  62. data/sample/multipart.rb +26 -0
  63. data/sample/parse-bench.rb +68 -0
  64. data/sample/parse-test.rb +19 -0
  65. data/sample/sendmail.rb +94 -0
  66. data/test/extctrl.rb +6 -0
  67. data/test/fixtures/raw_base64_decoded_string +0 -0
  68. data/test/fixtures/raw_base64_email +83 -0
  69. data/test/fixtures/raw_base64_encoded_string +1 -0
  70. data/test/fixtures/raw_email +14 -0
  71. data/test/fixtures/raw_email10 +20 -0
  72. data/test/fixtures/raw_email11 +34 -0
  73. data/test/fixtures/raw_email12 +32 -0
  74. data/test/fixtures/raw_email13 +29 -0
  75. data/test/fixtures/raw_email2 +114 -0
  76. data/test/fixtures/raw_email3 +70 -0
  77. data/test/fixtures/raw_email4 +59 -0
  78. data/test/fixtures/raw_email5 +19 -0
  79. data/test/fixtures/raw_email6 +20 -0
  80. data/test/fixtures/raw_email7 +66 -0
  81. data/test/fixtures/raw_email8 +47 -0
  82. data/test/fixtures/raw_email9 +28 -0
  83. data/test/fixtures/raw_email_quoted_with_0d0a +14 -0
  84. data/test/fixtures/raw_email_simple +11 -0
  85. data/test/fixtures/raw_email_with_illegal_boundary +58 -0
  86. data/test/fixtures/raw_email_with_multipart_mixed_quoted_boundary +50 -0
  87. data/test/fixtures/raw_email_with_nested_attachment +100 -0
  88. data/test/fixtures/raw_email_with_partially_quoted_subject +14 -0
  89. data/test/fixtures/raw_email_with_quoted_illegal_boundary +58 -0
  90. data/test/kcode.rb +14 -0
  91. data/test/test_address.rb +1128 -0
  92. data/test/test_attachments.rb +35 -0
  93. data/test/test_base64.rb +63 -0
  94. data/test/test_encode.rb +77 -0
  95. data/test/test_header.rb +885 -0
  96. data/test/test_helper.rb +2 -0
  97. data/test/test_mail.rb +623 -0
  98. data/test/test_mbox.rb +126 -0
  99. data/test/test_port.rb +430 -0
  100. data/test/test_scanner.rb +209 -0
  101. data/test/test_utils.rb +37 -0
  102. metadata +205 -0
@@ -0,0 +1 @@
1
+ require 'tmail/mailbox'
@@ -0,0 +1,507 @@
1
+ =begin rdoc
2
+
3
+ = Mail class
4
+
5
+ =end
6
+ #--
7
+ # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining
10
+ # a copy of this software and associated documentation files (the
11
+ # "Software"), to deal in the Software without restriction, including
12
+ # without limitation the rights to use, copy, modify, merge, publish,
13
+ # distribute, sublicense, and/or sell copies of the Software, and to
14
+ # permit persons to whom the Software is furnished to do so, subject to
15
+ # the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+ #
28
+ # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29
+ # with permission of Minero Aoki.
30
+ #++
31
+ # == TMail::Mail
32
+ # === Class Methods
33
+ #
34
+ #
35
+ #
36
+ #
37
+ #
38
+ #
39
+ #
40
+ #
41
+ #
42
+ #
43
+ #
44
+ #
45
+ #
46
+ #
47
+
48
+
49
+ require 'tmail/interface'
50
+ require 'tmail/encode'
51
+ require 'tmail/header'
52
+ require 'tmail/port'
53
+ require 'tmail/config'
54
+ require 'tmail/utils'
55
+ require 'tmail/attachments'
56
+ require 'tmail/quoting'
57
+ require 'socket'
58
+
59
+ module TMail
60
+
61
+ class Mail
62
+
63
+ class << self
64
+ def load( fname )
65
+ new(FilePort.new(fname))
66
+ end
67
+
68
+ alias load_from load
69
+ alias loadfrom load
70
+
71
+ def parse( str )
72
+ new(StringPort.new(str))
73
+ end
74
+
75
+ end
76
+
77
+ def initialize( port = nil, conf = DEFAULT_CONFIG )
78
+ @port = port || StringPort.new
79
+ @config = Config.to_config(conf)
80
+
81
+ @header = {}
82
+ @body_port = nil
83
+ @body_parsed = false
84
+ @epilogue = ''
85
+ @parts = []
86
+
87
+ @port.ropen {|f|
88
+ parse_header f
89
+ parse_body f unless @port.reproducible?
90
+ }
91
+ end
92
+
93
+ attr_reader :port
94
+
95
+ def inspect
96
+ "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
97
+ end
98
+
99
+ #
100
+ # to_s interfaces
101
+ #
102
+
103
+ public
104
+
105
+ include StrategyInterface
106
+
107
+ def write_back( eol = "\n", charset = 'e' )
108
+ parse_body
109
+ @port.wopen {|stream| encoded eol, charset, stream }
110
+ end
111
+
112
+ def accept( strategy )
113
+ with_multipart_encoding(strategy) {
114
+ ordered_each do |name, field|
115
+ next if field.empty?
116
+ strategy.header_name canonical(name)
117
+ field.accept strategy
118
+ strategy.puts
119
+ end
120
+ strategy.puts
121
+ body_port().ropen {|r|
122
+ strategy.write r.read
123
+ }
124
+ }
125
+ end
126
+
127
+ private
128
+
129
+ def canonical( name )
130
+ name.split(/-/).map {|s| s.capitalize }.join('-')
131
+ end
132
+
133
+ def with_multipart_encoding( strategy )
134
+ if parts().empty? # DO NOT USE @parts
135
+ yield
136
+
137
+ else
138
+ bound = ::TMail.new_boundary
139
+ if @header.key? 'content-type'
140
+ @header['content-type'].params['boundary'] = bound
141
+ else
142
+ store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
143
+ end
144
+
145
+ yield
146
+
147
+ parts().each do |tm|
148
+ strategy.puts
149
+ strategy.puts '--' + bound
150
+ tm.accept strategy
151
+ end
152
+ strategy.puts
153
+ strategy.puts '--' + bound + '--'
154
+ strategy.write epilogue()
155
+ end
156
+ end
157
+
158
+ ###
159
+ ### header
160
+ ###
161
+
162
+ public
163
+
164
+ ALLOW_MULTIPLE = {
165
+ 'received' => true,
166
+ 'resent-date' => true,
167
+ 'resent-from' => true,
168
+ 'resent-sender' => true,
169
+ 'resent-to' => true,
170
+ 'resent-cc' => true,
171
+ 'resent-bcc' => true,
172
+ 'resent-message-id' => true,
173
+ 'comments' => true,
174
+ 'keywords' => true
175
+ }
176
+ USE_ARRAY = ALLOW_MULTIPLE
177
+
178
+ def header
179
+ @header.dup
180
+ end
181
+
182
+ # Returns a TMail::AddressHeader object of the field you are querying.
183
+ # Examples:
184
+ # @mail['from'] #=> #<TMail::AddressHeader "mikel@test.com.au">
185
+ # @mail['to'] #=> #<TMail::AddressHeader "mikel@test.com.au">
186
+ #
187
+ # You can get the string value of this by passing "to_s" to the query:
188
+ # Example:
189
+ # @mail['to'].to_s #=> "mikel@test.com.au"
190
+ def []( key )
191
+ @header[key.downcase]
192
+ end
193
+
194
+ def sub_header(key, param)
195
+ (hdr = self[key]) ? hdr[param] : nil
196
+ end
197
+
198
+ alias fetch []
199
+
200
+ # Allows you to set or delete TMail header objects at will.
201
+ # Eamples:
202
+ # @mail = TMail::Mail.new
203
+ # @mail['to'].to_s # => 'mikel@test.com.au'
204
+ # @mail['to'] = 'mikel@elsewhere.org'
205
+ # @mail['to'].to_s # => 'mikel@elsewhere.org'
206
+ # @mail.encoded # => "To: mikel@elsewhere.org\r\n\r\n"
207
+ # @mail['to'] = nil
208
+ # @mail['to'].to_s # => nil
209
+ # @mail.encoded # => "\r\n"
210
+ #
211
+ # Note: setting mail[] = nil actualy deletes the header field in question from the object,
212
+ # it does not just set the value of the hash to nil
213
+ def []=( key, val )
214
+ dkey = key.downcase
215
+
216
+ if val.nil?
217
+ @header.delete dkey
218
+ return nil
219
+ end
220
+
221
+ case val
222
+ when String
223
+ header = new_hf(key, val)
224
+ when HeaderField
225
+ ;
226
+ when Array
227
+ ALLOW_MULTIPLE.include? dkey or
228
+ raise ArgumentError, "#{key}: Header must not be multiple"
229
+ @header[dkey] = val
230
+ return val
231
+ else
232
+ header = new_hf(key, val.to_s)
233
+ end
234
+ if ALLOW_MULTIPLE.include? dkey
235
+ (@header[dkey] ||= []).push header
236
+ else
237
+ @header[dkey] = header
238
+ end
239
+
240
+ val
241
+ end
242
+
243
+ alias store []=
244
+
245
+ # Allows you to loop through each header in the TMail::Mail object in a block
246
+ # Example:
247
+ # @mail['to'] = 'mikel@elsewhere.org'
248
+ # @mail['from'] = 'me@me.com'
249
+ # @mail.each_header { |k,v| puts "#{k} = #{v}" }
250
+ # # => from = me@me.com
251
+ # # => to = mikel@elsewhere.org
252
+ def each_header
253
+ @header.each do |key, val|
254
+ [val].flatten.each {|v| yield key, v }
255
+ end
256
+ end
257
+
258
+ alias each_pair each_header
259
+
260
+ def each_header_name( &block )
261
+ @header.each_key(&block)
262
+ end
263
+
264
+ alias each_key each_header_name
265
+
266
+ def each_field( &block )
267
+ @header.values.flatten.each(&block)
268
+ end
269
+
270
+ alias each_value each_field
271
+
272
+ FIELD_ORDER = %w(
273
+ return-path received
274
+ resent-date resent-from resent-sender resent-to
275
+ resent-cc resent-bcc resent-message-id
276
+ date from sender reply-to to cc bcc
277
+ message-id in-reply-to references
278
+ subject comments keywords
279
+ mime-version content-type content-transfer-encoding
280
+ content-disposition content-description
281
+ )
282
+
283
+ def ordered_each
284
+ list = @header.keys
285
+ FIELD_ORDER.each do |name|
286
+ if list.delete(name)
287
+ [@header[name]].flatten.each {|v| yield name, v }
288
+ end
289
+ end
290
+ list.each do |name|
291
+ [@header[name]].flatten.each {|v| yield name, v }
292
+ end
293
+ end
294
+
295
+ def clear
296
+ @header.clear
297
+ end
298
+
299
+ def delete( key )
300
+ @header.delete key.downcase
301
+ end
302
+
303
+ def delete_if
304
+ @header.delete_if do |key,val|
305
+ if Array === val
306
+ val.delete_if {|v| yield key, v }
307
+ val.empty?
308
+ else
309
+ yield key, val
310
+ end
311
+ end
312
+ end
313
+
314
+ def keys
315
+ @header.keys
316
+ end
317
+
318
+ def key?( key )
319
+ @header.key? key.downcase
320
+ end
321
+
322
+ def values_at( *args )
323
+ args.map {|k| @header[k.downcase] }.flatten
324
+ end
325
+
326
+ alias indexes values_at
327
+ alias indices values_at
328
+
329
+ private
330
+
331
+ def parse_header( f )
332
+ name = field = nil
333
+ unixfrom = nil
334
+
335
+ while line = f.gets
336
+ case line
337
+ when /\A[ \t]/ # continue from prev line
338
+ raise SyntaxError, 'mail is began by space' unless field
339
+ field << ' ' << line.strip
340
+
341
+ when /\A([^\: \t]+):\s*/ # new header line
342
+ add_hf name, field if field
343
+ name = $1
344
+ field = $' #.strip
345
+
346
+ when /\A\-*\s*\z/ # end of header
347
+ add_hf name, field if field
348
+ name = field = nil
349
+ break
350
+
351
+ when /\AFrom (\S+)/
352
+ unixfrom = $1
353
+
354
+ when /^charset=.*/
355
+
356
+ else
357
+ raise SyntaxError, "wrong mail header: '#{line.inspect}'"
358
+ end
359
+ end
360
+ add_hf name, field if name
361
+
362
+ if unixfrom
363
+ add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
364
+ end
365
+ end
366
+
367
+ def add_hf( name, field )
368
+ key = name.downcase
369
+ field = new_hf(name, field)
370
+
371
+ if ALLOW_MULTIPLE.include? key
372
+ (@header[key] ||= []).push field
373
+ else
374
+ @header[key] = field
375
+ end
376
+ end
377
+
378
+ def new_hf( name, field )
379
+ HeaderField.new(name, field, @config)
380
+ end
381
+
382
+ ###
383
+ ### body
384
+ ###
385
+
386
+ public
387
+
388
+ def body_port
389
+ parse_body
390
+ @body_port
391
+ end
392
+
393
+ def each( &block )
394
+ body_port().ropen {|f| f.each(&block) }
395
+ end
396
+
397
+ def quoted_body
398
+ parse_body
399
+ @body_port.ropen {|f|
400
+ return f.read
401
+ }
402
+ end
403
+
404
+ def body=( str )
405
+ # Sets the body of the email to a new (encoded) string.
406
+ #
407
+ # We also reparses the email if the body is ever reassigned, this is a performance hit, however when
408
+ # you assign the body, you usually want to be able to make sure that you can access the attachments etc.
409
+ #
410
+ # Usage:
411
+ #
412
+ # mail.body = "Hello, this is\nthe body text"
413
+ # # => "Hello, this is\nthe body"
414
+ # mail.body
415
+ # # => "Hello, this is\nthe body"
416
+ @body_parsed = false
417
+ parse_body(StringInput.new(str))
418
+ parse_body
419
+ @body_port.wopen {|f| f.write str }
420
+ str
421
+ end
422
+
423
+ alias preamble body
424
+ alias preamble= body=
425
+
426
+ def epilogue
427
+ parse_body
428
+ @epilogue.dup
429
+ end
430
+
431
+ def epilogue=( str )
432
+ parse_body
433
+ @epilogue = str
434
+ str
435
+ end
436
+
437
+ def parts
438
+ parse_body
439
+ @parts
440
+ end
441
+
442
+ def each_part( &block )
443
+ parts().each(&block)
444
+ end
445
+
446
+ private
447
+
448
+ def parse_body( f = nil )
449
+ return if @body_parsed
450
+ if f
451
+ parse_body_0 f
452
+ else
453
+ @port.ropen {|f|
454
+ skip_header f
455
+ parse_body_0 f
456
+ }
457
+ end
458
+ @body_parsed = true
459
+ end
460
+
461
+ def skip_header( f )
462
+ while line = f.gets
463
+ return if /\A[\r\n]*\z/ === line
464
+ end
465
+ end
466
+
467
+ def parse_body_0( f )
468
+ if multipart?
469
+ read_multipart f
470
+ else
471
+ @body_port = @config.new_body_port(self)
472
+ @body_port.wopen {|w|
473
+ w.write f.read
474
+ }
475
+ end
476
+ end
477
+
478
+ def read_multipart( src )
479
+ bound = @header['content-type'].params['boundary']
480
+ is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
481
+ lastbound = "--#{bound}--"
482
+
483
+ ports = [ @config.new_preamble_port(self) ]
484
+ begin
485
+ f = ports.last.wopen
486
+ while line = src.gets
487
+ if is_sep === line
488
+ f.close
489
+ break if line.strip == lastbound
490
+ ports.push @config.new_part_port(self)
491
+ f = ports.last.wopen
492
+ else
493
+ f << line
494
+ end
495
+ end
496
+ @epilogue = (src.read || '')
497
+ ensure
498
+ f.close if f and not f.closed?
499
+ end
500
+
501
+ @body_port = ports.shift
502
+ @parts = ports.map {|p| self.class.new(p, @config) }
503
+ end
504
+
505
+ end # class Mail
506
+
507
+ end # module TMail