hermeneutics 1.8

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef74159e9784d02b32aebb1b32555dbfb1ea388f4197990c649f326e0ad70e1c
4
+ data.tar.gz: aee45f2c54b9169d23253ed556fafaa8b5eaba4d22c7ae0b141047ef6e26f4aa
5
+ SHA512:
6
+ metadata.gz: db155da6e4b37944baf54a2d33d3b4a457a233c038afe7011fc0653fc56dd6bbe5832a168d1e43fb41a9faf706086b8e409b18c110a1d7935443e89ff3348a2f
7
+ data.tar.gz: 99d58cad68a872bcedbd669c72ba022f678062db94935baf78908ecc030b4e1c03af6a743f21ab70a4b6069d5eb2b435cc7a58028082e4b745a2f3158149f994
data/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ = Hermeneutics -- Ruby mail and CGI handling
2
+
3
+ Copyright (c) 2011-2013, Bertram Scharpf <software@bertram-scharpf.de>.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are
8
+ met:
9
+
10
+ * Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in
15
+ the documentation and/or other materials provided with the
16
+ distribution.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
22
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # hermesmail -- Mail filtering and delivery
5
+ #
6
+
7
+ begin
8
+ require "appl"
9
+ rescue LoadError
10
+ raise "This requires the Gem 'appl'."
11
+ end
12
+
13
+ require "hermeneutics/version"
14
+ require "hermeneutics/transports"
15
+ require "hermeneutics/cli/pop"
16
+
17
+
18
+ module Hermeneutics
19
+
20
+ class Processed < Mail
21
+
22
+ attr_accessor :debug
23
+
24
+ class Done < Exception ; end
25
+
26
+ # Do nothing, just finish.
27
+ def done
28
+ raise Done
29
+ end
30
+
31
+ alias delete done
32
+
33
+ # Save in a local mailbox
34
+ def deposit mailbox = nil
35
+ save mailbox
36
+ done
37
+ end
38
+
39
+ # Forward by SMTP
40
+ def forward_smtp to
41
+ send nil, to
42
+ done
43
+ end
44
+
45
+ # Forward by sendmail
46
+ def forward_sendmail to
47
+ sendmail to
48
+ done
49
+ end
50
+ alias forward forward_smtp
51
+
52
+
53
+ self.logfile = "hermesmail.log"
54
+ self.loglevel = :ERR
55
+
56
+ @failed_process = "=failed-process"
57
+
58
+ class <<self
59
+ attr_accessor :failed_process
60
+ def process input, debug = false
61
+ i = parse input
62
+ i.debug = debug
63
+ i.execute
64
+ rescue
65
+ raise if debug
66
+ open_failed { |f|
67
+ log_exception "Error while parsing mail", f.path
68
+ f.write input
69
+ }
70
+ end
71
+ def log_exception msg, *args
72
+ log :ERR, "#{msg}: #$! (#{$!.class})", *args
73
+ $!.backtrace.each { |b| log :INF, " #{b}" }
74
+ end
75
+ private
76
+ def open_failed
77
+ i = 0
78
+ d = expand_sysdir
79
+ w = Time.now.strftime "%Y%m%d%H%M%S"
80
+ begin
81
+ p = File.join d, "failed-#{w}-%05d" % i
82
+ File.open p, File::CREAT|File::EXCL|File::WRONLY do |f|
83
+ yield f
84
+ end
85
+ rescue Errno::ENOENT
86
+ Dir.mkdir! d and retry
87
+ rescue Errno::EEXIST
88
+ i +=1
89
+ retry
90
+ end
91
+ end
92
+ end
93
+
94
+ def execute
95
+ process
96
+ save
97
+ rescue Done
98
+ rescue
99
+ raise if @debug
100
+ log_exception "Error while processing mail"
101
+ b = cls.box cls.failed_process
102
+ save b
103
+ end
104
+
105
+ def log_exception msg, *args
106
+ cls.log_exception msg, *args
107
+ end
108
+
109
+ end
110
+
111
+
112
+ class Fetch
113
+
114
+ class <<self
115
+ private :new
116
+ def create *args, &block
117
+ i = new
118
+ i.instance_eval *args, &block
119
+ def i.each
120
+ @list.each { |a|
121
+ c = a[ :type].new *a[ :args]
122
+ a[ :logins].each { |l|
123
+ c.login *l do yield c end
124
+ }
125
+ }
126
+ end
127
+ i
128
+ end
129
+ end
130
+
131
+ def initialize
132
+ @list = []
133
+ end
134
+
135
+ def pop *args ; access Cli::Pop, *args do yield end ; end
136
+ def pops *args ; access Cli::Pops, *args do yield end ; end
137
+
138
+ def login *args
139
+ @access[ :logins].push args
140
+ nil
141
+ end
142
+
143
+ private
144
+
145
+ def access type, *args
146
+ @access and raise "Access methods must not be nested."
147
+ @access = { type: type, args: args, logins: [] }
148
+ yield
149
+ @list.push @access
150
+ nil
151
+ ensure
152
+ @access = nil
153
+ end
154
+
155
+ end
156
+
157
+
158
+ class MailApp < Application
159
+
160
+ NAME = "hermesmail"
161
+ VERSION = Hermeneutics::VERSION
162
+ SUMMARY = "A mail delivery agent written in Ruby"
163
+ COPYRIGHT = Hermeneutics::COPYRIGHT
164
+ LICENSE = Hermeneutics::LICENSE
165
+ AUTHORS = Hermeneutics::AUTHORS
166
+
167
+ DESCRIPTION = <<-EOT
168
+ This mail delivery agent (MDA) reads a configuration file
169
+ that is plain Ruby code. See the examples section for how
170
+ to write one.
171
+ EOT
172
+
173
+ attr_accessor :rulesfile, :mbox, :fetchfile
174
+ attr_bang :debug, :fetch, :keep
175
+ def quiet! ; @quiet += 1 ; end
176
+
177
+ def initialize *args
178
+ @quiet = 0
179
+ super
180
+ $*.concat @args # make them $<-able again
181
+ @args.clear
182
+ end
183
+
184
+ RULESFILE = "~/.hermesmail-rules"
185
+ FETCHFILE = "~/.hermesmail-fetch"
186
+
187
+ define_option "r", :rulesfile=, "NAME", RULESFILE, "filtering rules"
188
+ alias_option "r", "rulesfile"
189
+
190
+ define_option "M", :mbox=, "MBOX",
191
+ "process all in MBOX instead of one from stdin"
192
+ alias_option "M", "mbox"
193
+
194
+ define_option "f", :fetch!, "fetch from a POP server"
195
+ alias_option "f", "fetch"
196
+
197
+ define_option "F", :fetchfile=, "FILE", FETCHFILE,
198
+ "a PGP-encrypted file containing fetch methods"
199
+ alias_option "F", "fetchfile"
200
+ alias_option "F", "fetch-file"
201
+
202
+ define_option "k", :keep!, "don't delete the mails on the server"
203
+ alias_option "k", "keep"
204
+
205
+ define_option "q", :quiet!,
206
+ "less output (once = no progress, twice = nothing)"
207
+ alias_option "q", "quiet"
208
+
209
+ define_option "g", :debug!, "full Ruby error messages"
210
+ alias_option "g", "debug"
211
+
212
+ define_option "h", :help, "show options"
213
+ alias_option "h", "help"
214
+ define_option "V", :version, "show version"
215
+ alias_option "V", "version"
216
+
217
+ def run
218
+ Processed.class_eval read_rules
219
+ if @mbox and @fetch then
220
+ raise "Specify either mbox or fetch but not both."
221
+ end
222
+ if @mbox then
223
+ b = Box.find @mbox
224
+ b.each { |m| Processed.process m }
225
+ elsif @fetch then
226
+ read_fetches.each { |s|
227
+ c = s.count
228
+ puts "#{c} Mails in #{s.name}." if @quiet < 2
229
+ i = 0
230
+ s.each { |m|
231
+ print "\r#{i}/#{c} " if @quiet < 1
232
+ i += 1
233
+ Processed.process m
234
+ raise Cli::Pop::Keep if @keep
235
+ }
236
+ puts "\rDone. " if @quiet < 1
237
+ }
238
+ else
239
+ msg = $<.read
240
+ msg.force_encoding Encoding::ASCII_8BIT
241
+ Processed.process msg, @debug
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ def read_rules
248
+ r = File.expand_path @rulesfile
249
+ File.read r
250
+ end
251
+
252
+ def read_fetches
253
+ p = File.expand_path @fetchfile
254
+ Fetch.create `gpg -d #{p}`
255
+ end
256
+
257
+ end
258
+
259
+ MailApp.run
260
+
261
+ end
262
+
@@ -0,0 +1,34 @@
1
+ #
2
+ # exim.conf -- example Exim configuration
3
+ #
4
+
5
+ # ...
6
+
7
+ begin routers
8
+
9
+ hermesmail:
10
+ driver = accept
11
+ # This will change uid and gid to ${local_part}:
12
+ check_local_user
13
+ require_files = ${local_part}:+${home}:${home}/.hermesmail-rules:+/usr/local/bin/hermesmail
14
+ transport = hermesmail_pipe
15
+ no_verify
16
+
17
+ # ...
18
+
19
+ begin transports
20
+
21
+ hermesmail_pipe:
22
+ driver = pipe
23
+ # Ruby Gems will insert the right shebang line but in case it calls "env"
24
+ # you have to set the right path.
25
+ #
26
+ # path = "/bin:/usr/bin:/usr/local/bin"
27
+ command = "/usr/local/bin/hermesmail -r .hermesmail-rules"
28
+ delivery_date_add
29
+ envelope_to_add
30
+ message_suffix = ""
31
+ return_path_add
32
+
33
+ # ...
34
+
@@ -0,0 +1,687 @@
1
+ # encoding: UTF-8
2
+
3
+ #
4
+ # hermeneutics/addrs.rb -- Extract addresses out of a string
5
+ #
6
+
7
+ =begin rdoc
8
+
9
+ :section: Classes definied here
10
+
11
+ Hermeneutics::Addr is a single address
12
+ Hermeneutics::AddrList is a list of addresses in mail header fields.
13
+
14
+ = Remark
15
+
16
+ In my opinion, RFC 2822 allows too much features for address
17
+ fields (see A.5). Most of them I have never seen anywhere in
18
+ practice but only in the RFC. I doubt whether any mail-related
19
+ software implements the specification correctly. Maybe this
20
+ library does, but I cannot judge it as, after once tested, I
21
+ never again understood my own code. The specification
22
+ inevitably leeds to code of such kind. RFC 2822 address
23
+ specification is a pain.
24
+
25
+ =end
26
+
27
+ require "hermeneutics/escape"
28
+
29
+
30
+ class NilClass
31
+ def has? *args
32
+ end
33
+ def under_domain *args
34
+ end
35
+ end
36
+
37
+
38
+ module Hermeneutics
39
+
40
+ # A parser and generator for mail address fields.
41
+ #
42
+ # = Examples
43
+ #
44
+ # a = Addr.create "dummy@example.com", "John Doe"
45
+ # a.to_s #=> "John Doe <dummy@example.com>"
46
+ # a.quote #=> "John Doe <dummy@example.com>"
47
+ # a.encode #=> "John Doe <dummy@example.com>"
48
+ #
49
+ # a = Addr.create "dummy@example.com", "Müller, Fritz"
50
+ # a.to_s #=> "Müller, Fritz <dummy@example.com>"
51
+ # a.quote #=> "\"Müller, Fritz\" <dummy@example.com>"
52
+ # a.encode #=> "=?utf-8?q?M=C3=BCller=2C_Fritz?= <dummy@example.com>"
53
+ #
54
+ # = Parsing
55
+ #
56
+ # x = <<-'EOT'
57
+ # Jörg Q. Müller <jmuell@example.com>, "Meier, Hans"
58
+ # <hmei@example.com>, Möller\, Fritz <fmoel@example.com>
59
+ # EOT
60
+ # Addr.parse x do |a,g|
61
+ # puts a.quote
62
+ # end
63
+ #
64
+ # # Output:
65
+ # # Jörg Q. Müller <jmuell@example.com>
66
+ # # "Meier, Hans" <hmei@example.com>
67
+ # # "Möller, Fritz" <fmoel@example.com>
68
+ #
69
+ # x = "some: =?utf-8?q?M=C3=B6ller=2C_Fritz?= " +
70
+ # "<fmoeller@example.com> webmaster@example.com; foo@example.net"
71
+ # Addr.parse_decode x do |a,g|
72
+ # puts g.to_s
73
+ # puts a.quote
74
+ # end
75
+ #
76
+ # # Output:
77
+ # # some
78
+ # # "Möller, Fritz" <fmoeller@example.com>
79
+ # # some
80
+ # # <webmaster@example.com>
81
+ # #
82
+ # # <foo@example.net>
83
+ #
84
+ class Addr
85
+
86
+ class <<self
87
+
88
+ def create mail, real = nil
89
+ m = Token[ :addr, (Token.lexer mail)]
90
+ r = Token[ :text, (Token.lexer real)] if real
91
+ new m, r
92
+ end
93
+ alias [] create
94
+ private :new
95
+
96
+ end
97
+
98
+ attr_reader :mail, :real
99
+
100
+ def initialize mail, real
101
+ @mail, @real = mail, real
102
+ @mail.compact!
103
+ @real.compact! if @real
104
+ end
105
+
106
+ def == oth
107
+ plain == case oth
108
+ when Addr then oth.plain
109
+ else oth.to_s.downcase
110
+ end
111
+ end
112
+
113
+ def plain
114
+ @plain ||= mk_plain
115
+ end
116
+
117
+ def real
118
+ @real.to_s if @real
119
+ end
120
+
121
+ def inspect
122
+ "<##{self.class}: mail=#{@mail.inspect} real=#{@real.inspect}>"
123
+ end
124
+
125
+ def to_s
126
+ tokenized.to_s
127
+ end
128
+
129
+ def quote
130
+ tokenized.quote
131
+ end
132
+
133
+ def encode
134
+ tokenized.encode
135
+ end
136
+
137
+ def tokenized
138
+ r = Token[ :addr, [ Token[ :lang] , @mail, Token[ :rang]]]
139
+ if @real then
140
+ r = Token[ :text, [ @real, Token[ :space], r]]
141
+ end
142
+ r
143
+ end
144
+
145
+ private
146
+
147
+ def mk_plain
148
+ p = @mail.to_s
149
+ p.downcase!
150
+ p
151
+ end
152
+
153
+ @encoding_parameters = {}
154
+ class <<self
155
+ attr_reader :encoding_parameters
156
+ end
157
+
158
+ class Token
159
+
160
+ attr_accessor :sym, :data, :quot
161
+
162
+ class <<self
163
+ alias [] new
164
+ end
165
+
166
+ def initialize sym, data = nil, quot = nil
167
+ @sym, @data, @quot = sym, data, quot
168
+ end
169
+
170
+ def inspect
171
+ d = ": #{@data.inspect}" if @data
172
+ d << " Q" if @quot
173
+ "<##@sym#{d}>"
174
+ end
175
+
176
+ def === oth
177
+ case oth
178
+ when Symbol then @sym == oth
179
+ when Token then self == oth
180
+ end
181
+ end
182
+
183
+ def force_encoding enc
184
+ case @sym
185
+ when :text then @data.each { |x| x.force_encoding enc }
186
+ when :char then @data.force_encoding enc
187
+ end
188
+ end
189
+
190
+ def to_s
191
+ text
192
+ end
193
+
194
+ def text
195
+ case @sym
196
+ when :addr then data_map_join { |x| x.quote }
197
+ when :text then data_map_join { |x| x.text }
198
+ when :char then @data
199
+ when :space then " "
200
+ else SPECIAL_CHARS[ @sym]||""
201
+ end
202
+ rescue Encoding::CompatibilityError
203
+ force_encoding Encoding::ASCII_8BIT
204
+ retry
205
+ end
206
+
207
+ def quote
208
+ case @sym
209
+ when :text,
210
+ :addr then data_map_join { |x| x.quote }
211
+ when :char then quoted
212
+ when :space then " "
213
+ else SPECIAL_CHARS[ @sym]||""
214
+ end
215
+ rescue Encoding::CompatibilityError
216
+ force_encoding Encoding::ASCII_8BIT
217
+ retry
218
+ end
219
+
220
+ def encode
221
+ case @sym
222
+ when :addr then data_map_join { |x| x.quote }
223
+ when :text then data_map_join { |x| x.encode }
224
+ when :char then encoded
225
+ when :space then " "
226
+ else SPECIAL_CHARS[ @sym]||""
227
+ end
228
+ end
229
+
230
+ def compact!
231
+ case @sym
232
+ when :text then
233
+ return if @data.length <= 1
234
+ @data = [ Token[ :char, text, needs_quote?]]
235
+ when :addr then
236
+ d = []
237
+ while @data.any? do
238
+ x, y = d.last, @data.shift
239
+ if y === :char and x === :char then
240
+ x.data << y.data
241
+ x.quot ||= y.quot
242
+ else
243
+ y.compact!
244
+ d.push y
245
+ end
246
+ end
247
+ @data = d
248
+ end
249
+ end
250
+
251
+ def needs_quote?
252
+ case @sym
253
+ when :text then @data.find { |x| x.needs_quote? }
254
+ when :char then @quot
255
+ when :space then false
256
+ when :addr then false
257
+ else true
258
+ end
259
+ end
260
+
261
+ private
262
+
263
+ def data_map_join
264
+ @data.map { |x| yield x }.join
265
+ end
266
+
267
+ def quoted
268
+ if @quot then
269
+ q = @data.gsub "\"" do |c| "\\" + c end
270
+ %Q%"#{q}"%
271
+ else
272
+ @data
273
+ end
274
+ end
275
+
276
+ def encoded
277
+ if @quot or HeaderExt.needs? @data then
278
+ c = HeaderExt.new Addr.encoding_parameters
279
+ c.encode_whole @data
280
+ else
281
+ @data
282
+ end
283
+ end
284
+
285
+ class <<self
286
+
287
+ def lexer str
288
+ if block_given? then
289
+ while str =~ /./m do
290
+ h, str = $&, $'
291
+ t = SPECIAL[ h]
292
+ if respond_to? t, true then
293
+ t = send t, h, str
294
+ end
295
+ unless Token === t then
296
+ t = Token[ *t]
297
+ end
298
+ yield t
299
+ end
300
+ else
301
+ r = []
302
+ lexer str do |t| r.push t end
303
+ r
304
+ end
305
+ end
306
+
307
+ def lexer_decode str, &block
308
+ if block_given? then
309
+ HeaderExt.lexer str do |k,s|
310
+ case k
311
+ when :decoded then yield Token[ :char, s, true]
312
+ when :plain then lexer s, &block
313
+ when :space then yield Token[ :space]
314
+ end
315
+ end
316
+ else
317
+ r = []
318
+ lexer_decode str do |t| r.push t end
319
+ r
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ def escaped h, c
326
+ if h then
327
+ [h].pack "H2"
328
+ else
329
+ case c
330
+ when "n" then "\n"
331
+ when "r" then "\r"
332
+ when "t" then "\t"
333
+ when "f" then "\f"
334
+ when "v" then "\v"
335
+ when "b" then "\b"
336
+ when "a" then "\a"
337
+ when "e" then "\e"
338
+ when "0" then "\0"
339
+ else c
340
+ end
341
+ end
342
+ end
343
+
344
+ def lex_space h, str
345
+ str.slice! /\A\s*/
346
+ :space
347
+ end
348
+ def lex_bslash h, str
349
+ str.slice! /\A(?:x(..)|.)/
350
+ y = escaped $1, $&
351
+ Token[ :char, y, true]
352
+ end
353
+ def lex_squote h, str
354
+ str.slice! /\A((?:[^\\']|\\.)*)'?/
355
+ y = $1.gsub /\\(x(..)|.)/ do |c,x|
356
+ escaped x, c
357
+ end
358
+ Token[ :char, y, true]
359
+ end
360
+ def lex_dquote h, str
361
+ str.slice! /\A((?:[^\\"]|\\.)*)"?/
362
+ y = $1.gsub /\\(x(..)|.)/ do |c,x|
363
+ escaped x, c
364
+ end
365
+ Token[ :char, y, true]
366
+ end
367
+ def lex_other h, str
368
+ until str.empty? or SPECIAL.has_key? str.head do
369
+ h << (str.eat 1)
370
+ end
371
+ Token[ :char, h]
372
+ end
373
+
374
+ end
375
+
376
+ # :stopdoc:
377
+ SPECIAL = {
378
+ "<" => :lang,
379
+ ">" => :rang,
380
+ "(" => :lparen,
381
+ ")" => :rparen,
382
+ "," => :comma,
383
+ ";" => :semicol,
384
+ ":" => :colon,
385
+ "@" => :at,
386
+ "[" => :lbrack,
387
+ "]" => :rbrack,
388
+ " " => :lex_space,
389
+ "'" => :lex_squote,
390
+ "\"" => :lex_dquote,
391
+ "\\" => :lex_bslash,
392
+ }
393
+ "\t\n\f\r".each_char do |c| SPECIAL[ c] = SPECIAL[ " "] end
394
+ SPECIAL.default = :lex_other
395
+ SPECIAL_CHARS = SPECIAL.invert
396
+ # :startdoc:
397
+
398
+ end
399
+
400
+ class <<self
401
+
402
+ # Parse a line from a string that was entered by the user.
403
+ #
404
+ # x = "Meier, Hans <hmei@example.com>, foo@example.net"
405
+ # Addr.parse x do |a,g|
406
+ # puts a.quote
407
+ # end
408
+ #
409
+ # # Output:
410
+ # "Meier, Hans" <hmei@example.com>
411
+ # <foo@example.net>
412
+ #
413
+ def parse str, &block
414
+ l = Token.lexer str
415
+ compile l, &block
416
+ end
417
+
418
+ # Parse a line from a mail header field and make addresses of it.
419
+ #
420
+ # Internally the encoding class +HeaderExt+ will be used.
421
+ #
422
+ # x = "some: =?utf-8?q?M=C3=B6ller=2C_Fritz?= <fmoeller@example.com>"
423
+ # Addr.parse_decode x do |addr,group|
424
+ # puts group.to_s
425
+ # puts addr.quote
426
+ # end
427
+ #
428
+ # # Output:
429
+ # # some
430
+ # # "Möller, Fritz" <fmoeller@example.com>
431
+ #
432
+ def parse_decode str, &block
433
+ l = Token.lexer_decode str
434
+ compile l, &block
435
+ end
436
+
437
+ private
438
+
439
+ def compile l, &block
440
+ l = unspace l
441
+ l = uncomment l
442
+ g = split_groups l
443
+ groups_compile g, &block
444
+ end
445
+
446
+ def groups_compile g
447
+ if block_given? then
448
+ g.each { |k,v|
449
+ split_list v do |m,r|
450
+ a = new m, r
451
+ yield a, k
452
+ end
453
+ }
454
+ return
455
+ end
456
+ t = []
457
+ groups_compile g do |a,| t.push a end
458
+ t
459
+ end
460
+
461
+ def matches l, *tokens
462
+ z = tokens.zip l
463
+ z.each { |(s,e)|
464
+ e === s or return
465
+ }
466
+ true
467
+ end
468
+
469
+ def unspace l
470
+ r = []
471
+ while l.any? do
472
+ if matches l, :space then
473
+ l.shift
474
+ next
475
+ end
476
+ if matches l, :char then
477
+ e = Token[ :text, [ l.shift]]
478
+ loop do
479
+ if matches l, :char then
480
+ e.data.push l.shift
481
+ elsif matches l, :space, :char then
482
+ e.data.push l.shift
483
+ e.data.push l.shift
484
+ else
485
+ break
486
+ end
487
+ end
488
+ l.unshift e
489
+ end
490
+ r.push l.shift
491
+ end
492
+ r
493
+ end
494
+
495
+ def uncomment l
496
+ r = []
497
+ while l.any? do
498
+ if matches l, :lparen then
499
+ l.shift
500
+ l = uncomment l
501
+ until matches l, :rparen or l.empty? do
502
+ l.shift
503
+ end
504
+ l.shift
505
+ end
506
+ r.push l.shift
507
+ end
508
+ r
509
+ end
510
+
511
+ def split_groups l
512
+ g = []
513
+ n = nil
514
+ while l.any? do
515
+ n = if matches l, :text, :colon then
516
+ e = l.shift
517
+ l.shift
518
+ e.to_s
519
+ end
520
+ s = []
521
+ until matches l, :semicol or l.empty? do
522
+ s.push l.shift
523
+ end
524
+ l.shift
525
+ g.push [ n, s]
526
+ end
527
+ g
528
+ end
529
+
530
+ def split_list l
531
+ while l.any? do
532
+ if matches l, :text, :comma, :text, :lang then
533
+ t = l.first.to_s
534
+ if t =~ /[^a-z0-9_]/ then
535
+ e = Token[ :text, []]
536
+ e.data.push l.shift
537
+ e.data.push l.shift, Token[ :space]
538
+ e.data.push l.shift
539
+ l.unshift e
540
+ end
541
+ end
542
+ a, c = find_one_of l, :lang, :comma
543
+ if a then
544
+ real = l.shift a if a.nonzero?
545
+ l.shift
546
+ a, c = find_one_of l, :rang, :comma
547
+ mail = l.shift a||c||l.length
548
+ l.shift
549
+ l.shift if matches l, :comma
550
+ else
551
+ mail = l.shift c||l.length
552
+ l.shift
553
+ real = nil
554
+ end
555
+ yield Token[ :addr, mail], real&&Token[ :text, real]
556
+ end
557
+ end
558
+
559
+ def find_one_of l, s, t
560
+ l.each_with_index { |e,i|
561
+ if e === s then
562
+ return i, nil
563
+ elsif e === t then
564
+ return nil, i
565
+ end
566
+ }
567
+ nil
568
+ end
569
+
570
+ end
571
+
572
+ end
573
+
574
+ class AddrList
575
+
576
+ class <<self
577
+ def parse cont
578
+ new.add_encoded cont
579
+ end
580
+ end
581
+
582
+ private
583
+
584
+ def initialize *addrs
585
+ @list = []
586
+ push addrs
587
+ end
588
+
589
+ public
590
+
591
+ def push addrs
592
+ case addrs
593
+ when nil then
594
+ when String then add_encoded addrs
595
+ when Addr then @list.push addrs
596
+ else addrs.each { |a| push a }
597
+ end
598
+ end
599
+
600
+ def inspect
601
+ "<#{self.class}: " + (@list.map { |a| a.inspect }.join ", ") + ">"
602
+ end
603
+
604
+ def to_s
605
+ @list.map { |a| a.to_s }.join ", "
606
+ end
607
+
608
+ def quote
609
+ @list.map { |a| a.quote }.join ", "
610
+ end
611
+
612
+ def encode
613
+ r = []
614
+ @list.map { |a|
615
+ if r.last then r.last << "," end
616
+ r.push a.encode.dup
617
+ }
618
+ r
619
+ end
620
+
621
+ # :call-seq:
622
+ # each { |addr| ... } -> self
623
+ #
624
+ # Call block for each address.
625
+ #
626
+ def each
627
+ @list.each { |a| yield a }
628
+ end
629
+ include Enumerable
630
+
631
+ def has? *mails
632
+ mails.find { |m|
633
+ case m
634
+ when Regexp then
635
+ @list.find { |a|
636
+ if a.plain =~ m then
637
+ yield *$~.captures if block_given?
638
+ true
639
+ end
640
+ }
641
+ else
642
+ self == m
643
+ end
644
+ }
645
+ end
646
+ alias has has?
647
+
648
+ def under_domain *args
649
+ @list.each { |a|
650
+ a.plain =~ /(.*)@/ or next
651
+ l, d = $1, $'
652
+ case d
653
+ when *args then yield l
654
+ end
655
+ }
656
+ end
657
+
658
+ def == str
659
+ @list.find { |a| a == str }
660
+ end
661
+
662
+ def add mail, real = nil
663
+ if real or not Addr === mail then
664
+ mail = Addr.create mail, real
665
+ end
666
+ @list.push mail
667
+ self
668
+ end
669
+
670
+ def add_quoted str
671
+ Addr.parse str.to_s do |a,|
672
+ @list.push a
673
+ end
674
+ self
675
+ end
676
+
677
+ def add_encoded cont
678
+ Addr.parse_decode cont.to_s do |a,|
679
+ @list.push a
680
+ end
681
+ self
682
+ end
683
+
684
+ end
685
+
686
+ end
687
+