gurgitate-mail 1.7.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/gurgitate-mail +40 -0
- data/lib/gurgitate-mail.rb +262 -0
- data/lib/gurgitate/deliver.rb +84 -0
- data/lib/gurgitate/deliver/maildir.rb +100 -0
- data/lib/gurgitate/deliver/mbox.rb +47 -0
- data/lib/gurgitate/headers.rb +264 -0
- data/lib/gurgitate/mailmessage.rb +45 -0
- data/test.rb +527 -0
- metadata +50 -0
data/bin/gurgitate-mail
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!./bin/ruby -w
|
2
|
+
|
3
|
+
#------------------------------------------------------------------------
|
4
|
+
# Mail filter invocation script
|
5
|
+
#------------------------------------------------------------------------
|
6
|
+
|
7
|
+
require "gurgitate-mail"
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
# change this on installation to taste
|
11
|
+
GLOBAL_RULES="/etc/gurgitate-rules"
|
12
|
+
GLOBAL_RULES_POST="/etc/gurgitate-rules-default"
|
13
|
+
|
14
|
+
commandline_files = []
|
15
|
+
|
16
|
+
opts = OptionParser.new do |o|
|
17
|
+
o.on("-f FILE", "--file FILE", "Use FILE as a rules file") do |file|
|
18
|
+
commandline_files << file
|
19
|
+
end
|
20
|
+
o.on_tail("-h", "--help", "Show this message") do
|
21
|
+
puts opts
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.parse!(ARGV)
|
27
|
+
|
28
|
+
gurgitate = Gurgitate::Gurgitate.new(STDIN)
|
29
|
+
|
30
|
+
if commandline_files.length > 0
|
31
|
+
commandline_files.each do |file|
|
32
|
+
gurgitate.add_rules(file, :user => true)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
gurgitate.add_rules(GLOBAL_RULES, :system => true)
|
36
|
+
gurgitate.add_rules(:default)
|
37
|
+
gurgitate.add_rules(GLOBAL_RULES_POST, :system => true)
|
38
|
+
end
|
39
|
+
|
40
|
+
gurgitate.process
|
@@ -0,0 +1,262 @@
|
|
1
|
+
#!/opt/bin/ruby
|
2
|
+
#------------------------------------------------------------------------
|
3
|
+
# Mail filter package
|
4
|
+
#------------------------------------------------------------------------
|
5
|
+
|
6
|
+
require 'etc'
|
7
|
+
|
8
|
+
require 'gurgitate/mailmessage'
|
9
|
+
require 'gurgitate/deliver'
|
10
|
+
|
11
|
+
module Gurgitate
|
12
|
+
#========================================================================
|
13
|
+
# The actual gurgitator; reads a message and then it can do
|
14
|
+
# other stuff with it, like save to a mailbox or forward
|
15
|
+
# it somewhere else.
|
16
|
+
class Gurgitate < Mailmessage
|
17
|
+
include Deliver
|
18
|
+
|
19
|
+
# Instead of the usual attributes, I went with a
|
20
|
+
# reader-is-writer type thing (as seen quite often in Perl and
|
21
|
+
# C++ code) so that in your .gurgitate-rules, you can say
|
22
|
+
#
|
23
|
+
# maildir "#{homedir}/Mail"
|
24
|
+
# sendmail "/usr/sbin/sendmail"
|
25
|
+
# spoolfile "Maildir"
|
26
|
+
# spooldir homedir
|
27
|
+
#
|
28
|
+
# This is because of an oddity in Ruby where, even if an
|
29
|
+
# accessor exists in the current object, if you say:
|
30
|
+
# name = value
|
31
|
+
# it'll always create a local variable. Not quite what you
|
32
|
+
# want when you're trying to set a config parameter. You have
|
33
|
+
# to say "self.name = value", which (I think) is ugly.
|
34
|
+
#
|
35
|
+
# In the interests of promoting harmony, of course, the previous
|
36
|
+
# syntax will continue to work.
|
37
|
+
def self.attr_configparam(*syms)
|
38
|
+
syms.each do |sym|
|
39
|
+
class_eval %{
|
40
|
+
def #{sym} (*vals)
|
41
|
+
if vals.length == 1
|
42
|
+
@#{sym} = vals[0]
|
43
|
+
elsif vals.length == 0
|
44
|
+
@#{sym}
|
45
|
+
else
|
46
|
+
raise ArgumentError,
|
47
|
+
"wrong number of arguments " +
|
48
|
+
"(\#{vals.length} for 0 or 1)"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Don't break it for the nice people who use
|
53
|
+
# old-style accessors though. Breaking people's
|
54
|
+
# .gurgitate-rules is a bad idea.
|
55
|
+
attr_writer :#{sym}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# The directory you want to put mail folders into
|
61
|
+
attr_configparam :maildir
|
62
|
+
|
63
|
+
# The path to your log file
|
64
|
+
attr_configparam :logfile
|
65
|
+
|
66
|
+
# The full path of your "sendmail" program
|
67
|
+
attr_configparam :sendmail
|
68
|
+
|
69
|
+
# Your home directory
|
70
|
+
attr_configparam :homedir
|
71
|
+
|
72
|
+
# Your default mail spool
|
73
|
+
attr_configparam :spoolfile
|
74
|
+
|
75
|
+
# The directory where user mail spools live
|
76
|
+
attr_configparam :spooldir
|
77
|
+
|
78
|
+
# What kind of mailboxes you prefer
|
79
|
+
# attr_configparam :folderstyle
|
80
|
+
|
81
|
+
# What kind of mailboxes you prefer
|
82
|
+
def folderstyle(*style)
|
83
|
+
if style.length == 0 then
|
84
|
+
style[0]
|
85
|
+
elsif style.length == 1 then
|
86
|
+
if style[0] == Maildir then
|
87
|
+
spooldir homedir
|
88
|
+
spoolfile File.join(spooldir,"Maildir")
|
89
|
+
else
|
90
|
+
spooldir "/var/spool/mail"
|
91
|
+
spoolfile File.join(spooldir, @passwd.name)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise ArgumentError, "wrong number of arguments "+
|
95
|
+
"(#{style.length} for 0 or 1)"
|
96
|
+
end
|
97
|
+
|
98
|
+
@folderstyle = style[0]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set config params to defaults, read in mail message from
|
102
|
+
# +input+
|
103
|
+
# input::
|
104
|
+
# Either the text of the email message in RFC-822 format,
|
105
|
+
# or a filehandle where the email message can be read from
|
106
|
+
# spooldir::
|
107
|
+
# The location of the mail spools directory.
|
108
|
+
def initialize(input=nil,spooldir="/var/spool/mail",&block)
|
109
|
+
@passwd = Etc.getpwuid
|
110
|
+
@homedir = @passwd.dir;
|
111
|
+
@maildir = File.join(@passwd.dir,"Mail")
|
112
|
+
@logfile = File.join(@passwd.dir,".gurgitate.log")
|
113
|
+
@sendmail = "/usr/lib/sendmail"
|
114
|
+
@spooldir = spooldir
|
115
|
+
@spoolfile = File.join(@spooldir,@passwd.name )
|
116
|
+
@folderstyle = MBox
|
117
|
+
@rules = []
|
118
|
+
|
119
|
+
input_text = ""
|
120
|
+
input.each do |l| input_text << l end
|
121
|
+
super(input_text)
|
122
|
+
instance_eval(&block) if block_given?
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_rules(filename, options = {})
|
126
|
+
if not Hash === options
|
127
|
+
raise ArgumentError.new("Expected hash of options")
|
128
|
+
end
|
129
|
+
if filename == :default
|
130
|
+
filename=homedir+"/.gurgitate-rules"
|
131
|
+
end
|
132
|
+
if not FileTest.exist?(filename)
|
133
|
+
filename = filename + '.rb'
|
134
|
+
end
|
135
|
+
if not FileTest.exist?(filename)
|
136
|
+
if options.has_key?(:user)
|
137
|
+
log("#{filename} does not exist.")
|
138
|
+
end
|
139
|
+
return false
|
140
|
+
end
|
141
|
+
if FileTest.file?(filename) and
|
142
|
+
( ( not options.has_key? :system and
|
143
|
+
FileTest.owned?(filename) ) or
|
144
|
+
( options.has_key? :system and
|
145
|
+
options[:system] == true and
|
146
|
+
File.stat(filename).uid == 0 ) ) and
|
147
|
+
FileTest.readable?(filename)
|
148
|
+
@rules << filename
|
149
|
+
else
|
150
|
+
log("#{filename} has bad permissions or ownership, not using rules")
|
151
|
+
return false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Deletes the current message.
|
156
|
+
def delete
|
157
|
+
# Well, nothing here, really.
|
158
|
+
end
|
159
|
+
|
160
|
+
# This is a neat one. You can get any header as a method.
|
161
|
+
# Say, if you want the header "X-Face", then you call
|
162
|
+
# x_face and that gets it for you. It raises NameError if
|
163
|
+
# that header isn't found.
|
164
|
+
# meth::
|
165
|
+
# The method that the caller tried to call which isn't
|
166
|
+
# handled any other way.
|
167
|
+
def method_missing(meth)
|
168
|
+
headername=meth.to_s.split(/_/).map {|x| x.capitalize}.join("-")
|
169
|
+
if defined?(headers[headername]) then
|
170
|
+
return headers[headername]
|
171
|
+
else
|
172
|
+
raise NameError,"undefined local variable or method, or header not found `#{meth}' for #{self}:#{self.class}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Forwards the message to +address+.
|
177
|
+
# address::
|
178
|
+
# A valid email address to forward the message to.
|
179
|
+
def forward(address)
|
180
|
+
self.log "Forwarding to "+address
|
181
|
+
IO.popen(@sendmail+" "+address,"w") do |f|
|
182
|
+
f.print(self.to_s)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Writes +message+ to the log file.
|
187
|
+
def log(message)
|
188
|
+
if(@logfile)then
|
189
|
+
File.open(@logfile,"a") do |f|
|
190
|
+
f.flock(File::LOCK_EX)
|
191
|
+
f.print(Time.new.to_s+" "+message+"\n")
|
192
|
+
f.flock(File::LOCK_UN)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Pipes the message through +program+. If +program+
|
198
|
+
# fails, puts the message into +spoolfile+
|
199
|
+
def pipe(program)
|
200
|
+
self.log "Piping through "+program
|
201
|
+
IO.popen(program,"w") do |f|
|
202
|
+
f.print(self.to_s)
|
203
|
+
end
|
204
|
+
return $?>>8
|
205
|
+
rescue SystemCallError
|
206
|
+
save(spoolfile())
|
207
|
+
return -1
|
208
|
+
end
|
209
|
+
|
210
|
+
# Pipes the message through +program+, and returns another
|
211
|
+
# +Gurgitate+ object containing the output of the filter
|
212
|
+
def filter(program,&block)
|
213
|
+
self.log "Filtering with "+program
|
214
|
+
IO.popen("-","w+") do |filter|
|
215
|
+
if filter.nil? then
|
216
|
+
exec(program)
|
217
|
+
else
|
218
|
+
if fork
|
219
|
+
filter.close_write
|
220
|
+
g=Gurgitate.new(filter)
|
221
|
+
g.instance_eval(&block) if block_given?
|
222
|
+
return g
|
223
|
+
else
|
224
|
+
filter.close_read
|
225
|
+
filter.print(self.to_s)
|
226
|
+
filter.close
|
227
|
+
exit
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
rescue SystemCallError
|
232
|
+
save(Spoolfile)
|
233
|
+
return nil
|
234
|
+
end
|
235
|
+
|
236
|
+
# Processes your .gurgitate-rules.rb.
|
237
|
+
def process(&block)
|
238
|
+
if @rules.size > 0 or block
|
239
|
+
@rules.each do |configfilespec|
|
240
|
+
begin
|
241
|
+
eval File.new(configfilespec).read, nil,
|
242
|
+
configfilespec
|
243
|
+
save(spoolfile)
|
244
|
+
rescue ScriptError
|
245
|
+
log "Couldn't load #{configfilespec}: "+$!
|
246
|
+
save(spoolfile)
|
247
|
+
rescue Exception
|
248
|
+
log "Error while executing #{configfilespec}: #{$!}"
|
249
|
+
$@.each { |tr| log "Backtrace: #{tr}" }
|
250
|
+
folderstyle = MBox
|
251
|
+
save(spoolfile)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
if block
|
255
|
+
instance_eval(&block)
|
256
|
+
end
|
257
|
+
else
|
258
|
+
save(spoolfile)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
|
3
|
+
#------------------------------------------------------------------------
|
4
|
+
# Code to handle saving a message to a mailbox (and a framework for detecting
|
5
|
+
# what kind of mailbox it is)
|
6
|
+
#------------------------------------------------------------------------
|
7
|
+
|
8
|
+
require "gurgitate/deliver/mbox"
|
9
|
+
require "gurgitate/deliver/maildir"
|
10
|
+
|
11
|
+
module Gurgitate
|
12
|
+
module Deliver
|
13
|
+
|
14
|
+
class MailboxFound < Exception
|
15
|
+
# more of a "flag" than an exception really
|
16
|
+
end
|
17
|
+
|
18
|
+
# Saves a message to +mailbox+, after detecting what the mailbox's
|
19
|
+
# format is.
|
20
|
+
# mailbox::
|
21
|
+
# A string containing the path of the mailbox to save
|
22
|
+
# the message to. If it is of the form "=mailbox", it
|
23
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
24
|
+
# it simply saves the message to the file +mailbox+.
|
25
|
+
def save(mailbox)
|
26
|
+
|
27
|
+
if mailbox[0,1]=='=' and @maildir != nil then
|
28
|
+
mailbox["="]=@maildir+"/"
|
29
|
+
end
|
30
|
+
|
31
|
+
if mailbox[0,1] != '/' then
|
32
|
+
log("Cannot save to relative filenames! Saving to spool file");
|
33
|
+
mailbox=spoolfile
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
[MBox,Maildir].each do |mod|
|
38
|
+
if mod::check_mailbox(mailbox) then
|
39
|
+
self.class.instance_eval do
|
40
|
+
include mod
|
41
|
+
raise MailboxFound
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Huh, nothing could find anything. Oh well,
|
47
|
+
# let's default to whatever's in @folderstyle. (I
|
48
|
+
# guess we'll be making a new mailbox, eh?)
|
49
|
+
|
50
|
+
if defined? @folderstyle then
|
51
|
+
#
|
52
|
+
# Careful we don't get the wrong instance variable
|
53
|
+
folderstyle=@folderstyle
|
54
|
+
|
55
|
+
self.class.instance_eval do
|
56
|
+
include folderstyle
|
57
|
+
end
|
58
|
+
else
|
59
|
+
# No hints from the user either. Let's guess!
|
60
|
+
# I'll use the same heuristic that Postfix uses--if the
|
61
|
+
# mailbox name ends with a /, then make it a Maildir,
|
62
|
+
# otherwise make it a mail file
|
63
|
+
if mailbox =~ /\/$/ then
|
64
|
+
include Maildir
|
65
|
+
else
|
66
|
+
include MBox
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
rescue MailboxFound
|
71
|
+
# Don't need to do anything--we only have to worry
|
72
|
+
# about it if there wasn't a mailbox there.
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
begin
|
77
|
+
deliver_message(mailbox)
|
78
|
+
rescue SystemCallError
|
79
|
+
self.log "Gack! Something went wrong: "+$!
|
80
|
+
exit 75
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
|
3
|
+
#------------------------------------------------------------------------
|
4
|
+
# Deliver mail to a maildir (also, detects maildir)
|
5
|
+
#------------------------------------------------------------------------
|
6
|
+
|
7
|
+
require "socket" # for gethostname (!)
|
8
|
+
|
9
|
+
module Gurgitate
|
10
|
+
module Deliver
|
11
|
+
module Maildir
|
12
|
+
# Figures out if +mailbox+ is a Maildir mailbox
|
13
|
+
# mailbox::
|
14
|
+
# A string containing the path of the mailbox to save the
|
15
|
+
# message to. If it is of the form "=mailbox", it saves
|
16
|
+
# the message to +Maildir+/+mailbox+. Otherwise, it
|
17
|
+
# simply saves the message to the file +mailbox+.
|
18
|
+
def self::check_mailbox(mailbox)
|
19
|
+
begin
|
20
|
+
if File.stat(mailbox).directory? then
|
21
|
+
if File.stat(File.join(mailbox,"cur")).directory? then
|
22
|
+
return Maildir
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Figures out the first available filename in the mail dir
|
31
|
+
# +dir+ and returns the filename to use.
|
32
|
+
# dir::
|
33
|
+
# One of "+mailbox+/tmp" or "+mailbox+/new", but that's
|
34
|
+
# only because that's what the maildir spec
|
35
|
+
# (http://cr.yp.to/proto/maildir.html) says.
|
36
|
+
def maildir_getfilename(dir)
|
37
|
+
time=Time.now.to_f
|
38
|
+
counter=0
|
39
|
+
hostname=Socket::gethostname
|
40
|
+
filename=nil
|
41
|
+
loop do
|
42
|
+
filename=File.join(dir,sprintf("%.4f.%d_%d.%s",
|
43
|
+
time,$$,counter,hostname))
|
44
|
+
break if not File.exists?(filename)
|
45
|
+
counter+=1
|
46
|
+
end
|
47
|
+
return filename
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates a new Maildir folder +mailbox+
|
51
|
+
# mailbox::
|
52
|
+
# The full path of the new folder to be created
|
53
|
+
def make_mailbox(mailbox)
|
54
|
+
Dir.mkdir(mailbox)
|
55
|
+
%w{cur tmp new}.each do |dir|
|
56
|
+
Dir.mkdir(File.join(mailbox,dir))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Delivers a message to the maildir-format mailbox +mailbox+.
|
61
|
+
# mailbox::
|
62
|
+
# A string containing the path of the mailbox to save the
|
63
|
+
# message to. If it is of the form "=mailbox", it saves
|
64
|
+
# the message to +Maildir+/+mailbox+. Otherwise, it
|
65
|
+
# simply saves the message to the file +mailbox+.
|
66
|
+
def deliver_message(mailbox)
|
67
|
+
begin
|
68
|
+
File.stat(mailbox)
|
69
|
+
rescue Errno::ENOENT
|
70
|
+
make_mailbox(mailbox)
|
71
|
+
end
|
72
|
+
|
73
|
+
tmpfilename=maildir_getfilename(File.join(mailbox,"tmp"))
|
74
|
+
File.open(tmpfilename,File::CREAT|File::WRONLY) do |fh|
|
75
|
+
fh.write(self.to_s)
|
76
|
+
fh.flush
|
77
|
+
# I should put a caveat here, unfortunately. Ruby's
|
78
|
+
# IO#flush only flushes Ruby's buffers, not the
|
79
|
+
# operating system's. If anyone knows how to force
|
80
|
+
# a real fflush(), I'd love to know. Otherwise, I'm
|
81
|
+
# going to hope that closing the file does the trick
|
82
|
+
# for me.
|
83
|
+
end
|
84
|
+
|
85
|
+
# ...and link to new.
|
86
|
+
# (I guess Maildir mailboxes don't work too well
|
87
|
+
# on Windows, eh?)
|
88
|
+
newfilename = maildir_getfilename(
|
89
|
+
File.join(mailbox,"new"))
|
90
|
+
begin
|
91
|
+
File.link(tmpfilename,newfilename)
|
92
|
+
rescue SystemCallError
|
93
|
+
log("Couldn't create maildir link to \"new\"!")
|
94
|
+
exit 75 # Argh, I tried, it didn't work out
|
95
|
+
end
|
96
|
+
File.delete(tmpfilename)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|