gurgitate-mail 1.10.0 → 1.10.1

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 CHANGED
File without changes
@@ -9,10 +9,61 @@ require 'gurgitate/mailmessage'
9
9
  require 'gurgitate/deliver'
10
10
 
11
11
  module Gurgitate
12
- #========================================================================
13
- # The actual gurgitator; reads a message and then it can do
14
- # other stuff with it, like save to a mailbox or forward
12
+ # This is the actual gurgitator; it reads a message and then it can
13
+ # do other stuff with it, like saving it to a mailbox or forwarding
15
14
  # it somewhere else.
15
+ #
16
+ # To set configuration parameters for gurgitate-mail, use a keyword-
17
+ # based system. It's almost like an attribute, only if you give the
18
+ # accessor a parameter, it will set the configuration parameter to
19
+ # the parameter's value. For instance:
20
+ #
21
+ # maildir "#{homedir}/Mail"
22
+ # sendmail "/usr/sbin/sendmail"
23
+ # spoolfile "Maildir"
24
+ # spooldir homedir
25
+ #
26
+ # (This is because of an oddity in Ruby where, even if an
27
+ # accessor exists in the current object, if you say:
28
+ # name = value
29
+ # it'll always create a local variable. Not quite what you
30
+ # want when you're trying to set a config parameter. You have
31
+ # to say <code>self.name = value</code>, which [I think] is ugly.
32
+ #
33
+ # In the interests of promoting harmony, of course,
34
+ # <code>self.name = value</code> still works.)
35
+ #
36
+ # The attributes you can define are:
37
+ #
38
+ # homedir :: Your home directory. This defaults to what your
39
+ # actual home directory is.
40
+ #
41
+ # maildir :: The directory you store your mail in. This defaults
42
+ # to the "Mail" directory in your home dir.
43
+ #
44
+ # logfile :: The path to your gurgitate-mail log file. If you
45
+ # set this to +nil+, gurgitate-mail won't log anything.
46
+ # The default value is ".gurgitate.log" in your home
47
+ # directory.
48
+ #
49
+ # The following parameters are more likely to be interesting to the
50
+ # system administrator than the everyday user.
51
+ #
52
+ # sendmail :: The full path of your "sendmail" program, or at least
53
+ # a program that provides functionality equivalent to
54
+ # sendmail.
55
+ #
56
+ # spoolfile :: The default location to store mail messages, for the
57
+ # messages that have been unaffected by your gurgitate
58
+ # rules. If an exception is raised by your rules, the
59
+ # message will be delivered to the spoolfile.
60
+ #
61
+ # spooldir :: The location where users' system mail boxes live.
62
+ #
63
+ # folderstyle :: The style of mailbox to create (and to expect,
64
+ # although gurgitate-mail automatically detects the
65
+ # type of existing mailboxes). See the separate
66
+ # documentation for folderstyle for more details.
16
67
  class Gurgitate < Mailmessage
17
68
  include Deliver
18
69
 
@@ -36,7 +87,7 @@ module Gurgitate
36
87
  # syntax will continue to work.
37
88
  def self.attr_configparam(*syms)
38
89
  syms.each do |sym|
39
- class_eval %/
90
+ class_eval %{
40
91
  def #{sym} (*vals)
41
92
  if vals.length == 1
42
93
  @#{sym} = vals[0]
@@ -53,7 +104,7 @@ module Gurgitate
53
104
  # old-style accessors though. Breaking people's
54
105
  # .gurgitate-rules is a bad idea.
55
106
  attr_writer :#{sym}
56
- /
107
+ }
57
108
  end
58
109
  end
59
110
 
@@ -78,7 +129,36 @@ module Gurgitate
78
129
  # What kind of mailboxes you prefer
79
130
  # attr_configparam :folderstyle
80
131
 
81
- # What kind of mailboxes you prefer
132
+ # What kind of mailboxes you prefer. Treat this like a
133
+ # configuration parameter. If no argument is given, then
134
+ # return the current default type.
135
+ #
136
+ # Depending on what you set this to, some other configuration
137
+ # parameters change. You can set this to the following things:
138
+ #
139
+ # <code>Maildir</code> :: Create Maildir mailboxes.
140
+ #
141
+ # This sets +spooldir+ to your home
142
+ # directory, +spoolfile+ to
143
+ # $HOME/Maildir and creates
144
+ # mail folders underneath that.
145
+ #
146
+ # <code>MH</code> :: Create MH mail boxes.
147
+ #
148
+ # This reads your <code>.mh_profile</code>
149
+ # file to find out where you've told MH to
150
+ # find its mail folders, and uses that value.
151
+ # If it can't find that in your .mh_profile,
152
+ # it will assume you want mailboxes in
153
+ # $HOME/Mail. It sets +spoolfile+ to
154
+ # "inbox" in your mail directory.
155
+ #
156
+ # <code>Mbox</code> :: Create +mbox+ mailboxes.
157
+ #
158
+ # This sets +spooldir+ to
159
+ # <code>/var/spool/mail</code> and
160
+ # +spoolfile+ to a file with your username
161
+ # in <code>/var/spool/mail</code>.
82
162
  def folderstyle(*style)
83
163
  if style.length == 0 then
84
164
  @folderstyle
@@ -141,7 +221,7 @@ module Gurgitate
141
221
  instance_eval(&block) if block_given?
142
222
  end
143
223
 
144
- def add_rules(filename, options = {})
224
+ def add_rules(filename, options = {}) #:nodoc:
145
225
  if not Hash === options
146
226
  raise ArgumentError.new("Expected hash of options")
147
227
  end
@@ -171,15 +251,16 @@ module Gurgitate
171
251
  end
172
252
  end
173
253
 
174
- # Deletes the current message.
254
+ # Deletes (discards) the current message.
175
255
  def delete
176
256
  # Well, nothing here, really.
177
257
  end
178
258
 
179
- # This is a neat one. You can get any header as a method.
180
- # Say, if you want the header "X-Face", then you call
181
- # x_face and that gets it for you. It raises NameError if
182
- # that header isn't found.
259
+ # This is kind of neat. You can get a header by calling its
260
+ # name as a method. For example, if you want the header
261
+ # "X-Face", then you call x_face and that gets it for you. It
262
+ # raises NameError if that header isn't found.
263
+ #
183
264
  # meth::
184
265
  # The method that the caller tried to call which isn't
185
266
  # handled any other way.
@@ -193,6 +274,7 @@ module Gurgitate
193
274
  end
194
275
 
195
276
  # Forwards the message to +address+.
277
+ #
196
278
  # address::
197
279
  # A valid email address to forward the message to.
198
280
  def forward(address)
@@ -204,7 +286,7 @@ module Gurgitate
204
286
 
205
287
  # Writes +message+ to the log file.
206
288
  def log(message)
207
- if(@logfile)then
289
+ if @logfile then
208
290
  File.open(@logfile,"a") do |f|
209
291
  f.flock(File::LOCK_EX)
210
292
  f.print(Time.new.to_s+" "+message+"\n")
@@ -225,6 +307,17 @@ module Gurgitate
225
307
 
226
308
  # Pipes the message through +program+, and returns another
227
309
  # +Gurgitate+ object containing the output of the filter
310
+ #
311
+ # Use it like this:
312
+ #
313
+ # filter "bogofilter -p" do
314
+ # if x_bogosity =~ /Spam/ then
315
+ # log "Found spam"
316
+ # delete
317
+ # return
318
+ # end
319
+ # end
320
+ #
228
321
  def filter(program,&block)
229
322
  self.log "Filtering with "+program
230
323
  IO.popen("-","w+") do |filter|
@@ -255,8 +348,7 @@ module Gurgitate
255
348
  end
256
349
  end
257
350
 
258
- # Processes your .gurgitate-rules.rb.
259
- def process(&block)
351
+ def process(&block) #:nodoc:
260
352
  begin
261
353
  if @rules.size > 0 or block
262
354
  @rules.each do |configfilespec|
@@ -15,7 +15,7 @@ module Gurgitate
15
15
  # message to. If it is of the form "=mailbox", it saves
16
16
  # the message to +Maildir+/+mailbox+. Otherwise, it
17
17
  # simply saves the message to the file +mailbox+.
18
- def self::check_mailbox(mailbox)
18
+ def self::check_mailbox mailbox
19
19
  begin
20
20
  if File.stat(mailbox).directory? then
21
21
  if File.stat(File.join(mailbox,"cur")).directory? then
@@ -33,7 +33,7 @@ module Gurgitate
33
33
  # One of "+mailbox+/tmp" or "+mailbox+/new", but that's
34
34
  # only because that's what the maildir spec
35
35
  # (http://cr.yp.to/proto/maildir.html) says.
36
- def maildir_getfilename(dir)
36
+ def maildir_getfilename dir
37
37
  time=Time.now.to_f
38
38
  counter=0
39
39
  hostname=Socket::gethostname
@@ -50,7 +50,7 @@ module Gurgitate
50
50
  # Creates a new Maildir folder +mailbox+
51
51
  # mailbox::
52
52
  # The full path of the new folder to be created
53
- def make_mailbox(mailbox)
53
+ def make_mailbox mailbox
54
54
  Dir.mkdir(mailbox)
55
55
  %w{cur tmp new}.each do |dir|
56
56
  Dir.mkdir(File.join(mailbox,dir))
@@ -63,7 +63,7 @@ module Gurgitate
63
63
  # message to. If it is of the form "=mailbox", it saves
64
64
  # the message to +Maildir+/+mailbox+. Otherwise, it
65
65
  # simply saves the message to the file +mailbox+.
66
- def deliver_message(mailbox)
66
+ def deliver_message mailbox
67
67
  begin
68
68
  File.stat(mailbox)
69
69
  rescue Errno::ENOENT
@@ -13,10 +13,10 @@ module Gurgitate
13
13
  # the message to. If it is of the form "=mailbox", it
14
14
  # saves the message to +Maildir+/+mailbox+. Otherwise,
15
15
  # it simply saves the message to the file +mailbox+.
16
- def self::check_mailbox(mailbox)
16
+ def self::check_mailbox mailbox
17
17
 
18
18
  begin
19
- if File.stat(mailbox).file? then
19
+ if File.stat(mailbox).file? then
20
20
  return MBox
21
21
  else
22
22
  return nil
@@ -32,9 +32,9 @@ module Gurgitate
32
32
  # the message to. If it is of the form "=mailbox", it
33
33
  # saves the message to +Maildir+/+mailbox+. Otherwise,
34
34
  # it simply saves the message to the file +mailbox+.
35
- def deliver_message(mailbox)
36
- File.open(mailbox,File::WRONLY |
37
- File::APPEND |
35
+ def deliver_message mailbox
36
+ File.open(mailbox,File::WRONLY |
37
+ File::APPEND |
38
38
  File::CREAT) do |f|
39
39
  f.flock(File::LOCK_EX)
40
40
  message=(if f.stat.size > 0 then "\n" else "" end) + to_mbox
@@ -15,7 +15,7 @@ module Gurgitate
15
15
  # the message to. If it is of the form "=mailbox", it
16
16
  # saves the message to +Maildir+/+mailbox+. Otherwise,
17
17
  # it simply saves the message to the file +mailbox+.
18
- def self::check_mailbox(mailbox)
18
+ def self::check_mailbox mailbox
19
19
  begin
20
20
  # Rather annoyingly, pretty well any directory can
21
21
  # be a MH mailbox, but this just checks to make sure
@@ -24,10 +24,10 @@ module Gurgitate
24
24
  # I could put in a check for the path given in
25
25
  # $HOME/.mh_profile, but Claws-Mail uses MH mailboxes and
26
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
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
31
  return MH
32
32
  end
33
33
  rescue Errno::ENOENT
@@ -41,7 +41,7 @@ module Gurgitate
41
41
  # the message to. If it is of the form "=mailbox", it
42
42
  # saves the message to +Maildir+/+mailbox+. Otherwise,
43
43
  # it simply saves the message to the file +mailbox+.
44
- def deliver_message(mailbox)
44
+ def deliver_message mailbox
45
45
  if ! File.exists? mailbox then
46
46
  Dir.mkdir(mailbox)
47
47
  end
@@ -59,16 +59,16 @@ module Gurgitate
59
59
 
60
60
  private
61
61
 
62
- def update_sequences(mailbox, msgnum)
62
+ def update_sequences mailbox, msgnum
63
63
  sequences = File.join(mailbox, ".mh_sequences")
64
64
  lockfile = sequences + ".lock" # how quaint
65
65
  loop do
66
66
  begin
67
- File.open(lockfile,
68
- File::WRONLY |
69
- File::CREAT |
67
+ File.open(lockfile,
68
+ File::WRONLY |
69
+ File::CREAT |
70
70
  File::EXCL ) do |lock|
71
- File.open(sequences,
71
+ File.open(sequences,
72
72
  File::RDWR | File::CREAT) do |seq|
73
73
 
74
74
  seq.flock(File::LOCK_EX)
@@ -91,7 +91,7 @@ module Gurgitate
91
91
  rescue Errno::EEXIST
92
92
  # some other process is doing something, so wait a few
93
93
  # milliseconds until it's done
94
- sleep(0.01)
94
+ sleep(0.01)
95
95
  end
96
96
  end
97
97
  end
@@ -115,13 +115,13 @@ module Gurgitate
115
115
  end
116
116
  end
117
117
 
118
- def next_message(mailbox)
118
+ def next_message mailbox
119
119
  next_msgnum = Dir.open(mailbox).map { |ent| ent.to_i }.max + 1
120
120
  loop do
121
121
  begin
122
122
  File.open(File.join(mailbox, next_msgnum.to_s),
123
- File::WRONLY |
124
- File::CREAT |
123
+ File::WRONLY |
124
+ File::CREAT |
125
125
  File::EXCL ) do |filehandle|
126
126
  yield filehandle
127
127
  end
@@ -22,7 +22,7 @@ module Gurgitate
22
22
 
23
23
  def sub(regex, replacement)
24
24
  ::Gurgitate::HeaderBag.new(
25
- self.map do |header|
25
+ clone.map do |header|
26
26
  ::Gurgitate::Header.new(
27
27
  "#{header.name}: " + header.contents.sub(regex,
28
28
  replacement)
@@ -47,7 +47,7 @@ module Gurgitate
47
47
  # Figures out whether the first line of a mail message is an
48
48
  # mbox-style "From " line (say, if you get this from sendmail),
49
49
  # or whether it's just a normal header.
50
- # --
50
+ # --
51
51
  # If you run "fetchmail" with the -m option to feed the
52
52
  # mail message straight to gurgitate, skipping the "local
53
53
  # MTA" step, then it doesn't have a "From " line. So I
@@ -121,7 +121,8 @@ module Gurgitate
121
121
  @from=$+
122
122
  end
123
123
  else
124
- fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+)/
124
+ fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+
125
+ )/
125
126
  if self["Return-Path"] != nil then
126
127
  fromregex.match(self["Return-Path"][0].contents)
127
128
  else
@@ -135,12 +136,14 @@ module Gurgitate
135
136
  # assume that it's local mail, and doesn't have an @ in its
136
137
  # address.
137
138
  unless address_candidate
138
- if self["Return-Path"] != nil then
139
+ if self["Return-Path"] then
139
140
  self["Return-Path"][0].contents =~ /(\S+)/
140
141
  address_candidate=$+
141
142
  else
142
- self["From"][0].contents =~ /(\S+)/
143
- address_candidate=$+
143
+ if self["From"] then
144
+ self["From"][0].contents =~ /(\S+)/
145
+ address_candidate=$+
146
+ end
144
147
  end
145
148
  end
146
149
 
@@ -222,7 +225,7 @@ module Gurgitate
222
225
  # Change the envelope from line to whatever you want. This might
223
226
  # not be particularly neighborly, but oh well.
224
227
  # newfrom:: An email address
225
- def from=(newfrom)
228
+ def from=(newfrom)
226
229
  @from=newfrom
227
230
  @unix_from="From "+self.from+" "+Time.new.to_s
228
231
  end
@@ -0,0 +1,166 @@
1
+ #!/opt/bin/ruby -w
2
+
3
+ require "gurgitate/headers"
4
+
5
+ module Gurgitate
6
+ class IllegalHeader < RuntimeError ; end
7
+
8
+ # ========================================================================
9
+
10
+ # A slightly bigger class for all of a message's headers
11
+ class MailHeaders < Headers
12
+
13
+ private
14
+
15
+ # Figures out whether the first line of a mail message is an
16
+ # mbox-style "From " line (say, if you get this from sendmail),
17
+ # or whether it's just a normal header.
18
+ # --
19
+ # If you run "fetchmail" with the -m option to feed the
20
+ # mail message straight to gurgitate, skipping the "local
21
+ # MTA" step, then it doesn't have a "From " line. So I
22
+ # have to deal with that by hand. First, check to see if
23
+ # there's a "From " line present in the first place.
24
+ def figure_out_from_line(headertext)
25
+ (unix_from,normal_headers) = headertext.split(/\n/,2)
26
+
27
+ if unix_from =~ /^From / then
28
+ headertext=normal_headers
29
+ unix_from=unix_from
30
+ else
31
+ # If there isn't, then deal with it after we've
32
+ # worried about the rest of the headers, 'cos we'll
33
+ # have to make our own.
34
+ unix_from=nil
35
+ end
36
+ return unix_from, headertext
37
+ end
38
+
39
+ # Get the envelope From information. This comes with a
40
+ # whole category of rants: this information is absurdly hard
41
+ # to get your hands on. The best you can manage is a sort
42
+ # of educated guess. Thus, this horrible glob of hackiness.
43
+ # I don't recommend looking too closely at this code if you
44
+ # can avoid it, and further I recommend making sure to
45
+ # configure your MTA so that it sends proper sender and
46
+ # recipient information to gurgitate so that this code never
47
+ # has to be run at all.
48
+ def guess_sender
49
+ # Start by worrying about the "From foo@bar" line. If it's
50
+ # not there, then make one up from the Return-Path: header.
51
+ # If there isn't a "Return-Path:" header (then I suspect we
52
+ # have bigger problems, but still) then use From: as a wild
53
+ # guess. If I hope that this entire lot of code doesn't get
54
+ # used, then I _particularly_ hope that things never get so
55
+ # bad that poor gurgitate has to use the From: header as a
56
+ # source of authoritative information on anything.
57
+ #
58
+ # And then after all that fuss, if we're delivering to a
59
+ # Maildir, I have to get rid of it. And sometimes the MTA
60
+ # gives me a mbox-style From line and sometimes it doesn't.
61
+ # It's annoying, but I have no choice but to Just Deal With
62
+ # It.
63
+ if @unix_from then
64
+ # If it is there, then grab the email address in it and
65
+ # use that as our official "from".
66
+ fromregex=/^From ([^ ]+@[^ ]+) /
67
+ fromregex.match(@unix_from)
68
+ @from=$+
69
+
70
+ # or maybe it's local
71
+ if @from == nil then
72
+ @unix_from =~ /^From (\S+) /
73
+ @from=$+
74
+ end
75
+ else
76
+ fromregex=/([^ ]+@[^ ]+) \(.*\)|[^<]*[<](.*@.*)[>]|([^ ]+@[^ ]+)/
77
+ if self["Return-Path"] != nil then
78
+ fromregex.match(self["Return-Path"][0].contents)
79
+ else
80
+ if self["From"] != nil then
81
+ fromregex.match(self["From"][0].contents)
82
+ end
83
+ end
84
+ address_candidate=$+
85
+
86
+ # If there STILL isn't a match, then it's probably safe to
87
+ # assume that it's local mail, and doesn't have an @ in its
88
+ # address.
89
+ unless address_candidate
90
+ if self["Return-Path"] != nil then
91
+ self["Return-Path"][0].contents =~ /(\S+)/
92
+ address_candidate=$+
93
+ else
94
+ self["From"][0].contents =~ /(\S+)/
95
+ address_candidate=$+
96
+ end
97
+ end
98
+
99
+ @from=address_candidate
100
+
101
+ @unix_from="From "+self.from+" "+Time.new.to_s
102
+ end
103
+ end
104
+
105
+ public
106
+
107
+ # Creates a MailHeaders object.
108
+ # headertext::
109
+ # The text of the message headers.
110
+ def initialize(headertext=nil, sender=nil, recipient=nil)
111
+ @from = sender
112
+ @to = recipient
113
+ @headers = Hash.new(nil)
114
+
115
+ if Hash === headertext
116
+ @headers_changed = true
117
+ headertext.each_key do |key|
118
+
119
+ headername = key.to_s.gsub("_","-")
120
+
121
+ header=Header.new(headername, headertext[key])
122
+ @headers[header.name] ||= HeaderBag.new
123
+ @headers[header.name].push(header)
124
+ end
125
+ else
126
+ if headertext
127
+ @unix_from, @headertext = figure_out_from_line headertext
128
+ parse_headers if @headertext
129
+
130
+ if sender # then don't believe the mbox separator
131
+ @from = sender
132
+ @unix_from="From "+self.from+" "+Time.new.to_s
133
+ else
134
+ guess_sender
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # Who the message is to (the envelope to)
141
+ #
142
+ # Yet another bucket of rants. Unix mail sucks.
143
+ def to
144
+ return @to || @headers["X-Original-To"] || nil
145
+ end
146
+
147
+ # Who the message is from (the envelope from)
148
+ def from
149
+ return @from || ""
150
+ end
151
+
152
+ # Change the envelope from line to whatever you want. This might
153
+ # not be particularly neighborly, but oh well.
154
+ # newfrom:: An email address
155
+ def from=(newfrom)
156
+ @from=newfrom
157
+ @unix_from="From "+self.from+" "+Time.new.to_s
158
+ end
159
+
160
+ # Returns the headers properly formatted for an mbox-format
161
+ # email message.
162
+ def to_mbox
163
+ return @unix_from+"\n"+to_s
164
+ end
165
+ end
166
+ end