actionmailer 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionmailer might be problematic. Click here for more details.
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +21 -0
- data/README +102 -0
- data/install.rb +61 -0
- data/lib/action_mailer.rb +44 -0
- data/lib/action_mailer/base.rb +111 -0
- data/lib/action_mailer/mail_helper.rb +17 -0
- data/lib/action_mailer/vendor/text/format.rb +1447 -0
- data/lib/action_mailer/vendor/tmail.rb +4 -0
- data/lib/action_mailer/vendor/tmail/address.rb +223 -0
- data/lib/action_mailer/vendor/tmail/base64.rb +52 -0
- data/lib/action_mailer/vendor/tmail/config.rb +50 -0
- data/lib/action_mailer/vendor/tmail/encode.rb +447 -0
- data/lib/action_mailer/vendor/tmail/facade.rb +531 -0
- data/lib/action_mailer/vendor/tmail/header.rb +893 -0
- data/lib/action_mailer/vendor/tmail/info.rb +16 -0
- data/lib/action_mailer/vendor/tmail/loader.rb +1 -0
- data/lib/action_mailer/vendor/tmail/mail.rb +420 -0
- data/lib/action_mailer/vendor/tmail/mailbox.rb +414 -0
- data/lib/action_mailer/vendor/tmail/mbox.rb +1 -0
- data/lib/action_mailer/vendor/tmail/net.rb +261 -0
- data/lib/action_mailer/vendor/tmail/obsolete.rb +116 -0
- data/lib/action_mailer/vendor/tmail/parser.rb +1503 -0
- data/lib/action_mailer/vendor/tmail/port.rb +358 -0
- data/lib/action_mailer/vendor/tmail/scanner.rb +22 -0
- data/lib/action_mailer/vendor/tmail/scanner_r.rb +244 -0
- data/lib/action_mailer/vendor/tmail/stringio.rb +260 -0
- data/lib/action_mailer/vendor/tmail/tmail.rb +1 -0
- data/lib/action_mailer/vendor/tmail/utils.rb +215 -0
- data/rakefile +99 -0
- data/test/fixtures/templates/signed_up.rhtml +3 -0
- data/test/fixtures/test_mailer/signed_up.rhtml +3 -0
- data/test/mail_service_test.rb +53 -0
- metadata +86 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
#
|
2
|
+
# info.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2 or later.
|
9
|
+
#
|
10
|
+
|
11
|
+
module TMail
|
12
|
+
|
13
|
+
Version = '0.10.7'
|
14
|
+
Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'tmail/mailbox'
|
@@ -0,0 +1,420 @@
|
|
1
|
+
#
|
2
|
+
# mail.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2 or later.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'tmail/facade'
|
12
|
+
require 'tmail/encode'
|
13
|
+
require 'tmail/header'
|
14
|
+
require 'tmail/port'
|
15
|
+
require 'tmail/config'
|
16
|
+
require 'tmail/utils'
|
17
|
+
require 'socket'
|
18
|
+
|
19
|
+
|
20
|
+
module TMail
|
21
|
+
|
22
|
+
class Mail
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def load( fname )
|
26
|
+
new(FilePort.new(fname))
|
27
|
+
end
|
28
|
+
|
29
|
+
alias load_from load
|
30
|
+
alias loadfrom load
|
31
|
+
|
32
|
+
def parse( str )
|
33
|
+
new(StringPort.new(str))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize( port = nil, conf = DEFAULT_CONFIG )
|
38
|
+
@port = port || StringPort.new
|
39
|
+
@config = Config.to_config(conf)
|
40
|
+
|
41
|
+
@header = {}
|
42
|
+
@body_port = nil
|
43
|
+
@body_parsed = false
|
44
|
+
@epilogue = ''
|
45
|
+
@parts = []
|
46
|
+
|
47
|
+
@port.ropen {|f|
|
48
|
+
parse_header f
|
49
|
+
parse_body f unless @port.reproducible?
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :port
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# to_s interfaces
|
61
|
+
#
|
62
|
+
|
63
|
+
public
|
64
|
+
|
65
|
+
include StrategyInterface
|
66
|
+
|
67
|
+
def write_back( eol = "\n", charset = 'e' )
|
68
|
+
parse_body
|
69
|
+
@port.wopen {|stream| encoded eol, charset, stream }
|
70
|
+
end
|
71
|
+
|
72
|
+
def accept( strategy )
|
73
|
+
with_multipart_encoding(strategy) {
|
74
|
+
ordered_each do |name, field|
|
75
|
+
next if field.empty?
|
76
|
+
strategy.header_name canonical(name)
|
77
|
+
field.accept strategy
|
78
|
+
strategy.puts
|
79
|
+
end
|
80
|
+
strategy.puts
|
81
|
+
body_port().ropen {|r|
|
82
|
+
strategy.write r.read
|
83
|
+
}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def canonical( name )
|
90
|
+
name.split(/-/).map {|s| s.capitalize }.join('-')
|
91
|
+
end
|
92
|
+
|
93
|
+
def with_multipart_encoding( strategy )
|
94
|
+
if parts().empty? # DO NOT USE @parts
|
95
|
+
yield
|
96
|
+
|
97
|
+
else
|
98
|
+
bound = ::TMail.new_boundary
|
99
|
+
if @header.key? 'content-type'
|
100
|
+
@header['content-type'].params['boundary'] = bound
|
101
|
+
else
|
102
|
+
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
|
103
|
+
end
|
104
|
+
|
105
|
+
yield
|
106
|
+
|
107
|
+
parts().each do |tm|
|
108
|
+
strategy.puts
|
109
|
+
strategy.puts '--' + bound
|
110
|
+
tm.accept strategy
|
111
|
+
end
|
112
|
+
strategy.puts
|
113
|
+
strategy.puts '--' + bound + '--'
|
114
|
+
strategy.write epilogue()
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
###
|
119
|
+
### header
|
120
|
+
###
|
121
|
+
|
122
|
+
public
|
123
|
+
|
124
|
+
ALLOW_MULTIPLE = {
|
125
|
+
'received' => true,
|
126
|
+
'resent-date' => true,
|
127
|
+
'resent-from' => true,
|
128
|
+
'resent-sender' => true,
|
129
|
+
'resent-to' => true,
|
130
|
+
'resent-cc' => true,
|
131
|
+
'resent-bcc' => true,
|
132
|
+
'resent-message-id' => true,
|
133
|
+
'comments' => true,
|
134
|
+
'keywords' => true
|
135
|
+
}
|
136
|
+
USE_ARRAY = ALLOW_MULTIPLE
|
137
|
+
|
138
|
+
def header
|
139
|
+
@header.dup
|
140
|
+
end
|
141
|
+
|
142
|
+
def []( key )
|
143
|
+
@header[key.downcase]
|
144
|
+
end
|
145
|
+
|
146
|
+
alias fetch []
|
147
|
+
|
148
|
+
def []=( key, val )
|
149
|
+
dkey = key.downcase
|
150
|
+
|
151
|
+
if val.nil?
|
152
|
+
@header.delete dkey
|
153
|
+
return nil
|
154
|
+
end
|
155
|
+
|
156
|
+
case val
|
157
|
+
when String
|
158
|
+
header = new_hf(key, val)
|
159
|
+
when HeaderField
|
160
|
+
;
|
161
|
+
when Array
|
162
|
+
ALLOW_MULTIPLE.include? dkey or
|
163
|
+
raise ArgumentError, "#{key}: Header must not be multiple"
|
164
|
+
@header[dkey] = val
|
165
|
+
return val
|
166
|
+
else
|
167
|
+
header = new_hf(key, val.to_s)
|
168
|
+
end
|
169
|
+
if ALLOW_MULTIPLE.include? dkey
|
170
|
+
(@header[dkey] ||= []).push header
|
171
|
+
else
|
172
|
+
@header[dkey] = header
|
173
|
+
end
|
174
|
+
|
175
|
+
val
|
176
|
+
end
|
177
|
+
|
178
|
+
alias store []=
|
179
|
+
|
180
|
+
def each_header
|
181
|
+
@header.each do |key, val|
|
182
|
+
[val].flatten.each {|v| yield key, v }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
alias each_pair each_header
|
187
|
+
|
188
|
+
def each_header_name( &block )
|
189
|
+
@header.each_key(&block)
|
190
|
+
end
|
191
|
+
|
192
|
+
alias each_key each_header_name
|
193
|
+
|
194
|
+
def each_field( &block )
|
195
|
+
@header.values.flatten.each(&block)
|
196
|
+
end
|
197
|
+
|
198
|
+
alias each_value each_field
|
199
|
+
|
200
|
+
FIELD_ORDER = %w(
|
201
|
+
return-path received
|
202
|
+
resent-date resent-from resent-sender resent-to
|
203
|
+
resent-cc resent-bcc resent-message-id
|
204
|
+
date from sender reply-to to cc bcc
|
205
|
+
message-id in-reply-to references
|
206
|
+
subject comments keywords
|
207
|
+
mime-version content-type content-transfer-encoding
|
208
|
+
content-disposition content-description
|
209
|
+
)
|
210
|
+
|
211
|
+
def ordered_each
|
212
|
+
list = @header.keys
|
213
|
+
FIELD_ORDER.each do |name|
|
214
|
+
if list.delete(name)
|
215
|
+
[@header[name]].flatten.each {|v| yield name, v }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
list.each do |name|
|
219
|
+
[@header[name]].flatten.each {|v| yield name, v }
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def clear
|
224
|
+
@header.clear
|
225
|
+
end
|
226
|
+
|
227
|
+
def delete( key )
|
228
|
+
@header.delete key.downcase
|
229
|
+
end
|
230
|
+
|
231
|
+
def delete_if
|
232
|
+
@header.delete_if do |key,val|
|
233
|
+
if Array === val
|
234
|
+
val.delete_if {|v| yield key, v }
|
235
|
+
val.empty?
|
236
|
+
else
|
237
|
+
yield key, val
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def keys
|
243
|
+
@header.keys
|
244
|
+
end
|
245
|
+
|
246
|
+
def key?( key )
|
247
|
+
@header.key? key.downcase
|
248
|
+
end
|
249
|
+
|
250
|
+
def values_at( *args )
|
251
|
+
args.map {|k| @header[k.downcase] }.flatten
|
252
|
+
end
|
253
|
+
|
254
|
+
alias indexes values_at
|
255
|
+
alias indices values_at
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def parse_header( f )
|
260
|
+
name = field = nil
|
261
|
+
unixfrom = nil
|
262
|
+
|
263
|
+
while line = f.gets
|
264
|
+
case line
|
265
|
+
when /\A[ \t]/ # continue from prev line
|
266
|
+
raise SyntaxError, 'mail is began by space' unless field
|
267
|
+
field << ' ' << line.strip
|
268
|
+
|
269
|
+
when /\A([^\: \t]+):\s*/ # new header line
|
270
|
+
add_hf name, field if field
|
271
|
+
name = $1
|
272
|
+
field = $' #.strip
|
273
|
+
|
274
|
+
when /\A\-*\s*\z/ # end of header
|
275
|
+
add_hf name, field if field
|
276
|
+
name = field = nil
|
277
|
+
break
|
278
|
+
|
279
|
+
when /\AFrom (\S+)/
|
280
|
+
unixfrom = $1
|
281
|
+
|
282
|
+
else
|
283
|
+
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
add_hf name, field if name
|
287
|
+
|
288
|
+
if unixfrom
|
289
|
+
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def add_hf( name, field )
|
294
|
+
key = name.downcase
|
295
|
+
field = new_hf(name, field)
|
296
|
+
|
297
|
+
if ALLOW_MULTIPLE.include? key
|
298
|
+
(@header[key] ||= []).push field
|
299
|
+
else
|
300
|
+
@header[key] = field
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def new_hf( name, field )
|
305
|
+
HeaderField.new(name, field, @config)
|
306
|
+
end
|
307
|
+
|
308
|
+
###
|
309
|
+
### body
|
310
|
+
###
|
311
|
+
|
312
|
+
public
|
313
|
+
|
314
|
+
def body_port
|
315
|
+
parse_body
|
316
|
+
@body_port
|
317
|
+
end
|
318
|
+
|
319
|
+
def each( &block )
|
320
|
+
body_port().ropen {|f| f.each(&block) }
|
321
|
+
end
|
322
|
+
|
323
|
+
def body
|
324
|
+
parse_body
|
325
|
+
@body_port.ropen {|f|
|
326
|
+
return f.read
|
327
|
+
}
|
328
|
+
end
|
329
|
+
|
330
|
+
def body=( str )
|
331
|
+
parse_body
|
332
|
+
@body_port.wopen {|f| f.write str }
|
333
|
+
str
|
334
|
+
end
|
335
|
+
|
336
|
+
alias preamble body
|
337
|
+
alias preamble= body=
|
338
|
+
|
339
|
+
def epilogue
|
340
|
+
parse_body
|
341
|
+
@epilogue.dup
|
342
|
+
end
|
343
|
+
|
344
|
+
def epilogue=( str )
|
345
|
+
parse_body
|
346
|
+
@epilogue = str
|
347
|
+
str
|
348
|
+
end
|
349
|
+
|
350
|
+
def parts
|
351
|
+
parse_body
|
352
|
+
@parts
|
353
|
+
end
|
354
|
+
|
355
|
+
def each_part( &block )
|
356
|
+
parts().each(&block)
|
357
|
+
end
|
358
|
+
|
359
|
+
private
|
360
|
+
|
361
|
+
def parse_body( f = nil )
|
362
|
+
return if @body_parsed
|
363
|
+
if f
|
364
|
+
parse_body_0 f
|
365
|
+
else
|
366
|
+
@port.ropen {|f|
|
367
|
+
skip_header f
|
368
|
+
parse_body_0 f
|
369
|
+
}
|
370
|
+
end
|
371
|
+
@body_parsed = true
|
372
|
+
end
|
373
|
+
|
374
|
+
def skip_header( f )
|
375
|
+
while line = f.gets
|
376
|
+
return if /\A[\r\n]*\z/ === line
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def parse_body_0( f )
|
381
|
+
if multipart?
|
382
|
+
read_multipart f
|
383
|
+
else
|
384
|
+
@body_port = @config.new_body_port(self)
|
385
|
+
@body_port.wopen {|w|
|
386
|
+
w.write f.read
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def read_multipart( src )
|
392
|
+
bound = @header['content-type'].params['boundary']
|
393
|
+
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
|
394
|
+
lastbound = "--#{bound}--"
|
395
|
+
|
396
|
+
ports = [ @config.new_preamble_port(self) ]
|
397
|
+
begin
|
398
|
+
f = ports.last.wopen
|
399
|
+
while line = src.gets
|
400
|
+
if is_sep === line
|
401
|
+
f.close
|
402
|
+
break if line.strip == lastbound
|
403
|
+
ports.push @config.new_part_port(self)
|
404
|
+
f = ports.last.wopen
|
405
|
+
else
|
406
|
+
f << line
|
407
|
+
end
|
408
|
+
end
|
409
|
+
@epilogue = (src.read || '')
|
410
|
+
ensure
|
411
|
+
f.close if f and not f.closed?
|
412
|
+
end
|
413
|
+
|
414
|
+
@body_port = ports.shift
|
415
|
+
@parts = ports.map {|p| self.class.new(p, @config) }
|
416
|
+
end
|
417
|
+
|
418
|
+
end # class Mail
|
419
|
+
|
420
|
+
end # module TMail
|
@@ -0,0 +1,414 @@
|
|
1
|
+
#
|
2
|
+
# mailbox.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2 or later.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'tmail/port'
|
12
|
+
require 'socket'
|
13
|
+
require 'mutex_m'
|
14
|
+
|
15
|
+
|
16
|
+
unless [].respond_to?(:sort_by)
|
17
|
+
module Enumerable#:nodoc:
|
18
|
+
def sort_by
|
19
|
+
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
module TMail
|
26
|
+
|
27
|
+
class MhMailbox
|
28
|
+
|
29
|
+
PORT_CLASS = MhPort
|
30
|
+
|
31
|
+
def initialize( dir )
|
32
|
+
edir = File.expand_path(dir)
|
33
|
+
raise ArgumentError, "not directory: #{dir}"\
|
34
|
+
unless FileTest.directory? edir
|
35
|
+
@dirname = edir
|
36
|
+
@last_file = nil
|
37
|
+
@last_atime = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def directory
|
41
|
+
@dirname
|
42
|
+
end
|
43
|
+
|
44
|
+
alias dirname directory
|
45
|
+
|
46
|
+
attr_accessor :last_atime
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
"#<#{self.class} #{@dirname}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
end
|
54
|
+
|
55
|
+
def new_port
|
56
|
+
PORT_CLASS.new(next_file_name())
|
57
|
+
end
|
58
|
+
|
59
|
+
def each_port
|
60
|
+
mail_files().each do |path|
|
61
|
+
yield PORT_CLASS.new(path)
|
62
|
+
end
|
63
|
+
@last_atime = Time.now
|
64
|
+
end
|
65
|
+
|
66
|
+
alias each each_port
|
67
|
+
|
68
|
+
def reverse_each_port
|
69
|
+
mail_files().reverse_each do |path|
|
70
|
+
yield PORT_CLASS.new(path)
|
71
|
+
end
|
72
|
+
@last_atime = Time.now
|
73
|
+
end
|
74
|
+
|
75
|
+
alias reverse_each reverse_each_port
|
76
|
+
|
77
|
+
# old #each_mail returns Port
|
78
|
+
#def each_mail
|
79
|
+
# each_port do |port|
|
80
|
+
# yield Mail.new(port)
|
81
|
+
# end
|
82
|
+
#end
|
83
|
+
|
84
|
+
def each_new_port( mtime = nil, &block )
|
85
|
+
mtime ||= @last_atime
|
86
|
+
return each_port(&block) unless mtime
|
87
|
+
return unless File.mtime(@dirname) >= mtime
|
88
|
+
|
89
|
+
mail_files().each do |path|
|
90
|
+
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
|
91
|
+
end
|
92
|
+
@last_atime = Time.now
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def mail_files
|
98
|
+
Dir.entries(@dirname)\
|
99
|
+
.select {|s| /\A\d+\z/ === s }\
|
100
|
+
.map {|s| s.to_i }\
|
101
|
+
.sort\
|
102
|
+
.map {|i| "#{@dirname}/#{i}" }\
|
103
|
+
.select {|path| FileTest.file? path }
|
104
|
+
end
|
105
|
+
|
106
|
+
def next_file_name
|
107
|
+
unless n = @last_file
|
108
|
+
n = 0
|
109
|
+
Dir.entries(@dirname)\
|
110
|
+
.select {|s| /\A\d+\z/ === s }\
|
111
|
+
.map {|s| s.to_i }.sort\
|
112
|
+
.each do |i|
|
113
|
+
next unless FileTest.file? "#{@dirname}/#{i}"
|
114
|
+
n = i
|
115
|
+
end
|
116
|
+
end
|
117
|
+
begin
|
118
|
+
n += 1
|
119
|
+
end while FileTest.exist? "#{@dirname}/#{n}"
|
120
|
+
@last_file = n
|
121
|
+
|
122
|
+
"#{@dirname}/#{n}"
|
123
|
+
end
|
124
|
+
|
125
|
+
end # MhMailbox
|
126
|
+
|
127
|
+
MhLoader = MhMailbox
|
128
|
+
|
129
|
+
|
130
|
+
class UNIXMbox
|
131
|
+
|
132
|
+
def UNIXMbox.lock( fname )
|
133
|
+
begin
|
134
|
+
f = File.open(fname)
|
135
|
+
f.flock File::LOCK_EX
|
136
|
+
yield f
|
137
|
+
ensure
|
138
|
+
f.flock File::LOCK_UN
|
139
|
+
f.close if f and not f.closed?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class << self
|
144
|
+
alias newobj new
|
145
|
+
end
|
146
|
+
|
147
|
+
def UNIXMbox.new( fname, tmpdir = nil, readonly = false )
|
148
|
+
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
|
149
|
+
newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
|
150
|
+
end
|
151
|
+
|
152
|
+
def UNIXMbox.static_new( fname, dir, readonly = false )
|
153
|
+
newobj(fname, dir, readonly, true)
|
154
|
+
end
|
155
|
+
|
156
|
+
def initialize( fname, mhdir, readonly, static )
|
157
|
+
@filename = fname
|
158
|
+
@readonly = readonly
|
159
|
+
@closed = false
|
160
|
+
|
161
|
+
Dir.mkdir mhdir
|
162
|
+
@real = MhMailbox.new(mhdir)
|
163
|
+
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
|
164
|
+
ObjectSpace.define_finalizer self, @finalizer
|
165
|
+
end
|
166
|
+
|
167
|
+
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
|
168
|
+
lambda {
|
169
|
+
if writeback_p
|
170
|
+
lock(mboxfile) {|f|
|
171
|
+
mh.each_port do |port|
|
172
|
+
f.puts create_from_line(port)
|
173
|
+
port.ropen {|r|
|
174
|
+
f.puts r.read
|
175
|
+
}
|
176
|
+
end
|
177
|
+
}
|
178
|
+
end
|
179
|
+
if cleanup_p
|
180
|
+
Dir.foreach(mh.dirname) do |fname|
|
181
|
+
next if /\A\.\.?\z/ === fname
|
182
|
+
File.unlink "#{mh.dirname}/#{fname}"
|
183
|
+
end
|
184
|
+
Dir.rmdir mh.dirname
|
185
|
+
end
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
# make _From line
|
190
|
+
def UNIXMbox.create_from_line( port )
|
191
|
+
sprintf 'From %s %s',
|
192
|
+
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
|
193
|
+
end
|
194
|
+
|
195
|
+
def UNIXMbox.fromaddr
|
196
|
+
h = HeaderField.new_from_port(port, 'Return-Path') ||
|
197
|
+
HeaderField.new_from_port(port, 'From') or return 'nobody'
|
198
|
+
a = h.addrs[0] or return 'nobody'
|
199
|
+
a.spec
|
200
|
+
end
|
201
|
+
private_class_method :fromaddr
|
202
|
+
|
203
|
+
def close
|
204
|
+
return if @closed
|
205
|
+
|
206
|
+
ObjectSpace.undefine_finalizer self
|
207
|
+
@finalizer.call
|
208
|
+
@finalizer = nil
|
209
|
+
@real = nil
|
210
|
+
@closed = true
|
211
|
+
@updated = nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def each_port( &block )
|
215
|
+
close_check
|
216
|
+
update
|
217
|
+
@real.each_port(&block)
|
218
|
+
end
|
219
|
+
|
220
|
+
alias each each_port
|
221
|
+
|
222
|
+
def reverse_each_port( &block )
|
223
|
+
close_check
|
224
|
+
update
|
225
|
+
@real.reverse_each_port(&block)
|
226
|
+
end
|
227
|
+
|
228
|
+
alias reverse_each reverse_each_port
|
229
|
+
|
230
|
+
# old #each_mail returns Port
|
231
|
+
#def each_mail( &block )
|
232
|
+
# each_port do |port|
|
233
|
+
# yield Mail.new(port)
|
234
|
+
# end
|
235
|
+
#end
|
236
|
+
|
237
|
+
def each_new_port( mtime = nil )
|
238
|
+
close_check
|
239
|
+
update
|
240
|
+
@real.each_new_port(mtime) {|p| yield p }
|
241
|
+
end
|
242
|
+
|
243
|
+
def new_port
|
244
|
+
close_check
|
245
|
+
@real.new_port
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
|
250
|
+
def close_check
|
251
|
+
@closed and raise ArgumentError, 'accessing already closed mbox'
|
252
|
+
end
|
253
|
+
|
254
|
+
def update
|
255
|
+
return if FileTest.zero?(@filename)
|
256
|
+
return if @updated and File.mtime(@filename) < @updated
|
257
|
+
w = nil
|
258
|
+
port = nil
|
259
|
+
time = nil
|
260
|
+
UNIXMbox.lock(@filename) {|f|
|
261
|
+
begin
|
262
|
+
f.each do |line|
|
263
|
+
if /\AFrom / === line
|
264
|
+
w.close if w
|
265
|
+
File.utime time, time, port.filename if time
|
266
|
+
|
267
|
+
port = @real.new_port
|
268
|
+
w = port.wopen
|
269
|
+
time = fromline2time(line)
|
270
|
+
else
|
271
|
+
w.print line if w
|
272
|
+
end
|
273
|
+
end
|
274
|
+
ensure
|
275
|
+
if w and not w.closed?
|
276
|
+
w.close
|
277
|
+
File.utime time, time, port.filename if time
|
278
|
+
end
|
279
|
+
end
|
280
|
+
f.truncate(0) unless @readonly
|
281
|
+
@updated = Time.now
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
def fromline2time( line )
|
286
|
+
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
|
287
|
+
or return nil
|
288
|
+
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
|
289
|
+
end
|
290
|
+
|
291
|
+
end # UNIXMbox
|
292
|
+
|
293
|
+
MboxLoader = UNIXMbox
|
294
|
+
|
295
|
+
|
296
|
+
class Maildir
|
297
|
+
|
298
|
+
extend Mutex_m
|
299
|
+
|
300
|
+
PORT_CLASS = MaildirPort
|
301
|
+
|
302
|
+
@seq = 0
|
303
|
+
def Maildir.unique_number
|
304
|
+
synchronize {
|
305
|
+
@seq += 1
|
306
|
+
return @seq
|
307
|
+
}
|
308
|
+
end
|
309
|
+
|
310
|
+
def initialize( dir = nil )
|
311
|
+
@dirname = dir || ENV['MAILDIR']
|
312
|
+
raise ArgumentError, "not directory: #{@dirname}"\
|
313
|
+
unless FileTest.directory? @dirname
|
314
|
+
@new = "#{@dirname}/new"
|
315
|
+
@tmp = "#{@dirname}/tmp"
|
316
|
+
@cur = "#{@dirname}/cur"
|
317
|
+
end
|
318
|
+
|
319
|
+
def directory
|
320
|
+
@dirname
|
321
|
+
end
|
322
|
+
|
323
|
+
def inspect
|
324
|
+
"#<#{self.class} #{@dirname}>"
|
325
|
+
end
|
326
|
+
|
327
|
+
def close
|
328
|
+
end
|
329
|
+
|
330
|
+
def each_port
|
331
|
+
mail_files(@cur).each do |path|
|
332
|
+
yield PORT_CLASS.new(path)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
alias each each_port
|
337
|
+
|
338
|
+
def reverse_each_port
|
339
|
+
mail_files(@cur).reverse_each do |path|
|
340
|
+
yield PORT_CLASS.new(path)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
alias reverse_each reverse_each_port
|
345
|
+
|
346
|
+
def new_port
|
347
|
+
fname = nil
|
348
|
+
tmpfname = nil
|
349
|
+
newfname = nil
|
350
|
+
|
351
|
+
begin
|
352
|
+
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
|
353
|
+
|
354
|
+
tmpfname = "#{@tmp}/#{fname}"
|
355
|
+
newfname = "#{@new}/#{fname}"
|
356
|
+
end while FileTest.exist? tmpfname
|
357
|
+
|
358
|
+
if block_given?
|
359
|
+
File.open(tmpfname, 'w') {|f| yield f }
|
360
|
+
File.rename tmpfname, newfname
|
361
|
+
PORT_CLASS.new(newfname)
|
362
|
+
else
|
363
|
+
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
|
364
|
+
PORT_CLASS.new(tmpfname)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def each_new_port
|
369
|
+
mail_files(@new).each do |path|
|
370
|
+
dest = @cur + '/' + File.basename(path)
|
371
|
+
File.rename path, dest
|
372
|
+
yield PORT_CLASS.new(dest)
|
373
|
+
end
|
374
|
+
|
375
|
+
check_tmp
|
376
|
+
end
|
377
|
+
|
378
|
+
TOO_OLD = 60 * 60 * 36 # 36 hour
|
379
|
+
|
380
|
+
def check_tmp
|
381
|
+
old = Time.now.to_i - TOO_OLD
|
382
|
+
|
383
|
+
each_filename(@tmp) do |full, fname|
|
384
|
+
if FileTest.file? full and
|
385
|
+
File.stat(full).mtime.to_i < old
|
386
|
+
File.unlink full
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
private
|
392
|
+
|
393
|
+
def mail_files( dir )
|
394
|
+
Dir.entries(dir)\
|
395
|
+
.select {|s| s[0] != ?. }\
|
396
|
+
.sort_by {|s| s.slice(/\A\d+/).to_i }\
|
397
|
+
.map {|s| "#{dir}/#{s}" }\
|
398
|
+
.select {|path| FileTest.file? path }
|
399
|
+
end
|
400
|
+
|
401
|
+
def each_filename( dir )
|
402
|
+
Dir.foreach(dir) do |fname|
|
403
|
+
path = "#{dir}/#{fname}"
|
404
|
+
if fname[0] != ?. and FileTest.file? path
|
405
|
+
yield path, fname
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
end # Maildir
|
411
|
+
|
412
|
+
MaildirLoader = Maildir
|
413
|
+
|
414
|
+
end # module TMail
|