mailbox.rb
496 lines
| 11.9 KiB
| text/x-ruby
|
RubyLexer
|
r9346 | =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 | ||||