Use absolute paths in .gitignore
[stand.git] / stand
blobc01b0f7689a720fbac6ac29d4065027d4f8258c5
1 #!/usr/bin/ruby
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 # ruby
22 # libruby
23 # libopenssl-ruby
24 # libsqlite3-ruby
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
123 def increase_by(i)
124 set @current_value + i
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
137 @current_value = v
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
163 def to_s
164 "#{@user} at #{@host}:#{@port}#{ @use_ssl ? ' (SSL)' : ''}"
167 def account_uidl(uidl)
168 "#{@user}@#{@host}:#{@port}##{uidl}"
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) )
177 def delete_uidls_from_db(db,a)
178 a.each do |u|
179 db.execute( "DELETE FROM uidls WHERE uidl = ?", account_uidl(u) )
183 def db_has_uidl?(db,u)
184 rows = db.execute( "SELECT uidl FROM uidls WHERE uidl = ?", account_uidl(u) )
185 rows.length > 0
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
199 Net::POP3.enable_ssl(ssl_options)
200 else
201 Net::POP3.disable_ssl
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."
224 if i >= total
225 if i == 0
226 puts " No mail."
228 return
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
247 $db.commit
248 next
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
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"
274 if $?.success?
275 unless add_uidl_to_db $db, uidl
276 $db.rollback
277 raise "Adding the UIDL to the database failed."
279 else
280 $db.rollback
281 raise "The command \"#{@command}\" failed with exit code #{$?.exitstatus}"
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
293 puts
295 if $reconnect_after and Time.now - connected_at >= $reconnect_after
296 break
302 $db.transaction do |db|
303 delete_uidls_from_db db, delete_list
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."
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"
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'"
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']}\')"
362 else
363 a['Port'] = a['SSL'] ? 995 : 110
366 unless a['Command']
367 a['Command'] = 'procmail -f-'
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
390 rescue
391 STDERR.puts "Fatal error: #{$!}"