catfriend 0.1 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
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
- require 'imap'
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 'catfriend'
29
+ config_file = XDG['CONFIG'].find APP_NAME
27
30
  rescue NameError ; end
28
- config_file ||= "#{ENV['HOME']}/.config/catfriend"
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
- File.foreach(config_file) do |line|
38
- tokens = line.sub(/#.*/, '').scan(/\S+/)
39
-
40
- # parse tokens so use ugly loop instead of functional loop
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? 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
- case field
55
- when "host","imap"
56
- # host is deprecated
57
- if not current.empty? then
58
- servers << ImapServer.new(current)
59
- current = {}
60
- end
61
- current[:host] = shift_tokens.call
62
- when "notificationTimeout", "errorTimeout", "socketTimeout"
63
- # convert from camelCase to camel_case
64
- clean_field =
65
- field.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2.downcase}" }
66
- defaults[clean_field] = shift_tokens.call
67
- when "checkInterval"
68
- shift_tokens.call # deprecated, ignore parameter
69
- when "cert_file"
70
- cert_file = shift_tokens.call
71
- unless File.exists? cert_file
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
- "invalid config parameter '#{field}': #{line}"
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 ConfigError => e
102
- puts "misconfiguration: " + e.message
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 then
72
- if @cert_file then
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
- puts @imap.select(@mailbox || "INBOX")
106
+ @imap.select(@mailbox || "INBOX")
81
107
  end
82
108
 
83
- private :connect, :check_loop, :run, :error
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 started with start.
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.1'
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: 2011-12-30 00:00:00.000000000 Z
12
+ date: 2012-01-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: ruby-libnotify
16
- requirement: &9103360 !ruby/object:Gem::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.5'
21
+ version: '0.6'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *9103360
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
- - lib/imap.rb
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: