emilio 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in emilio.gemspec
4
+ gemspec
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,8 @@
1
+ module Emilio
2
+ class EmilioLogger < Logger
3
+ def format_message(severity, timestamp, progname, msg)
4
+ "#{timestamp.to_formatted_s(:db)} #{msg}\n"
5
+ end
6
+ end
7
+ end
8
+
@@ -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)
@@ -0,0 +1,3 @@
1
+ module Emilio
2
+ VERSION = "0.1.0"
3
+ end
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
+