things-fetcher 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
+ .rvmrc
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/CHANGELOG ADDED
@@ -0,0 +1 @@
1
+ v0.1.0. Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in babelyoda.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Andrey Subbotin
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.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ = things-fetcher
2
+
3
+ A simple tool to fetch messages from an IMAP folder and create a new to-do item in the Things app for each.
4
+
5
+ == How It Works
6
+
7
+ Say, you use Gmail. Say, you want all the mail with +[todo]+ in the subject field to be automatically routed to your Things' Inbox.
8
+
9
+ 1. Create a filter so that all subject mails end up in the same folder/label.
10
+ 2. <tt>gem install things-fetcher</tt>
11
+ 3. Run <tt>things-fetcher</tt> in Terminal to generate a default config file at <tt>~/.things_fetcher</tt>
12
+ 4. Edit the config file at <tt>~/.things_fetcher</tt> to specify your email and so on.
13
+ 5. Run <tt>things-fetcher</tt> in Terminal.
14
+
15
+ Set up a cron task to run <tt>things-fetcher</tt> periodically.
16
+
17
+ == Configuration
18
+
19
+ The following options can be specified in the <tt>~/.things_fetcher</tt> file:
20
+
21
+ [<tt>username</tt>] The username to authenticate with. You must specify one.
22
+ [<tt>password</tt>] The password to authenticate with. Default is to use the Keychain.
23
+ [<tt>server</tt>] The IP address or domain name of the IMAP server. Defaults to +imap.gmail.com+.
24
+ [<tt>port</tt>] The port to connect to. Defaults to +993+.
25
+ [<tt>ssl</tt>] Set to any value to use SSL encryption. Enabled by default.
26
+ [<tt>use_login</tt>] Set to any value to use the LOGIN command instead of AUTHENTICATE. Some servers, like GMail, do not support AUTHENTICATE. Enabled by default.
27
+ [<tt>in_folder</tt>] The name of the folder from which to read incoming mail. Defaults to +Things+.
28
+ [<tt>error_folder</tt>] The name a folder to move mail that causes an error during processing. Defaults to +Things+.
29
+ [<tt>list</tt>] The list in Things to create TODOs in. Defaults to +Inbox+.
30
+ [<tt>tag_names</tt>] The tags to apply to the newly created TODOs. Defaults to +Texted+.
31
+
32
+ == Note on Patches/Pull Requests
33
+
34
+ * Fork the project.
35
+ * Make your feature addition or bug fix.
36
+ * Add tests for it. This is important so I don't break it in a
37
+ future version unintentionally.
38
+ * Commit, do not mess with rakefile, version, or history.
39
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
40
+ * Send me a pull request. Bonus points for topic branches.
41
+
42
+ == Copyright
43
+
44
+ Copyright (c) 2012 Andrey Subbotin. See LICENSE for details.
45
+
46
+ == The IMAP fetching code
47
+
48
+ Created by Dan Weinand and Luke Francl. Development supported by {Slantwise Design}[http://slantwisedesign.com].
49
+ Licensed under the terms of the MIT License. Be excellent to each other.
50
+
51
+ The original repo can be found at: https://github.com/look/fetcher
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'things_fetcher'
5
+
6
+ # Main block
7
+
8
+ def main
9
+ config = ThingsFetcher::Config.new("~/.things_fetcher")
10
+ fetcher = ThingsFetcher::Fetcher.new(config)
11
+ fetcher.run
12
+ end
13
+
14
+ # Aux methods
15
+
16
+ main
@@ -0,0 +1,63 @@
1
+ module Fetcher
2
+ class Base
3
+ # Options:
4
+ # * <tt>:server</tt> - Server to connect to.
5
+ # * <tt>:username</tt> - Username to use when connecting to server.
6
+ # * <tt>:password</tt> - Password to use when connecting to server.
7
+ # * <tt>:receiver</tt> - Receiver object to pass messages to. Assumes the
8
+ # receiver object has a receive method that takes a message as it's argument
9
+ #
10
+ # Additional protocol-specific options implimented by sub-classes
11
+ #
12
+ # Example:
13
+ # Fetcher::Base.new(:server => 'mail.example.com',
14
+ # :username => 'pam',
15
+ # :password => 'test',
16
+ # :receiver => IncomingMailHandler)
17
+ def initialize(options={})
18
+ %w(server username password receiver).each do |opt|
19
+ raise ArgumentError, "#{opt} is required" unless options[opt.to_sym]
20
+ # convert receiver to a Class if it isn't already.
21
+ if opt == "receiver" && options[:receiver].is_a?(String)
22
+ options[:receiver] = Kernel.const_get(options[:receiver])
23
+ end
24
+
25
+ instance_eval("@#{opt} = options[:#{opt}]")
26
+ end
27
+ end
28
+
29
+ # Run the fetching process
30
+ def fetch
31
+ establish_connection
32
+ get_messages
33
+ close_connection
34
+ end
35
+
36
+ protected
37
+
38
+ # Stub. Should be overridden by subclass.
39
+ def establish_connection #:nodoc:
40
+ raise NotImplementedError, "This method should be overridden by subclass"
41
+ end
42
+
43
+ # Stub. Should be overridden by subclass.
44
+ def get_messages #:nodoc:
45
+ raise NotImplementedError, "This method should be overridden by subclass"
46
+ end
47
+
48
+ # Stub. Should be overridden by subclass.
49
+ def close_connection #:nodoc:
50
+ raise NotImplementedError, "This method should be overridden by subclass"
51
+ end
52
+
53
+ # Send message to receiver object
54
+ def process_message(message)
55
+ @receiver.receive(message)
56
+ end
57
+
58
+ # Stub. Should be overridden by subclass.
59
+ def handle_bogus_message(message) #:nodoc:
60
+ raise NotImplementedError, "This method should be overridden by subclass"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,88 @@
1
+ (RUBY_VERSION < '1.9.0') ? require('system_timer') : require('timeout')
2
+ require_relative 'plain_imap'
3
+
4
+ module Fetcher
5
+ class Imap < Base
6
+
7
+ PORT = 143
8
+
9
+ protected
10
+
11
+ # Additional Options:
12
+ # * <tt>:authentication</tt> - authentication type to use, defaults to PLAIN
13
+ # * <tt>:port</tt> - port to use (defaults to 143)
14
+ # * <tt>:ssl</tt> - use SSL to connect
15
+ # * <tt>:use_login</tt> - use LOGIN instead of AUTHENTICATE to connect (some IMAP servers, like GMail, do not support AUTHENTICATE)
16
+ # * <tt>:processed_folder</tt> - if set to the name of a mailbox, messages will be moved to that mailbox instead of deleted after processing. The mailbox will be created if it does not exist.
17
+ # * <tt>:error_folder:</tt> - the name of a mailbox where messages that cannot be processed (i.e., your receiver throws an exception) will be moved. Defaults to "bogus". The mailbox will be created if it does not exist.
18
+ def initialize(options={})
19
+ @authentication = options.delete(:authentication) || 'PLAIN'
20
+ @port = options.delete(:port) || PORT
21
+ @ssl = options.delete(:ssl)
22
+ @use_login = options.delete(:use_login)
23
+ @in_folder = options.delete(:in_folder) || 'INBOX'
24
+ @processed_folder = options.delete(:processed_folder)
25
+ @error_folder = options.delete(:error_folder) || 'bogus'
26
+ super(options)
27
+ end
28
+
29
+ # Open connection and login to server
30
+ def establish_connection
31
+ timeout_call = (RUBY_VERSION < '1.9.0') ? "SystemTimer.timeout_after(15.seconds) do" : "Timeout::timeout(15) do"
32
+
33
+ eval("#{timeout_call}
34
+ @connection = Net::IMAP.new(@server, @port, @ssl)
35
+ if @use_login
36
+ @connection.login(@username, @password)
37
+ else
38
+ @connection.authenticate(@authentication, @username, @password)
39
+ end
40
+ end")
41
+ end
42
+
43
+ # Retrieve messages from server
44
+ def get_messages
45
+ @connection.select(@in_folder)
46
+ @connection.uid_search(['ALL']).each do |uid|
47
+ msg = @connection.uid_fetch(uid,'RFC822').first.attr['RFC822']
48
+ begin
49
+ process_message(msg)
50
+ add_to_processed_folder(uid) if @processed_folder
51
+ rescue
52
+ handle_bogus_message(msg)
53
+ end
54
+ # Mark message as deleted
55
+ @connection.uid_store(uid, "+FLAGS", [:Seen, :Deleted])
56
+ end
57
+ end
58
+
59
+ # Store the message for inspection if the receiver errors
60
+ def handle_bogus_message(message)
61
+ create_mailbox(@error_folder)
62
+ @connection.append(@error_folder, message)
63
+ end
64
+
65
+ # Delete messages and log out
66
+ def close_connection
67
+ @connection.expunge
68
+ @connection.logout
69
+ begin
70
+ @connection.disconnect unless @connection.disconnected?
71
+ rescue
72
+ Rails.logger.info("Fetcher: Remote closed connection before I could disconnect.")
73
+ end
74
+ end
75
+
76
+ def add_to_processed_folder(uid)
77
+ create_mailbox(@processed_folder)
78
+ @connection.uid_copy(uid, @processed_folder)
79
+ end
80
+
81
+ def create_mailbox(mailbox)
82
+ unless @connection.list("", mailbox)
83
+ @connection.create(mailbox)
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,21 @@
1
+ require 'net/imap'
2
+ # add plain as an authentication type...
3
+ # This is taken from:
4
+ # http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/lib/net/imap.rb?revision=7657&view=markup&pathrev=10966
5
+
6
+ # Authenticator for the "PLAIN" authentication type. See
7
+ # #authenticate().
8
+ class PlainAuthenticator
9
+ def process(data)
10
+ return "\0#{@user}\0#{@password}"
11
+ end
12
+
13
+ private
14
+
15
+ def initialize(user, password)
16
+ @user = user
17
+ @password = password
18
+ end
19
+ end
20
+
21
+ Net::IMAP.add_authenticator "PLAIN", PlainAuthenticator
data/lib/fetcher.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Fetcher
2
+ # Use factory-style initialization or insantiate directly from a subclass
3
+ #
4
+ # Options:
5
+ # * <tt>:type</tt> - Name of class as a symbol to instantiate
6
+ #
7
+ # Other options are the same as Fetcher::Base.new
8
+ #
9
+ # Example:
10
+ #
11
+ # Fetcher.create(:type => :pop) is equivalent to
12
+ # Fetcher::Pop.new()
13
+ def self.create(options={})
14
+ klass = options.delete(:type)
15
+ raise ArgumentError, 'Must supply a type' unless klass
16
+ module_eval "#{klass.to_s.capitalize}.new(options)"
17
+ end
18
+ end
19
+
20
+ require_relative 'fetcher/base'
21
+ require_relative 'fetcher/imap'
data/lib/keychain.rb ADDED
@@ -0,0 +1,23 @@
1
+ # blog post: http://blog.slashpoundbang.com/post/1521530410/accessing-the-os-x-keychain-from-ruby
2
+
3
+ class KeyChain
4
+ def self.method_missing(meth, *args)
5
+ run args.unshift(meth)
6
+ end
7
+
8
+ def self.find_internet_password(*args)
9
+ # -g: Display the password for the item found
10
+ output = quiet args.unshift('find-internet-password', '-g')
11
+ output[/^password: "(.*)"$/, 1]
12
+ end
13
+
14
+ private
15
+
16
+ def self.run(*args)
17
+ `security #{args.join(' ')}`
18
+ end
19
+
20
+ def self.quiet(*args)
21
+ run args.unshift('2>&1 >/dev/null')
22
+ end
23
+ end
@@ -0,0 +1,122 @@
1
+ require 'yaml'
2
+
3
+ require_relative '../keychain'
4
+
5
+ module ThingsFetcher
6
+ class Config
7
+ def initialize(path)
8
+ super
9
+
10
+ @data = default_config
11
+ @data.merge! load_from_file(path)
12
+ end
13
+
14
+ def [](key)
15
+ data.has_key?(key) ? data[key] : send(key.to_s)
16
+ end
17
+
18
+ def password
19
+ KeyChain.find_internet_password '-s', data[:server], '-a', data[:username]
20
+ end
21
+
22
+ private
23
+ attr_accessor :data
24
+
25
+ def default_config
26
+ {
27
+ :server => 'imap.gmail.com',
28
+ :port => 993,
29
+ :ssl => true,
30
+ :use_login => true,
31
+ :in_folder => 'Things',
32
+ :error_folder => 'Things',
33
+ :tag_names => 'Texted',
34
+ :list => 'Inbox'
35
+ }
36
+ end
37
+
38
+ def load_from_file(path)
39
+ expanded_path = File.expand_path(path)
40
+ if File.exists?(expanded_path)
41
+ loaded_config = YAML::load(File.open(expanded_path))
42
+ result = {}
43
+ config_keys.each do |k|
44
+ if loaded_config.has_key?(k.to_s)
45
+ result[k] = loaded_config[k.to_s]
46
+ end
47
+ end
48
+ if result.has_key?(:username)
49
+ result
50
+ else
51
+ puts "ERROR: No username specified in the config."
52
+ puts " Please edit the config file at: #{expanded_path}"
53
+ exit 0
54
+ end
55
+ else
56
+ generate_config_file(expanded_path)
57
+ puts "ERROR: No config file found at: #{expanded_path}"
58
+ puts " A new config has been generated."
59
+ puts " Please edit."
60
+ exit 0
61
+ end
62
+ end
63
+
64
+ def config_keys
65
+ [
66
+ :server,
67
+ :port,
68
+ :ssl,
69
+ :use_login,
70
+ :in_folder,
71
+ :error_folder,
72
+ :tag_names,
73
+ :list,
74
+ :username,
75
+ :password
76
+ ]
77
+ end
78
+
79
+ def generate_config_file(path)
80
+ File.open(path, "w") do |f|
81
+ f << <<-EOS
82
+ # Any line that starts with a # is a comment.
83
+ # Leave any of the configuration options commented out to use the default value.
84
+
85
+ # The username to authenticate with.
86
+ # Uncoment and edit the following line.
87
+ # username: email@domain.com
88
+
89
+ # The password to authenticate with.
90
+ # Leave the following line commented out to you the password stored in the Keychain.
91
+ # Uncomment and edit otherwise.
92
+ # password: somepassword
93
+
94
+ # The IP address or domain name of the IMAP server
95
+ # server: imap.gmail.com
96
+
97
+ # The port to connect to (defaults to the standard port for the type of server)
98
+ # port: 993
99
+
100
+ # Set to any value to use SSL encryption
101
+ # ssl: true
102
+
103
+ # Set to any value to use the LOGIN command instead of AUTHENTICATE.
104
+ # Some servers, like GMail, do not support AUTHENTICATE (IMAP only).
105
+ # use_login: true
106
+
107
+ # The name of the folder from which to read incoming mail (IMAP only). Defaults to INBOX.
108
+ # in_folder: Things
109
+
110
+ # The name a folder to move mail that causes an error during processing (IMAP only).
111
+ # error_folder: Things
112
+
113
+ # The tags to apply to the newly created TODOs.
114
+ # tag_names: Texted
115
+
116
+ # The list in Things to create TODOs in.
117
+ # list: Inbox
118
+ EOS
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../fetcher'
2
+ require_relative 'mail_handler'
3
+
4
+ module ThingsFetcher
5
+ class Fetcher
6
+ def initialize(config)
7
+ super
8
+ @config = config
9
+ end
10
+
11
+ def run
12
+ fetcher = ::Fetcher.create(fetcher_options)
13
+ fetcher.fetch
14
+ end
15
+
16
+ private
17
+ attr_accessor :config
18
+
19
+ def fetcher_options
20
+ {
21
+ :type => :imap,
22
+ :receiver => handler,
23
+ :server => config[:server],
24
+ :port => config[:port],
25
+ :ssl => config[:ssl],
26
+ :use_login => config[:use_login],
27
+ :username => config[:username],
28
+ :password => config[:password],
29
+ :in_folder => config[:in_folder],
30
+ :error_folder => config[:error_folder]
31
+ }
32
+ end
33
+
34
+ def handler
35
+ ThingsFetcher::MailHandler.new(config)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ require 'appscript'
2
+ require 'mail'
3
+ require 'osax'
4
+
5
+ module ThingsFetcher
6
+ class MailHandler
7
+ def initialize(config)
8
+ super
9
+ @config = config
10
+ end
11
+
12
+ def receive(data)
13
+ msg = Mail.read_from_string(data)
14
+ make_todo msg.subject
15
+ end
16
+
17
+ private
18
+ include Appscript
19
+ include OSAX
20
+
21
+ attr_accessor :config
22
+
23
+ def make_todo(name)
24
+ things = app("Things")
25
+ todo = things.make(:new => :to_do, :at => things.lists[config[:list]], :with_properties => { :name => name })
26
+ todo.tag_names.set config[:tag_names]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module ThingsFetcher
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'things_fetcher/config'
2
+ require_relative 'things_fetcher/fetcher'
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "things_fetcher/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "things-fetcher"
7
+ s.version = ThingsFetcher::VERSION
8
+ s.authors = ["Andrey Subbotin"]
9
+ s.email = ["andrey@subbotin.me"]
10
+ s.homepage = "http://github.com/eploko/things-fetcher"
11
+ s.summary = "Imports to-dos from IMAP to Things.app"
12
+ s.description = "A simple daemon to periodically check an IMAP folder and create a new to-dos in the Things app for each new message."
13
+
14
+ s.rubyforge_project = "things-fetcher"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.require_paths = ["lib"]
18
+
19
+ s.executables = ["things-fetcher"]
20
+ s.default_executable = ["things-fetcher"]
21
+ s.extra_rdoc_files = [
22
+ "LICENSE",
23
+ "README.rdoc"
24
+ ]
25
+
26
+ s.add_development_dependency "rake"
27
+
28
+ s.add_runtime_dependency "mail"
29
+ s.add_runtime_dependency "rb-appscript"
30
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: things-fetcher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrey Subbotin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-19 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70233798421180 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70233798421180
25
+ - !ruby/object:Gem::Dependency
26
+ name: mail
27
+ requirement: &70233798420760 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70233798420760
36
+ - !ruby/object:Gem::Dependency
37
+ name: rb-appscript
38
+ requirement: &70233798420340 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70233798420340
47
+ description: A simple daemon to periodically check an IMAP folder and create a new
48
+ to-dos in the Things app for each new message.
49
+ email:
50
+ - andrey@subbotin.me
51
+ executables:
52
+ - things-fetcher
53
+ extensions: []
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.rdoc
57
+ files:
58
+ - .gitignore
59
+ - CHANGELOG
60
+ - Gemfile
61
+ - LICENSE
62
+ - README.rdoc
63
+ - Rakefile
64
+ - bin/things-fetcher
65
+ - lib/fetcher.rb
66
+ - lib/fetcher/base.rb
67
+ - lib/fetcher/imap.rb
68
+ - lib/fetcher/plain_imap.rb
69
+ - lib/keychain.rb
70
+ - lib/things_fetcher.rb
71
+ - lib/things_fetcher/config.rb
72
+ - lib/things_fetcher/fetcher.rb
73
+ - lib/things_fetcher/mail_handler.rb
74
+ - lib/things_fetcher/version.rb
75
+ - things-fetcher.gemspec
76
+ homepage: http://github.com/eploko/things-fetcher
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project: things-fetcher
96
+ rubygems_version: 1.8.10
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Imports to-dos from IMAP to Things.app
100
+ test_files: []