catfriend 0.16 → 0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|