emilio 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +123 -0
- data/Rakefile +1 -0
- data/emilio.gemspec +22 -0
- data/lib/emilio/checker.rb +50 -0
- data/lib/emilio/logger.rb +8 -0
- data/lib/emilio/railtie.rb +22 -0
- data/lib/emilio/receiver.rb +32 -0
- data/lib/emilio/schedulers/base.rb +15 -0
- data/lib/emilio/schedulers/delayed_job/scheduler.rb +29 -0
- data/lib/emilio/schedulers/delayed_job.rb +17 -0
- data/lib/emilio/version.rb +3 -0
- data/lib/emilio.rb +48 -0
- metadata +96 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
Emilio
|
2
|
+
======
|
3
|
+
|
4
|
+
With Emilio you can parse external emails sent to your application using the
|
5
|
+
IMAP protocol.
|
6
|
+
|
7
|
+
|
8
|
+
Usage
|
9
|
+
=====
|
10
|
+
|
11
|
+
Once configured, you can do:
|
12
|
+
|
13
|
+
Emilio::Checker.check_emails
|
14
|
+
|
15
|
+
To issue an IMAP connection to your server and fetch the new emails to be
|
16
|
+
parsed.
|
17
|
+
|
18
|
+
|
19
|
+
Configuration
|
20
|
+
-------------
|
21
|
+
|
22
|
+
You must setup Emilio in order to provide your IMAP server settings and your
|
23
|
+
credentials, among other optional stuff:
|
24
|
+
|
25
|
+
Emilio.configure do |config|
|
26
|
+
config.host = "imap.gmail.com"
|
27
|
+
config.port = 993
|
28
|
+
config.username = "your@username.com"
|
29
|
+
config.password = "password"
|
30
|
+
|
31
|
+
config.add_label = "processed"
|
32
|
+
config.mailbox = "Inbox"
|
33
|
+
config.scheduler = :delayed_job
|
34
|
+
config.run_every = 10.minutes
|
35
|
+
|
36
|
+
config.parser = :my_parser
|
37
|
+
end
|
38
|
+
|
39
|
+
This is recommended to be inside an initializer, something like
|
40
|
+
`config/initializers/emilio.rb` will do the job. The first configuration
|
41
|
+
options are very self explanatory, host and account credentials.
|
42
|
+
|
43
|
+
- `add_label` is an optional label which you already must have in your GMail
|
44
|
+
account, that will be applied to each processed email. I'm talking here
|
45
|
+
about *labels* but this will apply to *folders* in other non-gmail providers
|
46
|
+
too.
|
47
|
+
|
48
|
+
- `mailbox` is the name of the mailbox you want to parse emails from. By
|
49
|
+
default it's "Inbox", but it can be personalized to anything if you want to
|
50
|
+
choose which specific emails have to be parsed. This can be acomplished
|
51
|
+
simply by adding filters in your email account that moves the selected
|
52
|
+
emails into the *parsing* folder. In GMail this can be a label name too.
|
53
|
+
|
54
|
+
- `scheduler` is the name of the scheduler you want to use to perform
|
55
|
+
recurring email checkings. By now Emilio ships with a :delayed_job
|
56
|
+
integration but more can be added in the future.
|
57
|
+
|
58
|
+
- `run_every` is the amount of time between recurring checks, if you're using
|
59
|
+
a scheduler.
|
60
|
+
|
61
|
+
- `parser` is the class name of your parser, and it's a requirment. More in
|
62
|
+
the next paragraph...
|
63
|
+
|
64
|
+
|
65
|
+
Your parser class
|
66
|
+
-----------------
|
67
|
+
|
68
|
+
In order to do things with the emails your application receive, you must setup
|
69
|
+
a class to do the parsing stuff, something like:
|
70
|
+
|
71
|
+
class MyParser < Emilio::Receiver
|
72
|
+
def parse
|
73
|
+
# Find a reference in the subject or sender
|
74
|
+
reference = find_a_reference_in(@subject, @sender)
|
75
|
+
|
76
|
+
# Do stuff
|
77
|
+
Message.create! :text => @body, :author => @sender
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Your class must inherit from `Emilio::Receiver` and implement a `parse`
|
82
|
+
method, or if you like to do things the hard way you could also use a regular
|
83
|
+
ActionMailer class:
|
84
|
+
|
85
|
+
class MyParser < ActionMailer::Base
|
86
|
+
def receive(email)
|
87
|
+
# email is a TMail object
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
The `Emilio::Receiver` class will make things a little easier for you,
|
92
|
+
providing the following instance variables you can use in `parse`:
|
93
|
+
|
94
|
+
- @email: The TMail object representing the original email.
|
95
|
+
- @html: true or false.
|
96
|
+
- @attachments: Simply a shortcut to @email.attachments
|
97
|
+
- @sender: The sender of the email, same as @email.from
|
98
|
+
- @body: The body of the email correctly encoded as UTF-8.
|
99
|
+
- @subject: Subject encoded as UTF-8.
|
100
|
+
|
101
|
+
As you can see `Emilio::Receiver` doesn't make a lot of work, but it solves
|
102
|
+
some encoding issues.
|
103
|
+
|
104
|
+
|
105
|
+
Schedulers
|
106
|
+
----------
|
107
|
+
|
108
|
+
The schedulers are meant to cover the necessity of a recurring email checking
|
109
|
+
system. You can run `Emilio::Checker.check_emails` to do the job, but you
|
110
|
+
probably want to run it every 5 minutes or so, to keep you app up to date.
|
111
|
+
|
112
|
+
If you want you can run your own solution, maybe with a Cron entry (the
|
113
|
+
awesome gem [Whenever](https://github.com/javan/whenever) makes a great job
|
114
|
+
dealing with cron) but I've found that loading the entire application stack
|
115
|
+
every 5 minutes in my VPS shared host with 512 of RAM is overkilling.
|
116
|
+
|
117
|
+
This is why I've come up with this solution taking advantage of the also
|
118
|
+
excellent DelayedJob, using jobs that make the checking some time in the
|
119
|
+
future (making use of the :run_at option) and then re-enqueuing themselves.
|
120
|
+
|
121
|
+
While by now there is only delayed_job integration, the schedulers
|
122
|
+
arquitecture is flexible enough to allow an easy implementation of another
|
123
|
+
solutions like Resque, etc...
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/emilio.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "emilio/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "emilio"
|
7
|
+
s.version = Emilio::VERSION
|
8
|
+
s.authors = ["Roger Campos"]
|
9
|
+
s.email = ["roger@itnig.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Parse incoming emails with IMAP}
|
12
|
+
s.description = %q{Parse incoming emails with IMAP}
|
13
|
+
|
14
|
+
s.rubyforge_project = "emilio"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'rails', '~> 3.0.0'
|
22
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Emilio
|
2
|
+
class Checker
|
3
|
+
def self.check_emails
|
4
|
+
# Make sure we use a compatible Time format, otherwise TMail will
|
5
|
+
# not be able to parse the email timestamp.
|
6
|
+
old_time_format = Time::DATE_FORMATS[:default]
|
7
|
+
Time::DATE_FORMATS[:default] = '%m/%d/%Y %H:%M'
|
8
|
+
|
9
|
+
|
10
|
+
begin
|
11
|
+
# make a connection to imap account
|
12
|
+
imap = Net::IMAP.new(Emilio.host, Emilio.port, true)
|
13
|
+
imap.login(Emilio.username, Emilio.password)
|
14
|
+
|
15
|
+
# select which mailbox to process
|
16
|
+
imap.select(Emilio.mailbox)
|
17
|
+
|
18
|
+
# get all emails in that mailbox that have not been deleted
|
19
|
+
imap.uid_search(["NOT", "DELETED"]).each do |uid|
|
20
|
+
# fetches the straight up source of the email for tmail to parse
|
21
|
+
source = imap.uid_fetch(uid, ['RFC822']).first.attr['RFC822']
|
22
|
+
|
23
|
+
Emilio.parser.classify.constantize.receive(source)
|
24
|
+
|
25
|
+
# Optionally assign it some label
|
26
|
+
imap.uid_copy(uid, Emilio.add_label) if Emilio.add_label
|
27
|
+
|
28
|
+
# Delete it from Inbox (Gmail Archive)
|
29
|
+
imap.uid_store(uid, "+FLAGS", [:Deleted])
|
30
|
+
end
|
31
|
+
|
32
|
+
# expunge removes the deleted emails
|
33
|
+
imap.expunge
|
34
|
+
imap.logout
|
35
|
+
imap.disconnect
|
36
|
+
|
37
|
+
# NoResponseError and ByeResponseError happen often when imap'ing
|
38
|
+
rescue Net::IMAP::NoResponseError => e
|
39
|
+
Emilio.logger.error("No response: #{e}")
|
40
|
+
rescue Net::IMAP::ByeResponseError => e
|
41
|
+
Emilio.logger.error("Bye response: #{e}")
|
42
|
+
rescue => e
|
43
|
+
Emilio.logger.error("Error: #{e}")
|
44
|
+
end
|
45
|
+
|
46
|
+
Time::DATE_FORMATS[:default] = old_time_format
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Emilio
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer "emilio.setup_logger" do |app|
|
4
|
+
logfile = File.open("#{Rails.root}/log/emilio.log", 'a')
|
5
|
+
logfile.sync = true
|
6
|
+
Emilio.logger = EmilioLogger.new(logfile)
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer "emilio.init_scheduler" do |app|
|
10
|
+
app.config.after_initialize do
|
11
|
+
if Emilio.scheduler
|
12
|
+
if Emilio.scheduler.respond_to?(:init)
|
13
|
+
Emilio.scheduler.init
|
14
|
+
else
|
15
|
+
raise LoadError, "It seems that #{Emilio.scheduler.inspect.split("::").last} was in your Gemfile but declared after Emilio. Please make sure you declare it before Emilio in order to avoid requiring order issues like this."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Emilio
|
2
|
+
class Receiver < ActionMailer::Base
|
3
|
+
def receive(email)
|
4
|
+
@email = email
|
5
|
+
@html = false
|
6
|
+
@attachments = email.attachments
|
7
|
+
@sender = email.from.to_s
|
8
|
+
|
9
|
+
if email.multipart?
|
10
|
+
if email.html_part.present?
|
11
|
+
ic = Iconv.new('utf-8', email.html_part.charset)
|
12
|
+
@body = ic.iconv(email.html_part.body.to_s)
|
13
|
+
@html = true
|
14
|
+
else
|
15
|
+
ic = Iconv.new('utf-8', email.text_part.charset)
|
16
|
+
@body = ic.iconv(email.text_part.body.to_s)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
ic = Iconv.new('utf-8', email.charset)
|
20
|
+
@body = ic.iconv(email.body.to_s)
|
21
|
+
end
|
22
|
+
@subject = ic.iconv(email.subject)
|
23
|
+
Emilio.logger.info("Parsed email [#{@subject}] from [#{@sender}]")
|
24
|
+
|
25
|
+
parse
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def parse
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Emilio
|
2
|
+
module Schedulers
|
3
|
+
mattr_accessor :last_check_at
|
4
|
+
mattr_accessor :registered_schedulers
|
5
|
+
@@registered_schedulers = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.scheduler=(type)
|
9
|
+
unless Schedulers.registered_schedulers.include?(type.to_sym)
|
10
|
+
raise NotImplementedError, "This scheduler is not supported."
|
11
|
+
end
|
12
|
+
|
13
|
+
@@scheduler = "Emilio::Schedulers::#{type.to_s.classify}".constantize.setup
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Emilio
|
2
|
+
module Schedulers
|
3
|
+
module DelayedJob
|
4
|
+
def self.init
|
5
|
+
# Ideally this should be a recurring Job and this implementation is a
|
6
|
+
# poor workaround. Must be refactored if DJ implements real recurring jobs:
|
7
|
+
# https://github.com/collectiveidea/delayed_job/wiki/FEATURE:-Adding-Recurring-Job-Support-to-Delayed_Job
|
8
|
+
Delayed::Job.enqueue ScheduleJob.new, :run_at => Time.now + Emilio.run_every
|
9
|
+
end
|
10
|
+
|
11
|
+
class ScheduleJob
|
12
|
+
def perform
|
13
|
+
unless Emilio::Schedulers.last_check_at.nil? || ( Time.now > Emilio::Schedulers.last_check_at + Emilio.run_every * 0.9 )
|
14
|
+
# Break here to avoid two chains of recurring jobs. Please
|
15
|
+
# refactor me! We need your recurring jobs DJ!
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
Emilio::Checker.check_emails
|
20
|
+
Emilio::Schedulers.last_check_at = Time.now
|
21
|
+
|
22
|
+
Delayed::Job.enqueue ScheduleJob.new, :run_at => Time.now + Emilio.run_every
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Emilio::Schedulers.registered_schedulers << :delayed_job
|
2
|
+
|
3
|
+
module Emilio
|
4
|
+
module Schedulers
|
5
|
+
module DelayedJob
|
6
|
+
def self.setup
|
7
|
+
unless defined?(Delayed)
|
8
|
+
raise LoadError, "Please include 'delayed_job' in your Gemfile or require it manually before using this scheduler."
|
9
|
+
end
|
10
|
+
|
11
|
+
Emilio::Schedulers::DelayedJob
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'emilio/schedulers/delayed_job/scheduler' if defined?(Delayed)
|
data/lib/emilio.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'emilio/version'
|
2
|
+
|
3
|
+
require 'net/imap'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
require 'emilio/logger'
|
7
|
+
require 'emilio/railtie' if defined?(Rails)
|
8
|
+
require 'emilio/checker'
|
9
|
+
require 'emilio/receiver'
|
10
|
+
|
11
|
+
module Emilio
|
12
|
+
mattr_accessor :logger
|
13
|
+
|
14
|
+
# Your parser class, must be defined
|
15
|
+
mattr_accessor :parser
|
16
|
+
|
17
|
+
# Optional label to be added to parsed emails
|
18
|
+
mattr_accessor :add_label
|
19
|
+
|
20
|
+
# In which mailbox look for new emails to be parsed. This is "Inbox" by
|
21
|
+
# default, but can be changed to anything if you want custom behaviour. For
|
22
|
+
# instance you can define a filter or a set of filters in your Gmail account to
|
23
|
+
# move emails to be parsed into a specific folder (assign a label) and only
|
24
|
+
# parse emails with that label (equivalent mailbox name).
|
25
|
+
mattr_accessor :mailbox
|
26
|
+
|
27
|
+
# Which sheduler use, if any
|
28
|
+
mattr_accessor :scheduler
|
29
|
+
# Amount of time between each run, when a scheduler is used. Accepts 1.hour
|
30
|
+
# and this kind of sugar syntax
|
31
|
+
mattr_accessor :run_every
|
32
|
+
|
33
|
+
# Settings of your IMAP account
|
34
|
+
mattr_accessor :host
|
35
|
+
mattr_accessor :port
|
36
|
+
mattr_accessor :username
|
37
|
+
mattr_accessor :password
|
38
|
+
|
39
|
+
@@mailbox = "Inbox"
|
40
|
+
@@run_every = 10.minutes
|
41
|
+
|
42
|
+
def self.configure
|
43
|
+
yield self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Dir["#{File.dirname(__FILE__)}/emilio/schedulers/*.rb"].each{|f| require f}
|
48
|
+
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: emilio
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Roger Campos
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-13 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rails
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
version: 3.0.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: Parse incoming emails with IMAP
|
38
|
+
email:
|
39
|
+
- roger@itnig.net
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- .gitignore
|
48
|
+
- Gemfile
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- emilio.gemspec
|
52
|
+
- lib/emilio.rb
|
53
|
+
- lib/emilio/checker.rb
|
54
|
+
- lib/emilio/logger.rb
|
55
|
+
- lib/emilio/railtie.rb
|
56
|
+
- lib/emilio/receiver.rb
|
57
|
+
- lib/emilio/schedulers/base.rb
|
58
|
+
- lib/emilio/schedulers/delayed_job.rb
|
59
|
+
- lib/emilio/schedulers/delayed_job/scheduler.rb
|
60
|
+
- lib/emilio/version.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: ""
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
requirements: []
|
89
|
+
|
90
|
+
rubyforge_project: emilio
|
91
|
+
rubygems_version: 1.6.2
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: Parse incoming emails with IMAP
|
95
|
+
test_files: []
|
96
|
+
|