gurgitate-mail 1.8.4 → 1.8.5
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 +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
|