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