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:
26 # See http://mythic-beasts.com/~mark/software/stand/
27 # for further remarks.
29 $program_name = 'stand'
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"
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.
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 ]
77 $reconnect_after = nil
81 options
.each
do |opt
, arg
|
89 when "--no-ssl-verify"
91 when "--configuration-file"
92 $configuration_file = arg
93 when "--reconnect-after"
94 $reconnect_after = Float(arg
)
101 STDERR.print
"Bad command line option: #{$!}\n"
107 if $keep and $reconnect_after
108 STDERR.puts
"There should be no reason to specify --reconnect-after with --keep"
112 # ----------------------------------------------------------------------
114 # A fairly idiotic progress class.
118 def initialize(maximum
)
124 set
@current_value + i
128 block_change
= (v
/ 1024) - (@current_value / 1024)
130 (1..block_change
).each
{ print
"." }
133 backspaces
= - block_change
134 (1..backspaces
).each
{ print
"\b" }
142 # ----------------------------------------------------------------------
144 # This class deals with connecting to a POP3 server, and fetching the
149 attr_accessor
:host, :user, :port
153 def initialize(host
, user
, password
, use_ssl
, use_apop
, port
, command
)
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
)
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
) )
188 # Fetch all the new messages from this POP3 account, deleting
189 # successfully downloaded messages.
195 ssl_options
= OpenSSL
::SSL::VERIFY_PEER
197 ssl_options
= OpenSSL
::SSL::VERIFY_NONE
199 Net
::POP3.enable_ssl(ssl_options
)
201 Net
::POP3.disable_ssl
204 # Process all the messages
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
?
220 puts
" There " + (plural
? "are" : "is") + " #{total} message" +
221 (plural
? "s" : "") + " available."
235 print
" Retrieving message #{i} of #{total} [#{m.length} bytes]: "
239 if db_has_uidl
? $db, uidl
243 puts
" [Skipping, Deleting]"
245 delete_list
.push uidl
251 progress
= Progress
.new(m
.size
)
253 # Fetch the message...
256 chunk
.gsub
!(/\r\n/, "\n")
258 progress
.increase_by chunk
.length
261 # Deliver the message...
262 Kernel
.open("|-", "w+") do |f
|
269 raise "Couldn't exec \"#{@command}\": #{$!}\n"
275 unless add_uidl_to_db
$db, uidl
277 raise "Adding the UIDL to the database failed."
281 raise "The command \"#{@command}\" failed with exit code #{$?.exitstatus}"
285 # We've successfully dealt with the message now...
289 print
" [Fetched, Deleting]"
291 delete_list
.push uidl
295 if $reconnect_after and Time
.now
- connected_at
>= $reconnect_after
302 $db.transaction
do |db
|
303 delete_uidls_from_db db
, delete_list
311 # ----------------------------------------------------------------------
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
329 accounts
.each_index
do |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
|
352 unless [ TrueClass
, FalseClass
].include? a
[boolean
].class
353 raise "In account #{i + 1}, `#{boolean}' property must be `yes', `no', `true' or `false'"
359 unless a
['Port'].class == Fixnum
360 raise "In account #{i + 1} the Port property must be an integer (not `#{a['Port']}\')"
363 a
['Port'] = a
['SSL'] ? 995 : 110
367 a
['Command'] = 'procmail -f-'
370 account
= POP3Account
.new(a
['Host'], a
['User'], a
['Pass'], a
['SSL'],
371 a
['APOP'], a
['Port'], a
['Command'])
374 puts
"Checking account: #{account}"
375 puts
" Piping mail to the command: #{account.command}"
377 rescue Interrupt
=> e
378 $db.rollback
if $db.transaction_active
?
379 STDERR.puts
"Interrupt received: exiting."
380 STDERR.puts $
!.backtrace
383 $db.rollback
if $db.transaction_active
?
384 STDERR.puts
" Error fetching mail from account `#{account}': #{$!}"
385 STDERR.puts $
!.backtrace
391 STDERR.puts
"Fatal error: #{$!}"