gurgitate-mail 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|