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.
- 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
|
+
|