hermeneutics 1.10 → 1.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 721abdbe281ba643a27fefdcb8016c0f1a0202ed65e6c9741b5a90c3320c121e
4
- data.tar.gz: c78e80af37ced64e8ab89d19b729da96f3b6d0e888f0bf0c5de96274bbdf30c3
3
+ metadata.gz: d06afea9e09641e4c0c05bc3fca3590c3b4002047092034a379ab9a096867a57
4
+ data.tar.gz: 21944a76c2279a66b7de8ea9d32ad6db9328898c7ff944a2b736e0c3500dd61c
5
5
  SHA512:
6
- metadata.gz: 30f6297db18bab61106428eee10338bfe4907dae30fa12bfbff8dc3e94a931b349befc4f5fa65bd21c9b053d9c9e8904d8c7d9920fc2f63f51d393167dc13a0b
7
- data.tar.gz: 9f96a1b60536b83254252029fa36fa3e7ed739f270799bd8ccd155e068407a4b31de0073bb7049c5306b7096f5c9b54dca7ba53f8564966f7e82e6830cc644f4
6
+ metadata.gz: 399dd807c48582880089a94580b1c939ad57fc5da148b9987dff1f77729e731b62863f2f44a8a7540aefc4edc9dd211ab8dfe5a759d654ab6f9de682c968f9e7
7
+ data.tar.gz: 61514f34f392828df42f4123e49c6d7af7aecaf0dd3a503241ee60d5ee811e5e8fecb957cebee7bca4a407afab656ce96922f5ebf7d6c683e584acc3c56bf70f
data/README ADDED
@@ -0,0 +1,11 @@
1
+ = Hermeneutics
2
+
3
+ Fast and easy CGI and mail handling using Ruby
4
+
5
+ == Description
6
+
7
+ I'm working with this library on several projects for over a decade and I am
8
+ very happy with it.
9
+
10
+ Yet, it is not documented very well.
11
+
data/bin/hermesmail CHANGED
@@ -11,8 +11,7 @@ rescue LoadError
11
11
  end
12
12
 
13
13
  require "hermeneutics/version"
14
- require "hermeneutics/transports"
15
- require "hermeneutics/cli/pop"
14
+ require "hermeneutics/mail"
16
15
 
17
16
 
18
17
  module Hermeneutics
@@ -38,7 +37,7 @@ module Hermeneutics
38
37
 
39
38
  # Forward by SMTP
40
39
  def forward_smtp to
41
- send nil, to
40
+ send! nil, to
42
41
  done
43
42
  end
44
43
 
@@ -50,45 +49,26 @@ module Hermeneutics
50
49
  alias forward forward_smtp
51
50
 
52
51
 
53
- self.logfile = "hermesmail.log"
54
- self.loglevel = :ERR
55
-
56
52
  @failed_process = "=failed-process"
53
+ @failed_parse = "=failed-parse"
57
54
 
58
55
  class <<self
59
- attr_accessor :failed_process
56
+ attr_accessor :failed_process, :failed_parse
60
57
  def process input, debug = false
61
58
  i = parse input
62
59
  i.debug = debug
63
60
  i.execute
64
61
  rescue
65
62
  raise if debug
66
- open_failed { |f|
67
- log_exception "Error while parsing mail", f.path
68
- f.write input
69
- }
63
+ log_exception "Error while parsing mail"
64
+ b = box @failed_parse
65
+ log :INF, "Saving to", b.path
66
+ b.store_raw input, nil, nil
70
67
  end
71
68
  def log_exception msg, *args
72
69
  log :ERR, "#{msg}: #$! (#{$!.class})", *args
73
70
  $!.backtrace.each { |b| log :INF, " #{b}" }
74
71
  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
72
  end
93
73
 
94
74
  def execute
@@ -98,12 +78,12 @@ module Hermeneutics
98
78
  rescue
99
79
  raise if @debug
100
80
  log_exception "Error while processing mail"
101
- b = cls.box cls.failed_process
81
+ b = self.class.box self.class.failed_process
102
82
  save b
103
83
  end
104
84
 
105
85
  def log_exception msg, *args
106
- cls.log_exception msg, *args
86
+ self.class.log_exception msg, *args
107
87
  end
108
88
 
109
89
  end
@@ -112,44 +92,99 @@ module Hermeneutics
112
92
  class Fetch
113
93
 
114
94
  class <<self
95
+
115
96
  private :new
116
97
  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
98
+ @list = []
99
+ class_eval *args, &block
100
+ new @list
101
+ ensure
102
+ @list = nil
103
+ end
104
+
105
+ def pop *args, **kwargs
106
+ access Pop, *args, **kwargs do yield end
107
+ end
108
+ def login *args
109
+ @access[ :logins].push args
110
+ nil
111
+ end
112
+
113
+ private
114
+ def access type, *args, **kwargs
115
+ @access and raise "Access methods must not be nested."
116
+ @access = { type: type, args: args, kwargs: kwargs, logins: [] }
117
+ yield
118
+ @list.push @access
119
+ nil
120
+ ensure
121
+ @access = nil
128
122
  end
129
- end
130
123
 
131
- def initialize
132
- @list = []
133
124
  end
134
125
 
135
- def pop *args ; access Cli::Pop, *args do yield end ; end
136
- def pops *args ; access Cli::Pops, *args do yield end ; end
126
+ def initialize list
127
+ @list = list
128
+ end
137
129
 
138
- def login *args
139
- @access[ :logins].push args
140
- nil
130
+ def each
131
+ @list.each { |a|
132
+ c = a[ :type].new *a[ :args], **a[ :kwargs]
133
+ a[ :logins].each { |l|
134
+ c.login *l do yield c end
135
+ }
136
+ }
141
137
  end
142
138
 
139
+ class Keep < Exception ; end
140
+
143
141
  private
144
142
 
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
143
+
144
+ class Pop
145
+
146
+ def initialize host, port = nil, ssl: nil
147
+ if not port and host =~ /:(\d+)\z/ then
148
+ host, port = $`, $1.to_i
149
+ end
150
+ @host, @port, @ssl = host, port, ssl
151
+ end
152
+
153
+ def login user, password
154
+ require "hermeneutics/cli/pop3"
155
+ Cli::POP3.open @host, @port, ssl: @ssl do |pop|
156
+ @user, @password, @pop = user, password, pop
157
+ @pop.authenticate @user, @password
158
+ yield
159
+ @pop.quit
160
+ end
161
+ ensure
162
+ @user, @password, @pop = nil, nil, nil
163
+ end
164
+
165
+ def name
166
+ @user or raise "Not logged in."
167
+ r = "#@user@#@host"
168
+ r << ":#@port" if @port
169
+ r
170
+ end
171
+
172
+ def count
173
+ c, = @pop.stat
174
+ c
175
+ end
176
+
177
+ def each
178
+ @pop.list.each { |k,|
179
+ text = @pop.retr k
180
+ begin
181
+ yield text
182
+ @pop.dele k
183
+ rescue Keep
184
+ end
185
+ }
186
+ end
187
+
153
188
  end
154
189
 
155
190
  end
@@ -164,10 +199,10 @@ module Hermeneutics
164
199
  LICENSE = Hermeneutics::LICENSE
165
200
  AUTHORS = Hermeneutics::AUTHORS
166
201
 
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.
202
+ DESCRIPTION = <<~EOT
203
+ This mail delivery agent (MDA) reads a configuration file
204
+ that is plain Ruby code. See the examples section for how
205
+ to write one.
171
206
  EOT
172
207
 
173
208
  attr_accessor :rulesfile, :mbox, :fetchfile
@@ -231,7 +266,7 @@ to write one.
231
266
  print "\r#{i}/#{c} " if @quiet < 1
232
267
  i += 1
233
268
  Processed.process m
234
- raise Cli::Pop::Keep if @keep
269
+ raise Fetch::Keep if @keep
235
270
  }
236
271
  puts "\rDone. " if @quiet < 1
237
272
  }
@@ -95,8 +95,6 @@ module Hermeneutics
95
95
 
96
96
  end
97
97
 
98
- attr_reader :mail, :real
99
-
100
98
  def initialize mail, real
101
99
  @mail, @real = mail, real
102
100
  @mail.compact!
@@ -138,6 +136,8 @@ module Hermeneutics
138
136
  tokenized.encode
139
137
  end
140
138
 
139
+ private
140
+
141
141
  def tokenized
142
142
  r = Token[ :addr, [ Token[ :lang] , @mail, Token[ :rang]]]
143
143
  if @real then
@@ -146,8 +146,6 @@ module Hermeneutics
146
146
  r
147
147
  end
148
148
 
149
- private
150
-
151
149
  def mk_plain
152
150
  p = @mail.to_s
153
151
  p.downcase!
@@ -592,6 +590,9 @@ module Hermeneutics
592
590
 
593
591
  public
594
592
 
593
+ def empty? ; @list.empty? ; end
594
+ def notempty? ; self if @list.notempty? ; end
595
+
595
596
  def push addrs
596
597
  case addrs
597
598
  when nil then
@@ -600,6 +601,7 @@ module Hermeneutics
600
601
  else addrs.each { |a| push a }
601
602
  end
602
603
  end
604
+ alias << push
603
605
 
604
606
  def inspect
605
607
  "<#{self.class}: " + (@list.map { |a| a.inspect }.join ", ") + ">"
@@ -4,7 +4,7 @@
4
4
 
5
5
  =begin rdoc
6
6
 
7
- :section: Classes definied here
7
+ :section: Classes defined here
8
8
 
9
9
  Hermeneutics::Box is a general Mailbox.
10
10
 
@@ -18,7 +18,6 @@ Hermeneutics::Maildir is the maildir format.
18
18
 
19
19
 
20
20
  require "supplement"
21
- require "supplement/locked"
22
21
  require "date"
23
22
 
24
23
 
@@ -31,15 +30,18 @@ module Hermeneutics
31
30
 
32
31
  class <<self
33
32
 
33
+ attr_accessor :default_format
34
+
34
35
  # :call-seq:
35
36
  # Box.find( path, default = nil) -> box
36
37
  #
37
38
  # Create a Box object (some subclass of Box), depending on
38
- # what type the box is found at <code>path</code>.
39
+ # what type the box is found at +path+.
39
40
  #
40
41
  def find path, default_format = nil
41
42
  b = @boxes.find { |b| b.check path }
42
43
  b ||= default_format
44
+ b ||= @default_format
43
45
  b ||= if File.directory? path then
44
46
  Maildir
45
47
  elsif File.file? path then
@@ -71,7 +73,7 @@ module Hermeneutics
71
73
  # :call-seq:
72
74
  # Box.new( path) -> box
73
75
  #
74
- # Instantiate a Box object, just store the <code>path</code>.
76
+ # Instantiate a Box object, just store the +path+.
75
77
  #
76
78
  def initialize mailbox
77
79
  @mailbox = mailbox
@@ -84,12 +86,39 @@ module Hermeneutics
84
86
  # :call-seq:
85
87
  # box.exists? -> true or false
86
88
  #
87
- # Test whether the <code>Box</code> exists.
89
+ # Test whether the +Box+ exists.
88
90
  #
89
91
  def exists?
90
92
  self.class.check @mailbox
91
93
  end
92
94
 
95
+ # :call-seq:
96
+ # mbox.store( msg) -> nil
97
+ #
98
+ # Store the mail to the local +MBox+.
99
+ #
100
+ def store msg
101
+ store_raw msg.to_s, msg.plain_from, msg.created
102
+ end
103
+
104
+ # :call-seq:
105
+ # mbox.each { |mail| ... } -> nil
106
+ #
107
+ # Iterate through +MBox+.
108
+ # Alias for +MBox#each_mail+.
109
+ #
110
+ def each &block ; each_mail &block ; end
111
+ include Enumerable
112
+
113
+ private
114
+
115
+ def local_from
116
+ require "etc"
117
+ require "socket"
118
+ s = File.stat @mailbox
119
+ lfrom = "#{(Etc.getpwuid s.uid).name}@#{Socket.gethostname}"
120
+ end
121
+
93
122
  end
94
123
 
95
124
  class MBox < Box
@@ -102,7 +131,7 @@ module Hermeneutics
102
131
  # :call-seq:
103
132
  # MBox.check( path) -> true or false
104
133
  #
105
- # Check whether path is a <code>MBox</code>.
134
+ # Check whether path is a +MBox+.
106
135
  #
107
136
  def check path
108
137
  if File.file? path then
@@ -114,48 +143,10 @@ module Hermeneutics
114
143
 
115
144
  end
116
145
 
117
- # :stopdoc:
118
- class Region
119
- class <<self
120
- private :new
121
- def open file, start, stop
122
- t = file.tell
123
- begin
124
- i = new file, start, stop
125
- yield i
126
- ensure
127
- file.seek t
128
- end
129
- end
130
- end
131
- def initialize file, start, stop
132
- @file, @start, @stop = file, start, stop
133
- rewind
134
- end
135
- def rewind ; @file.seek @start ; end
136
- def read n = nil
137
- m = @stop - @file.tell
138
- n = m if not n or n > m
139
- @file.read n
140
- end
141
- def to_s
142
- rewind
143
- read
144
- end
145
- def each_line
146
- @file.each_line { |l|
147
- break if @file.tell > @stop
148
- yield l
149
- }
150
- end
151
- alias eat_lines each_line
152
- end
153
- # :startdoc:
154
-
155
146
  # :call-seq:
156
147
  # mbox.create -> self
157
148
  #
158
- # Create the <code>MBox</code>.
149
+ # Create the +MBox+.
159
150
  #
160
151
  def create
161
152
  d = File.dirname @mailbox
@@ -165,65 +156,69 @@ module Hermeneutics
165
156
  end
166
157
 
167
158
  # :call-seq:
168
- # mbox.deliver( msg) -> nil
159
+ # mbox.store_raw( text, from, created) -> nil
169
160
  #
170
- # Store the mail into the local <code>MBox</code>.
161
+ # Store some text that appears like a mail to the local +MBox+.
171
162
  #
172
- def deliver msg
173
- pos = nil
174
- LockedFile.open @mailbox, "r+", encoding: Encoding::ASCII_8BIT do |f|
163
+ def store_raw text, from, created
164
+ from ||= local_from
165
+ created ||= Time.now
166
+ File.open @mailbox, "r+", encoding: Encoding::ASCII_8BIT do |f|
175
167
  f.seek [ f.size - 4, 0].max
176
- last = ""
168
+ last = nil
177
169
  f.read.each_line { |l| last = l }
178
- f.puts unless last =~ /^$/
179
- pos = f.size
180
- m = msg.to_s
181
- i = 1
182
- while (i = m.index RE_F, i rescue nil) do m.insert i, ">" end
183
- f.write m
170
+ f.puts if last and not last =~ RE_N
171
+
172
+ f.puts "From #{from.gsub ' ', '_'} #{created.to_time.gmtime.asctime}"
173
+ text.each_line { |l|
174
+ l.chomp!
175
+ f.print ">" if l =~ RE_F
176
+ f.puts l
177
+ }
184
178
  f.puts
185
179
  end
186
- pos
180
+ nil
187
181
  end
188
182
 
189
183
  # :call-seq:
190
- # mbox.each { |mail| ... } -> nil
184
+ # mbox.each_mail { |mail| ... } -> nil
191
185
  #
192
- # Iterate through <code>MBox</code>.
186
+ # Iterate through +MBox+.
193
187
  #
194
- def each &block
188
+ def each_mail
195
189
  File.open @mailbox, encoding: Encoding::ASCII_8BIT do |f|
196
- m, e = nil, true
197
- s, t = t, f.tell
190
+ nl_seen = false
191
+ from, created, text = nil, nil, nil
198
192
  f.each_line { |l|
199
- s, t = t, f.tell
200
- if is_from_line? l and e then
193
+ l.chomp!
194
+ if l =~ RE_F then
195
+ l = $'
196
+ yield text, from, created if text
197
+ length_tried = false
198
+ from, created = l.split nil, 2
201
199
  begin
202
- m and Region.open f, m, e, &block
203
- ensure
204
- m, e = s, nil
200
+ created = DateTime.parse created
201
+ rescue Date::Error
202
+ unless length_tried then
203
+ from = $'
204
+ created = from.slice! from.length-Time.now.ctime.length, from.length
205
+ from.strip!
206
+ length_tried = true
207
+ retry
208
+ end
209
+ raise "#@mailbox does not seem to be a mailbox: From line '#{l}'."
205
210
  end
211
+ text, nl_seen = "", false
206
212
  else
207
- m or raise "#@mailbox does not seem to be a mailbox."
208
- e = l =~ RE_N && s
213
+ from or raise "#@mailbox does not seem to be a mailbox. No 'From' line."
214
+ text << "\n" if nl_seen
215
+ nl_seen = l =~ RE_N
216
+ nl_seen or text << l << "\n"
209
217
  end
210
218
  }
211
- # Treat it gracefully when there is no empty last line.
212
- e ||= f.tell
213
- m and Region.open f, m, e, &block
219
+ text and yield text, from, created
214
220
  end
215
221
  end
216
- include Enumerable
217
-
218
- private
219
-
220
- def is_from_line? l
221
- l =~ RE_F or return
222
- addr, time = $'.split nil, 2
223
- DateTime.parse time
224
- addr =~ /@/
225
- rescue ArgumentError, TypeError
226
- end
227
222
 
228
223
  end
229
224
 
@@ -237,7 +232,7 @@ module Hermeneutics
237
232
  # :call-seq:
238
233
  # Maildir.check( path) -> true or false
239
234
  #
240
- # Check whether path is a <code>Maildir</code>.
235
+ # Check whether path is a +Maildir+.
241
236
  #
242
237
  def check mailbox
243
238
  if File.directory? mailbox then
@@ -254,7 +249,7 @@ module Hermeneutics
254
249
  # :call-seq:
255
250
  # maildir.create -> self
256
251
  #
257
- # Create the <code>Maildir</code>.
252
+ # Create the +Maildir+.
258
253
  #
259
254
  def create
260
255
  Dir.mkdir! @mailbox
@@ -266,53 +261,73 @@ module Hermeneutics
266
261
  end
267
262
 
268
263
  # :call-seq:
269
- # maildir.deliver( msg) -> nil
264
+ # maildir.store_raw( text, from, created) -> nil
270
265
  #
271
- # Store the mail into the local <code>Maildir</code>.
266
+ # Store some text that appears like a mail to the local +MBox+.
272
267
  #
273
- def deliver msg
274
- tmp = mkfilename TMP
275
- File.open tmp, "w" do |f|
276
- f.write msg
277
- end
278
- new = mkfilename NEW
279
- File.rename tmp, new
280
- new
268
+ def store_raw text, from, created
269
+ filename = mkfilename from, created
270
+ tpath = File.join @mailbox, TMP, filename
271
+ File.open tpath, File::CREAT|File::EXCL|File::WRONLY do |f| f.puts text end
272
+ cpath = File.join @mailbox, NEW, filename
273
+ File.link tpath, cpath
274
+ filename
275
+ rescue Errno::EEXIST
276
+ File.unlink tpath rescue nil
277
+ retry
278
+ ensure
279
+ File.unlink tpath
280
+ end
281
+
282
+ # :call-seq:
283
+ # mbox.each_file { |filename| ... } -> nil
284
+ #
285
+ # Iterate through +Maildir+.
286
+ #
287
+ def each_file new = nil
288
+ p = File.join @mailbox, new ? NEW : CUR
289
+ (Dir.new p).sort.each { |fn|
290
+ next if fn.starts_with? "."
291
+ path = File.join p, fn
292
+ yield path
293
+ }
281
294
  end
282
295
 
283
296
  # :call-seq:
284
297
  # mbox.each { |mail| ... } -> nil
285
298
  #
286
- # Iterate through <code>MBox</code>.
299
+ # Iterate through +Maildir+.
287
300
  #
288
- def each
289
- p = File.join @mailbox, CUR
290
- d = Dir.new p
291
- d.each { |f|
292
- next if f.starts_with? "."
293
- File.open f, encoding: Encoding::ASCII_8BIT do |f|
294
- yield f
301
+ def each_mail new = nil
302
+ lfrom = local_from
303
+ each_file new do |fn|
304
+ created = Time.at fn[ /\A(\d+)/, 1].to_i + fn[ /M(\d+)/, 1].to_i*0.000001
305
+ File.open fn, encoding: Encoding::ASCII_8BIT do |f|
306
+ from_host = fn[ /\.([.a-z0-9_+-]+)/, 1]
307
+ text = f.read
308
+ from = text[ /[a-z0-9.+-]+@#{Regexp.quote from_host}/]
309
+ yield text, from||lfrom, created
295
310
  end
296
- }
311
+ end
297
312
  end
298
- include Enumerable
299
313
 
300
314
  private
301
315
 
302
- autoload :Socket, "socket"
303
-
304
- def mkfilename d
305
- dir = File.join @mailbox, d
306
- c = 0
307
- begin
308
- n = "%.4f.%d_%d.%s" % [ Time.now.to_f, $$, c, Socket.gethostname]
309
- path = File.join dir, n
310
- File.open path, File::CREAT|File::EXCL do |f| end
311
- path
312
- rescue Errno::EEXIST
313
- c += 1
314
- retry
316
+ @seq = 0
317
+ class <<self
318
+ def seq! ; @seq += 1 ; end
319
+ end
320
+
321
+ def mkfilename from, created
322
+ host = if from =~ /@/ then
323
+ $'
324
+ else
325
+ require "socket"
326
+ Socket.gethostname
315
327
  end
328
+ created ||= Time.now
329
+ created = created.to_time
330
+ "#{created.to_i}M#{created.usec}P#$$Q#{self.class.seq!}.#{host}"
316
331
  end
317
332
 
318
333
  end