hermeneutics 1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +29 -0
- data/bin/hermesmail +262 -0
- data/etc/exim.conf +34 -0
- data/lib/hermeneutics/addrs.rb +687 -0
- data/lib/hermeneutics/boxes.rb +321 -0
- data/lib/hermeneutics/cgi.rb +253 -0
- data/lib/hermeneutics/cli/pop.rb +102 -0
- data/lib/hermeneutics/color.rb +275 -0
- data/lib/hermeneutics/contents.rb +351 -0
- data/lib/hermeneutics/css.rb +261 -0
- data/lib/hermeneutics/escape.rb +826 -0
- data/lib/hermeneutics/html.rb +462 -0
- data/lib/hermeneutics/mail.rb +105 -0
- data/lib/hermeneutics/message.rb +626 -0
- data/lib/hermeneutics/tags.rb +317 -0
- data/lib/hermeneutics/transports.rb +230 -0
- data/lib/hermeneutics/types.rb +137 -0
- data/lib/hermeneutics/version.rb +32 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -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
|
+
|
data/bin/hermesmail
ADDED
@@ -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
|
+
|
data/etc/exim.conf
ADDED
@@ -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
|
+
|