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