gurgitate-mail 1.10.9 → 1.10.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/gurgitate-mail.rb +386 -0
- data/lib/gurgitate/deliver.rb +87 -0
- data/lib/gurgitate/deliver/maildir.rb +104 -0
- data/lib/gurgitate/deliver/mbox.rb +47 -0
- data/lib/gurgitate/deliver/mh.rb +147 -0
- data/lib/gurgitate/header.rb +82 -0
- data/lib/gurgitate/headers.rb +283 -0
- data/lib/gurgitate/mail_headers.rb +167 -0
- data/lib/gurgitate/mailmessage.rb +142 -0
- data/lib/gurgitate/message.rb +114 -0
- data/test/test_delivery.rb +1 -1
- metadata +58 -65
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 578b870727d42aad907a83379eff2df9dab9c704
|
4
|
+
data.tar.gz: f76eaacb28f9cbc0582241dacc17f82db06a5013
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b636bf531b2c0a5d217d9a673d9b52e3cd7fa01ae8306380f7a8d3e0cb97778dfcc0eec7e0b1774fb247c1b542da34899b5612bdb2e176f0ea7887ce08326434
|
7
|
+
data.tar.gz: a99dacff953b461fce793845509976505e9e2840d272dd18a4c96e1427810c4d94a794b0d7d949250dedd6c9eeed273872d9a380e3db58a5034834ff2d1cf0cb
|
@@ -0,0 +1,386 @@
|
|
1
|
+
#!/opt/bin/ruby
|
2
|
+
# -*- encoding : utf-8 -*-
|
3
|
+
#------------------------------------------------------------------------
|
4
|
+
# Mail filter package
|
5
|
+
#------------------------------------------------------------------------
|
6
|
+
|
7
|
+
require 'etc'
|
8
|
+
|
9
|
+
require 'gurgitate/mailmessage'
|
10
|
+
require 'gurgitate/deliver'
|
11
|
+
|
12
|
+
module Gurgitate
|
13
|
+
# This is the actual gurgitator; it reads a message and then it can
|
14
|
+
# do other stuff with it, like saving it to a mailbox or forwarding
|
15
|
+
# it somewhere else.
|
16
|
+
#
|
17
|
+
# To set configuration parameters for gurgitate-mail, use a keyword-
|
18
|
+
# based system. It's almost like an attribute, only if you give the
|
19
|
+
# accessor a parameter, it will set the configuration parameter to
|
20
|
+
# the parameter's value. For instance:
|
21
|
+
#
|
22
|
+
# maildir "#{homedir}/Mail"
|
23
|
+
# sendmail "/usr/sbin/sendmail"
|
24
|
+
# spoolfile "Maildir"
|
25
|
+
# spooldir homedir
|
26
|
+
#
|
27
|
+
# (This is because of an oddity in Ruby where, even if an
|
28
|
+
# accessor exists in the current object, if you say:
|
29
|
+
# name = value
|
30
|
+
# it'll always create a local variable. Not quite what you
|
31
|
+
# want when you're trying to set a config parameter. You have
|
32
|
+
# to say <code>self.name = value</code>, which [I think] is ugly.
|
33
|
+
#
|
34
|
+
# In the interests of promoting harmony, of course,
|
35
|
+
# <code>self.name = value</code> still works.)
|
36
|
+
#
|
37
|
+
# The attributes you can define are:
|
38
|
+
#
|
39
|
+
# homedir :: Your home directory. This defaults to what your
|
40
|
+
# actual home directory is.
|
41
|
+
#
|
42
|
+
# maildir :: The directory you store your mail in. This defaults
|
43
|
+
# to the "Mail" directory in your home dir.
|
44
|
+
#
|
45
|
+
# logfile :: The path to your gurgitate-mail log file. If you
|
46
|
+
# set this to +nil+, gurgitate-mail won't log anything.
|
47
|
+
# The default value is ".gurgitate.log" in your home
|
48
|
+
# directory.
|
49
|
+
#
|
50
|
+
# The following parameters are more likely to be interesting to the
|
51
|
+
# system administrator than the everyday user.
|
52
|
+
#
|
53
|
+
# sendmail :: The full path of your "sendmail" program, or at least
|
54
|
+
# a program that provides functionality equivalent to
|
55
|
+
# sendmail.
|
56
|
+
#
|
57
|
+
# spoolfile :: The default location to store mail messages, for the
|
58
|
+
# messages that have been unaffected by your gurgitate
|
59
|
+
# rules. If an exception is raised by your rules, the
|
60
|
+
# message will be delivered to the spoolfile.
|
61
|
+
#
|
62
|
+
# spooldir :: The location where users' system mail boxes live.
|
63
|
+
#
|
64
|
+
# folderstyle :: The style of mailbox to create (and to expect,
|
65
|
+
# although gurgitate-mail automatically detects the
|
66
|
+
# type of existing mailboxes). See the separate
|
67
|
+
# documentation for folderstyle for more details.
|
68
|
+
class Gurgitate < Mailmessage
|
69
|
+
include Deliver
|
70
|
+
|
71
|
+
# Instead of the usual attributes, I went with a
|
72
|
+
# reader-is-writer type thing (as seen quite often in Perl and
|
73
|
+
# C++ code) so that in your .gurgitate-rules, you can say
|
74
|
+
#
|
75
|
+
# maildir "#{homedir}/Mail"
|
76
|
+
# sendmail "/usr/sbin/sendmail"
|
77
|
+
# spoolfile "Maildir"
|
78
|
+
# spooldir homedir
|
79
|
+
#
|
80
|
+
# This is because of an oddity in Ruby where, even if an
|
81
|
+
# accessor exists in the current object, if you say:
|
82
|
+
# name = value
|
83
|
+
# it'll always create a local variable. Not quite what you
|
84
|
+
# want when you're trying to set a config parameter. You have
|
85
|
+
# to say "self.name = value", which (I think) is ugly.
|
86
|
+
#
|
87
|
+
# In the interests of promoting harmony, of course, the previous
|
88
|
+
# syntax will continue to work.
|
89
|
+
def self.attr_configparam(*syms)
|
90
|
+
syms.each do |sym|
|
91
|
+
class_eval %{
|
92
|
+
def #{sym} (*vals)
|
93
|
+
if vals.length == 1
|
94
|
+
@#{sym} = vals[0]
|
95
|
+
elsif vals.length == 0
|
96
|
+
@#{sym}
|
97
|
+
else
|
98
|
+
raise ArgumentError,
|
99
|
+
"wrong number of arguments " +
|
100
|
+
"(\#{vals.length} for 0 or 1)"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Don't break it for the nice people who use
|
105
|
+
# old-style accessors though. Breaking people's
|
106
|
+
# .gurgitate-rules is a bad idea.
|
107
|
+
attr_writer :#{sym}
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# The directory you want to put mail folders into
|
113
|
+
attr_configparam :maildir
|
114
|
+
|
115
|
+
# The path to your log file
|
116
|
+
attr_configparam :logfile
|
117
|
+
|
118
|
+
# The full path of your "sendmail" program
|
119
|
+
attr_configparam :sendmail
|
120
|
+
|
121
|
+
# Your home directory
|
122
|
+
attr_configparam :homedir
|
123
|
+
|
124
|
+
# Your default mail spool
|
125
|
+
attr_configparam :spoolfile
|
126
|
+
|
127
|
+
# The directory where user mail spools live
|
128
|
+
attr_configparam :spooldir
|
129
|
+
|
130
|
+
# What kind of mailboxes you prefer
|
131
|
+
# attr_configparam :folderstyle
|
132
|
+
|
133
|
+
# What kind of mailboxes you prefer. Treat this like a
|
134
|
+
# configuration parameter. If no argument is given, then
|
135
|
+
# return the current default type.
|
136
|
+
#
|
137
|
+
# Depending on what you set this to, some other configuration
|
138
|
+
# parameters change. You can set this to the following things:
|
139
|
+
#
|
140
|
+
# <code>Maildir</code> :: Create Maildir mailboxes.
|
141
|
+
#
|
142
|
+
# This sets +spooldir+ to your home
|
143
|
+
# directory, +spoolfile+ to
|
144
|
+
# $HOME/Maildir and creates
|
145
|
+
# mail folders underneath that.
|
146
|
+
#
|
147
|
+
# <code>MH</code> :: Create MH mail boxes.
|
148
|
+
#
|
149
|
+
# This reads your <code>.mh_profile</code>
|
150
|
+
# file to find out where you've told MH to
|
151
|
+
# find its mail folders, and uses that value.
|
152
|
+
# If it can't find that in your .mh_profile,
|
153
|
+
# it will assume you want mailboxes in
|
154
|
+
# $HOME/Mail. It sets +spoolfile+ to
|
155
|
+
# "inbox" in your mail directory.
|
156
|
+
#
|
157
|
+
# <code>Mbox</code> :: Create +mbox+ mailboxes.
|
158
|
+
#
|
159
|
+
# This sets +spooldir+ to
|
160
|
+
# <code>/var/spool/mail</code> and
|
161
|
+
# +spoolfile+ to a file with your username
|
162
|
+
# in <code>/var/spool/mail</code>.
|
163
|
+
def folderstyle(*style)
|
164
|
+
if style.length == 0 then
|
165
|
+
@folderstyle
|
166
|
+
elsif style.length == 1 then
|
167
|
+
if style[0] == Maildir then
|
168
|
+
spooldir homedir
|
169
|
+
spoolfile File.join(spooldir,"Maildir")
|
170
|
+
maildir spoolfile
|
171
|
+
elsif style[0] == MH then
|
172
|
+
mh_profile_path = File.join(ENV["HOME"],".mh_profile")
|
173
|
+
if File.exists?(mh_profile_path) then
|
174
|
+
mh_profile = YAML.load(File.read(mh_profile_path))
|
175
|
+
maildir mh_profile["Path"]
|
176
|
+
else
|
177
|
+
maildir File.join(ENV["HOME"],"Mail")
|
178
|
+
end
|
179
|
+
spoolfile File.join(maildir,"inbox")
|
180
|
+
else
|
181
|
+
spooldir "/var/spool/mail"
|
182
|
+
spoolfile File.join(spooldir, @passwd.name)
|
183
|
+
end
|
184
|
+
|
185
|
+
@folderstyle = style[0]
|
186
|
+
else
|
187
|
+
raise ArgumentError, "wrong number of arguments "+
|
188
|
+
"(#{style.length} for 0 or 1)"
|
189
|
+
end
|
190
|
+
@folderstyle
|
191
|
+
end
|
192
|
+
|
193
|
+
# Set config params to defaults, read in mail message from
|
194
|
+
# +input+
|
195
|
+
# input::
|
196
|
+
# Either the text of the email message in RFC-822 format,
|
197
|
+
# or a filehandle where the email message can be read from
|
198
|
+
# recipient::
|
199
|
+
# The contents of the envelope recipient parameter
|
200
|
+
# sender::
|
201
|
+
# The envelope sender parameter
|
202
|
+
# spooldir::
|
203
|
+
# The location of the mail spools directory.
|
204
|
+
def initialize(input=nil,
|
205
|
+
recipient=nil,
|
206
|
+
sender=nil,
|
207
|
+
spooldir="/var/spool/mail",
|
208
|
+
&block)
|
209
|
+
@passwd = Etc.getpwuid
|
210
|
+
@homedir = @passwd.dir;
|
211
|
+
@maildir = File.join(@passwd.dir,"Mail")
|
212
|
+
@logfile = File.join(@passwd.dir,".gurgitate.log")
|
213
|
+
@sendmail = "/usr/lib/sendmail"
|
214
|
+
@spooldir = spooldir
|
215
|
+
@spoolfile = File.join(@spooldir,@passwd.name )
|
216
|
+
@folderstyle = MBox
|
217
|
+
@rules = []
|
218
|
+
|
219
|
+
input_text = ""
|
220
|
+
input.each_line do |l| input_text << l end
|
221
|
+
super(input_text, recipient, sender)
|
222
|
+
instance_eval(&block) if block_given?
|
223
|
+
end
|
224
|
+
|
225
|
+
def add_rules(filename, options = {}) #:nodoc:
|
226
|
+
if not Hash === options
|
227
|
+
raise ArgumentError.new("Expected hash of options")
|
228
|
+
end
|
229
|
+
if filename == :default
|
230
|
+
filename=homedir+"/.gurgitate-rules"
|
231
|
+
end
|
232
|
+
if not FileTest.exist?(filename)
|
233
|
+
filename = filename + '.rb'
|
234
|
+
end
|
235
|
+
if not FileTest.exist?(filename)
|
236
|
+
if options.has_key?(:user)
|
237
|
+
log("#{filename} does not exist.")
|
238
|
+
end
|
239
|
+
return false
|
240
|
+
end
|
241
|
+
if FileTest.file?(filename) and
|
242
|
+
( ( not options.has_key? :system and
|
243
|
+
FileTest.owned?(filename) ) or
|
244
|
+
( options.has_key? :system and
|
245
|
+
options[:system] == true and
|
246
|
+
File.stat(filename).uid == 0 ) ) and
|
247
|
+
FileTest.readable?(filename)
|
248
|
+
@rules << filename
|
249
|
+
else
|
250
|
+
log("#{filename} has bad permissions or ownership, not using rules")
|
251
|
+
return false
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Deletes (discards) the current message.
|
256
|
+
def delete
|
257
|
+
# Well, nothing here, really.
|
258
|
+
end
|
259
|
+
|
260
|
+
# This is kind of neat. You can get a header by calling its
|
261
|
+
# name as a method. For example, if you want the header
|
262
|
+
# "X-Face", then you call x_face and that gets it for you. It
|
263
|
+
# raises NameError if that header isn't found.
|
264
|
+
#
|
265
|
+
# meth::
|
266
|
+
# The method that the caller tried to call which isn't
|
267
|
+
# handled any other way.
|
268
|
+
def method_missing(meth)
|
269
|
+
headername=meth.to_s.split(/_/).map {|x| x.capitalize}.join("-")
|
270
|
+
if headers[headername] then
|
271
|
+
return headers[headername]
|
272
|
+
else
|
273
|
+
raise NameError,"undefined local variable or method, or header not found `#{meth}' for #{self}:#{self.class}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Forwards the message to +address+.
|
278
|
+
#
|
279
|
+
# address::
|
280
|
+
# A valid email address to forward the message to.
|
281
|
+
def forward(address)
|
282
|
+
self.log "Forwarding to "+address
|
283
|
+
IO.popen(@sendmail+" "+address,"w") do |f|
|
284
|
+
f.print(self.to_s)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Writes +message+ to the log file.
|
289
|
+
def log(message)
|
290
|
+
if @logfile then
|
291
|
+
File.open(@logfile,"a") do |f|
|
292
|
+
f.flock(File::LOCK_EX)
|
293
|
+
f.print(Time.new.to_s+" "+message+"\n")
|
294
|
+
f.flock(File::LOCK_UN)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Pipes the message through +program+. If +program+
|
300
|
+
# fails, puts the message into +spoolfile+
|
301
|
+
def pipe(program)
|
302
|
+
self.log "Piping through "+program
|
303
|
+
IO.popen(program,"w") do |f|
|
304
|
+
f.print(self.to_s)
|
305
|
+
end
|
306
|
+
return $?>>8
|
307
|
+
end
|
308
|
+
|
309
|
+
# Pipes the message through +program+, and returns another
|
310
|
+
# +Gurgitate+ object containing the output of the filter
|
311
|
+
#
|
312
|
+
# Use it like this:
|
313
|
+
#
|
314
|
+
# filter "bogofilter -p" do
|
315
|
+
# if x_bogosity =~ /Spam/ then
|
316
|
+
# log "Found spam"
|
317
|
+
# delete
|
318
|
+
# return
|
319
|
+
# end
|
320
|
+
# end
|
321
|
+
#
|
322
|
+
def filter(program,&block)
|
323
|
+
self.log "Filtering with "+program
|
324
|
+
IO.popen("-","w+") do |filter|
|
325
|
+
unless filter then
|
326
|
+
begin
|
327
|
+
exec(program)
|
328
|
+
rescue
|
329
|
+
exit! # should not get here anyway
|
330
|
+
end
|
331
|
+
else
|
332
|
+
if fork
|
333
|
+
filter.close_write
|
334
|
+
g=Gurgitate.new(filter)
|
335
|
+
g.instance_eval(&block) if block_given?
|
336
|
+
return g
|
337
|
+
else
|
338
|
+
begin
|
339
|
+
filter.close_read
|
340
|
+
filter.print to_s
|
341
|
+
filter.close
|
342
|
+
rescue
|
343
|
+
nil
|
344
|
+
ensure
|
345
|
+
exit!
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def process(&block) #:nodoc:
|
353
|
+
begin
|
354
|
+
if @rules.size > 0 or block
|
355
|
+
@rules.each do |configfilespec|
|
356
|
+
begin
|
357
|
+
eval File.new(configfilespec).read, nil,
|
358
|
+
configfilespec
|
359
|
+
rescue ScriptError
|
360
|
+
log "Couldn't load #{configfilespec}: "+$!
|
361
|
+
save(spoolfile)
|
362
|
+
rescue Exception
|
363
|
+
log "Error while executing #{configfilespec}: #{$!}"
|
364
|
+
$@.each { |tr| log "Backtrace: #{tr}" }
|
365
|
+
folderstyle MBox
|
366
|
+
save(spoolfile)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
if block
|
370
|
+
instance_eval(&block)
|
371
|
+
end
|
372
|
+
log "Mail not covered by rules, saving to default spool"
|
373
|
+
save(spoolfile)
|
374
|
+
else
|
375
|
+
save(spoolfile)
|
376
|
+
end
|
377
|
+
rescue Exception
|
378
|
+
log "Error while executing rules: #{$!}"
|
379
|
+
$@.each { |tr| log "Backtrace: #{tr}" }
|
380
|
+
log "Attempting to save to spoolfile after error"
|
381
|
+
folderstyle MBox
|
382
|
+
save(spoolfile)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
# -*- encoding : utf-8 -*-
|
3
|
+
|
4
|
+
#------------------------------------------------------------------------
|
5
|
+
# Code to handle saving a message to a mailbox (and a framework for detecting
|
6
|
+
# what kind of mailbox it is)
|
7
|
+
#------------------------------------------------------------------------
|
8
|
+
|
9
|
+
require "gurgitate/deliver/mbox"
|
10
|
+
require "gurgitate/deliver/maildir"
|
11
|
+
require "gurgitate/deliver/mh"
|
12
|
+
|
13
|
+
module Gurgitate
|
14
|
+
module Deliver
|
15
|
+
|
16
|
+
class MailboxFound < Exception
|
17
|
+
# more of a "flag" than an exception really
|
18
|
+
end
|
19
|
+
|
20
|
+
# Saves a message to +mailbox+, after detecting what the mailbox's
|
21
|
+
# format is.
|
22
|
+
# mailbox::
|
23
|
+
# A string containing the path of the mailbox to save
|
24
|
+
# the message to. If it is of the form "=mailbox", it
|
25
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
26
|
+
# it simply saves the message to the file +mailbox+.
|
27
|
+
def save(mailbox)
|
28
|
+
|
29
|
+
if mailbox[0,1]=='=' and @maildir != nil then
|
30
|
+
if @folderstyle == Maildir and mailbox !~ /^=\./ then
|
31
|
+
mailbox["="]=@maildir+"/."
|
32
|
+
else
|
33
|
+
mailbox["="]=@maildir+"/"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if mailbox[0,1] != '/' then
|
38
|
+
log("Cannot save to relative filenames! Saving to spool file");
|
39
|
+
mailbox=spoolfile
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
[MBox,Maildir,MH].each do |mod|
|
44
|
+
if mod::check_mailbox(mailbox) then
|
45
|
+
extend mod
|
46
|
+
raise MailboxFound
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Huh, nothing could find anything. Oh well,
|
51
|
+
# let's default to whatever's in @folderstyle. (I
|
52
|
+
# guess we'll be making a new mailbox, eh?)
|
53
|
+
|
54
|
+
if Module === @folderstyle then
|
55
|
+
#
|
56
|
+
# Careful we don't get the wrong instance variable
|
57
|
+
folderstyle=@folderstyle
|
58
|
+
|
59
|
+
extend folderstyle
|
60
|
+
else
|
61
|
+
# No hints from the user either. Let's guess!
|
62
|
+
# I'll use the same heuristic that Postfix uses--if the
|
63
|
+
# mailbox name ends with a /, then make it a Maildir,
|
64
|
+
# otherwise make it a mail file
|
65
|
+
if mailbox =~ /\/$/ then
|
66
|
+
extend Maildir
|
67
|
+
else
|
68
|
+
extend MBox
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
rescue MailboxFound
|
73
|
+
# Don't need to do anything--we only have to worry
|
74
|
+
# about it if there wasn't a mailbox there.
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
begin
|
79
|
+
deliver_message(mailbox)
|
80
|
+
rescue SystemCallError
|
81
|
+
self.log "Gack! Something went wrong: " + $!.to_s
|
82
|
+
raise
|
83
|
+
# exit 75
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|