1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
|
#
# Copyright (C) 2001, 2002, 2003 Matt Armstrong. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote
# products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
module RFilter
# This is a module containing methods that know how deliver to
# various kinds of message folder types.
module Deliver
@@mail_deliver_maildir_count = 0
SYNC_IF_NO_FSYNC = RUBY_VERSION >= "1.7" ? 0 : File::SYNC
class DeliveryError < StandardError
end
class NotAFile < DeliveryError
end
class NotAMailbox < DeliveryError
end
class LockingError < DeliveryError
end
# Deliver +message+ to an mbox +filename+.
#
# The +each+ method on +message+ is used to get each line of the
# message. If the first line of the message is not an mbox
# <tt>From_</tt> header, a fake one will be generated.
#
# The file named by +filename+ is opened for append, and +flock+
# locking is used to prevent other processes from modifying the
# file during delivery. No ".lock" style locking is performed.
# If that is desired, it should be performed before calling this
# method.
#
# Returns the name of the file delivered to, or raises an
# exception if delivery failed.
def deliver_mbox(filename, message)
return filename if filename == '/dev/null'
File.open(filename,
File::APPEND|File::WRONLY|File::CREAT|SYNC_IF_NO_FSYNC,
0600) { |f|
max = 5
max.times { |i|
break if f.flock(File::LOCK_EX | File::LOCK_NB)
raise LockingError, "Timeout locking mailbox." if i == max - 1
sleep(1)
}
st = f.lstat
unless st.file?
raise NotAFile,
"Can not deliver to #{filename}, not a regular file."
end
begin
# Ignore SIGXFSZ, since we want to get the Errno::EFBIG
# exception when the file is too big.
old_handler = trap('XFSZ', 'IGNORE') || 'DEFAULT'
write_to_mbox(f, message)
begin
f.fsync
rescue NameError
# NameError happens with older versions of Ruby that have
# no File#fsync
f.flush
end
rescue Exception => e
begin
begin
f.flush
rescue Exception
end
f.truncate(st.size)
ensure
raise e
end
ensure
if old_handler
trap('XFSZ', old_handler)
end
end
f.flock(File::LOCK_UN)
}
filename
end
module_function :deliver_mbox
# Write to an already opened mbox file. This low level function
# just takes care of escaping From_ lines in the message. See
# deliver_mbox for a more robust version.
def write_to_mbox(output_io, message)
first = true
message.each { |line|
if first
first = false
if line !~ /^From .*\d$/
from = "From foo@bar " + Time.now.asctime + "\n"
output_io << from
end
elsif line =~ /^From /
output_io << '>'
end
output_io << line
output_io << "\n" unless line[-1] == ?\n
}
output_io << "\n"
end
module_function :write_to_mbox
# Deliver +message+ to a pipe.
#
# The supplied +command+ is run in a sub process, and
# <tt>message.each</tt> is used to get each line of the message
# and write it to the pipe.
#
# This method captures the <tt>Errno::EPIPE</tt> and ignores it,
# since this exception can be generated when the command exits
# before the entire message is written to it (which may or may not
# be an error).
#
# The caller can (and should!) examine <tt>$?</tt> to see the exit
# status of the pipe command.
def deliver_pipe(command, message)
begin
IO.popen(command, "w") { |io|
message.each { |line|
io << line
io << "\n" unless line[-1] == ?\n
}
}
rescue Errno::EPIPE
# Just ignore.
end
end
module_function :deliver_pipe
# Deliver +message+ to a filter and provide the io stream for
# reading the filtered content to the supplied block.
#
# The supplied +command+ is run in a sub process, and
# <tt>message.each</tt> is used to get each line of the message
# and write it to the filter.
#
# The block passed to the function is run with IO objects for the
# stdout of the child process.
#
# Returns the exit status of the child process.
def deliver_filter(message, *command)
begin
to_r, to_w = IO.pipe
from_r, from_w = IO.pipe
if pid = fork
# parent
to_r.close
from_w.close
writer = Thread::new {
message.each { |line|
to_w << line
to_w << "\n" unless line[-1] == ?\n
}
to_w.close
}
yield from_r
else
# child
begin
to_w.close
from_r.close
STDIN.reopen(to_r)
to_r.close
STDOUT.reopen(from_w)
from_w.close
exec(*command)
ensure
exit!
end
end
ensure
writer.kill if writer and writer.alive?
[ to_r, to_w, from_r, from_w ].each { |io|
if io && !io.closed?
begin
io.close
rescue Errno::EPIPE
end
end
}
end
Process.waitpid2(pid, 0)[1]
end
module_function :deliver_filter
# Delivery +message+ to a Maildir.
#
# See http://cr.yp.to/proto/maildir.html for a description of the
# maildir mailbox format. Its primary advantage is that it
# requires no locks -- delivery and access to the mailbox can
# occur at the same time.
#
# The +each+ method on +message+ is used to get each line of the
# message. If the first line of the message is an mbox
# <tt>From_</tt> line, it is discarded.
#
# The filename of the successfully delivered message is returned.
# Will raise exceptions on any kind of error.
#
# This method will attempt to create the Maildir if it does not
# exist.
def deliver_maildir(dir, message)
require 'socket'
# First, make the required directories
new = File.join(dir, 'new')
tmp = File.join(dir, 'tmp')
[ dir, new, tmp, File.join(dir, 'cur') ].each { |d|
begin
Dir.mkdir(d, 0700)
rescue Errno::EEXIST
raise unless FileTest::directory?(d)
end
}
sequence = @@mail_deliver_maildir_count
@@mail_deliver_maildir_count = @@mail_deliver_maildir_count.next
tmp_name = nil
new_name = nil
hostname = Socket::gethostname.gsub(/[^\w]/, '_').untaint
pid = Process::pid
3.times { |i|
now = Time::now
name = sprintf("%d.M%XP%dQ%d.%s",
Time::now.tv_sec, Time::now.tv_usec,
pid, sequence, hostname)
tmp_name = File.join(tmp, name)
new_name = File.join(new, name)
begin
File::stat(tmp_name)
rescue Errno::ENOENT
break
rescue Exception
raise if i == 2
end
raise "Too many tmp file conflicts." if i == 2
sleep(2)
}
begin
File.open(tmp_name,
File::CREAT|File::EXCL|File::WRONLY|SYNC_IF_NO_FSYNC,
0600) { |f|
# Write the message to the file
first = true
message.each { |line|
if first
first = false
next if line =~ /From /
end
f << line
f << "\n" unless line[-1] == ?\n
}
f.fsync if defined? f.fsync
}
File.link(tmp_name, new_name)
ensure
begin
File.delete(tmp_name)
rescue Errno::ENOENT
end
end
new_name
end
module_function :deliver_maildir
end
end
|