gurgitate-mail 1.8.4 → 1.8.5
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/gurgitate-mail +5 -1
- data/lib/gurgitate-mail.rb +30 -28
- data/lib/gurgitate/deliver.rb +8 -11
- data/lib/gurgitate/deliver/maildir.rb +5 -1
- data/lib/gurgitate/headers.rb +106 -51
- data/lib/gurgitate/mailmessage.rb +56 -11
- data/test/gurgitate-test.rb +62 -0
- data/{test.rb → test/runtests.rb} +4 -1
- data/test/test_configuration.rb +35 -0
- data/test/test_delivery.rb +213 -0
- data/test/test_header.rb +154 -0
- data/test/test_headers.rb +478 -0
- data/test/test_process.rb +87 -0
- data/test/test_rules.rb +108 -0
- data/test/test_writing.rb +56 -0
- metadata +14 -6
data/bin/gurgitate-mail
CHANGED
data/lib/gurgitate-mail.rb
CHANGED
@@ -36,7 +36,7 @@ module Gurgitate
|
|
36
36
|
# syntax will continue to work.
|
37
37
|
def self.attr_configparam(*syms)
|
38
38
|
syms.each do |sym|
|
39
|
-
class_eval
|
39
|
+
class_eval %/
|
40
40
|
def #{sym} (*vals)
|
41
41
|
if vals.length == 1
|
42
42
|
@#{sym} = vals[0]
|
@@ -53,7 +53,7 @@ module Gurgitate
|
|
53
53
|
# old-style accessors though. Breaking people's
|
54
54
|
# .gurgitate-rules is a bad idea.
|
55
55
|
attr_writer :#{sym}
|
56
|
-
|
56
|
+
/
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
@@ -175,7 +175,7 @@ module Gurgitate
|
|
175
175
|
# handled any other way.
|
176
176
|
def method_missing(meth)
|
177
177
|
headername=meth.to_s.split(/_/).map {|x| x.capitalize}.join("-")
|
178
|
-
if
|
178
|
+
if headers[headername] then
|
179
179
|
return headers[headername]
|
180
180
|
else
|
181
181
|
raise NameError,"undefined local variable or method, or header not found `#{meth}' for #{self}:#{self.class}"
|
@@ -211,9 +211,6 @@ module Gurgitate
|
|
211
211
|
f.print(self.to_s)
|
212
212
|
end
|
213
213
|
return $?>>8
|
214
|
-
rescue SystemCallError
|
215
|
-
save(spoolfile())
|
216
|
-
return -1
|
217
214
|
end
|
218
215
|
|
219
216
|
# Pipes the message through +program+, and returns another
|
@@ -237,34 +234,39 @@ module Gurgitate
|
|
237
234
|
end
|
238
235
|
end
|
239
236
|
end
|
240
|
-
rescue SystemCallError
|
241
|
-
save(Spoolfile)
|
242
|
-
return nil
|
243
237
|
end
|
244
238
|
|
245
239
|
# Processes your .gurgitate-rules.rb.
|
246
240
|
def process(&block)
|
247
|
-
|
248
|
-
@rules.
|
249
|
-
|
250
|
-
|
251
|
-
configfilespec
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
241
|
+
begin
|
242
|
+
if @rules.size > 0 or block
|
243
|
+
@rules.each do |configfilespec|
|
244
|
+
begin
|
245
|
+
eval File.new(configfilespec).read, nil,
|
246
|
+
configfilespec
|
247
|
+
rescue ScriptError
|
248
|
+
log "Couldn't load #{configfilespec}: "+$!
|
249
|
+
save(spoolfile)
|
250
|
+
rescue Exception
|
251
|
+
log "Error while executing #{configfilespec}: #{$!}"
|
252
|
+
$@.each { |tr| log "Backtrace: #{tr}" }
|
253
|
+
folderstyle = MBox
|
254
|
+
save(spoolfile)
|
255
|
+
end
|
260
256
|
end
|
257
|
+
if block
|
258
|
+
instance_eval(&block)
|
259
|
+
end
|
260
|
+
log "Mail not covered by rules, saving to default spool"
|
261
|
+
save(spoolfile)
|
262
|
+
else
|
263
|
+
save(spoolfile)
|
261
264
|
end
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
log "
|
266
|
-
|
267
|
-
else
|
265
|
+
rescue Exception
|
266
|
+
log "Error while executing rules: #{$!}"
|
267
|
+
$@.each { |tr| log "Backtrace: #{tr}" }
|
268
|
+
log "Attempting to save to spoolfile after error"
|
269
|
+
folderstyle = MBox
|
268
270
|
save(spoolfile)
|
269
271
|
end
|
270
272
|
end
|
data/lib/gurgitate/deliver.rb
CHANGED
@@ -40,10 +40,8 @@ module Gurgitate
|
|
40
40
|
begin
|
41
41
|
[MBox,Maildir].each do |mod|
|
42
42
|
if mod::check_mailbox(mailbox) then
|
43
|
-
|
44
|
-
|
45
|
-
raise MailboxFound
|
46
|
-
end
|
43
|
+
extend mod
|
44
|
+
raise MailboxFound
|
47
45
|
end
|
48
46
|
end
|
49
47
|
|
@@ -51,23 +49,21 @@ module Gurgitate
|
|
51
49
|
# let's default to whatever's in @folderstyle. (I
|
52
50
|
# guess we'll be making a new mailbox, eh?)
|
53
51
|
|
54
|
-
if
|
52
|
+
if Module === @folderstyle then
|
55
53
|
#
|
56
54
|
# Careful we don't get the wrong instance variable
|
57
55
|
folderstyle=@folderstyle
|
58
56
|
|
59
|
-
|
60
|
-
include folderstyle
|
61
|
-
end
|
57
|
+
extend folderstyle
|
62
58
|
else
|
63
59
|
# No hints from the user either. Let's guess!
|
64
60
|
# I'll use the same heuristic that Postfix uses--if the
|
65
61
|
# mailbox name ends with a /, then make it a Maildir,
|
66
62
|
# otherwise make it a mail file
|
67
63
|
if mailbox =~ /\/$/ then
|
68
|
-
|
64
|
+
extend Maildir
|
69
65
|
else
|
70
|
-
|
66
|
+
extend MBox
|
71
67
|
end
|
72
68
|
end
|
73
69
|
|
@@ -81,7 +77,8 @@ module Gurgitate
|
|
81
77
|
deliver_message(mailbox)
|
82
78
|
rescue SystemCallError
|
83
79
|
self.log "Gack! Something went wrong: "+$!
|
84
|
-
|
80
|
+
raise
|
81
|
+
# exit 75
|
85
82
|
end
|
86
83
|
end
|
87
84
|
end
|
@@ -70,6 +70,10 @@ module Gurgitate
|
|
70
70
|
make_mailbox(mailbox)
|
71
71
|
end
|
72
72
|
|
73
|
+
unless File.stat(mailbox).directory?
|
74
|
+
raise SystemError, 'not a directory'
|
75
|
+
end
|
76
|
+
|
73
77
|
tmpfilename=maildir_getfilename(File.join(mailbox,"tmp"))
|
74
78
|
File.open(tmpfilename,File::CREAT|File::WRONLY) do |fh|
|
75
79
|
fh.write(self.to_s)
|
@@ -91,7 +95,7 @@ module Gurgitate
|
|
91
95
|
File.link(tmpfilename,newfilename)
|
92
96
|
rescue SystemCallError
|
93
97
|
log("Couldn't create maildir link to \"new\"!")
|
94
|
-
|
98
|
+
raise
|
95
99
|
end
|
96
100
|
File.delete(tmpfilename)
|
97
101
|
end
|
data/lib/gurgitate/headers.rb
CHANGED
@@ -65,6 +65,9 @@ module Gurgitate
|
|
65
65
|
# regex::
|
66
66
|
# The regular expression to match against the header's contents
|
67
67
|
def matches (regex)
|
68
|
+
if String === regex
|
69
|
+
regex = Regexp.new(Regexp.escape(regex))
|
70
|
+
end
|
68
71
|
@contents =~ regex
|
69
72
|
end
|
70
73
|
|
@@ -89,7 +92,6 @@ module Gurgitate
|
|
89
92
|
each do |header|
|
90
93
|
header.contents = header.contents.sub regex, replacement
|
91
94
|
end
|
92
|
-
p self
|
93
95
|
end
|
94
96
|
|
95
97
|
def sub(regex, replacement)
|
@@ -108,61 +110,91 @@ module Gurgitate
|
|
108
110
|
# A slightly bigger class for all of a message's headers
|
109
111
|
class Headers
|
110
112
|
|
111
|
-
|
112
|
-
# headertext::
|
113
|
-
# The text of the message headers.
|
114
|
-
def initialize(headertext)
|
115
|
-
@headers=Hash.new(nil)
|
116
|
-
@headertext=headertext
|
113
|
+
private
|
117
114
|
|
118
|
-
|
115
|
+
# Figures out whether the first line of a mail message is an
|
116
|
+
# mbox-style "From " line (say, if you get this from sendmail),
|
117
|
+
# or whether it's just a normal header.
|
118
|
+
# --
|
119
|
+
# If you run "fetchmail" with the -m option to feed the
|
120
|
+
# mail message straight to gurgitate, skipping the "local
|
121
|
+
# MTA" step, then it doesn't have a "From " line. So I
|
122
|
+
# have to deal with that by hand. First, check to see if
|
123
|
+
# there's a "From " line present in the first place.
|
124
|
+
def figure_out_from_line(headertext)
|
125
|
+
(unix_from,normal_headers) = headertext.split(/\n/,2)
|
119
126
|
|
120
|
-
# If you run "fetchmail" with the -m option to feed the
|
121
|
-
# mail message straight to gurgitate, skipping the "local
|
122
|
-
# MTA" step, then it doesn't have a "From " line. So I
|
123
|
-
# have to deal with that by hand. First, check to see if
|
124
|
-
# there's a "From " line present in the first place.
|
125
127
|
if unix_from =~ /^From / then
|
126
|
-
|
127
|
-
|
128
|
+
headertext=normal_headers
|
129
|
+
unix_from=unix_from
|
128
130
|
else
|
129
131
|
# If there isn't, then deal with it after we've
|
130
132
|
# worried about the rest of the headers, 'cos we'll
|
131
133
|
# have to make our own.
|
132
|
-
unix_from=
|
134
|
+
unix_from=nil
|
133
135
|
end
|
136
|
+
return unix_from, headertext
|
137
|
+
end
|
134
138
|
|
139
|
+
def parse_headers
|
135
140
|
@headertext.each do |h|
|
136
141
|
h.chomp!
|
137
142
|
if(h=~/^\s+/) then
|
138
143
|
@lastheader << h
|
139
144
|
else
|
140
145
|
header=Header.new(h)
|
141
|
-
@headers[header.name] ||= HeaderBag.new
|
146
|
+
@headers[header.name] ||= HeaderBag.new
|
142
147
|
@headers[header.name].push(header)
|
143
148
|
@lastheader=header
|
144
149
|
end
|
145
150
|
end
|
146
151
|
|
147
152
|
@headers_changed=false
|
153
|
+
end
|
148
154
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
155
|
+
# Get the envelope From information. This comes with a
|
156
|
+
# whole category of rants: this information is absurdly hard
|
157
|
+
# to get your hands on. The best you can manage is a sort
|
158
|
+
# of educated guess. Thus, this horrible glob of hackiness.
|
159
|
+
# I don't recommend looking too closely at this code if you
|
160
|
+
# can avoid it, and further I recommend making sure to
|
161
|
+
# configure your MTA so that it sends proper sender and
|
162
|
+
# recipient information to gurgitate so that this code never
|
163
|
+
# has to be run at all.
|
164
|
+
def guess_sender
|
165
|
+
# Start by worrying about the "From foo@bar" line. If it's
|
166
|
+
# not there, then make one up from the Return-Path: header.
|
167
|
+
# If there isn't a "Return-Path:" header (then I suspect we
|
168
|
+
# have bigger problems, but still) then use From: as a wild
|
169
|
+
# guess. If I hope that this entire lot of code doesn't get
|
170
|
+
# used, then I _particularly_ hope that things never get so
|
171
|
+
# bad that poor gurgitate has to use the From: header as a
|
172
|
+
# source of authoritative information on anything.
|
153
173
|
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
if unix_from
|
160
|
-
|
174
|
+
# And then after all that fuss, if we're delivering to a
|
175
|
+
# Maildir, I have to get rid of it. And sometimes the MTA
|
176
|
+
# gives me a mbox-style From line and sometimes it doesn't.
|
177
|
+
# It's annoying, but I have no choice but to Just Deal With
|
178
|
+
# It.
|
179
|
+
if @unix_from then
|
180
|
+
# If it is there, then grab the email address in it and
|
181
|
+
# use that as our official "from".
|
182
|
+
fromregex=/^From ([^ ]+@[^ ]+) /
|
183
|
+
fromregex.match(@unix_from)
|
184
|
+
@from=$+
|
185
|
+
|
186
|
+
# or maybe it's local
|
187
|
+
if @from == nil then
|
188
|
+
@unix_from =~ /^From (\S+) /
|
189
|
+
@from=$+
|
190
|
+
end
|
191
|
+
else
|
192
|
+
fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+)/
|
161
193
|
if self["Return-Path"] != nil then
|
162
|
-
fromregex.match(self["Return-Path"][0].contents)
|
194
|
+
fromregex.match(self["Return-Path"][0].contents)
|
163
195
|
else
|
164
196
|
if self["From"] != nil then
|
165
|
-
fromregex.match(self["From"][0].contents)
|
197
|
+
fromregex.match(self["From"][0].contents)
|
166
198
|
end
|
167
199
|
end
|
168
200
|
address_candidate=$+
|
@@ -170,32 +202,53 @@ module Gurgitate
|
|
170
202
|
# If there STILL isn't a match, then it's probably safe to
|
171
203
|
# assume that it's local mail, and doesn't have an @ in its
|
172
204
|
# address.
|
173
|
-
|
205
|
+
unless address_candidate
|
174
206
|
if self["Return-Path"] != nil then
|
175
207
|
self["Return-Path"][0].contents =~ /(\S+)/
|
176
208
|
address_candidate=$+
|
177
209
|
else
|
178
|
-
|
179
|
-
self["From"][0].contents =~ /(\S+)/
|
180
|
-
end
|
210
|
+
self["From"][0].contents =~ /(\S+)/
|
181
211
|
address_candidate=$+
|
182
212
|
end
|
183
213
|
end
|
184
214
|
|
185
215
|
@from=address_candidate
|
186
216
|
|
187
|
-
@unix_from="From "+self.from+" "+Time.new.to_s
|
217
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
public
|
222
|
+
|
223
|
+
# Creates a Headers object.
|
224
|
+
# headertext::
|
225
|
+
# The text of the message headers.
|
226
|
+
def initialize(headertext=nil,sender=nil, recipient=nil)
|
227
|
+
@from = sender
|
228
|
+
@to = recipient
|
229
|
+
@headers = Hash.new(nil)
|
230
|
+
|
231
|
+
if Hash === headertext
|
232
|
+
@headers_changed = true
|
233
|
+
headertext.each_key do |key|
|
234
|
+
|
235
|
+
headername = key.to_s.gsub("_","-")
|
236
|
+
|
237
|
+
header=Header.new(headername, headertext[key])
|
238
|
+
@headers[header.name] ||= HeaderBag.new
|
239
|
+
@headers[header.name].push(header)
|
240
|
+
end
|
188
241
|
else
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
fromregex.match(unix_from);
|
193
|
-
@from=$+
|
242
|
+
if headertext
|
243
|
+
@unix_from, @headertext = figure_out_from_line headertext
|
244
|
+
parse_headers if @headertext
|
194
245
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
246
|
+
if sender # then don't believe the mbox separator
|
247
|
+
@from = sender
|
248
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
249
|
+
else
|
250
|
+
guess_sender
|
251
|
+
end
|
199
252
|
end
|
200
253
|
end
|
201
254
|
end
|
@@ -226,7 +279,7 @@ module Gurgitate
|
|
226
279
|
#
|
227
280
|
# Yet another bucket of rants. Unix mail sucks.
|
228
281
|
def to
|
229
|
-
return @headers["X-Original-To"] || nil
|
282
|
+
return @to || @headers["X-Original-To"] || nil
|
230
283
|
end
|
231
284
|
|
232
285
|
# Who the message is from (the envelope from)
|
@@ -239,7 +292,7 @@ module Gurgitate
|
|
239
292
|
# newfrom:: An email address
|
240
293
|
def from=(newfrom)
|
241
294
|
@from=newfrom
|
242
|
-
@unix_from="From "+self.from+" "+Time.new.to_s
|
295
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
243
296
|
end
|
244
297
|
|
245
298
|
# Match header +name+ against +regex+
|
@@ -262,9 +315,11 @@ module Gurgitate
|
|
262
315
|
# regex:: The regex to match the headers against.
|
263
316
|
def matches(names,regex)
|
264
317
|
ret=false
|
265
|
-
|
266
|
-
|
318
|
+
|
319
|
+
if names.class == String then
|
320
|
+
names=[names]
|
267
321
|
end
|
322
|
+
|
268
323
|
names.each do |n|
|
269
324
|
ret |= match(n,regex)
|
270
325
|
end
|
@@ -281,9 +336,9 @@ module Gurgitate
|
|
281
336
|
# the "From " line
|
282
337
|
def to_s
|
283
338
|
if @headers_changed then
|
284
|
-
return @headers.
|
285
|
-
h.
|
286
|
-
|
339
|
+
return @headers.map do |n,h|
|
340
|
+
h.map do |h| h.to_s end.join("\n")
|
341
|
+
end.join("\n")
|
287
342
|
else
|
288
343
|
return @headertext
|
289
344
|
end
|
@@ -7,22 +7,67 @@
|
|
7
7
|
require 'gurgitate/headers'
|
8
8
|
|
9
9
|
module Gurgitate
|
10
|
-
# A complete mail message.
|
11
10
|
|
11
|
+
# A complete mail message.
|
12
12
|
class Mailmessage
|
13
|
+
|
14
|
+
Fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<][<](.*@.*)[>]|([^ ]+@[^ ]+)/;
|
15
|
+
|
13
16
|
# The headers of the message
|
14
17
|
attr_reader :headers
|
15
18
|
# The body of the message
|
16
19
|
attr_accessor :body
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# The envelope sender and recipient, if anyone thought to
|
22
|
+
# mention them to us.
|
23
|
+
attr_accessor :sender
|
24
|
+
attr_accessor :recipient
|
25
|
+
|
26
|
+
# Creates a new mail message with headers built from the options hash,
|
27
|
+
# and the body of the message in a string.
|
28
|
+
def self.create(*args)
|
29
|
+
options = body = nil
|
30
|
+
|
31
|
+
if String === args[0]
|
32
|
+
options = args[1]
|
33
|
+
body = args[0]
|
34
|
+
elsif Hash === args[0]
|
35
|
+
options = args[0]
|
36
|
+
end
|
37
|
+
|
38
|
+
options = options.clone # just in case, since I'm meddling with it
|
39
|
+
message = self.new
|
40
|
+
|
41
|
+
message.instance_eval do
|
42
|
+
if body
|
43
|
+
@body=body
|
44
|
+
end
|
45
|
+
|
46
|
+
%w/sender recipient body/.each do |key|
|
47
|
+
if options.has_key? key.to_sym
|
48
|
+
instance_variable_set("@#{key}", options[key.to_sym])
|
49
|
+
options.delete key
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@headers = Headers.new(options)
|
54
|
+
end
|
55
|
+
|
56
|
+
message
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(text=nil, recipient=nil, sender=nil)
|
60
|
+
if text
|
61
|
+
(@headertext,@body)=text.split(/\n\n/,2)
|
62
|
+
@headers=Headers.new(@headertext);
|
63
|
+
Fromregex.match(@headers["From"][0].contents);
|
64
|
+
@from=$+
|
65
|
+
@recipient = recipient
|
66
|
+
@sender = sender
|
67
|
+
else
|
68
|
+
@headers = Headers.new
|
69
|
+
@body = ""
|
70
|
+
end
|
26
71
|
end
|
27
72
|
|
28
73
|
# Returns the header +name+
|
@@ -40,9 +85,9 @@ module Gurgitate
|
|
40
85
|
# def to; @headers["To","Cc"]; end
|
41
86
|
|
42
87
|
# Returns the formatted mail message
|
43
|
-
def to_s; @headers.to_s + ( @body || ""); end
|
88
|
+
def to_s; @headers.to_s + "\n\n" + ( @body || ""); end
|
44
89
|
|
45
90
|
# Returns the mail message formatted for mbox
|
46
|
-
def to_mbox; @headers.to_mbox + @body; end
|
91
|
+
def to_mbox; @headers.to_mbox + "\n\n" + @body; end
|
47
92
|
end
|
48
93
|
end
|