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,142 @@
1
+ =begin rdoc
2
+
3
+ = Quoting methods
4
+
5
+ =end
6
+ module TMail
7
+ class Mail
8
+ def subject(to_charset = 'utf-8')
9
+ Unquoter.unquote_and_convert_to(quoted_subject, to_charset)
10
+ end
11
+
12
+ def unquoted_body(to_charset = 'utf-8')
13
+ from_charset = sub_header("content-type", "charset")
14
+ case (content_transfer_encoding || "7bit").downcase
15
+ when "quoted-printable"
16
+ # the default charset is set to iso-8859-1 instead of 'us-ascii'.
17
+ # This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
18
+ if !from_charset.blank? && from_charset.downcase == 'us-ascii'
19
+ from_charset = 'iso-8859-1'
20
+ end
21
+
22
+ Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
23
+ to_charset, from_charset, true)
24
+ when "base64"
25
+ Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
26
+ from_charset)
27
+ when "7bit", "8bit"
28
+ Unquoter.convert_to(quoted_body, to_charset, from_charset)
29
+ when "binary"
30
+ quoted_body
31
+ else
32
+ quoted_body
33
+ end
34
+ end
35
+
36
+ def body(to_charset = 'utf-8', &block)
37
+ attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" }
38
+
39
+ if multipart?
40
+ parts.collect { |part|
41
+ header = part["content-type"]
42
+
43
+ if part.multipart?
44
+ part.body(to_charset, &attachment_presenter)
45
+ elsif header.nil?
46
+ ""
47
+ elsif !attachment?(part)
48
+ part.unquoted_body(to_charset)
49
+ else
50
+ attachment_presenter.call(header["name"] || "(unnamed)")
51
+ end
52
+ }.join
53
+ else
54
+ unquoted_body(to_charset)
55
+ end
56
+ end
57
+ end
58
+
59
+ class Unquoter
60
+ class << self
61
+ def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
62
+ return "" if text.nil?
63
+ text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do
64
+ before = $1
65
+ from_charset = $2
66
+ quoting_method = $3
67
+ text = $4
68
+
69
+ before = convert_to(before, to_charset, from_charset) if before.length > 0
70
+ before + case quoting_method
71
+ when "q", "Q" then
72
+ unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
73
+ when "b", "B" then
74
+ unquote_base64_and_convert_to(text, to_charset, from_charset)
75
+ when nil then
76
+ # will be nil at the end of the string, due to the nature of
77
+ # the regex used.
78
+ ""
79
+ else
80
+ raise "unknown quoting method #{quoting_method.inspect}"
81
+ end
82
+ end
83
+ end
84
+
85
+ def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
86
+ text = text.gsub(/_/, " ") unless preserve_underscores
87
+ text = text.gsub(/\r\n|\r/, "\n") # normalize newlines
88
+ convert_to(text.unpack("M*").first, to, from)
89
+ end
90
+
91
+ def unquote_base64_and_convert_to(text, to, from)
92
+ convert_to(Base64.decode(text), to, from)
93
+ end
94
+
95
+ begin
96
+ require 'iconv'
97
+ def convert_to(text, to, from)
98
+ return text unless to && from
99
+ text ? Iconv.iconv(to, from, text).first : ""
100
+ rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
101
+ # the 'from' parameter specifies a charset other than what the text
102
+ # actually is...not much we can do in this case but just return the
103
+ # unconverted text.
104
+ #
105
+ # Ditto if either parameter represents an unknown charset, like
106
+ # X-UNKNOWN.
107
+ text
108
+ end
109
+ rescue LoadError
110
+ # Not providing quoting support
111
+ def convert_to(text, to, from)
112
+ warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})"
113
+ text
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ if __FILE__ == $0
121
+ require 'test/unit'
122
+
123
+ class TC_Unquoter < Test::Unit::TestCase
124
+ def test_unquote_quoted_printable
125
+ a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?="
126
+ b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
127
+ assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
128
+ end
129
+
130
+ def test_unquote_base64
131
+ a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?="
132
+ b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
133
+ assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
134
+ end
135
+
136
+ def test_unquote_without_charset
137
+ a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber"
138
+ b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
139
+ assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,56 @@
1
+ require 'rbconfig'
2
+
3
+ # Attempts to require anative extension.
4
+ # Falls back to pure-ruby version, if it fails.
5
+ #
6
+ # This uses Config::CONFIG['arch'] from rbconfig.
7
+
8
+ def require_arch(fname)
9
+ arch = Config::CONFIG['arch']
10
+ begin
11
+ path = File.join("tmail", arch, fname)
12
+ require path
13
+ rescue LoadError => e
14
+ # try pre-built Windows binaries
15
+ if arch =~ /mswin/
16
+ require File.join("tmail", 'mswin32', fname)
17
+ else
18
+ raise e
19
+ end
20
+ end
21
+ end
22
+
23
+
24
+ # def require_arch(fname)
25
+ # dext = Config::CONFIG['DLEXT']
26
+ # begin
27
+ # if File.extname(fname) == dext
28
+ # path = fname
29
+ # else
30
+ # path = File.join("tmail","#{fname}.#{dext}")
31
+ # end
32
+ # require path
33
+ # rescue LoadError => e
34
+ # begin
35
+ # arch = Config::CONFIG['arch']
36
+ # path = File.join("tmail", arch, "#{fname}.#{dext}")
37
+ # require path
38
+ # rescue LoadError
39
+ # case path
40
+ # when /i686/
41
+ # path.sub!('i686', 'i586')
42
+ # when /i586/
43
+ # path.sub!('i586', 'i486')
44
+ # when /i486/
45
+ # path.sub!('i486', 'i386')
46
+ # else
47
+ # begin
48
+ # require fname + '.rb'
49
+ # rescue LoadError
50
+ # raise e
51
+ # end
52
+ # end
53
+ # retry
54
+ # end
55
+ # end
56
+ # end
@@ -0,0 +1,44 @@
1
+ =begin rdoc
2
+
3
+ = Scanner for TMail
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
+
32
+ require 'tmail/require_arch'
33
+ require 'tmail/utils'
34
+
35
+ module TMail
36
+ require 'tmail/scanner_r.rb'
37
+ begin
38
+ raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT']
39
+ require_arch('scanner_c') #require 'tmail/scanner_c.so'
40
+ Scanner = Scanner_C
41
+ rescue LoadError
42
+ Scanner = Scanner_R
43
+ end
44
+ end
@@ -0,0 +1,263 @@
1
+ #
2
+ # scanner_r.rb
3
+ #
4
+ #--
5
+ # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ #
26
+ # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
27
+ # with permission of Minero Aoki.
28
+ #++
29
+
30
+ require 'tmail/config'
31
+
32
+
33
+ module TMail
34
+
35
+ class Scanner_R
36
+
37
+ Version = '0.10.7'
38
+ Version.freeze
39
+
40
+ MIME_HEADERS = {
41
+ :CTYPE => true,
42
+ :CENCODING => true,
43
+ :CDISPOSITION => true
44
+ }
45
+
46
+ alnum = 'a-zA-Z0-9'
47
+ atomsyms = %q[ _#!$%&`'*+-{|}~^@/=? ].strip
48
+ tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip
49
+
50
+ atomchars = alnum + Regexp.quote(atomsyms)
51
+ tokenchars = alnum + Regexp.quote(tokensyms)
52
+ iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
53
+
54
+ eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+'
55
+ sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+'
56
+ utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+'
57
+
58
+ quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
59
+ domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
60
+ comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
61
+
62
+ quoted_without_iso2022 = /\A[^\\"]+/n
63
+ domlit_without_iso2022 = /\A[^\\\]]+/n
64
+ comment_without_iso2022 = /\A[^\\()]+/n
65
+
66
+ PATTERN_TABLE = {}
67
+ PATTERN_TABLE['EUC'] =
68
+ [
69
+ /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
70
+ /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
71
+ quoted_with_iso2022,
72
+ domlit_with_iso2022,
73
+ comment_with_iso2022
74
+ ]
75
+ PATTERN_TABLE['SJIS'] =
76
+ [
77
+ /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
78
+ /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
79
+ quoted_with_iso2022,
80
+ domlit_with_iso2022,
81
+ comment_with_iso2022
82
+ ]
83
+ PATTERN_TABLE['UTF8'] =
84
+ [
85
+ /\A(?:[#{atomchars}]+|#{utf8str})+/n,
86
+ /\A(?:[#{tokenchars}]+|#{utf8str})+/n,
87
+ quoted_without_iso2022,
88
+ domlit_without_iso2022,
89
+ comment_without_iso2022
90
+ ]
91
+ PATTERN_TABLE['NONE'] =
92
+ [
93
+ /\A[#{atomchars}]+/n,
94
+ /\A[#{tokenchars}]+/n,
95
+ quoted_without_iso2022,
96
+ domlit_without_iso2022,
97
+ comment_without_iso2022
98
+ ]
99
+
100
+
101
+ def initialize( str, scantype, comments )
102
+ init_scanner str
103
+ @comments = comments || []
104
+ @debug = false
105
+
106
+ # fix scanner mode
107
+ @received = (scantype == :RECEIVED)
108
+ @is_mime_header = MIME_HEADERS[scantype]
109
+
110
+ atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE]
111
+ @word_re = (MIME_HEADERS[scantype] ? token : atom)
112
+ end
113
+
114
+ attr_accessor :debug
115
+
116
+ def scan( &block )
117
+ if @debug
118
+ scan_main do |arr|
119
+ s, v = arr
120
+ printf "%7d %-10s %s\n",
121
+ rest_size(),
122
+ s.respond_to?(:id2name) ? s.id2name : s.inspect,
123
+ v.inspect
124
+ yield arr
125
+ end
126
+ else
127
+ scan_main(&block)
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ RECV_TOKEN = {
134
+ 'from' => :FROM,
135
+ 'by' => :BY,
136
+ 'via' => :VIA,
137
+ 'with' => :WITH,
138
+ 'id' => :ID,
139
+ 'for' => :FOR
140
+ }
141
+
142
+ def scan_main
143
+ until eof?
144
+ if skip(/\A[\n\r\t ]+/n) # LWSP
145
+ break if eof?
146
+ end
147
+
148
+ if s = readstr(@word_re)
149
+ if @is_mime_header
150
+ yield :TOKEN, s
151
+ else
152
+ # atom
153
+ if /\A\d+\z/ === s
154
+ yield :DIGIT, s
155
+ elsif @received
156
+ yield RECV_TOKEN[s.downcase] || :ATOM, s
157
+ else
158
+ yield :ATOM, s
159
+ end
160
+ end
161
+
162
+ elsif skip(/\A"/)
163
+ yield :QUOTED, scan_quoted_word()
164
+
165
+ elsif skip(/\A\[/)
166
+ yield :DOMLIT, scan_domain_literal()
167
+
168
+ elsif skip(/\A\(/)
169
+ @comments.push scan_comment()
170
+
171
+ else
172
+ c = readchar()
173
+ yield c, c
174
+ end
175
+ end
176
+
177
+ yield false, '$'
178
+ end
179
+
180
+ def scan_quoted_word
181
+ scan_qstr(@quoted_re, /\A"/, 'quoted-word')
182
+ end
183
+
184
+ def scan_domain_literal
185
+ '[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
186
+ end
187
+
188
+ def scan_qstr( pattern, terminal, type )
189
+ result = ''
190
+ until eof?
191
+ if s = readstr(pattern) then result << s
192
+ elsif skip(terminal) then return result
193
+ elsif skip(/\A\\/) then result << readchar()
194
+ else
195
+ raise "TMail FATAL: not match in #{type}"
196
+ end
197
+ end
198
+ scan_error! "found unterminated #{type}"
199
+ end
200
+
201
+ def scan_comment
202
+ result = ''
203
+ nest = 1
204
+ content = @comment_re
205
+
206
+ until eof?
207
+ if s = readstr(content) then result << s
208
+ elsif skip(/\A\)/) then nest -= 1
209
+ return result if nest == 0
210
+ result << ')'
211
+ elsif skip(/\A\(/) then nest += 1
212
+ result << '('
213
+ elsif skip(/\A\\/) then result << readchar()
214
+ else
215
+ raise 'TMail FATAL: not match in comment'
216
+ end
217
+ end
218
+ scan_error! 'found unterminated comment'
219
+ end
220
+
221
+ # string scanner
222
+
223
+ def init_scanner( str )
224
+ @src = str
225
+ end
226
+
227
+ def eof?
228
+ @src.empty?
229
+ end
230
+
231
+ def rest_size
232
+ @src.size
233
+ end
234
+
235
+ def readstr( re )
236
+ if m = re.match(@src)
237
+ @src = m.post_match
238
+ m[0]
239
+ else
240
+ nil
241
+ end
242
+ end
243
+
244
+ def readchar
245
+ readstr(/\A./)
246
+ end
247
+
248
+ def skip( re )
249
+ if m = re.match(@src)
250
+ @src = m.post_match
251
+ true
252
+ else
253
+ false
254
+ end
255
+ end
256
+
257
+ def scan_error!( msg )
258
+ raise SyntaxError, msg
259
+ end
260
+
261
+ end
262
+
263
+ end # module TMail