Check that the current directory is named correctly for package building
[stand.git] / stand
blob0e7b0d3877102cc22916d03253ee139c93c40a5a
1 #!/usr/bin/ruby1.9.1
3 # Copyright Mark Longair (c) 2002, 2005, 2008 Reuben Thomas (c) 2005-2007.
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation, either version 3 of the License, or
8 #    (at your option) any later version.
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 # This is a simple script for fetching mail from POP3 accounts.
19 # On a Debian system, you'll need the following packages:
21 #   ruby1.9.1
22 #   libruby1.9.1
23 #   libopenssl-ruby1.9.1
24 #   libsqlite3-ruby1.9.1
26 # See http://mythic-beasts.com/~mark/software/stand/
27 # for further remarks.
29 $program_name = 'stand'
31 require 'getoptlong'
32 require 'yaml'
33 require 'socket'
34 require 'net/pop'
35 require 'openssl'
36 require 'timeout'
37 require 'sqlite3'
39 # ----------------------------------------------------------------------
41 # Command line argument parsing...
43 # Some default values...
45 $configuration_file = "#{ENV['HOME']}/.stand.yaml"
46 $configuration_file_pretty = "~/.stand.yaml"
47 $uidl_database_filename = "#{ENV['HOME']}/.stand-uidls"
49 def usage
51   print <<EOF
52 Usage: #{$program_name} [OPTION]...
54   -h, --help           Display this message and exit.
55   -d, --delete         Delete messages on server after fetching.
56   -n, --no-ssl-verify  Don\'t verify the peer\'s certificate when using SSL.
57   -f <FILENAME>, --configuration-file=<FILENAME>
58                        Specify the configuration file to use.
59                        (default: #{$configuration_file_pretty})
60   -r <SECONDS>, --reconnect-after=<SECONDS>
61                        Reconnect to the server if <SECONDS> seconds has
62                        elapsed since the last connection.
63 EOF
65 end
67 options = GetoptLong.new(
68   [ "--help", "-h", GetoptLong::NO_ARGUMENT ],
69   [ "--delete", "-d", GetoptLong::NO_ARGUMENT ],
70   [ "--no-ssl-verify", "-n", GetoptLong::NO_ARGUMENT ],
71   [ "--configuration-file", "-f", GetoptLong::REQUIRED_ARGUMENT ],
72   [ "--reconnect-after", "-r",  GetoptLong::REQUIRED_ARGUMENT ]
75 $keep = true
76 $ssl_verify = true
77 $reconnect_after = nil
79 begin
81   options.each do |opt, arg|
83     case opt
84     when "--help"
85       usage
86       exit
87     when "--delete"
88       $keep = false
89     when "--no-ssl-verify"
90       $ssl_verify = false
91     when "--configuration-file"
92       $configuration_file = arg
93     when "--reconnect-after"
94       $reconnect_after = Float(arg)
95     end
97   end
99 rescue
101   STDERR.print "Bad command line option: #{$!}\n"
102   usage
103   exit
107 if $keep and $reconnect_after
108   STDERR.puts "There should be no reason to specify --reconnect-after with --keep"
109   exit
112 # ----------------------------------------------------------------------
114 # A fairly idiotic progress class.
116 class Progress
118   def initialize(maximum)
119     @maximum = maximum
120     @current_value = 0
121   end
123   def increase_by(i)
124     set @current_value + i
125   end
127   def set(v)
128     block_change = (v / 1024) - (@current_value / 1024)
129     if block_change >= 0
130       (1..block_change).each { print "." }
131       $stdout.flush
132     else
133       backspaces = - block_change
134       (1..backspaces).each { print "\b" }
135       $stdout.flush
136     end
137     @current_value = v
138   end
142 # ----------------------------------------------------------------------
144 # This class deals with connecting to a POP3 server, and fetching the
145 # mail from it.
147 class POP3Account
149   attr_accessor :host, :user, :port
151   attr_reader :command
153   def initialize(host, user, password, use_ssl, use_apop, port, command)
154     @host = host
155     @user = user
156     @password = password
157     @use_ssl = use_ssl
158     @use_apop = use_apop
159     @port = port
160     @command = command
161   end
163   def to_s
164     "#{@user} at #{@host}:#{@port}#{ @use_ssl ? ' (SSL)' : ''}"
165   end
167   def account_uidl(uidl)
168     "#{@user}@#{@host}:#{@port}##{uidl}"
169   end
171   # These three methods manipulate the UIDL database.
173   def add_uidl_to_db(db,u)
174     db.execute( "INSERT INTO uidls VALUES (?)", account_uidl(u) )
175   end
177   def delete_uidls_from_db(db,a)
178     a.each do |u|
179       db.execute( "DELETE FROM uidls WHERE uidl = ?", account_uidl(u) )
180     end
181   end
183   def db_has_uidl?(db,u)
184     rows = db.execute( "SELECT uidl FROM uidls WHERE uidl = ?", account_uidl(u) )
185     rows.length > 0
186   end
188   # Fetch all the new messages from this POP3 account, deleting
189   # successfully downloaded messages.
191   def fetch_all
193     if @use_ssl
194       if @ssl_verify
195         ssl_options = OpenSSL::SSL::VERIFY_PEER
196       else
197         ssl_options = OpenSSL::SSL::VERIFY_NONE
198       end
199       Net::POP3.enable_ssl(ssl_options)
200    else
201       Net::POP3.disable_ssl
202     end
204     # Process all the messages
206     i = 0
207     total = 0
209     while true
211       delete_list = []
213       Net::POP3.start(@host, @port, @user, @password, @use_apop) do |pop|
215         connected_at = Time.now
217         if i == 0 and not pop.mails.empty?
218           total = pop.n_mails
219           plural = total != 1
220           puts "  There " + (plural ? "are" : "is") + " #{total} message" +
221             (plural ? "s" : "") + " available."
222         end
224         if i >= total
225           if i == 0
226             puts "  No mail."
227           end
228           return
229         end
231         pop.each_mail do |m|
233           i += 1
235           print "  Retrieving message #{i} of #{total} [#{m.length} bytes]: "
237           uidl = m.uidl
238           $db.transaction
239           if db_has_uidl? $db, uidl
240             if $keep
241               puts " [Skipping]"
242             else
243               puts " [Skipping, Deleting]"
244               m.delete
245               delete_list.push uidl
246             end
247             $db.commit
248             next
249           end
251           progress = Progress.new(m.size)
253           # Fetch the message...
254           message = ""
255           m.pop do |chunk|
256             chunk.gsub!(/\r\n/, "\n")
257             message << chunk
258             progress.increase_by chunk.length
259           end
261           # Deliver the message...
262           Kernel.open("|-", "w+") do |f|
263             if f
264               f.write message
265             else
266               begin
267                 exec @command
268               rescue
269                 raise "Couldn't exec \"#{@command}\": #{$!}\n"
270               end
271             end
272           end
274           if $?.success?
275             unless add_uidl_to_db $db, uidl
276               $db.rollback
277               raise "Adding the UIDL to the database failed."
278             end
279           else
280             $db.rollback
281             raise "The command \"#{@command}\" failed with exit code #{$?.exitstatus}"
282           end
283           $db.commit
285           # We've successfully dealt with the message now...
286           if $keep
287             print " [Fetched]"
288           else
289             print " [Fetched, Deleting]"
290             m.delete
291             delete_list.push uidl
292           end
293           puts
295           if $reconnect_after and Time.now - connected_at >= $reconnect_after
296             break
297           end
299         end
300       end
302       $db.transaction do |db|
303         delete_uidls_from_db db, delete_list
304       end
306     end
307   end
311 # ----------------------------------------------------------------------
313 begin
315   # Make sure that the UIDL database file is present.
317   $db = SQLite3::Database.new($uidl_database_filename)
319   $db.execute( "CREATE TABLE IF NOT EXISTS uidls ( uidl TEXT PRIMARY KEY )" )
321   # Read in the configuration file...
323   accounts = open($configuration_file, "r") { |f| YAML.load(f) }
324   raise "No accounts specified in configuration file." if accounts.length == 0
326   # This is the main loop.  Go through every account, fetching all the
327   # new mail...
329   accounts.each_index do |i|
331     a = accounts[i]
333     # Do some quick checks on what we've just parsed...
335     valid_account_properties = ['Host', 'User', 'Pass', 'Command', 'SSL', 'APOP', 'Port']
337     (a.keys - valid_account_properties).each do |property|
338       STDERR.puts "Warning: in account #{i + 1}, the unknown account property `#{property}' was ignored."
339     end
341     ['Host', 'User', 'Pass'].each do |required|
342       raise "Missing `#{required}:' line in account #{i + 1}" unless a[required]
343       unless a[required].class == String
344         raise "The value in the `#{required}:' property of account #{i + 1} " +
345           "(`#{a[required].to_s}' was not interpreted as a string; you " +
346           "may need to quote it"
347       end
348     end
350     ['SSL', 'APOP'].each do |boolean|
351       if a['boolean']
352         unless [ TrueClass, FalseClass ].include? a[boolean].class
353           raise "In account #{i + 1}, `#{boolean}' property must be `yes', `no', `true' or `false'"
354         end
355       end
356     end
358     if a['Port']
359       unless a['Port'].class == Fixnum
360         raise "In account #{i + 1} the Port property must be an integer (not `#{a['Port']}\')"
361       end
362     else
363       a['Port'] = a['SSL'] ? 995 : 110
364     end
366     unless a['Command']
367       a['Command'] = 'procmail -f-'
368     end
370     account = POP3Account.new(a['Host'], a['User'], a['Pass'], a['SSL'],
371                               a['APOP'], a['Port'], a['Command'])
373     begin
374       puts "Checking account: #{account}"
375       puts "  Piping mail to the command: #{account.command}"
376       account.fetch_all
377     rescue Interrupt => e
378       $db.rollback if $db.transaction_active?
379       STDERR.puts "Interrupt received: exiting."
380       STDERR.puts $!.backtrace
381       break
382     rescue
383       $db.rollback if $db.transaction_active?
384       STDERR.puts "  Error fetching mail from account `#{account}': #{$!}"
385       STDERR.puts $!.backtrace
386     end
388   end
390 rescue
391   STDERR.puts "Fatal error: #{$!}"
392   STDERR.puts $!.backtrace