gurgitate-mail 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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