hermeneutics 1.11 → 1.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f95dbd49c6d0f467ff381f459e23b2ede01e74a36cbffc71d7840efb1fd6e6c3
4
- data.tar.gz: cafb0d288c12b99cfac80512d0da471124be8f8fb76f23f5f1c0b0ccaae602e1
3
+ metadata.gz: 34855e35779618671f657756ec5e16cb4f57da8925d822a605aa5a727aa3362c
4
+ data.tar.gz: 6ebed997b49c501692cd829c2fd010fd06bd4ef34e1f76ca88a6970ca0ffab67
5
5
  SHA512:
6
- metadata.gz: 18e1e83b59d712df7c0d30adeb6d1c9d6cb80ed2d9ee5ebd3a6a458f1405a764c16ad85871bafaaac07752f4703f33853834df1e5257fe0bf569a8ad0c16fc5e
7
- data.tar.gz: 5cf7d8ee879802e39c0483c871cab99609fb5b338f7598e9c4cf26762bc500dfceda23e68ba67848e47eecfb140b9a1d8fb3ffd3d1533d11747edfa72413f81a
6
+ metadata.gz: 04a4d6586ee2dd68e571b6e5a4e9da0bc329fd715dd619a1c72392e0e68992dd6e84bd70f8542645545c8e9d25f81e46c2814cbc794ff0d4e3cd3a11f3794161
7
+ data.tar.gz: d44b4fdf6b8285938b3bf4bf66ec5f7627fdd274d0be9ca764ffc918dcfe2ddd431ca9ffdefa988ee6ac5f4e5cffafb5f4b4d227442e9b3dab0f8968835421cc
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
@@ -231,7 +266,7 @@ module Hermeneutics
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