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 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: