3 # Copyright Mark Longair (c) 2002, 2005, Reuben Thomas (c) 2005, 2006.
5 # This is a simple script for fetching mail from POP3 accounts.
6 # On a Debian system, you'll need the following packages:
13 # See http://mythic-beasts.com/~mark/software/stand/
14 # for further remarks.
17 $program_name = 'stand'
27 # FIXME: I don't understand the recent changes that have been made to
28 # InternetMessageIO and broke the old version of these two classes, so
29 # while this seems to work at the moment, I expect it to break at any
34 class SSLIO
< InternetMessageIO
36 def SSLIO
.old_open(addr
, port
, open_timeout
= 30, read_timeout
= 30, debug_output
= nil)
38 tcp_socket
= timeout(open_timeout
) { TCPsocket
.new(addr
, port
) }
40 @ssl_context = OpenSSL
::SSL::SSLContext.new()
43 @ssl_context.ca_path
= '/etc/ssl/certs'
44 @ssl_context.verify_mode
= OpenSSL
::SSL::VERIFY_PEER
46 @ssl_context.verify_mode
= OpenSSL
::SSL::VERIFY_NONE
49 socket
= OpenSSL
::SSL::SSLSocket.new(tcp_socket
, @ssl_context)
54 io
.read_timeout
= read_timeout
55 io
.debug_output
= debug_output
64 # ----------------------------------------------------------------------
84 # ----------------------------------------------------------------------
86 # Command line argument parsing...
88 # Some default values...
90 $configuration_file = "#{ENV['HOME']}/.stand.yaml"
91 $configuration_file_pretty = "~/.stand.yaml"
92 $uidl_database_filename = "#{ENV['HOME']}/.stand-uidls"
97 Usage: #{$program_name} [OPTION]...
99 -h, --help Display this message and exit.
100 -k, --keep Don't delete messages on server.
101 -n, --no-ssl-verify Don't verify the peer's certificate when using SSL.
102 -f <FILENAME>, --configuration-file=<FILENAME>
103 Specify the configuration file to use.
104 (default: #{$configuration_file_pretty})
109 options
= GetoptLong
.new(
110 [ "--help", "-h", GetoptLong
::NO_ARGUMENT ],
111 [ "--keep", "-k", GetoptLong
::NO_ARGUMENT ],
112 [ "--no-ssl-verify", "-n", GetoptLong
::NO_ARGUMENT ],
113 [ "--configuration-file", "-f", GetoptLong
::REQUIRED_ARGUMENT ]
122 options
.each
do |opt
, arg
|
130 when "--no-ssl-verify"
132 when "--configuration-file"
133 $configuration_file = arg
140 print
"Bad command line option: " + $
! + "\n"
146 # ----------------------------------------------------------------------
148 # A fairly idiotic progress class.
152 def initialize(maximum
)
158 set
@current_value + i
162 block_change
= (v
/ 1024) - (@current_value / 1024)
164 (1..block_change
).each
{ print
"." }
167 backspaces
= - block_change
168 (1..backspaces
).each
{ print
"\b" }
176 # ----------------------------------------------------------------------
178 # This class deals with connecting to a POP3 server, and fetching the
183 attr_accessor
:host, :user, :port
187 def initialize(host
, user
, password
, use_ssl
, use_apop
, port
, command
)
198 "#{@user} at #{@host}:#{@port}#{ @use_ssl ? ' (SSL)' : ''}"
201 def account_uidl(uidl
)
202 "#{@user}@#{@host}:#{@port}##{uidl}"
205 # These three methods manipulate the UIDL database.
207 def add_uidl_to_db(db
,u
)
208 db
.execute( "INSERT INTO uidls VALUES (?)", account_uidl(u
) )
211 def delete_uidls_from_db(db
,a
)
213 db
.execute( "DELETE FROM uidls WHERE uidl = ?", account_uidl(u
) )
217 def db_has_uidl
?(db
,u
)
218 rows
= db
.execute( "SELECT uidl FROM uidls WHERE uidl = ?", account_uidl(u
) )
222 # Fetch all the new messages from this POP3 account, deleting
223 # successfully downloaded messages.
227 # Choose correct POP class
230 pop3_class
= Net
::POP3s
232 pop3_class
= Net
::POP3
235 # Process all the messages
241 $db.transaction
do |db
|
243 pop3_class
.start(@host, @port, @user, @password, @use_apop) do |pop
|
251 puts
" There " + (plural
? "are" : "is") + " #{total} message" +
252 (plural
? "s" : "") + " available."
259 print
" Retrieving message #{i} of #{total} [#{m.length} bytes]: "
262 if db_has_uidl
? db
, uidl
266 puts
" [Skipping and deleting]"
268 delete_list
.push uidl
273 progress
= Progress
.new(m
.size
)
275 # Fetch the message...
278 chunk
.gsub
!(/\r\n/, "\n")
280 progress
.increase_by chunk
.length
283 # Deliver the message...
284 Kernel
.open("|-", "w+") do |f
|
291 raise "Couldn't exec \"#{@command}\": #{$!}\n"
297 add_uidl_to_db db
, uidl
299 raise "The command \"#{@command}\" failed with exit code #{$?.exitstatus}"
302 # We've successfully dealt with the message now...
306 delete_list
.push uidl
313 delete_uidls_from_db db
, delete_list
320 # ----------------------------------------------------------------------
323 # ----------------------------------------------------------------------
327 # Make sure that the UIDL database file is present.
329 $db = SQLite3
::Database.new($uidl_database_filename)
331 $db.transaction
do |d
|
332 d
.execute( "CREATE TABLE IF NOT EXISTS uidls ( uidl TEXT PRIMARY KEY )" )
335 # Read in the configuration file...
337 accounts
= open($configuration_file, "r") { |f
| YAML
.load(f
) }
338 raise "No accounts specified in configuration file." if accounts
.length
== 0
340 # This is the main loop. Go through every account, fetching all the
343 accounts
.each_index
do |i
|
347 # Do some quick checks on what we've just parsed...
349 valid_account_properties
= ['Host', 'User', 'Pass', 'Command', 'SSL', 'APOP', 'Port']
351 (a
.keys
- valid_account_properties
).each
do |property
|
352 puts
"Warning: in account #{i + 1}, the unknown account property `#{property}' was ignored."
355 ['Host', 'User', 'Pass'].each
do |required
|
356 raise "Missing `#{required}:' line in account #{i + 1}" unless a
[required
]
357 unless a
[required
].class == String
358 raise "The value in the `#{required}:' property of account #{i + 1} " +
359 "(`#{a[required].to_s}' was not interpreted as a string; you " +
360 "may need to quote it"
364 ['SSL', 'APOP'].each
do |boolean
|
366 unless [ TrueClass
, FalseClass
].include? a
[boolean
].class
367 raise "In account #{i + 1}, `#{boolean}' property must be `yes', `no', `true' or `false'"
373 unless a
['Port'].class == Fixnum
374 raise "In account #{i + 1} the Port property must be an integer (not `#{a['Port']}\')"
377 a
['Port'] = a
['SSL'] ? 995 : 110
381 a
['Command'] = 'procmail -f-'
384 account
= POP3Account
.new(a
['Host'], a
['User'], a
['Pass'], a
['SSL'],
385 a
['APOP'], a
['Port'], a
['Command'])
388 puts
"Checking account: #{account}"
389 puts
" Piping mail to the command: #{account.command}"
391 rescue Interrupt
=> i_exception
392 puts
"Interrupt Exception from fetch_all, backtrace:\n" + i_exception
.backtrace
.join("\n")
394 puts
" Error fetching mail from account `#{account}': " + $
!
400 puts
"Interrupt received: exiting."
403 puts
"Fatal error: " + $
!