gurgitate-mail 1.10.0 → 1.10.1

Sign up to get free protection for your applications and to get access to all the features.
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