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.
@@ -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