clacks 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +104 -0
- data/bin/clacks +3 -0
- data/clacks.gemspec +30 -0
- data/lib/clacks/command.rb +171 -0
- data/lib/clacks/configurator.rb +95 -0
- data/lib/clacks/service.rb +223 -0
- data/lib/clacks/stdlib_extensions/ruby_1_8.rb +41 -0
- data/lib/clacks/version.rb +4 -0
- data/lib/clacks.rb +30 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTYyNjIzMjk1NzcxNjcxMDAwYWExMDAzYzJhZGJlMjBjOTZlM2YyNg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
N2NmMjk0Yjk3OWEwZWMxZmIyMGNiMTRjZDM1ZTA1MTFiN2NmNmRiOA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NTFlMzIyYjMzMWU4OTY1MGY3ODU3NmFjZDQxNmU2MGRjYjRlMTNkY2ZmNDg5
|
10
|
+
MGE1Y2RiY2JlM2NhOTMxN2ZiNWQyZjBkYzNmNDZmNzhhNzliZjIyMmI5MDhk
|
11
|
+
ZTg4NzA1NjYwMzVkMTc1MTRiZmIxNWYyMjgyZThkMmQwOTU1MDk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OTJiMjBjYzliODA0Y2QzZWU5NWFhZTU2N2ZjODc4ODEyYTY1ZWMwODZiNTk1
|
14
|
+
M2YxODZkOWFkMWVhMmYxMDEyY2ZhNDM3NmU5NjBhZjZkNWYzODNiZTU2ZDMy
|
15
|
+
YjQ4Mjg2MDNkYTcxN2U5NTliYzMzYTQ4M2I3NGYzNzUxZDYyOWY=
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 ITRP (http://developer.itrp.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Clacks
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/itrp/clacks.png)](http://travis-ci.org/itrp/clacks?branch=master)
|
4
|
+
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/itrp/clacks)
|
5
|
+
[![Gem Version](https://fury-badge.herokuapp.com/rb/clacks.png)](http://badge.fury.io/rb/clacks)
|
6
|
+
|
7
|
+
"The clacks is a system of shutter semaphore towers which occupies roughly the same cultural space as telegraphy in nineteenth century Europe." ^[[1]](http://en.wikipedia.org/wiki/Technology_of_the_Discworld#The_clacks)
|
8
|
+
|
9
|
+
Clacks is an easy way to process incoming emails in ruby. It uses the POP3 or IMAP protocol. If the IMAP protocol is used and the IMAP server advertises the [IDLE](http://tools.ietf.org/rfc/rfc2177.txt) capability it will use that. Which means that emails are pushed to your email processor instead of having to poll for it at regular intervals, which in turn means emails arrive near real-time at your systems.
|
10
|
+
|
11
|
+
Clacks can be used standalone and/or within a Rails environment.
|
12
|
+
|
13
|
+
|
14
|
+
Installation and Usage
|
15
|
+
----------------------
|
16
|
+
|
17
|
+
If you use Rails, add this to your Gemfile:
|
18
|
+
|
19
|
+
gem 'clacks', :require => nil
|
20
|
+
|
21
|
+
Then create a configuration file, using ruby syntax, such as:
|
22
|
+
|
23
|
+
``` ruby
|
24
|
+
# -*- encoding: binary -*-
|
25
|
+
# Configuration of clacks
|
26
|
+
# See Clacks::Configurator for documentation on options
|
27
|
+
#
|
28
|
+
# Put this in: <RAILS_ROOT>/config/clacks.rb
|
29
|
+
#
|
30
|
+
|
31
|
+
poll_interval 30
|
32
|
+
pid "tmp/pids/clacks.pid"
|
33
|
+
stdout_path 'log/clacks.log'
|
34
|
+
stderr_path 'log/clacks.log'
|
35
|
+
|
36
|
+
imap({
|
37
|
+
:address => "imap.googlemail.com",
|
38
|
+
:port => 993,
|
39
|
+
:user_name => '<user_name>'
|
40
|
+
:password => '<password>'
|
41
|
+
:enable_ssl => true,
|
42
|
+
})
|
43
|
+
|
44
|
+
find_options({
|
45
|
+
:mailbox => 'INBOX',
|
46
|
+
:archivebox => '[Gmail]/All Mail',
|
47
|
+
:delete_after_find => true
|
48
|
+
})
|
49
|
+
|
50
|
+
on_mail do |mail|
|
51
|
+
Clacks.logger.info "Got new mail from #{mail.from.first}, subject: #{mail.subject}"
|
52
|
+
|
53
|
+
to = mail.to.first
|
54
|
+
if to =~ /^task-(\d+)@example.com/
|
55
|
+
Task.find($1).add_note(mail)
|
56
|
+
elsif to =~ /^(\w+)@example.com/
|
57
|
+
Account.find_by_name($1).tickets.create_from_mail!(mail)
|
58
|
+
else
|
59
|
+
# Prevent deletion of this mail after all
|
60
|
+
mail.skip_deletion
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
See [Clacks::Configurator](https://github.com/itrp/clacks/tree/master/lib/clacks/configurator.rb) for documentation on all options.
|
66
|
+
|
67
|
+
Start clacks:
|
68
|
+
|
69
|
+
```
|
70
|
+
/project/my_rails_app$ clacks --help
|
71
|
+
/project/my_rails_app$ clacks
|
72
|
+
```
|
73
|
+
|
74
|
+
Clacks can run as a daemon:
|
75
|
+
|
76
|
+
```
|
77
|
+
/project/my_rails_app$ clacks -D
|
78
|
+
```
|
79
|
+
|
80
|
+
Once it's running as a daemon process you can control it via sending signals. See the available signals below.
|
81
|
+
|
82
|
+
See the [contrib](https://github.com/itrp/clacks/tree/master/contrib/) directory for handy init.d, logrotate and monit scripts.
|
83
|
+
|
84
|
+
|
85
|
+
Signals
|
86
|
+
-------
|
87
|
+
|
88
|
+
* INT/TERM - quick shutdown, kills the process immediately.
|
89
|
+
|
90
|
+
* QUIT - graceful shutdown, waits for the worker process to finish processing an email.
|
91
|
+
|
92
|
+
* USR1 - reopen logs
|
93
|
+
|
94
|
+
|
95
|
+
Author
|
96
|
+
----------
|
97
|
+
|
98
|
+
ITRP, mathijs.sterk@itrp.com, [developer.itrp.com](http://developer.itrp.com)
|
99
|
+
|
100
|
+
|
101
|
+
Copyright
|
102
|
+
-----------
|
103
|
+
|
104
|
+
Copyright (c) 2013 ITRP. See MIT-LICENSE for details.
|
data/bin/clacks
ADDED
data/clacks.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "clacks/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = %q{clacks}
|
7
|
+
s.version = Clacks::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["ITRP"]
|
10
|
+
s.email = %q{mathijs.sterk@itrp.com}
|
11
|
+
s.homepage = %q{http://github.com/itrp/clacks}
|
12
|
+
s.summary = %q{Clacks system for receiving emails}
|
13
|
+
s.description = %q{Clacks system for receiving emails to be processed in ruby}
|
14
|
+
s.date = Time.now.utc.strftime("%Y-%m-%d")
|
15
|
+
s.files = Dir.glob("lib/**/*") + [
|
16
|
+
"MIT-LICENSE",
|
17
|
+
"README.md",
|
18
|
+
"Gemfile",
|
19
|
+
"clacks.gemspec"
|
20
|
+
]
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
25
|
+
s.add_dependency('mail')
|
26
|
+
s.add_development_dependency "rake"
|
27
|
+
# s.add_development_dependency "rcov"
|
28
|
+
s.add_development_dependency "rspec"
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Clacks
|
3
|
+
class Command
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
PROC_NAME = ::File.basename($0.dup)
|
7
|
+
PROC_ARGV = ARGV.map { |a| a.dup }
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@options = { :config_file => "config/clacks.rb" }
|
11
|
+
|
12
|
+
opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: #{PROC_NAME} [options]"
|
14
|
+
|
15
|
+
if Clacks.rails_env?
|
16
|
+
opts.separator "Rails options:"
|
17
|
+
opts.on("-E", "--env RAILS_ENV", "use RAILS_ENV for defaults (default: development)") do |e|
|
18
|
+
ENV['RAILS_ENV'] = e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.separator "Ruby options:"
|
23
|
+
|
24
|
+
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do
|
25
|
+
$DEBUG = true
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-w", "--warn", "turn warnings on for your script") do
|
29
|
+
$-w = true
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.separator "Clacks options:"
|
33
|
+
|
34
|
+
opts.on("-c", "--config-file FILE", "Clacks-specific config file (default: #{@options[:config_file]})") do |f|
|
35
|
+
@options[:config_file] = f
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
|
39
|
+
@options[:daemonize] = !!d
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("-P", "--pid FILE", "file to store PID (default: clacks.pid)") { |f|
|
43
|
+
@options[:pid] = f
|
44
|
+
}
|
45
|
+
|
46
|
+
opts.separator "Common options:"
|
47
|
+
|
48
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
49
|
+
puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on_tail("-v", "--version", "Show version") do
|
54
|
+
puts "#{PROC_NAME} v#{Clacks::VERSION}"
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@args = opts.parse!(args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def exec
|
62
|
+
daemonize if @options[:daemonize]
|
63
|
+
|
64
|
+
Clacks.require_rails if Clacks.rails_env?
|
65
|
+
|
66
|
+
Clacks.config = config = Clacks::Configurator.new(@options[:config_file])
|
67
|
+
unless config[:pop3] || config[:imap]
|
68
|
+
$stderr.puts "Either a POP3 or an IMAP server must be configured"
|
69
|
+
exit!(1)
|
70
|
+
end
|
71
|
+
|
72
|
+
reopen_io($stdout, config[:stdout_path])
|
73
|
+
reopen_io($stderr, config[:stderr_path])
|
74
|
+
|
75
|
+
unless config[:pid]
|
76
|
+
config.pid(@options[:pid] || (defined?(Rails) && Rails.version =~ /^2/ ? 'tmp/pids/clacks.pid' : 'clacks.pid'))
|
77
|
+
end
|
78
|
+
pid = config[:pid]
|
79
|
+
if wpid = running?(pid)
|
80
|
+
$stderr.puts "#{Clacks::Command::PROC_NAME} already running with pid: #{wpid} (or stale #{pid})"
|
81
|
+
exit!(1)
|
82
|
+
end
|
83
|
+
write_pid(pid)
|
84
|
+
|
85
|
+
proc_name('master')
|
86
|
+
|
87
|
+
setup_signal_handling
|
88
|
+
|
89
|
+
@service = Clacks::Service.new
|
90
|
+
@service.run
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# See Stevens's "Advanced Programming in the UNIX Environment" chapter 13
|
96
|
+
def daemonize(safe = true)
|
97
|
+
$stdin.reopen '/dev/null'
|
98
|
+
|
99
|
+
# Fork and have the parent exit.
|
100
|
+
# This makes the shell or boot script think the command is done.
|
101
|
+
# Also, the child process is guaranteed not to be a process group
|
102
|
+
# leader (a prerequisite for setsid next)
|
103
|
+
exit if fork
|
104
|
+
|
105
|
+
# Call setsid to create a new session. This does three things:
|
106
|
+
# - The process becomes a session leader of a new session
|
107
|
+
# - The process becomes the process group leader of a new process group
|
108
|
+
# - The process has no controlling terminal
|
109
|
+
Process.setsid
|
110
|
+
|
111
|
+
# Fork again and have the parent exit.
|
112
|
+
# This guarantes that the daemon is not a session leader nor can
|
113
|
+
# it acquire a controlling terminal (under SVR4)
|
114
|
+
exit if fork
|
115
|
+
|
116
|
+
unless safe
|
117
|
+
::Dir.chdir('/')
|
118
|
+
::File.umask(0000)
|
119
|
+
end
|
120
|
+
|
121
|
+
cfg_defaults = Clacks::Configurator::DEFAULTS
|
122
|
+
cfg_defaults[:stdout_path] ||= "/dev/null"
|
123
|
+
cfg_defaults[:stderr_path] ||= "/dev/null"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Redirect file descriptors inherited from the parent.
|
127
|
+
def reopen_io(io, path)
|
128
|
+
io.reopen(::File.open(path, "ab")) if path
|
129
|
+
io.sync = true
|
130
|
+
end
|
131
|
+
|
132
|
+
# Read the working pid from the pid file.
|
133
|
+
def running?(path)
|
134
|
+
wpid = ::File.read(path).to_i
|
135
|
+
return if wpid <= 0
|
136
|
+
Process.kill(0, wpid)
|
137
|
+
wpid
|
138
|
+
rescue Errno::EPERM, Errno::ESRCH, Errno::ENOENT
|
139
|
+
# noop
|
140
|
+
end
|
141
|
+
|
142
|
+
# Write the pid.
|
143
|
+
def write_pid(pid)
|
144
|
+
::File.open(pid, 'w') { |f| f.write("#{Process.pid}") }
|
145
|
+
at_exit { ::File.delete(pid) if ::File.exist?(pid) rescue nil }
|
146
|
+
end
|
147
|
+
|
148
|
+
def proc_name(tag)
|
149
|
+
$0 = [ Clacks::Command::PROC_NAME, tag, Clacks::Command::PROC_ARGV ].join(' ')
|
150
|
+
end
|
151
|
+
|
152
|
+
def setup_signal_handling
|
153
|
+
stop_signal = (Signal.list.keys & ['QUIT', 'INT']).first
|
154
|
+
Signal.trap(stop_signal) do
|
155
|
+
Clacks.logger.info 'QUIT signal received. Shutting down gracefully.'
|
156
|
+
@service.stop if @service
|
157
|
+
end unless stop_signal.nil?
|
158
|
+
|
159
|
+
Signal.trap('USR1') do
|
160
|
+
Clacks.logger.info 'USR1 signal received. Rotating logs.'
|
161
|
+
rotate_logs
|
162
|
+
end if Signal.list['USR1']
|
163
|
+
end
|
164
|
+
|
165
|
+
def rotate_logs
|
166
|
+
reopen_io($stdout, Clacks.config[:stdout_path])
|
167
|
+
reopen_io($stderr, Clacks.config[:stderr_path])
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Clacks
|
3
|
+
class Configurator
|
4
|
+
require 'logger'
|
5
|
+
attr_accessor :map, :config_file
|
6
|
+
|
7
|
+
DEFAULTS = {
|
8
|
+
:poll_interval => 60,
|
9
|
+
:logger => Logger.new($stderr),
|
10
|
+
:on_mail => lambda { |mail|
|
11
|
+
Clacks.logger.info("Mail from #{mail.from.first}, subject: #{mail.subject}")
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(config_file = nil)
|
16
|
+
self.map = Hash.new
|
17
|
+
map.merge!(DEFAULTS)
|
18
|
+
self.config_file = config_file
|
19
|
+
instance_eval(File.read(config_file), config_file) if config_file
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key) # :nodoc:
|
23
|
+
map[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def poll_interval(value)
|
27
|
+
map[:poll_interval] = value.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def pid(path)
|
31
|
+
set_path(:pid, path)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets the Logger-like object.
|
35
|
+
# The default Logger will log its output to Rails.logger if
|
36
|
+
# you're running within a rails environment, otherwise it will
|
37
|
+
# output to the path specified by +stdout_path+.
|
38
|
+
def logger(obj)
|
39
|
+
%w(debug info warn error fatal level).each do |m|
|
40
|
+
next if obj.respond_to?(m)
|
41
|
+
raise ArgumentError, "logger #{obj} does not respond to method #{m}"
|
42
|
+
end
|
43
|
+
map[:logger] = obj
|
44
|
+
end
|
45
|
+
|
46
|
+
# If you're running Clacks daemonized, then you must specify a path
|
47
|
+
# to prevent error messages from going to /dev/null.
|
48
|
+
def stdout_path(path)
|
49
|
+
set_path(:stdout_path, path)
|
50
|
+
end
|
51
|
+
|
52
|
+
# If you're running Clacks daemonized, then you must specify a path
|
53
|
+
# to prevent error messages from going to /dev/null.
|
54
|
+
def stderr_path(path)
|
55
|
+
set_path(:stderr_path, path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def pop3(hash)
|
59
|
+
set_hash(:pop3, hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
def imap(hash)
|
63
|
+
set_hash(:imap, hash)
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_options(hash)
|
67
|
+
set_hash(:find_options, hash)
|
68
|
+
end
|
69
|
+
|
70
|
+
def on_mail(*args, &block)
|
71
|
+
set_hook(:on_mail, block_given? ? block : args[0])
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def set_path(var, path) #:nodoc:
|
77
|
+
raise ArgumentError unless path.nil? || path.is_a?(String)
|
78
|
+
map[var] = path ? ::File.expand_path(path) : nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_hash(var, hash) #:nodoc:
|
82
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
83
|
+
map[var] = hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_hook(var, proc) #:nodoc:
|
87
|
+
raise ArgumentError unless proc.is_a?(Proc)
|
88
|
+
unless proc.arity == 1
|
89
|
+
raise ArgumentError, "#{var}=#{proc.inspect} has invalid arity: #{proc.arity} (need 1)"
|
90
|
+
end
|
91
|
+
map[var] = proc
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Clacks
|
3
|
+
class Service
|
4
|
+
require 'mail'
|
5
|
+
|
6
|
+
# In practice timeouts occur when there is no activity keeping an IMAP connection open.
|
7
|
+
# Timeouts occuring are:
|
8
|
+
# IMAP server timeout: typically after 30 minutes with no activity.
|
9
|
+
# NAT Gateway timeout: typically after 15 minutes with an idle connection.
|
10
|
+
# The solution to this is for the IMAP client to issue a NOOP (No Operation) command
|
11
|
+
# at intervals, typically every 15 minutes.
|
12
|
+
IMAP_NOOP_SLEEP = 15 * 60 # 15 minutes
|
13
|
+
|
14
|
+
def run
|
15
|
+
Clacks.logger.info "Clacks v#{Clacks::VERSION} started"
|
16
|
+
if Clacks.config[:pop3]
|
17
|
+
run_pop3
|
18
|
+
elsif Clacks.config[:imap]
|
19
|
+
run_imap
|
20
|
+
else
|
21
|
+
raise "Either a POP3 or an IMAP server must be configured"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
$STOPPING = true
|
27
|
+
exit unless finding?
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def run_pop3
|
33
|
+
config = Clacks.config[:pop3]
|
34
|
+
Clacks.logger.info("Clacks POP3 polling #{config[:user_name]}@#{config[:address]}")
|
35
|
+
# TODO: if $DEBUG
|
36
|
+
processor = Mail::IMAP.new(config)
|
37
|
+
poll(processor)
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_imap
|
41
|
+
config = Clacks.config[:imap]
|
42
|
+
options = Clacks.config[:find_options]
|
43
|
+
processor = Mail::IMAP.new(config)
|
44
|
+
require 'clacks/stdlib_extensions/ruby_1_8' if RUBY_VERSION.to_f < 1.9
|
45
|
+
Net::IMAP.debug = $DEBUG
|
46
|
+
imap_validate_options(options)
|
47
|
+
if imap_idle_support?(processor)
|
48
|
+
Clacks.logger.info("Clacks IMAP idling #{config[:user_name]}@#{config[:address]}")
|
49
|
+
imap_idling(processor)
|
50
|
+
else
|
51
|
+
Clacks.logger.info("Clacks IMAP polling #{config[:user_name]}@#{config[:address]}")
|
52
|
+
poll(processor)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Follows mostly the defaults from the Mail gem
|
57
|
+
def imap_validate_options(options)
|
58
|
+
options ||= {}
|
59
|
+
options[:mailbox] ||= 'INBOX'
|
60
|
+
options[:count] ||= 5
|
61
|
+
options[:order] ||= :asc
|
62
|
+
options[:what] ||= :first
|
63
|
+
options[:keys] ||= 'ALL'
|
64
|
+
options[:delete_after_find] ||= false
|
65
|
+
options[:mailbox] = Net::IMAP.encode_utf7(options[:mailbox])
|
66
|
+
if options[:archivebox]
|
67
|
+
options[:archivebox] = Net::IMAP.encode_utf7(options[:archivebox])
|
68
|
+
end
|
69
|
+
options
|
70
|
+
end
|
71
|
+
|
72
|
+
def imap_idle_support?(processor)
|
73
|
+
processor.connection { |imap| imap.capability.include?("IDLE") }
|
74
|
+
end
|
75
|
+
|
76
|
+
def imap_idling(processor)
|
77
|
+
imap_nooper
|
78
|
+
loop do
|
79
|
+
begin
|
80
|
+
processor.connection do |imap|
|
81
|
+
@imap = imap
|
82
|
+
# select the mailbox to process
|
83
|
+
imap.select(Clacks.config[:find_options][:mailbox])
|
84
|
+
loop {
|
85
|
+
break if stopping?
|
86
|
+
finding { imap_find(imap) }
|
87
|
+
# http://tools.ietf.org/rfc/rfc2177.txt
|
88
|
+
imap.idle do |r|
|
89
|
+
if r.instance_of?(Net::IMAP::UntaggedResponse) && r.name == 'EXISTS'
|
90
|
+
imap.idle_done unless r.data == 0
|
91
|
+
elsif r.instance_of?(Net::IMAP::ContinuationRequest)
|
92
|
+
Clacks.logger.info(r.data.text)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
rescue Net::IMAP::BadResponseError => e
|
98
|
+
unless e.message == 'Could not parse command'
|
99
|
+
Clacks.logger.error("#{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}")
|
100
|
+
end
|
101
|
+
# reconnect in next loop
|
102
|
+
rescue Net::IMAP::Error, IOError => e
|
103
|
+
# OK: reconnect in next loop
|
104
|
+
rescue => e
|
105
|
+
Clacks.logger.error("#{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}")
|
106
|
+
sleep(5) unless stopping?
|
107
|
+
end
|
108
|
+
break if stopping?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def imap_nooper
|
113
|
+
@imap_nooper = Thread.new do
|
114
|
+
loop do
|
115
|
+
begin
|
116
|
+
sleep IMAP_NOOP_SLEEP
|
117
|
+
@imap.idle_done
|
118
|
+
@imap.noop
|
119
|
+
rescue
|
120
|
+
# noop
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Keep processing emails until nothing is found anymore,
|
127
|
+
# or until a QUIT signal is received to stop the process.
|
128
|
+
def imap_find(imap)
|
129
|
+
options = Clacks.config[:find_options]
|
130
|
+
begin
|
131
|
+
break if stopping?
|
132
|
+
uids = imap.uid_search(options[:keys] || 'ALL')
|
133
|
+
uids.reverse! if options[:what].to_sym == :last
|
134
|
+
uids = uids.first(options[:count]) if options[:count].is_a?(Integer)
|
135
|
+
uids.reverse! if (options[:what].to_sym == :last && options[:order].to_sym == :asc) ||
|
136
|
+
(options[:what].to_sym != :last && options[:order].to_sym == :desc)
|
137
|
+
processed = 0
|
138
|
+
uids.each do |uid|
|
139
|
+
break if stopping?
|
140
|
+
source = imap.uid_fetch(uid, ['RFC822']).first.attr['RFC822']
|
141
|
+
break if stopping?
|
142
|
+
mail = Mail.new(source)
|
143
|
+
mail.mark_for_delete = true if options[:delete_after_find]
|
144
|
+
begin
|
145
|
+
Clacks.config[:on_mail].call(mail)
|
146
|
+
rescue Exception => e
|
147
|
+
Clacks.logger.debug(e.message)
|
148
|
+
Clacks.logger.debug(e.backtrace)
|
149
|
+
end
|
150
|
+
begin
|
151
|
+
imap.uid_copy(uid, options[:archivebox]) if options[:archivebox]
|
152
|
+
if options[:delete_after_find] && mail.is_marked_for_delete?
|
153
|
+
imap.uid_store(uid, "+FLAGS", [Net::IMAP::DELETED])
|
154
|
+
end
|
155
|
+
rescue Exception => e
|
156
|
+
Clacks.logger.error(e.message)
|
157
|
+
end
|
158
|
+
processed += 1
|
159
|
+
end
|
160
|
+
imap.expunge if options[:delete_after_find]
|
161
|
+
end while uids.any? && processed == uids.length
|
162
|
+
end
|
163
|
+
|
164
|
+
def poll(processor)
|
165
|
+
polling_msg = if polling?
|
166
|
+
"Clacks polling every #{poll_interval} seconds."
|
167
|
+
else
|
168
|
+
"Clacks polling for messages once."
|
169
|
+
end
|
170
|
+
Clacks.logger.info(polling_msg)
|
171
|
+
|
172
|
+
find_options = Clacks.config[:find_options]
|
173
|
+
on_mail = Clacks.config[:on_mail]
|
174
|
+
loop do
|
175
|
+
break if stopping?
|
176
|
+
finding {
|
177
|
+
processor.find(find_options) do |mail|
|
178
|
+
if stopping?
|
179
|
+
mail.skip_deletion
|
180
|
+
else
|
181
|
+
begin
|
182
|
+
on_mail.call(mail)
|
183
|
+
rescue Exception => e
|
184
|
+
Clacks.logger.debug(e.message)
|
185
|
+
Clacks.logger.debug(e.backtrace)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
}
|
190
|
+
break if stopping? || !polling?
|
191
|
+
sleep(poll_interval)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def poll_interval
|
196
|
+
Clacks.config[:poll_interval]
|
197
|
+
end
|
198
|
+
|
199
|
+
def polling?
|
200
|
+
poll_interval > 0
|
201
|
+
end
|
202
|
+
|
203
|
+
def finding(&block)
|
204
|
+
@finding = true
|
205
|
+
yield
|
206
|
+
ensure
|
207
|
+
@finding = false
|
208
|
+
end
|
209
|
+
|
210
|
+
def finding?
|
211
|
+
@finding
|
212
|
+
end
|
213
|
+
|
214
|
+
def stopping?
|
215
|
+
$STOPPING
|
216
|
+
end
|
217
|
+
|
218
|
+
at_exit {
|
219
|
+
Clacks.logger.info("Clacks stopped.") if $STOPPING
|
220
|
+
}
|
221
|
+
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Backport from ruby 1.9.3 source to ruby 1.8.7
|
3
|
+
class Net::IMAP
|
4
|
+
def idle(&response_handler)
|
5
|
+
raise LocalJumpError, "no block given" unless response_handler
|
6
|
+
|
7
|
+
response = nil
|
8
|
+
|
9
|
+
synchronize do
|
10
|
+
tag = Thread.current[:net_imap_tag] = generate_tag
|
11
|
+
put_string("#{tag} IDLE#{CRLF}")
|
12
|
+
|
13
|
+
begin
|
14
|
+
add_response_handler(response_handler)
|
15
|
+
@idle_done_cond = new_cond
|
16
|
+
@idle_done_cond.wait
|
17
|
+
@idle_done_cond = nil
|
18
|
+
if @receiver_thread_terminating
|
19
|
+
raise Net::IMAP::Error, "connection closed"
|
20
|
+
end
|
21
|
+
ensure
|
22
|
+
unless @receiver_thread_terminating
|
23
|
+
remove_response_handler(response_handler)
|
24
|
+
put_string("DONE#{CRLF}")
|
25
|
+
response = get_tagged_response(tag) #, "IDLE")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return response
|
31
|
+
end
|
32
|
+
|
33
|
+
def idle_done
|
34
|
+
synchronize do
|
35
|
+
if @idle_done_cond.nil?
|
36
|
+
raise Net::IMAP::Error, "not during IDLE"
|
37
|
+
end
|
38
|
+
@idle_done_cond.signal
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/clacks.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
module Clacks
|
3
|
+
require 'clacks/version'
|
4
|
+
require 'clacks/configurator'
|
5
|
+
require 'clacks/command'
|
6
|
+
require 'clacks/service'
|
7
|
+
|
8
|
+
def self.config=(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.config
|
13
|
+
@config ||= Clacks::Configurator.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.logger
|
17
|
+
@logger ||= Clacks.config[:logger]
|
18
|
+
end
|
19
|
+
|
20
|
+
RAILS_CONFIG_ENV = 'config/environment.rb'
|
21
|
+
def self.rails_env?
|
22
|
+
@rails_env ||= defined?(Rails) || File.readable?(RAILS_CONFIG_ENV)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.require_rails
|
26
|
+
ENV['RAILS_ENV'] ||= 'development'
|
27
|
+
require "#{Dir.pwd}/#{RAILS_CONFIG_ENV}" unless defined?(Rails)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clacks
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: !binary |-
|
5
|
+
MS4wLjQ=
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- ITRP
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mail
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
description: Clacks system for receiving emails to be processed in ruby
|
57
|
+
email: mathijs.sterk@itrp.com
|
58
|
+
executables:
|
59
|
+
- clacks
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- lib/clacks/service.rb
|
64
|
+
- lib/clacks/version.rb
|
65
|
+
- lib/clacks/stdlib_extensions/ruby_1_8.rb
|
66
|
+
- lib/clacks/configurator.rb
|
67
|
+
- lib/clacks/command.rb
|
68
|
+
- lib/clacks.rb
|
69
|
+
- MIT-LICENSE
|
70
|
+
- README.md
|
71
|
+
- Gemfile
|
72
|
+
- clacks.gemspec
|
73
|
+
- bin/clacks
|
74
|
+
homepage: http://github.com/itrp/clacks
|
75
|
+
licenses: []
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options:
|
79
|
+
- --charset=UTF-8
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 2.0.3
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Clacks system for receiving emails
|
98
|
+
test_files: []
|