catfriend 0.16 → 0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/catfriend +124 -122
- data/lib/catfriend/dbus.rb +49 -49
- data/lib/catfriend/filetokenstack.rb +30 -30
- data/lib/catfriend/imap.rb +146 -139
- data/lib/catfriend/net_imap_exchange_patch.rb +27 -0
- data/lib/catfriend/server.rb +12 -12
- data/lib/catfriend/thread.rb +16 -16
- metadata +37 -21
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d3298ab27969cad7ca4f284d27fdfe153f83bbfb
|
4
|
+
data.tar.gz: dced1579477fff11e1acf0252908539893f6ca6f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c16e9adb4de53431a43c83d1b681f1a9ed59e350f4f1885d9d13203b825185c644ea4177a790bfdb8bf39f7b3afd0c0ec43b574d3c7b2c1a56fde2a6c633da8c
|
7
|
+
data.tar.gz: 46f30291ee03d29ae6ecd795fc207f8b78c760dacc638c4b30e7a3161df86dd42399172201d9ef8baf676eb75473f5de0d1b30122aa280ecee09a28cb3fa64ee
|
data/bin/catfriend
CHANGED
@@ -4,12 +4,14 @@
|
|
4
4
|
# new e-mail arrives.
|
5
5
|
#
|
6
6
|
# Author:: James Pike (mailto:catfriend@chilon.net)
|
7
|
-
# Copyright:: Copyright (c) 2011 James Pike
|
7
|
+
# Copyright:: Copyright (c) 2011-2014 James Pike
|
8
8
|
# License:: MIT
|
9
|
+
|
10
|
+
$LOAD_PATH << 'lib' if File.exists? 'lib/catfriend'
|
11
|
+
|
9
12
|
require 'catfriend/filetokenstack'
|
10
13
|
require 'catfriend/imap'
|
11
14
|
require 'catfriend/dbus'
|
12
|
-
require 'net/imap'
|
13
15
|
require 'optparse'
|
14
16
|
|
15
17
|
module Catfriend
|
@@ -20,149 +22,149 @@ APP_NAME = "catfriend"
|
|
20
22
|
stderr_bak = $stderr.dup
|
21
23
|
$stderr.reopen '/dev/null', 'w'
|
22
24
|
begin
|
23
|
-
|
25
|
+
require 'xdg'
|
24
26
|
rescue LoadError ; end
|
25
27
|
$stderr = stderr_bak # restore stderr
|
26
28
|
|
27
29
|
# Reads a simple configuration format and returns an array of servers.
|
28
30
|
def self.parse_config
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
31
|
+
# xdg is optional
|
32
|
+
begin
|
33
|
+
config_file = XDG['CONFIG'].find APP_NAME
|
34
|
+
rescue NameError ; end
|
35
|
+
config_file ||= "#{ENV['HOME']}/.config/#{APP_NAME}"
|
36
|
+
|
37
|
+
# for location of certificate file
|
38
|
+
Dir.chdir File.dirname(config_file)
|
39
|
+
|
40
|
+
servers = []
|
41
|
+
current = {}
|
42
|
+
defaults = {}
|
43
|
+
|
44
|
+
tokens = FileTokenStack.new config_file
|
45
|
+
until tokens.empty?
|
46
|
+
field = tokens.shift
|
47
|
+
|
48
|
+
# obviously assigning it in a loop like this is slow but hey it's
|
49
|
+
# only run-once config and ruby people say DRY a lot.
|
50
|
+
shift_tokens = lambda do
|
51
|
+
if tokens.empty?
|
52
|
+
raise ConfigError, "field #{field} requires parameter"
|
53
|
+
end
|
54
|
+
return tokens.shift
|
55
|
+
end
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
57
|
+
case field
|
58
|
+
when "host","imap"
|
59
|
+
# host is deprecated
|
60
|
+
if not current.empty?
|
61
|
+
servers << ImapServer.new(current)
|
62
|
+
current = {}
|
63
|
+
end
|
64
|
+
current[:host] = shift_tokens.call
|
65
|
+
when "notificationTimeout", "errorTimeout", "socketTimeout"
|
66
|
+
# convert from camelCase to camel_case
|
67
|
+
clean_field =
|
68
|
+
field.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2.downcase}" }
|
69
|
+
defaults[clean_field] = shift_tokens.call
|
70
|
+
when "checkInterval"
|
71
|
+
shift_tokens.call # deprecated, ignore parameter
|
72
|
+
when "cert_file"
|
73
|
+
cert_file = shift_tokens.call
|
74
|
+
unless File.exists? cert_file
|
75
|
+
raise ConfigError,
|
76
|
+
"non-existant SSL certificate `#{cert_file}'" +
|
77
|
+
", search path: #{File.dirname(config_file)}/"
|
78
|
+
end
|
79
|
+
current[:cert_file] = cert_file
|
80
|
+
when "mailbox", "id", "user", "password"
|
81
|
+
current[field] = shift_tokens.call
|
82
|
+
when "nossl"
|
83
|
+
current[:no_ssl] = true
|
84
|
+
when "work"
|
85
|
+
current[:work_account] = true
|
86
|
+
else
|
87
|
+
raise ConfigError,
|
88
|
+
"invalid config parameter '#{field}'"
|
88
89
|
end
|
90
|
+
end
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
+
servers << ImapServer.new(current) unless current.empty?
|
93
|
+
Catfriend.notification_timeout = (defaults["notification_timeout"] or 60).to_i
|
92
94
|
|
93
|
-
|
95
|
+
servers
|
94
96
|
end
|
95
97
|
|
96
98
|
# Main interface to the application. Reads all servers from config then runs
|
97
99
|
# each one in a thread. The program exits when all threads encounter an
|
98
100
|
# unrecoverable error. Perhaps I should make it exit if any thread exits.
|
99
101
|
def self.main args
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
else
|
124
|
-
puts "could not send shutdown signal, no server running?"
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end.parse!
|
128
|
-
|
129
|
-
return 0 if done_action
|
130
|
-
|
131
|
-
Catfriend.verbose = false unless foreground
|
132
|
-
|
133
|
-
servers = parse_config
|
134
|
-
servers.reject! { |s| s.work_account } unless work_accounts
|
135
|
-
raise ConfigError, "no servers to check" if servers.empty?
|
136
|
-
|
137
|
-
if foreground
|
138
|
-
main_loop servers
|
102
|
+
work_accounts = false
|
103
|
+
foreground = false
|
104
|
+
Catfriend.verbose = false
|
105
|
+
done_action = false
|
106
|
+
begin
|
107
|
+
OptionParser.new do |opts|
|
108
|
+
opts.banner = "usage: #{APP_NAME} [options]"
|
109
|
+
opts.on("-f", "--foreground", "run in foreground") do
|
110
|
+
foreground = true
|
111
|
+
end
|
112
|
+
|
113
|
+
opts.on("-w", "--work", "enable work accounts") do
|
114
|
+
work_accounts = true
|
115
|
+
end
|
116
|
+
opts.on("-v", "--verbose", "verbose output to console") do
|
117
|
+
Catfriend.verbose = true
|
118
|
+
end
|
119
|
+
|
120
|
+
opts.on("-s", "--stop", "shut down running server") do
|
121
|
+
done_action = true
|
122
|
+
dbus = DBus.new
|
123
|
+
if dbus.send_shutdown
|
124
|
+
puts "sent shutdown signal"
|
139
125
|
else
|
140
|
-
|
141
|
-
main_loop servers
|
142
|
-
end
|
143
|
-
Process.detach pid
|
126
|
+
puts "could not send shutdown signal, no server running?"
|
144
127
|
end
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
128
|
+
end
|
129
|
+
end.parse!
|
130
|
+
|
131
|
+
return 0 if done_action
|
132
|
+
|
133
|
+
Catfriend.verbose = false unless foreground
|
134
|
+
|
135
|
+
servers = parse_config
|
136
|
+
servers.reject! { |s| s.work_account } unless work_accounts
|
137
|
+
raise ConfigError, "no servers to check" if servers.empty?
|
138
|
+
|
139
|
+
if foreground
|
140
|
+
main_loop servers
|
151
141
|
else
|
152
|
-
|
142
|
+
pid = fork do
|
143
|
+
main_loop servers
|
144
|
+
end
|
145
|
+
Process.detach pid
|
153
146
|
end
|
154
|
-
|
155
|
-
|
147
|
+
rescue ConfigError => e
|
148
|
+
puts "misconfiguration: #{e.message}"
|
149
|
+
rescue Interrupt
|
150
|
+
servers.each { |s| s.kill }
|
151
|
+
rescue => e
|
152
|
+
puts "unknown error #{e.message}\n#{e.backtrace.join("\n")}"
|
153
|
+
else
|
154
|
+
return 0
|
155
|
+
end
|
156
|
+
|
157
|
+
1
|
156
158
|
end
|
157
159
|
|
158
160
|
def self.main_loop servers
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
161
|
+
dbus = DBus.new servers
|
162
|
+
dbus.start
|
163
|
+
servers.each { |s| s.start }
|
164
|
+
servers.each { |s| s.join }
|
165
|
+
dbus.join
|
164
166
|
end
|
165
167
|
|
166
|
-
end
|
168
|
+
end
|
167
169
|
|
168
170
|
exit Catfriend.main ARGV
|
data/lib/catfriend/dbus.rb
CHANGED
@@ -2,70 +2,70 @@ require 'catfriend/thread'
|
|
2
2
|
require 'catfriend/server'
|
3
3
|
require 'dbus'
|
4
4
|
|
5
|
-
module Catfriend
|
5
|
+
module Catfriend
|
6
6
|
|
7
7
|
SERVICE = "org.freedesktop.Catfriend"
|
8
8
|
PATH = "/org/freedesktop/Catfriend"
|
9
9
|
INTERFACE = "org.freedesktop.Catfriend.System"
|
10
10
|
|
11
11
|
class DBus
|
12
|
-
|
12
|
+
include Thread
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
dbus_interface INTERFACE do
|
22
|
-
dbus_method :stop do
|
23
|
-
Catfriend.whisper "received shutdown request"
|
24
|
-
@main.quit # this must be run from within method handler
|
25
|
-
@servers.each { |s| s.disconnect }
|
26
|
-
end
|
27
|
-
end
|
14
|
+
class DBusObject < ::DBus::Object
|
15
|
+
def initialize(main, servers)
|
16
|
+
@main = main
|
17
|
+
@servers = servers
|
18
|
+
super PATH
|
28
19
|
end
|
29
20
|
|
30
|
-
|
31
|
-
|
21
|
+
dbus_interface INTERFACE do
|
22
|
+
dbus_method :stop do
|
23
|
+
Catfriend.whisper "received shutdown request"
|
24
|
+
@main.quit # this must be run from within method handler
|
25
|
+
@servers.each { |s| s.disconnect }
|
26
|
+
end
|
32
27
|
end
|
28
|
+
end
|
33
29
|
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
def initialize(servers = nil)
|
31
|
+
@servers = servers
|
32
|
+
end
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
object = service.object(PATH)
|
42
|
-
object.introspect
|
43
|
-
object.default_iface = INTERFACE
|
44
|
-
object.stop
|
45
|
-
true
|
46
|
-
rescue
|
47
|
-
false
|
48
|
-
end
|
34
|
+
def init
|
35
|
+
@bus = ::DBus::SessionBus.instance unless @bus
|
36
|
+
end
|
49
37
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
38
|
+
def send_shutdown
|
39
|
+
init
|
40
|
+
service = @bus.service(SERVICE)
|
41
|
+
object = service.object(PATH)
|
42
|
+
object.introspect
|
43
|
+
object.default_iface = INTERFACE
|
44
|
+
object.stop
|
45
|
+
true
|
46
|
+
rescue
|
47
|
+
false
|
48
|
+
end
|
55
49
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
50
|
+
def start_service
|
51
|
+
object = DBusObject.new(@main, @servers)
|
52
|
+
service = @bus.request_service(SERVICE)
|
53
|
+
service.export object
|
54
|
+
end
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
rescue => e
|
67
|
-
puts "dbus unknown error #{e.message}\n#{e.backtrace.join("\n")}"
|
56
|
+
def run
|
57
|
+
init
|
58
|
+
if send_shutdown
|
59
|
+
Catfriend.whisper "shut down existing catfriend"
|
68
60
|
end
|
61
|
+
|
62
|
+
@main = ::DBus::Main.new
|
63
|
+
start_service
|
64
|
+
@main << @bus
|
65
|
+
@main.run
|
66
|
+
rescue => e
|
67
|
+
puts "dbus unknown error #{e.message}\n#{e.backtrace.join("\n")}"
|
68
|
+
end
|
69
69
|
end
|
70
70
|
|
71
|
-
end
|
71
|
+
end
|
@@ -2,40 +2,40 @@ module Catfriend
|
|
2
2
|
|
3
3
|
# Adapt a file to a stack of tokens.
|
4
4
|
class FileTokenStack
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
# Initialize the token stack with the path of the file, the given token
|
6
|
+
# match and comment skipping regexs.
|
7
|
+
def initialize file, token_match = /\S+/, comment_match = /#.*/
|
8
|
+
@token_match = token_match
|
9
|
+
@comment_match = comment_match
|
10
|
+
@stream = File.new file, "r"
|
11
|
+
@tokens = []
|
12
|
+
get_next_tokens
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
15
|
+
def get_next_tokens
|
16
|
+
# Never let the token stack get empty so that empty? can always
|
17
|
+
# work ahead of time.. this wouldn't be good for a network stream as
|
18
|
+
# it would block before delivering the last token.
|
19
|
+
while @tokens.empty?
|
20
|
+
@line = @stream.gets
|
21
|
+
return unless @line
|
22
|
+
@tokens = @line.sub(@comment_match, '').scan(@token_match) if @line
|
24
23
|
end
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
# Shift the next token from the current stream position.
|
27
|
+
def shift
|
28
|
+
ret = @tokens.shift
|
29
|
+
get_next_tokens
|
30
|
+
ret
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
# Report if any tokens remain
|
34
|
+
def empty?
|
35
|
+
@stream.eof and @tokens.empty?
|
36
|
+
end
|
37
37
|
|
38
|
-
|
38
|
+
private :get_next_tokens
|
39
39
|
end
|
40
40
|
|
41
|
-
end
|
41
|
+
end
|
data/lib/catfriend/imap.rb
CHANGED
@@ -2,169 +2,176 @@ require 'libnotify'
|
|
2
2
|
require 'catfriend/server'
|
3
3
|
require 'catfriend/thread'
|
4
4
|
|
5
|
+
require 'net/imap'
|
6
|
+
|
5
7
|
# unless I do this I get random errors from Libnotify on startup 90% of the
|
6
8
|
# time... this could be a bug in autoload or ruby 1.9 rather than libnotify
|
7
9
|
module Libnotify
|
8
|
-
|
10
|
+
class API ; end
|
9
11
|
end
|
10
12
|
|
11
13
|
module Catfriend
|
12
14
|
|
15
|
+
require_relative 'net_imap_exchange_patch'
|
16
|
+
|
13
17
|
# This class represents a thread capable of checking and creating
|
14
18
|
# notifications for a single mailbox on a single IMAP server.
|
15
19
|
class ImapServer
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
include Thread
|
21
|
+
include AccessorsFromHash
|
22
|
+
|
23
|
+
# Create new IMAP server with optional full configuration hash.
|
24
|
+
# If the hash is not supplied at construction a further call must be
|
25
|
+
# made to #configure before #start is called to start the thread.
|
26
|
+
def initialize(args = nil)
|
27
|
+
configure args if args
|
28
|
+
end
|
29
|
+
|
30
|
+
# Configure all attributes based on hash then make sure this
|
31
|
+
# represents a total valid configuration.
|
32
|
+
def configure args
|
33
|
+
super args
|
34
|
+
|
35
|
+
if not @user
|
36
|
+
raise ConfigError, "imap user not set"
|
24
37
|
end
|
25
|
-
|
26
|
-
|
27
|
-
# represents a total valid configuration.
|
28
|
-
def configure args
|
29
|
-
super args
|
30
|
-
|
31
|
-
if not @user
|
32
|
-
raise ConfigError, "imap user not set"
|
33
|
-
end
|
34
|
-
if not @host
|
35
|
-
raise ConfigError, "imap host not set"
|
36
|
-
end
|
37
|
-
if not @password
|
38
|
-
raise ConfigError, "imap password not set"
|
39
|
-
end
|
38
|
+
if not @host
|
39
|
+
raise ConfigError, "imap host not set"
|
40
40
|
end
|
41
|
-
|
42
|
-
|
43
|
-
# notifications and is set to the host unless over-ridden by the
|
44
|
-
# configuration file
|
45
|
-
def id ; @id || @host ; end
|
46
|
-
|
47
|
-
# Raise an error related to this particular server.
|
48
|
-
def error message
|
49
|
-
# consider raising notification instead?
|
50
|
-
puts "#{id}: #{message}"
|
41
|
+
if not @password
|
42
|
+
raise ConfigError, "imap password not set"
|
51
43
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
44
|
+
end
|
45
|
+
|
46
|
+
# The id is a token which represents this server when displaying
|
47
|
+
# notifications and is set to the host unless over-ridden by the
|
48
|
+
# configuration file
|
49
|
+
def id ; @id || @host ; end
|
50
|
+
|
51
|
+
# Raise an error related to this particular server.
|
52
|
+
def error message
|
53
|
+
# consider raising notification instead?
|
54
|
+
puts "#{id}: #{message}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# ThreadMixin interface. This connects to the mailserver and then
|
58
|
+
# runs #check_loop to do the e-mail checking if the connection
|
59
|
+
# succeeds.
|
60
|
+
def run
|
61
|
+
begin
|
62
|
+
@notification =
|
63
|
+
Libnotify.new :body => nil,
|
64
|
+
:timeout => Catfriend.notification_timeout
|
65
|
+
@message_count = connect
|
66
|
+
notify_message @message_count
|
67
|
+
# :body => nil means summary only
|
68
|
+
rescue OpenSSL::SSL::SSLError
|
69
|
+
error "try providing ssl certificate"
|
70
|
+
rescue Net::IMAP::NoResponseError
|
71
|
+
error "no response to connect, try ssl"
|
72
|
+
else
|
73
|
+
loop {
|
74
|
+
check_loop
|
75
|
+
break if stopping?
|
76
|
+
}
|
74
77
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
if r.data != @message_count
|
90
|
-
notify_message(r.data) if r.data > @message_count
|
91
|
-
@message_count = r.data
|
92
|
-
end
|
93
|
-
when 'EXPUNGE'
|
94
|
-
@message_count -= 1
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
Catfriend.whisper "idle loop over"
|
100
|
-
rescue Net::IMAP::Error, IOError
|
101
|
-
# reconnect and carry on
|
102
|
-
reconnect unless stopping?
|
103
|
-
rescue => e
|
104
|
-
unless stopping?
|
105
|
-
# todo: see if we have to re-open socket
|
106
|
-
notify_message "#{@message_count} [error: #{e.message}]"
|
107
|
-
puts e.backtrace.join "\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Continually waits for new e-mail raising notifications when new
|
81
|
+
# e-mail arrives or when error conditions happen. This methods only exits
|
82
|
+
# on an unrecoverable error.
|
83
|
+
def check_loop
|
84
|
+
@imap.idle do |r|
|
85
|
+
Catfriend.whisper "#{id}: #{r}"
|
86
|
+
next if r.instance_of? Net::IMAP::ContinuationRequest
|
87
|
+
|
88
|
+
if r.instance_of? Net::IMAP::UntaggedResponse
|
89
|
+
case r.name
|
90
|
+
when 'EXISTS', 'EXPUNGE'
|
91
|
+
@imap.idle_done
|
108
92
|
end
|
93
|
+
end
|
109
94
|
end
|
110
95
|
|
111
|
-
|
112
|
-
|
113
|
-
|
96
|
+
Catfriend.whisper "idle loop over"
|
97
|
+
count = get_unseen_count
|
98
|
+
if count != @message_count
|
99
|
+
notify_message(count) if count > @message_count
|
100
|
+
@message_count = count
|
114
101
|
end
|
115
|
-
|
116
|
-
|
117
|
-
|
102
|
+
rescue Net::IMAP::Error, IOError
|
103
|
+
# reconnect and carry on
|
104
|
+
reconnect unless stopping?
|
105
|
+
rescue => e
|
106
|
+
unless stopping?
|
107
|
+
# todo: see if we have to re-open socket
|
108
|
+
notify_message "#{@message_count} [error: #{e.message}]"
|
109
|
+
puts e.backtrace.join "\n"
|
118
110
|
end
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
111
|
+
end
|
112
|
+
|
113
|
+
def notify_message message
|
114
|
+
@notification.update :summary => "#{id}: #{message}"
|
115
|
+
Catfriend.whisper @notification.summary
|
116
|
+
end
|
117
|
+
|
118
|
+
def stopping?
|
119
|
+
stopped? or @stopping
|
120
|
+
end
|
121
|
+
|
122
|
+
def kill
|
123
|
+
disconnect
|
124
|
+
super
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_unseen_count
|
128
|
+
begin
|
129
|
+
# fetch raises an exception when the mailbox is empty
|
130
|
+
@imap.status(@mailbox || "INBOX", ["UNSEEN"])["UNSEEN"]
|
131
|
+
rescue => e
|
132
|
+
error "failed to get count of unseen messages"
|
133
|
+
0
|
123
134
|
end
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@imap = Net::IMAP.new(@host, args)
|
136
|
-
@imap.login(@user, @password)
|
137
|
-
@imap.select(@mailbox || "INBOX")
|
138
|
-
|
139
|
-
begin
|
140
|
-
# fetch raises an exception when the mailbox is empty
|
141
|
-
@imap.fetch('*', 'UID').first.seqno
|
142
|
-
rescue
|
143
|
-
0
|
144
|
-
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Connect to the configured IMAP server and return message count.
|
138
|
+
def connect
|
139
|
+
args = nil
|
140
|
+
if not @no_ssl
|
141
|
+
if @cert_file
|
142
|
+
args = { :ssl => { :ca_file => @cert_file } }
|
143
|
+
else
|
144
|
+
args = { :ssl => true }
|
145
|
+
end
|
145
146
|
end
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
147
|
+
@imap = Net::IMAP.new(@host, args)
|
148
|
+
@imap.login(@user, @password)
|
149
|
+
@imap.select(@mailbox || "INBOX")
|
150
|
+
|
151
|
+
get_unseen_count
|
152
|
+
end
|
153
|
+
|
154
|
+
def reconnect
|
155
|
+
notify_message "#{@message_count} [reconnecting]"
|
156
|
+
new_count = connect
|
157
|
+
if new_count != @message_count
|
158
|
+
notify_message new_count
|
159
|
+
else
|
160
|
+
# todo: only if it was still open
|
161
|
+
@notification.close
|
157
162
|
end
|
163
|
+
@message_count = new_count
|
164
|
+
end
|
158
165
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
166
|
+
def disconnect
|
167
|
+
@stopping = true
|
168
|
+
@imap.disconnect
|
169
|
+
end
|
163
170
|
|
164
|
-
|
171
|
+
private :connect, :reconnect, :check_loop, :run, :error, :notify_message
|
165
172
|
|
166
|
-
|
167
|
-
|
173
|
+
attr_writer :host, :password, :id, :user, :no_ssl, :cert_file, :mailbox
|
174
|
+
attr_accessor :work_account
|
168
175
|
end
|
169
176
|
|
170
|
-
end
|
177
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
# source:
|
4
|
+
# http://claudiofloreani.blogspot.co.uk/2012/01/monkeypatching-ruby-imap-class-to-build.html
|
5
|
+
# thank you!
|
6
|
+
|
7
|
+
module Net
|
8
|
+
class IMAP
|
9
|
+
class ResponseParser
|
10
|
+
def response
|
11
|
+
token = lookahead
|
12
|
+
case token.symbol
|
13
|
+
when T_PLUS
|
14
|
+
result = continue_req
|
15
|
+
when T_STAR
|
16
|
+
result = response_untagged
|
17
|
+
else
|
18
|
+
result = response_tagged
|
19
|
+
end
|
20
|
+
match(T_SPACE) if lookahead.symbol == T_SPACE
|
21
|
+
match(T_CRLF)
|
22
|
+
match(T_EOF)
|
23
|
+
return result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/catfriend/server.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
module Catfriend
|
2
2
|
|
3
3
|
class << self
|
4
|
-
|
4
|
+
attr_accessor :notification_timeout, :verbose
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
# puts something if -v was used
|
7
|
+
def whisper *args
|
8
|
+
puts *args if verbose
|
9
|
+
end
|
10
10
|
end
|
11
11
|
|
12
12
|
# Mixin to provide #configure which allows all instance variables with write
|
13
13
|
# accessors declared to be set from a hash.
|
14
14
|
module AccessorsFromHash
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
15
|
+
# Call this to tranfer the hash data to corresponding attributes. Any
|
16
|
+
# hash keys that do not have a corresponding write accessor in the
|
17
|
+
# class are silently ignored.
|
18
|
+
def configure args
|
19
|
+
args.each do |opt, val|
|
20
|
+
instance_variable_set("@#{opt}", val) if respond_to? "#{opt}="
|
22
21
|
end
|
22
|
+
end
|
23
23
|
end
|
24
24
|
|
25
25
|
# This class is used to signal the user made an error in their configuration.
|
data/lib/catfriend/thread.rb
CHANGED
@@ -2,28 +2,28 @@ module Catfriend
|
|
2
2
|
|
3
3
|
# Mixin this module and define "run" for a simple runnable/joinable thread
|
4
4
|
module Thread
|
5
|
-
|
6
|
-
|
5
|
+
# Call to start a thread running via the start method.
|
6
|
+
def start ; @thread = ::Thread.new { run } ; end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
# Test whether thread is currently stopped or closing down.
|
9
|
+
def stopped? ; @thread.nil? ; end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
11
|
+
# Join thread if it has started.
|
12
|
+
def join
|
13
|
+
unless stopped?
|
14
|
+
@thread.join
|
15
|
+
@thread = nil
|
17
16
|
end
|
17
|
+
end
|
18
18
|
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
20
|
+
# Kill thread if it has started.
|
21
|
+
def kill
|
22
|
+
unless stopped?
|
23
|
+
@thread.kill
|
24
|
+
@thread = nil
|
26
25
|
end
|
26
|
+
end
|
27
27
|
end
|
28
28
|
|
29
29
|
end # end Catfriend module
|
metadata
CHANGED
@@ -1,38 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: catfriend
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
5
|
-
prerelease:
|
4
|
+
version: '0.17'
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- James Pike
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2014-03-09 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: libnotify
|
16
|
-
requirement:
|
17
|
-
none: false
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: 0.7.1
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0.8'
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
|
-
version_requirements:
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.7.1
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.8'
|
25
33
|
- !ruby/object:Gem::Dependency
|
26
34
|
name: ruby-dbus
|
27
|
-
requirement:
|
28
|
-
none: false
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
29
36
|
requirements:
|
30
|
-
- -
|
37
|
+
- - '>='
|
31
38
|
- !ruby/object:Gem::Version
|
32
39
|
version: '0.7'
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0.11'
|
33
43
|
type: :runtime
|
34
44
|
prerelease: false
|
35
|
-
version_requirements:
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0.7'
|
50
|
+
- - ~>
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0.11'
|
36
53
|
description: E-mail checker with libnotify desktop notifications.
|
37
54
|
email:
|
38
55
|
- catfriend@chilon.net
|
@@ -43,36 +60,35 @@ extra_rdoc_files: []
|
|
43
60
|
files:
|
44
61
|
- LICENSE
|
45
62
|
- catfriend.example
|
46
|
-
- lib/catfriend/dbus.rb
|
47
63
|
- lib/catfriend/server.rb
|
48
|
-
- lib/catfriend/
|
64
|
+
- lib/catfriend/dbus.rb
|
49
65
|
- lib/catfriend/thread.rb
|
66
|
+
- lib/catfriend/net_imap_exchange_patch.rb
|
50
67
|
- lib/catfriend/imap.rb
|
51
|
-
-
|
52
|
-
|
68
|
+
- lib/catfriend/filetokenstack.rb
|
69
|
+
- bin/catfriend
|
53
70
|
homepage: https://github.com/nuisanceofcats/catfriend
|
54
71
|
licenses:
|
55
72
|
- Expat
|
73
|
+
metadata: {}
|
56
74
|
post_install_message:
|
57
75
|
rdoc_options: []
|
58
76
|
require_paths:
|
59
77
|
- lib
|
60
78
|
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
-
none: false
|
62
79
|
requirements:
|
63
|
-
- -
|
80
|
+
- - '>='
|
64
81
|
- !ruby/object:Gem::Version
|
65
82
|
version: '0'
|
66
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
-
none: false
|
68
84
|
requirements:
|
69
|
-
- -
|
85
|
+
- - '>='
|
70
86
|
- !ruby/object:Gem::Version
|
71
87
|
version: '1.3'
|
72
88
|
requirements: []
|
73
89
|
rubyforge_project:
|
74
|
-
rubygems_version:
|
90
|
+
rubygems_version: 2.0.14
|
75
91
|
signing_key:
|
76
|
-
specification_version:
|
92
|
+
specification_version: 4
|
77
93
|
summary: E-mail checker with desktop notifications.
|
78
94
|
test_files: []
|