497 lines
12 KiB
Ruby
497 lines
12 KiB
Ruby
=begin rdoc
|
|
|
|
= Mailbox and Mbox interaction class
|
|
|
|
=end
|
|
#--
|
|
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
#
|
|
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
|
# with permission of Minero Aoki.
|
|
#++
|
|
|
|
require 'tmail/port'
|
|
require 'socket'
|
|
require 'mutex_m'
|
|
|
|
|
|
unless [].respond_to?(:sort_by)
|
|
module Enumerable#:nodoc:
|
|
def sort_by
|
|
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
module TMail
|
|
|
|
class MhMailbox
|
|
|
|
PORT_CLASS = MhPort
|
|
|
|
def initialize( dir )
|
|
edir = File.expand_path(dir)
|
|
raise ArgumentError, "not directory: #{dir}"\
|
|
unless FileTest.directory? edir
|
|
@dirname = edir
|
|
@last_file = nil
|
|
@last_atime = nil
|
|
end
|
|
|
|
def directory
|
|
@dirname
|
|
end
|
|
|
|
alias dirname directory
|
|
|
|
attr_accessor :last_atime
|
|
|
|
def inspect
|
|
"#<#{self.class} #{@dirname}>"
|
|
end
|
|
|
|
def close
|
|
end
|
|
|
|
def new_port
|
|
PORT_CLASS.new(next_file_name())
|
|
end
|
|
|
|
def each_port
|
|
mail_files().each do |path|
|
|
yield PORT_CLASS.new(path)
|
|
end
|
|
@last_atime = Time.now
|
|
end
|
|
|
|
alias each each_port
|
|
|
|
def reverse_each_port
|
|
mail_files().reverse_each do |path|
|
|
yield PORT_CLASS.new(path)
|
|
end
|
|
@last_atime = Time.now
|
|
end
|
|
|
|
alias reverse_each reverse_each_port
|
|
|
|
# old #each_mail returns Port
|
|
#def each_mail
|
|
# each_port do |port|
|
|
# yield Mail.new(port)
|
|
# end
|
|
#end
|
|
|
|
def each_new_port( mtime = nil, &block )
|
|
mtime ||= @last_atime
|
|
return each_port(&block) unless mtime
|
|
return unless File.mtime(@dirname) >= mtime
|
|
|
|
mail_files().each do |path|
|
|
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
|
|
end
|
|
@last_atime = Time.now
|
|
end
|
|
|
|
private
|
|
|
|
def mail_files
|
|
Dir.entries(@dirname)\
|
|
.select {|s| /\A\d+\z/ === s }\
|
|
.map {|s| s.to_i }\
|
|
.sort\
|
|
.map {|i| "#{@dirname}/#{i}" }\
|
|
.select {|path| FileTest.file? path }
|
|
end
|
|
|
|
def next_file_name
|
|
unless n = @last_file
|
|
n = 0
|
|
Dir.entries(@dirname)\
|
|
.select {|s| /\A\d+\z/ === s }\
|
|
.map {|s| s.to_i }.sort\
|
|
.each do |i|
|
|
next unless FileTest.file? "#{@dirname}/#{i}"
|
|
n = i
|
|
end
|
|
end
|
|
begin
|
|
n += 1
|
|
end while FileTest.exist? "#{@dirname}/#{n}"
|
|
@last_file = n
|
|
|
|
"#{@dirname}/#{n}"
|
|
end
|
|
|
|
end # MhMailbox
|
|
|
|
MhLoader = MhMailbox
|
|
|
|
|
|
class UNIXMbox
|
|
|
|
class << self
|
|
alias newobj new
|
|
end
|
|
|
|
# Creates a new mailbox object that you can iterate through to collect the
|
|
# emails from with "each_port".
|
|
#
|
|
# You need to pass it a filename of a unix mailbox format file, the format of this
|
|
# file can be researched at this page at {wikipedia}[link:http://en.wikipedia.org/wiki/Mbox]
|
|
#
|
|
# ==== Parameters
|
|
#
|
|
# +filename+: The filename of the mailbox you want to open
|
|
#
|
|
# +tmpdir+: Can be set to override TMail using the system environment's temp dir. TMail will first
|
|
# use the temp dir specified by you (if any) or then the temp dir specified in the Environment's TEMP
|
|
# value then the value in the Environment's TMP value or failing all of the above, '/tmp'
|
|
#
|
|
# +readonly+: If set to false, each email you take from the mail box will be removed from the mailbox.
|
|
# default is *false* - ie, it *WILL* truncate your mailbox file to ZERO once it has read the emails out.
|
|
#
|
|
# ==== Options:
|
|
#
|
|
# None
|
|
#
|
|
# ==== Examples:
|
|
#
|
|
# # First show using readonly true:
|
|
#
|
|
# require 'ftools'
|
|
# File.size("../test/fixtures/mailbox")
|
|
# #=> 20426
|
|
#
|
|
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox", nil, true)
|
|
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=true.....>
|
|
#
|
|
# mailbox.each_port do |port|
|
|
# mail = TMail::Mail.new(port)
|
|
# puts mail.subject
|
|
# end
|
|
# #Testing mailbox 1
|
|
# #Testing mailbox 2
|
|
# #Testing mailbox 3
|
|
# #Testing mailbox 4
|
|
# require 'ftools'
|
|
# File.size?("../test/fixtures/mailbox")
|
|
# #=> 20426
|
|
#
|
|
# # Now show with readonly set to the default false
|
|
#
|
|
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox")
|
|
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=false.....>
|
|
#
|
|
# mailbox.each_port do |port|
|
|
# mail = TMail::Mail.new(port)
|
|
# puts mail.subject
|
|
# end
|
|
# #Testing mailbox 1
|
|
# #Testing mailbox 2
|
|
# #Testing mailbox 3
|
|
# #Testing mailbox 4
|
|
#
|
|
# File.size?("../test/fixtures/mailbox")
|
|
# #=> nil
|
|
def UNIXMbox.new( filename, tmpdir = nil, readonly = false )
|
|
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
|
|
newobj(filename, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
|
|
end
|
|
|
|
def UNIXMbox.lock( fname )
|
|
begin
|
|
f = File.open(fname, 'r+')
|
|
f.flock File::LOCK_EX
|
|
yield f
|
|
ensure
|
|
f.flock File::LOCK_UN
|
|
f.close if f and not f.closed?
|
|
end
|
|
end
|
|
|
|
def UNIXMbox.static_new( fname, dir, readonly = false )
|
|
newobj(fname, dir, readonly, true)
|
|
end
|
|
|
|
def initialize( fname, mhdir, readonly, static )
|
|
@filename = fname
|
|
@readonly = readonly
|
|
@closed = false
|
|
|
|
Dir.mkdir mhdir
|
|
@real = MhMailbox.new(mhdir)
|
|
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
|
|
ObjectSpace.define_finalizer self, @finalizer
|
|
end
|
|
|
|
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
|
|
lambda {
|
|
if writeback_p
|
|
lock(mboxfile) {|f|
|
|
mh.each_port do |port|
|
|
f.puts create_from_line(port)
|
|
port.ropen {|r|
|
|
f.puts r.read
|
|
}
|
|
end
|
|
}
|
|
end
|
|
if cleanup_p
|
|
Dir.foreach(mh.dirname) do |fname|
|
|
next if /\A\.\.?\z/ === fname
|
|
File.unlink "#{mh.dirname}/#{fname}"
|
|
end
|
|
Dir.rmdir mh.dirname
|
|
end
|
|
}
|
|
end
|
|
|
|
# make _From line
|
|
def UNIXMbox.create_from_line( port )
|
|
sprintf 'From %s %s',
|
|
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
|
|
end
|
|
|
|
def UNIXMbox.fromaddr(port)
|
|
h = HeaderField.new_from_port(port, 'Return-Path') ||
|
|
HeaderField.new_from_port(port, 'From') ||
|
|
HeaderField.new_from_port(port, 'EnvelopeSender') or return 'nobody'
|
|
a = h.addrs[0] or return 'nobody'
|
|
a.spec
|
|
end
|
|
|
|
def close
|
|
return if @closed
|
|
|
|
ObjectSpace.undefine_finalizer self
|
|
@finalizer.call
|
|
@finalizer = nil
|
|
@real = nil
|
|
@closed = true
|
|
@updated = nil
|
|
end
|
|
|
|
def each_port( &block )
|
|
close_check
|
|
update
|
|
@real.each_port(&block)
|
|
end
|
|
|
|
alias each each_port
|
|
|
|
def reverse_each_port( &block )
|
|
close_check
|
|
update
|
|
@real.reverse_each_port(&block)
|
|
end
|
|
|
|
alias reverse_each reverse_each_port
|
|
|
|
# old #each_mail returns Port
|
|
#def each_mail( &block )
|
|
# each_port do |port|
|
|
# yield Mail.new(port)
|
|
# end
|
|
#end
|
|
|
|
def each_new_port( mtime = nil )
|
|
close_check
|
|
update
|
|
@real.each_new_port(mtime) {|p| yield p }
|
|
end
|
|
|
|
def new_port
|
|
close_check
|
|
@real.new_port
|
|
end
|
|
|
|
private
|
|
|
|
def close_check
|
|
@closed and raise ArgumentError, 'accessing already closed mbox'
|
|
end
|
|
|
|
def update
|
|
return if FileTest.zero?(@filename)
|
|
return if @updated and File.mtime(@filename) < @updated
|
|
w = nil
|
|
port = nil
|
|
time = nil
|
|
UNIXMbox.lock(@filename) {|f|
|
|
begin
|
|
f.each do |line|
|
|
if /\AFrom / === line
|
|
w.close if w
|
|
File.utime time, time, port.filename if time
|
|
|
|
port = @real.new_port
|
|
w = port.wopen
|
|
time = fromline2time(line)
|
|
else
|
|
w.print line if w
|
|
end
|
|
end
|
|
ensure
|
|
if w and not w.closed?
|
|
w.close
|
|
File.utime time, time, port.filename if time
|
|
end
|
|
end
|
|
f.truncate(0) unless @readonly
|
|
@updated = Time.now
|
|
}
|
|
end
|
|
|
|
def fromline2time( line )
|
|
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) or return nil
|
|
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
|
|
rescue
|
|
nil
|
|
end
|
|
|
|
end # UNIXMbox
|
|
|
|
MboxLoader = UNIXMbox
|
|
|
|
|
|
class Maildir
|
|
|
|
extend Mutex_m
|
|
|
|
PORT_CLASS = MaildirPort
|
|
|
|
@seq = 0
|
|
def Maildir.unique_number
|
|
synchronize {
|
|
@seq += 1
|
|
return @seq
|
|
}
|
|
end
|
|
|
|
def initialize( dir = nil )
|
|
@dirname = dir || ENV['MAILDIR']
|
|
raise ArgumentError, "not directory: #{@dirname}"\
|
|
unless FileTest.directory? @dirname
|
|
@new = "#{@dirname}/new"
|
|
@tmp = "#{@dirname}/tmp"
|
|
@cur = "#{@dirname}/cur"
|
|
end
|
|
|
|
def directory
|
|
@dirname
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class} #{@dirname}>"
|
|
end
|
|
|
|
def close
|
|
end
|
|
|
|
def each_port
|
|
mail_files(@cur).each do |path|
|
|
yield PORT_CLASS.new(path)
|
|
end
|
|
end
|
|
|
|
alias each each_port
|
|
|
|
def reverse_each_port
|
|
mail_files(@cur).reverse_each do |path|
|
|
yield PORT_CLASS.new(path)
|
|
end
|
|
end
|
|
|
|
alias reverse_each reverse_each_port
|
|
|
|
def new_port
|
|
fname = nil
|
|
tmpfname = nil
|
|
newfname = nil
|
|
|
|
begin
|
|
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
|
|
|
|
tmpfname = "#{@tmp}/#{fname}"
|
|
newfname = "#{@new}/#{fname}"
|
|
end while FileTest.exist? tmpfname
|
|
|
|
if block_given?
|
|
File.open(tmpfname, 'w') {|f| yield f }
|
|
File.rename tmpfname, newfname
|
|
PORT_CLASS.new(newfname)
|
|
else
|
|
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
|
|
PORT_CLASS.new(tmpfname)
|
|
end
|
|
end
|
|
|
|
def each_new_port
|
|
mail_files(@new).each do |path|
|
|
dest = @cur + '/' + File.basename(path)
|
|
File.rename path, dest
|
|
yield PORT_CLASS.new(dest)
|
|
end
|
|
|
|
check_tmp
|
|
end
|
|
|
|
TOO_OLD = 60 * 60 * 36 # 36 hour
|
|
|
|
def check_tmp
|
|
old = Time.now.to_i - TOO_OLD
|
|
|
|
each_filename(@tmp) do |full, fname|
|
|
if FileTest.file? full and
|
|
File.stat(full).mtime.to_i < old
|
|
File.unlink full
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def mail_files( dir )
|
|
Dir.entries(dir)\
|
|
.select {|s| s[0] != ?. }\
|
|
.sort_by {|s| s.slice(/\A\d+/).to_i }\
|
|
.map {|s| "#{dir}/#{s}" }\
|
|
.select {|path| FileTest.file? path }
|
|
end
|
|
|
|
def each_filename( dir )
|
|
Dir.foreach(dir) do |fname|
|
|
path = "#{dir}/#{fname}"
|
|
if fname[0] != ?. and FileTest.file? path
|
|
yield path, fname
|
|
end
|
|
end
|
|
end
|
|
|
|
end # Maildir
|
|
|
|
MaildirLoader = Maildir
|
|
|
|
end # module TMail
|