catfriend 0.1 → 0.10
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.
- data/bin/catfriend +57 -50
- data/catfriend.example +16 -0
- data/lib/catfriend/filetokenstack.rb +41 -0
- data/lib/{imap.rb → catfriend/imap.rb} +36 -7
- data/lib/catfriend/notify.rb +50 -0
- data/lib/{server.rb → catfriend/server.rb} +13 -2
- metadata +11 -19
data/bin/catfriend
CHANGED
@@ -6,10 +6,13 @@
|
|
6
6
|
# Author:: James Pike (mailto:catfriend@chilon.net)
|
7
7
|
# Copyright:: Copyright (c) 2011 James Pike
|
8
8
|
# License:: MIT
|
9
|
+
require 'catfriend/filetokenstack'
|
10
|
+
require 'catfriend/imap'
|
11
|
+
require 'net/imap'
|
12
|
+
|
9
13
|
module Catfriend
|
10
14
|
|
11
|
-
|
12
|
-
require 'net/imap'
|
15
|
+
APP_NAME = "catfriend"
|
13
16
|
|
14
17
|
# xdg is optional. it outputs to stderr, so i redirect it for a while.
|
15
18
|
stderr_bak = $stderr.dup
|
@@ -23,9 +26,9 @@ $stderr = stderr_bak # restore stderr
|
|
23
26
|
def self.parse_config
|
24
27
|
# xdg is optional
|
25
28
|
begin
|
26
|
-
config_file = XDG['CONFIG'].find
|
29
|
+
config_file = XDG['CONFIG'].find APP_NAME
|
27
30
|
rescue NameError ; end
|
28
|
-
config_file ||= "#{ENV['HOME']}/.config
|
31
|
+
config_file ||= "#{ENV['HOME']}/.config/#{APP_NAME}"
|
29
32
|
|
30
33
|
# for location of certificate file
|
31
34
|
Dir.chdir File.dirname(config_file)
|
@@ -34,58 +37,54 @@ def self.parse_config
|
|
34
37
|
current = {}
|
35
38
|
defaults = {}
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
shift_tokens = lambda do
|
47
|
-
if tokens.empty? then
|
48
|
-
raise ConfigError,
|
49
|
-
"field #{field} requires parameter"
|
50
|
-
end
|
51
|
-
return tokens.shift
|
40
|
+
tokens = FileTokenStack.new config_file
|
41
|
+
until tokens.empty?
|
42
|
+
field = tokens.shift
|
43
|
+
|
44
|
+
# obviously assigning it in a loop like this is slow but hey it's
|
45
|
+
# only run-once config and ruby people say DRY a lot.
|
46
|
+
shift_tokens = lambda do
|
47
|
+
if tokens.empty?
|
48
|
+
raise ConfigError, "field #{field} requires parameter"
|
52
49
|
end
|
50
|
+
return tokens.shift
|
51
|
+
end
|
53
52
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
raise ConfigError,
|
73
|
-
"non-existant SSL certificate `#{cert_file}'" +
|
74
|
-
", search path: #{File.dirname(config_file)}/"
|
75
|
-
end
|
76
|
-
current[:cert_file] = cert_file
|
77
|
-
when "mailbox", "id", "user", "password"
|
78
|
-
current[field] = shift_tokens.call
|
79
|
-
when "nossl"
|
80
|
-
current[:no_ssl] = true
|
81
|
-
else
|
53
|
+
case field
|
54
|
+
when "host","imap"
|
55
|
+
# host is deprecated
|
56
|
+
if not current.empty?
|
57
|
+
servers << ImapServer.new(current)
|
58
|
+
current = {}
|
59
|
+
end
|
60
|
+
current[:host] = shift_tokens.call
|
61
|
+
when "notificationTimeout", "errorTimeout", "socketTimeout"
|
62
|
+
# convert from camelCase to camel_case
|
63
|
+
clean_field =
|
64
|
+
field.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2.downcase}" }
|
65
|
+
defaults[clean_field] = shift_tokens.call
|
66
|
+
when "checkInterval"
|
67
|
+
shift_tokens.call # deprecated, ignore parameter
|
68
|
+
when "cert_file"
|
69
|
+
cert_file = shift_tokens.call
|
70
|
+
unless File.exists? cert_file
|
82
71
|
raise ConfigError,
|
83
|
-
|
72
|
+
"non-existant SSL certificate `#{cert_file}'" +
|
73
|
+
", search path: #{File.dirname(config_file)}/"
|
84
74
|
end
|
75
|
+
current[:cert_file] = cert_file
|
76
|
+
when "mailbox", "id", "user", "password"
|
77
|
+
current[field] = shift_tokens.call
|
78
|
+
when "nossl"
|
79
|
+
current[:no_ssl] = true
|
80
|
+
else
|
81
|
+
raise ConfigError,
|
82
|
+
"invalid config parameter '#{field}': #{line}"
|
85
83
|
end
|
86
84
|
end
|
87
85
|
|
88
86
|
servers << ImapServer.new(current) unless current.empty?
|
87
|
+
@@notification_timeout = (defaults["notification_timeout"] or 60).to_i
|
89
88
|
|
90
89
|
servers
|
91
90
|
end
|
@@ -96,10 +95,18 @@ end
|
|
96
95
|
def self.main
|
97
96
|
begin
|
98
97
|
servers = parse_config
|
98
|
+
rescue ConfigError => e
|
99
|
+
puts "misconfiguration: #{e.message}"
|
100
|
+
end
|
101
|
+
|
102
|
+
begin
|
103
|
+
# todo: daemonize here unless certain command line argument given
|
99
104
|
servers.each { |s| s.start }
|
100
105
|
servers.each { |s| s.join }
|
101
|
-
rescue
|
102
|
-
|
106
|
+
rescue Interrupt
|
107
|
+
servers.each { |s| s.kill }
|
108
|
+
rescue => e
|
109
|
+
puts "unknown error #{e.message}\n#{e.backtrace.join("\n")}"
|
103
110
|
end
|
104
111
|
end
|
105
112
|
|
data/catfriend.example
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
host secure.work.com
|
2
|
+
user bossman@work.com
|
3
|
+
password secure
|
4
|
+
nossl # turn off ssl, it is on by default
|
5
|
+
|
6
|
+
host imap.gmail.com
|
7
|
+
id fun # used instead of host in nofifications when available
|
8
|
+
user friend@gmail.com
|
9
|
+
password faptap
|
10
|
+
cert_file server.pem # path relative to config file
|
11
|
+
|
12
|
+
# time notification remains on screen in milliseconds
|
13
|
+
notificationTimeout 10000
|
14
|
+
errorTimeout 60000 # as above but for error notifications
|
15
|
+
socketTimeout 60 # server socket timeout in seconds
|
16
|
+
checkInterval 60 # how often to wait between checks in seconds
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Catfriend
|
2
|
+
|
3
|
+
# Adapt a file to a stack of tokens.
|
4
|
+
class FileTokenStack
|
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
|
+
|
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
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
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
|
+
|
33
|
+
# Report if any tokens remain
|
34
|
+
def empty?
|
35
|
+
@stream.eof and @tokens.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
private :get_next_tokens
|
39
|
+
end
|
40
|
+
|
41
|
+
end # end module
|
@@ -1,4 +1,7 @@
|
|
1
|
-
require 'server'
|
1
|
+
require 'catfriend/server'
|
2
|
+
require 'catfriend/notify'
|
3
|
+
|
4
|
+
module Catfriend
|
2
5
|
|
3
6
|
# This class represents a thread capable of checking and creating
|
4
7
|
# notifications for a single mailbox on a single IMAP server.
|
@@ -46,9 +49,12 @@ class ImapServer
|
|
46
49
|
# runs #check_loop to do the e-mail checking if the connection
|
47
50
|
# succeeds.
|
48
51
|
def run
|
49
|
-
# connect and go
|
50
52
|
begin
|
51
53
|
connect
|
54
|
+
# :body => nil means summary only
|
55
|
+
@notification =
|
56
|
+
Libnotify.new :body => nil,
|
57
|
+
:timeout => Catfriend.notification_timeout
|
52
58
|
rescue OpenSSL::SSL::SSLError
|
53
59
|
error "try providing ssl certificate"
|
54
60
|
rescue Net::IMAP::NoResponseError
|
@@ -62,14 +68,34 @@ class ImapServer
|
|
62
68
|
# e-mail arrives or when error conditions happen. This methods only exits
|
63
69
|
# on an unrecoverable error.
|
64
70
|
def check_loop
|
65
|
-
|
71
|
+
req = @imap.fetch('*', 'UID').first
|
72
|
+
notify_n_messages req.seqno
|
73
|
+
|
74
|
+
@imap.idle do |r|
|
75
|
+
next if r.instance_of? Net::IMAP::ContinuationRequest
|
76
|
+
|
77
|
+
if r.instance_of? Net::IMAP::UntaggedResponse and r.name == 'EXISTS'
|
78
|
+
notify_n_messages r.data
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def notify_n_messages n_messages
|
84
|
+
@notification.update do |n|
|
85
|
+
n.summary = "#{id}: #{n_messages}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def kill
|
90
|
+
disconnect
|
91
|
+
super
|
66
92
|
end
|
67
93
|
|
68
94
|
# Connect to the configured IMAP server.
|
69
95
|
def connect
|
70
96
|
args = nil
|
71
|
-
if not @no_ssl
|
72
|
-
if @cert_file
|
97
|
+
if not @no_ssl
|
98
|
+
if @cert_file
|
73
99
|
args = { :ssl => { :ca_file => @cert_file } }
|
74
100
|
else
|
75
101
|
args = { :ssl => true }
|
@@ -77,10 +103,13 @@ class ImapServer
|
|
77
103
|
end
|
78
104
|
@imap = Net::IMAP.new(@host, args)
|
79
105
|
@imap.login(@user, @password)
|
80
|
-
|
106
|
+
@imap.select(@mailbox || "INBOX")
|
81
107
|
end
|
82
108
|
|
83
|
-
|
109
|
+
def disconnect ; @imap.disconnect ; end
|
110
|
+
|
111
|
+
private :connect, :disconnect, :check_loop, :run, :error, :notify_n_messages
|
84
112
|
attr_writer :host, :password, :id, :user, :no_ssl, :cert_file, :mailbox
|
85
113
|
end
|
86
114
|
|
115
|
+
end # end module
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'libnotify'
|
2
|
+
|
3
|
+
# Patch libnotify to add notification updating support. I have pushed
|
4
|
+
# this patch to the libnotify project and it should drop with 0.7
|
5
|
+
module Libnotify
|
6
|
+
# Re-open to import notification update.
|
7
|
+
module FFI
|
8
|
+
class << self
|
9
|
+
alias_method :orig_attach_functions!, :attach_functions!
|
10
|
+
end
|
11
|
+
def self.attach_functions!
|
12
|
+
attach_function :notify_notification_update,
|
13
|
+
[:pointer, :string, :string, :string, :pointer],
|
14
|
+
:pointer
|
15
|
+
orig_attach_functions!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Re-open and add update method.
|
20
|
+
class API
|
21
|
+
# Rewrite show to store notification in an instance variable.
|
22
|
+
def show!
|
23
|
+
notify_init(self.class.to_s) or raise "notify_init failed"
|
24
|
+
@notification = notify_notification_new(summary, body, icon_path, nil)
|
25
|
+
notify_notification_set_urgency(@notification, lookup_urgency(urgency))
|
26
|
+
notify_notification_set_timeout(@notification, timeout || -1)
|
27
|
+
if append
|
28
|
+
notify_notification_set_hint_string(@notification, "x-canonical-append", "")
|
29
|
+
notify_notification_set_hint_string(@notification, "append", "")
|
30
|
+
end
|
31
|
+
if transient
|
32
|
+
notify_notification_set_hint_uint32(@notification, "transient", 1)
|
33
|
+
end
|
34
|
+
notify_notification_show(@notification, nil)
|
35
|
+
ensure
|
36
|
+
notify_notification_clear_hints(@notification) if (append || transient)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Updates a previously shown notification.
|
40
|
+
def update(&block)
|
41
|
+
yield(self) if block_given?
|
42
|
+
if @notification
|
43
|
+
notify_notification_update(@notification, summary, body, icon_path, nil)
|
44
|
+
notify_notification_show(@notification, nil)
|
45
|
+
else
|
46
|
+
show!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,10 +1,19 @@
|
|
1
|
+
module Catfriend
|
2
|
+
|
3
|
+
def self.notification_timeout
|
4
|
+
@@notification_timeout
|
5
|
+
end
|
6
|
+
|
1
7
|
# Mixin this module and define "run" for a simple runnable/joinable thread
|
2
8
|
module ThreadMixin
|
3
9
|
# Call to start a thread running via the start method.
|
4
10
|
def start ; @thread = Thread.new { run } ; end
|
5
11
|
|
6
|
-
# Join thread
|
7
|
-
def join ; @thread.join ; end
|
12
|
+
# Join thread if it has started.
|
13
|
+
def join ; @thread.join if @thread ; end
|
14
|
+
|
15
|
+
# Kill thread if it has started.
|
16
|
+
def kill ; @thread.kill if @thread ; end
|
8
17
|
end
|
9
18
|
|
10
19
|
# Mixin to provide #configure which allows all instance variables with write
|
@@ -22,3 +31,5 @@ end
|
|
22
31
|
|
23
32
|
# This class is used to signal the user made an error in their configuration.
|
24
33
|
class ConfigError < Exception ; end
|
34
|
+
|
35
|
+
end # end module
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: catfriend
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.10'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,30 +9,19 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-01-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
-
requirement: &
|
15
|
+
name: libnotify
|
16
|
+
requirement: &14394100 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: '0.
|
21
|
+
version: '0.6'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
- !ruby/object:Gem::Dependency
|
26
|
-
name: xdg
|
27
|
-
requirement: &9102900 !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ! '>='
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '2'
|
33
|
-
type: :runtime
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: *9102900
|
24
|
+
version_requirements: *14394100
|
36
25
|
description: E-mail checker with libnotify desktop notifications.
|
37
26
|
email:
|
38
27
|
- catfriend@chilon.net
|
@@ -42,8 +31,11 @@ extensions: []
|
|
42
31
|
extra_rdoc_files: []
|
43
32
|
files:
|
44
33
|
- LICENSE
|
45
|
-
-
|
46
|
-
- lib/server.rb
|
34
|
+
- catfriend.example
|
35
|
+
- lib/catfriend/server.rb
|
36
|
+
- lib/catfriend/notify.rb
|
37
|
+
- lib/catfriend/filetokenstack.rb
|
38
|
+
- lib/catfriend/imap.rb
|
47
39
|
- bin/catfriend
|
48
40
|
homepage: https://github.com/nuisanceofcats/catfriend
|
49
41
|
licenses:
|