emilio 0.1.0
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/.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
|
+
|