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
@@ -0,0 +1,104 @@
|
|
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.exist?(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
|
+
unless File.stat(mailbox).directory?
|
74
|
+
raise SystemError, 'not a directory'
|
75
|
+
end
|
76
|
+
|
77
|
+
tmpfilename=maildir_getfilename(File.join(mailbox,"tmp"))
|
78
|
+
File.open(tmpfilename,File::CREAT|File::WRONLY) do |fh|
|
79
|
+
fh.write(self.to_s)
|
80
|
+
fh.flush
|
81
|
+
# I should put a caveat here, unfortunately. Ruby's
|
82
|
+
# IO#flush only flushes Ruby's buffers, not the
|
83
|
+
# operating system's. If anyone knows how to force
|
84
|
+
# a real fflush(), I'd love to know. Otherwise, I'm
|
85
|
+
# going to hope that closing the file does the trick
|
86
|
+
# for me.
|
87
|
+
end
|
88
|
+
|
89
|
+
# ...and link to new.
|
90
|
+
# (I guess Maildir mailboxes don't work too well
|
91
|
+
# on Windows, eh?)
|
92
|
+
newfilename = maildir_getfilename(
|
93
|
+
File.join(mailbox,"new"))
|
94
|
+
begin
|
95
|
+
File.link(tmpfilename,newfilename)
|
96
|
+
rescue SystemCallError
|
97
|
+
log("Couldn't create maildir link to \"new\"!")
|
98
|
+
exit 75 # Argh, I tried, it didn't work out
|
99
|
+
end
|
100
|
+
File.delete(tmpfilename)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
|
3
|
+
#------------------------------------------------------------------------
|
4
|
+
# Delivers a message to an mbox (also includes mbox detector)
|
5
|
+
#------------------------------------------------------------------------
|
6
|
+
|
7
|
+
module Gurgitate
|
8
|
+
module Deliver
|
9
|
+
module MBox
|
10
|
+
# Checks to see if +mailbox+ is an mbox mailbox
|
11
|
+
# mailbox::
|
12
|
+
# A string containing the path of the mailbox to save
|
13
|
+
# the message to. If it is of the form "=mailbox", it
|
14
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
15
|
+
# it simply saves the message to the file +mailbox+.
|
16
|
+
def self::check_mailbox mailbox
|
17
|
+
|
18
|
+
begin
|
19
|
+
if File.stat(mailbox).file? then
|
20
|
+
return MBox
|
21
|
+
else
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
rescue Errno::ENOENT
|
25
|
+
return nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Delivers the message to +mailbox+
|
30
|
+
# mailbox::
|
31
|
+
# A string containing the path of the mailbox to save
|
32
|
+
# the message to. If it is of the form "=mailbox", it
|
33
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
34
|
+
# it simply saves the message to the file +mailbox+.
|
35
|
+
def deliver_message mailbox
|
36
|
+
File.open(mailbox,File::WRONLY |
|
37
|
+
File::APPEND |
|
38
|
+
File::CREAT) do |f|
|
39
|
+
f.flock(File::LOCK_EX)
|
40
|
+
message=(if f.stat.size > 0 then "\n" else "" end) + to_mbox
|
41
|
+
f.print message
|
42
|
+
f.flock(File::LOCK_UN)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
#------------------------------------------------------------------------
|
6
|
+
# Delivers a message to an mbox (also includes mbox detector)
|
7
|
+
#------------------------------------------------------------------------
|
8
|
+
|
9
|
+
module Gurgitate
|
10
|
+
module Deliver
|
11
|
+
module MH
|
12
|
+
# Checks to see if +mailbox+ is an mbox mailbox
|
13
|
+
# mailbox::
|
14
|
+
# A string containing the path of the mailbox to save
|
15
|
+
# the message to. If it is of the form "=mailbox", it
|
16
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
17
|
+
# it simply saves the message to the file +mailbox+.
|
18
|
+
def self::check_mailbox mailbox
|
19
|
+
begin
|
20
|
+
# Rather annoyingly, pretty well any directory can
|
21
|
+
# be a MH mailbox, but this just checks to make sure
|
22
|
+
# it's not actually a Maildir by mistake.
|
23
|
+
#
|
24
|
+
# I could put in a check for the path given in
|
25
|
+
# $HOME/.mh_profile, but Claws-Mail uses MH mailboxes and
|
26
|
+
# disregards $HOME/.mh_profile.
|
27
|
+
if( File.stat(mailbox).directory? and
|
28
|
+
not ( File.exists?(File.join(mailbox, "cur")) or
|
29
|
+
File.exists?(File.join(mailbox, "tmp")) or
|
30
|
+
File.exists?(File.join(mailbox, "new")) ) ) then
|
31
|
+
return MH
|
32
|
+
end
|
33
|
+
rescue Errno::ENOENT
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Delivers the message to +mailbox+
|
39
|
+
# mailbox::
|
40
|
+
# A string containing the path of the mailbox to save
|
41
|
+
# the message to. If it is of the form "=mailbox", it
|
42
|
+
# saves the message to +Maildir+/+mailbox+. Otherwise,
|
43
|
+
# it simply saves the message to the file +mailbox+.
|
44
|
+
def deliver_message mailbox
|
45
|
+
if ! File.exists? mailbox then
|
46
|
+
Dir.mkdir(mailbox)
|
47
|
+
end
|
48
|
+
|
49
|
+
if File.exists? mailbox and not File.directory? mailbox then
|
50
|
+
raise SystemError, "not a directory"
|
51
|
+
end
|
52
|
+
|
53
|
+
new_msgnum = next_message(mailbox) do |filehandle|
|
54
|
+
filehandle.print self.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
update_sequences(mailbox, new_msgnum)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def update_sequences mailbox, msgnum
|
63
|
+
sequences = File.join(mailbox, ".mh_sequences")
|
64
|
+
lockfile = sequences + ".lock" # how quaint
|
65
|
+
counter=0
|
66
|
+
while counter < 10 do
|
67
|
+
begin
|
68
|
+
File.open(lockfile,
|
69
|
+
File::WRONLY |
|
70
|
+
File::CREAT |
|
71
|
+
File::EXCL ) do |lock|
|
72
|
+
File.open(sequences,
|
73
|
+
File::RDWR | File::CREAT) do |seq|
|
74
|
+
|
75
|
+
seq.flock(File::LOCK_EX)
|
76
|
+
metadata = YAML.load(seq.read) || Hash.new
|
77
|
+
|
78
|
+
metadata["unseen"] = update_unseen \
|
79
|
+
metadata["unseen"], msgnum
|
80
|
+
|
81
|
+
seq.rewind
|
82
|
+
metadata.each do |key, val|
|
83
|
+
seq.puts "#{key}: #{val}"
|
84
|
+
end
|
85
|
+
seq.truncate seq.tell
|
86
|
+
seq.flock(File::LOCK_UN)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
File.unlink lockfile
|
91
|
+
break
|
92
|
+
rescue Errno::EEXIST
|
93
|
+
# some other process is doing something, so wait a few
|
94
|
+
# milliseconds until it's done
|
95
|
+
counter += 1
|
96
|
+
sleep(0.1)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# If it's still around after 10 tries, then obviously
|
101
|
+
# something bigger went wrong; forcibly remove it and
|
102
|
+
# try again.
|
103
|
+
if counter == 10 then
|
104
|
+
File.unlink lockfile
|
105
|
+
update_sequences mailbox, msgnum
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def update_unseen unseen, msgnum
|
110
|
+
prevmsg = msgnum - 1
|
111
|
+
if unseen
|
112
|
+
unseenstring = unseen.to_s
|
113
|
+
|
114
|
+
if unseenstring =~ /-#{prevmsg}/ then
|
115
|
+
return unseenstring.sub(/\b#{prevmsg}\b/, msgnum.to_s)
|
116
|
+
end
|
117
|
+
|
118
|
+
if unseenstring.match(/\b#{prevmsg}\b/) then
|
119
|
+
return "#{unseenstring}-#{msgnum}"
|
120
|
+
end
|
121
|
+
|
122
|
+
return "#{unseenstring} #{msgnum}"
|
123
|
+
else
|
124
|
+
return msgnum
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def next_message mailbox
|
129
|
+
next_msgnum = Dir.open(mailbox).map { |ent| ent.to_i }.max + 1
|
130
|
+
loop do
|
131
|
+
begin
|
132
|
+
File.open(File.join(mailbox, next_msgnum.to_s),
|
133
|
+
File::WRONLY |
|
134
|
+
File::CREAT |
|
135
|
+
File::EXCL ) do |filehandle|
|
136
|
+
yield filehandle
|
137
|
+
end
|
138
|
+
break
|
139
|
+
rescue Errno::EEXIST
|
140
|
+
next_msgnum += 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
return next_msgnum
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
# -*- encoding : utf-8 -*-
|
3
|
+
|
4
|
+
module Gurgitate
|
5
|
+
class IllegalHeader < RuntimeError ; end
|
6
|
+
|
7
|
+
# A little class for a single header
|
8
|
+
class Header
|
9
|
+
# The name of the header
|
10
|
+
attr_accessor :name
|
11
|
+
# The contents of the header
|
12
|
+
attr_accessor :contents
|
13
|
+
|
14
|
+
alias_method :value, :contents
|
15
|
+
|
16
|
+
# A recent rash of viruses has forced me to canonicalize
|
17
|
+
# the capitalization of headers. Sigh.
|
18
|
+
def capitalize_words(s)
|
19
|
+
return s.split(/-/).map { |w| w.capitalize }.join("-")
|
20
|
+
rescue
|
21
|
+
return s
|
22
|
+
end
|
23
|
+
|
24
|
+
private :capitalize_words
|
25
|
+
|
26
|
+
# Creates a Header object.
|
27
|
+
# header::
|
28
|
+
# The text of the email-message header
|
29
|
+
def initialize(*header)
|
30
|
+
name,contents=nil,nil
|
31
|
+
if header.length == 1 then
|
32
|
+
# RFC822 says that a header consists of some (printable,
|
33
|
+
# non-whitespace) crap, followed by a colon, followed by
|
34
|
+
# some more (printable, but can include whitespaces)
|
35
|
+
# crap.
|
36
|
+
if(header[0] =~ /^[\x21-\x39\x3b-\x7e]+:/) then
|
37
|
+
(name,contents)=header[0].split(/:\s*/,2)
|
38
|
+
if(name =~ /:$/ and contents == nil) then
|
39
|
+
# It looks like someone is using Becky!
|
40
|
+
name=header[0].gsub(/:$/,"")
|
41
|
+
contents = ""
|
42
|
+
end
|
43
|
+
|
44
|
+
raise IllegalHeader, "Empty name" \
|
45
|
+
if (name == "" or name == nil)
|
46
|
+
contents="" if contents == nil
|
47
|
+
|
48
|
+
@@lastname=name
|
49
|
+
else
|
50
|
+
raise IllegalHeader, "Bad header syntax: no colon in #{header}"
|
51
|
+
end
|
52
|
+
elsif header.length == 2 then
|
53
|
+
name,contents = *header
|
54
|
+
end
|
55
|
+
|
56
|
+
@name=capitalize_words(name)
|
57
|
+
@contents=contents
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extended header
|
61
|
+
def << (text)
|
62
|
+
@contents += "\n" + text
|
63
|
+
end
|
64
|
+
|
65
|
+
# Matches a header's contents.
|
66
|
+
# regex::
|
67
|
+
# The regular expression to match against the header's contents
|
68
|
+
def matches (regex)
|
69
|
+
if String === regex
|
70
|
+
regex = Regexp.new(Regexp.escape(regex))
|
71
|
+
end
|
72
|
+
@contents =~ regex
|
73
|
+
end
|
74
|
+
|
75
|
+
alias :=~ :matches
|
76
|
+
|
77
|
+
# Returns the header, ready to put into an email message
|
78
|
+
def to_s
|
79
|
+
@name+": "+@contents
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
#!/opt/bin/ruby -w
|
2
|
+
# -*- encoding : utf-8 -*-
|
3
|
+
|
4
|
+
require "gurgitate/header"
|
5
|
+
|
6
|
+
module Gurgitate
|
7
|
+
class IllegalHeader < RuntimeError ; end
|
8
|
+
|
9
|
+
# ========================================================================
|
10
|
+
|
11
|
+
class HeaderBag < Array
|
12
|
+
def =~(regex)
|
13
|
+
inject(false) do |y,x|
|
14
|
+
y or ( ( x =~ regex ) != nil )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def sub!(regex, replacement)
|
19
|
+
each do |header|
|
20
|
+
header.contents = header.contents.sub regex, replacement
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def sub(regex, replacement)
|
25
|
+
::Gurgitate::HeaderBag.new(
|
26
|
+
clone.map do |header|
|
27
|
+
::Gurgitate::Header.new(
|
28
|
+
"#{header.name}: " + header.contents.sub(regex,
|
29
|
+
replacement)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
map do |member|
|
37
|
+
member.to_s
|
38
|
+
end.join ""
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# A slightly bigger class for all of a message's headers
|
44
|
+
class Headers
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Figures out whether the first line of a mail message is an
|
49
|
+
# mbox-style "From " line (say, if you get this from sendmail),
|
50
|
+
# or whether it's just a normal header.
|
51
|
+
# --
|
52
|
+
# If you run "fetchmail" with the -m option to feed the
|
53
|
+
# mail message straight to gurgitate, skipping the "local
|
54
|
+
# MTA" step, then it doesn't have a "From " line. So I
|
55
|
+
# have to deal with that by hand. First, check to see if
|
56
|
+
# there's a "From " line present in the first place.
|
57
|
+
def figure_out_from_line(headertext)
|
58
|
+
(unix_from,normal_headers) = headertext.split(/\n/,2)
|
59
|
+
|
60
|
+
if unix_from =~ /^From / then
|
61
|
+
headertext=normal_headers
|
62
|
+
unix_from=unix_from
|
63
|
+
else
|
64
|
+
# If there isn't, then deal with it after we've
|
65
|
+
# worried about the rest of the headers, 'cos we'll
|
66
|
+
# have to make our own.
|
67
|
+
unix_from=nil
|
68
|
+
end
|
69
|
+
return unix_from, headertext
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_headers
|
73
|
+
@headertext.each_line do |h|
|
74
|
+
h.chomp!
|
75
|
+
if(h=~/^\s+/) then
|
76
|
+
@lastheader << h
|
77
|
+
else
|
78
|
+
header=Header.new(h)
|
79
|
+
@headers[header.name] ||= HeaderBag.new
|
80
|
+
@headers[header.name].push(header)
|
81
|
+
@lastheader=header
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
@headers_changed=false
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the envelope From information. This comes with a
|
89
|
+
# whole category of rants: this information is absurdly hard
|
90
|
+
# to get your hands on. The best you can manage is a sort
|
91
|
+
# of educated guess. Thus, this horrible glob of hackiness.
|
92
|
+
# I don't recommend looking too closely at this code if you
|
93
|
+
# can avoid it, and further I recommend making sure to
|
94
|
+
# configure your MTA so that it sends proper sender and
|
95
|
+
# recipient information to gurgitate so that this code never
|
96
|
+
# has to be run at all.
|
97
|
+
def guess_sender
|
98
|
+
# Start by worrying about the "From foo@bar" line. If it's
|
99
|
+
# not there, then make one up from the Return-Path: header.
|
100
|
+
# If there isn't a "Return-Path:" header (then I suspect we
|
101
|
+
# have bigger problems, but still) then use From: as a wild
|
102
|
+
# guess. If I hope that this entire lot of code doesn't get
|
103
|
+
# used, then I _particularly_ hope that things never get so
|
104
|
+
# bad that poor gurgitate has to use the From: header as a
|
105
|
+
# source of authoritative information on anything.
|
106
|
+
#
|
107
|
+
# And then after all that fuss, if we're delivering to a
|
108
|
+
# Maildir, I have to get rid of it. And sometimes the MTA
|
109
|
+
# gives me a mbox-style From line and sometimes it doesn't.
|
110
|
+
# It's annoying, but I have no choice but to Just Deal With
|
111
|
+
# It.
|
112
|
+
if @unix_from then
|
113
|
+
# If it is there, then grab the email address in it and
|
114
|
+
# use that as our official "from".
|
115
|
+
fromregex=/^From ([^ ]+@[^ ]+) /
|
116
|
+
fromregex.match(@unix_from)
|
117
|
+
@from=$+
|
118
|
+
|
119
|
+
# or maybe it's local
|
120
|
+
if @from == nil then
|
121
|
+
@unix_from =~ /^From (\S+) /
|
122
|
+
@from=$+
|
123
|
+
end
|
124
|
+
else
|
125
|
+
fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+
|
126
|
+
)/
|
127
|
+
if self["Return-Path"] != nil then
|
128
|
+
fromregex.match(self["Return-Path"][0].contents)
|
129
|
+
else
|
130
|
+
if self["From"] != nil then
|
131
|
+
fromregex.match(self["From"][0].contents)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
address_candidate=$+
|
135
|
+
|
136
|
+
# If there STILL isn't a match, then it's probably safe to
|
137
|
+
# assume that it's local mail, and doesn't have an @ in its
|
138
|
+
# address.
|
139
|
+
unless address_candidate
|
140
|
+
if self["Return-Path"] then
|
141
|
+
self["Return-Path"][0].contents =~ /(\S+)/
|
142
|
+
address_candidate=$+
|
143
|
+
else
|
144
|
+
if self["From"] then
|
145
|
+
self["From"][0].contents =~ /(\S+)/
|
146
|
+
address_candidate=$+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
@from=address_candidate
|
152
|
+
|
153
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
public
|
158
|
+
|
159
|
+
# Creates a Headers object.
|
160
|
+
# headertext::
|
161
|
+
# The text of the message headers.
|
162
|
+
def initialize(headertext=nil, sender=nil, recipient=nil)
|
163
|
+
@from = sender
|
164
|
+
@to = recipient
|
165
|
+
@headers = Hash.new(nil)
|
166
|
+
|
167
|
+
if Hash === headertext
|
168
|
+
@headers_changed = true
|
169
|
+
headertext.each_key do |key|
|
170
|
+
|
171
|
+
headername = key.to_s.gsub("_","-")
|
172
|
+
|
173
|
+
header=Header.new(headername, headertext[key])
|
174
|
+
@headers[header.name] ||= HeaderBag.new
|
175
|
+
@headers[header.name].push(header)
|
176
|
+
end
|
177
|
+
else
|
178
|
+
if headertext
|
179
|
+
@unix_from, @headertext = figure_out_from_line headertext
|
180
|
+
parse_headers if @headertext
|
181
|
+
|
182
|
+
if sender # then don't believe the mbox separator
|
183
|
+
@from = sender
|
184
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
185
|
+
else
|
186
|
+
guess_sender
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Grab the headers with names +names+
|
193
|
+
# names:: The names of the header.
|
194
|
+
def [](*names)
|
195
|
+
if names.inject(false) do |accum,name|
|
196
|
+
accum or @headers.has_key? name
|
197
|
+
end then
|
198
|
+
return HeaderBag.new(names.collect { |name|
|
199
|
+
@headers[name]
|
200
|
+
}.flatten.delete_if { |e| e == nil } )
|
201
|
+
else
|
202
|
+
return nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Set the header named +name+ to +value+
|
207
|
+
# name:: The name of the header.
|
208
|
+
# value:: The new value of the header.
|
209
|
+
def []=(name,value)
|
210
|
+
@headers_changed = true
|
211
|
+
@headers[name]=HeaderBag.new([Header.new(name,value)])
|
212
|
+
end
|
213
|
+
|
214
|
+
# Who the message is to (the envelope to)
|
215
|
+
#
|
216
|
+
# Yet another bucket of rants. Unix mail sucks.
|
217
|
+
def to
|
218
|
+
return @to || @headers["X-Original-To"] || nil
|
219
|
+
end
|
220
|
+
|
221
|
+
# Who the message is from (the envelope from)
|
222
|
+
def from
|
223
|
+
return @from || ""
|
224
|
+
end
|
225
|
+
|
226
|
+
# Change the envelope from line to whatever you want. This might
|
227
|
+
# not be particularly neighborly, but oh well.
|
228
|
+
# newfrom:: An email address
|
229
|
+
def from=(newfrom)
|
230
|
+
@from=newfrom
|
231
|
+
@unix_from="From "+self.from+" "+Time.new.to_s
|
232
|
+
end
|
233
|
+
|
234
|
+
# Match header +name+ against +regex+
|
235
|
+
# name::
|
236
|
+
# A string containing the name of the header to match (for example,
|
237
|
+
# "From")
|
238
|
+
# regex:: The regex to match it against (for example, /@aol.com/)
|
239
|
+
def match(name,regex)
|
240
|
+
ret=false
|
241
|
+
if(@headers[name]) then
|
242
|
+
@headers[name].each do |h|
|
243
|
+
ret |= h.matches(regex)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
return ret
|
247
|
+
end
|
248
|
+
|
249
|
+
# Return true if headers +names+ match +regex+
|
250
|
+
# names:: An array of header names (for example, %w{From Reply-To})
|
251
|
+
# regex:: The regex to match the headers against.
|
252
|
+
def matches(names,regex)
|
253
|
+
ret=false
|
254
|
+
|
255
|
+
if names.class == String then
|
256
|
+
names=[names]
|
257
|
+
end
|
258
|
+
|
259
|
+
names.each do |n|
|
260
|
+
ret |= match(n,regex)
|
261
|
+
end
|
262
|
+
return ret
|
263
|
+
end
|
264
|
+
|
265
|
+
# Returns the headers properly formatted for an email
|
266
|
+
# message.
|
267
|
+
def to_mbox
|
268
|
+
return @unix_from+"\n"+to_s
|
269
|
+
end
|
270
|
+
|
271
|
+
# Returns the headers formatted for an email message (without
|
272
|
+
# the "From " line
|
273
|
+
def to_s
|
274
|
+
if @headers_changed then
|
275
|
+
return @headers.map do |name,hdr|
|
276
|
+
hdr.map do |hdr_content| hdr_content.to_s end.join("\n")
|
277
|
+
end.join("\n")
|
278
|
+
else
|
279
|
+
return @headertext
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|