hermeneutics 1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+