hermeneutics 1.10 → 1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,73 +3,45 @@
3
3
  #
4
4
 
5
5
  require "hermeneutics/message"
6
+ require "hermeneutics/boxes"
7
+
6
8
 
7
9
  module Hermeneutics
8
10
 
9
11
  class Mail < Message
10
12
 
11
- # :stopdoc:
12
- class FromReader
13
- class <<self
14
- def open file
15
- i = new file
16
- yield i
17
- end
18
- private :new
19
- end
20
- attr_reader :from
21
- def initialize file
22
- @file = file
23
- @file.eat_lines { |l|
24
- l =~ /^From .*/ rescue nil
25
- if $& then
26
- @from = l
27
- @from.chomp!
28
- else
29
- @first = l
30
- end
31
- break
32
- }
33
- end
34
- def eat_lines &block
35
- if @first then
36
- yield @first
37
- @first = nil
38
- end
39
- @file.eat_lines &block
40
- end
41
- end
42
- # :startdoc:
43
-
44
13
  class <<self
45
14
 
46
- def parse input
47
- FromReader.open input do |fr|
48
- parse_hb fr do |h,b|
49
- new fr.from, h, b
50
- end
15
+ private :new
16
+
17
+ def parse input, from = nil, created = nil
18
+ input.gsub! "\r\n", "\n"
19
+ parse_hb input do |h,b|
20
+ new h, b, from, created
51
21
  end
52
22
  end
53
23
 
54
24
  def create
55
- new nil, nil, nil
25
+ new nil, nil, nil, nil
56
26
  end
57
27
 
58
28
  end
59
29
 
60
- def initialize from, headers, body
30
+ def initialize headers, body, from, created
61
31
  super headers, body
62
- @from = from
32
+ @plain_from, @created = from, created
63
33
  end
64
34
 
65
- # String representation with "From " line.
66
- # Mails reside in mbox files etc. and so have to end in a newline.
67
- def to_s
68
- set_unix_from
69
- r = ""
70
- r << @from << $/ << super
71
- r.ends_with? $/ or r << $/
72
- r
35
+ def plain_from
36
+ @plain_from ||= find_plain_from
37
+ end
38
+
39
+ def created
40
+ @created ||= Time.now
41
+ end
42
+
43
+ def senders
44
+ addresses_of :from, :sender, :return_path
73
45
  end
74
46
 
75
47
  def receivers
@@ -78,25 +50,238 @@ module Hermeneutics
78
50
 
79
51
  private
80
52
 
53
+ def find_plain_from
54
+ addresses_of :from, :sender, :return_path do |e|
55
+ return e.plain
56
+ end
57
+ require "etc"
58
+ require "socket"
59
+ "#{Etc.getlogin}@#{Socket.gethostname}"
60
+ end
61
+
81
62
  def addresses_of *args
82
- l = args.map { |f| @headers.field f }
83
- AddrList.new *l
63
+ if block_given? then
64
+ l = args.map { |f| @headers.field f }
65
+ a = AddrList.new *l
66
+ a.each { |e| yield e }
67
+ else
68
+ r = []
69
+ addresses_of *args do |e| r.push e end
70
+ r
71
+ end
72
+ end
73
+
74
+
75
+ public
76
+
77
+
78
+ SENDMAIL = "/usr/sbin/sendmail"
79
+
80
+
81
+ SPOOLDIR = "/var/mail"
82
+ SPOOLFILE = nil
83
+ MAILDIR = "~/Mail"
84
+ SYSDIR = ".hermeneutics"
85
+
86
+ LOGLEVEL = :ERR
87
+ LOGFILE = "#{File.basename $0}.log"
88
+
89
+ LEVEL = %i(
90
+ NON
91
+ ERR
92
+ INF
93
+ DBG
94
+ ).inject Hash.new do |h,k| h[k] = h.length ; h end
95
+
96
+ class <<self
97
+
98
+ def box path = nil, default_format = nil
99
+ @cache ||= {}
100
+ @cache[ path] ||= find_box path, default_format
101
+ end
102
+
103
+ private
104
+
105
+ def find_box path, default_format
106
+ b = case path
107
+ when Box then
108
+ path
109
+ when nil then
110
+ m = File.expand_path self::SPOOLFILE||getuser, self::SPOOLDIR
111
+ MBox.new m
112
+ else
113
+ m = if path =~ /\A=/ then
114
+ File.join expand_maildir, $'
115
+ else
116
+ File.expand_path path, "~"
117
+ end
118
+ Box.find m, default_format
119
+ end
120
+ b.exists? or b.create
121
+ b
122
+ end
123
+
124
+ public
125
+
126
+ def log type, *message
127
+ self::LOGFILE or return
128
+ return if LEVEL[ type] > LEVEL[ self::LOGLEVEL].to_i
129
+ l = File.expand_path self::LOGFILE, expand_sysdir
130
+ File.open l, "a" do |log|
131
+ log.puts "[#{Time.new}] [#$$] [#{type}] #{message.join ' '}"
132
+ end
133
+ nil
134
+ rescue Errno::ENOENT
135
+ d = File.dirname l
136
+ Dir.mkdir! d and retry
137
+ end
138
+
139
+ def expand_maildir
140
+ File.expand_path self::MAILDIR, "~"
141
+ end
142
+
143
+ def expand_sysdir
144
+ File.expand_path self::SYSDIR, expand_maildir
145
+ end
146
+
147
+ private
148
+
149
+ def getuser
150
+ require "etc"
151
+ (Etc.getpwuid Process.uid).name
152
+ end
153
+
154
+ end
155
+
156
+ # :call-seq:
157
+ # obj.save( path, default_format = nil) -> mb
158
+ #
159
+ # Save into local mailbox.
160
+ #
161
+ def save mailbox = nil, default_format = nil
162
+ b = self.class.box mailbox, default_format
163
+ log :INF, "Delivering to", b.path
164
+ b.store self
84
165
  end
85
166
 
86
- def set_unix_from
87
- return if @from
88
- # Common MTA's will issue a proper "From" line; some MDA's
89
- # won't. Then, build it using the "From:" header.
90
- addr = nil
91
- l = addresses_of :from, :return_path
92
- # Prefer the non-local version if present.
93
- l.each { |a|
94
- if not addr or addr !~ /@/ then
95
- addr = a
167
+ # :call-seq:
168
+ # obj.pipe( cmd, *args) -> status
169
+ #
170
+ # Pipe into an external program. If a block is given, the programs
171
+ # output will be yielded there.
172
+ #
173
+ def pipe cmd, *args
174
+ log :INF, "Piping through:", cmd, *args
175
+ ri, wi = IO.pipe
176
+ ro, wo = IO.pipe
177
+ child = fork do
178
+ wi.close ; ro.close
179
+ $stdout.reopen wo ; wo.close
180
+ $stdin .reopen ri ; ri.close
181
+ exec cmd, *args
182
+ end
183
+ ri.close ; wo.close
184
+ t = Thread.new wi do |wi|
185
+ begin
186
+ wi.write to_s
187
+ ensure
188
+ wi.close
96
189
  end
190
+ end
191
+ begin
192
+ r = ro.read
193
+ yield r if block_given?
194
+ ensure
195
+ ro.close
196
+ end
197
+ t.join
198
+ Process.wait child
199
+ $?.success? or
200
+ log :ERR, "Pipe failed with error code %d." % $?.exitstatus
201
+ $?
202
+ end
203
+
204
+ # :call-seq:
205
+ # obj.sendmail( *tos) -> status
206
+ #
207
+ # Send by sendmail; leave the +tos+ list empty to
208
+ # use Sendmail's -t option.
209
+ #
210
+ def sendmail *tos
211
+ args = []
212
+ if tos.empty? then
213
+ args.push "-t"
214
+ else
215
+ tos.flatten!
216
+ tos.each { |t|
217
+ to = case t
218
+ when Addr then t.plain
219
+ else t.delete %q-,;"'<>(){}[]$&*?- # security
220
+ end
221
+ args.push to
222
+ }
223
+ end
224
+ pipe self::SENDMAIL, *args
225
+ end
226
+
227
+ # :call-seq:
228
+ # obj.send!( smtp, *tos) -> response
229
+ #
230
+ # Send by SMTP.
231
+ #
232
+ # Be aware that +#send+ without bang is a
233
+ # standard Ruby method.
234
+ #
235
+ def send! conn = nil, *tos
236
+ if tos.empty? then
237
+ tos = (addresses_of :to, :cc).map { |t| t.plain }
238
+ else
239
+ tos.flatten!
240
+ end
241
+ f, m = true, ""
242
+ to_s.each_line { |l|
243
+ if f then
244
+ f = false
245
+ next if l =~ /^From /
246
+ end
247
+ m << l
97
248
  }
98
- addr or raise ArgumentError, "No From: field present."
99
- @from = "From #{addr.plain} #{Time.now.gmtime.asctime}"
249
+ open_smtp conn do |smtp|
250
+ log :INF, "Sending to", *tos
251
+ smtp.mail_from headers.from.first.plain
252
+ tos.each { |t| smtp.rcpt_to t }
253
+ smtp.data m
254
+ end
255
+ rescue NoMethodError
256
+ raise "Missing field: #{$!.name}."
257
+ end
258
+
259
+ private
260
+
261
+ def open_smtp arg, &block
262
+ if [ :mail_from, :rcpt_to, :data].map { |m| arg.respond_to? m }.all? then
263
+ yield arg
264
+ return
265
+ end
266
+ a = {}
267
+ case arg
268
+ when nil then h, p = "localhost", nil
269
+ when String then h, p = arg.split ":" ; p &&= Integer p
270
+ when Array then h, p = *arg
271
+ when Hash then a = arg.clone ; h, p = (a.delete :host), (a.delete :port)
272
+ else h, p = arg.host, arg.port ; arg.scheme == "smtps" and a[ :ssl] = true
273
+ end
274
+ require "hermeneutics/cli/smtp"
275
+ Cli::SMTP.open h, p, **a do |smtp|
276
+ smtp.helo
277
+ yield smtp
278
+ ensure
279
+ smtp.quit
280
+ end
281
+ end
282
+
283
+ def log level, *msg
284
+ self.class.log level, *msg
100
285
  end
101
286
 
102
287
  end