luggage 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fbf6c643e587287b8d3df07c913163c0c9b56c89
4
+ data.tar.gz: d7bd6e2bf725860cf8392469f2ca1195358f9863
5
+ SHA512:
6
+ metadata.gz: c6e39c9d15b0980de7e56c3df266bde5180388b114d145b01b26d66de2b129cde2e019e88dd0bbf3e0557a6b4fb8dce83ddd7fc0759ba88d2733785f68978264
7
+ data.tar.gz: c070bbb000e6ed0654240cc40c41e015317d6625a07e1c89515ed932770b46b3f74f94c0f29e007087385f940a1d204ceeb5e816a3a2d792c12a726fbf385397
data/.document ADDED
@@ -0,0 +1,4 @@
1
+ lib/**/*.rb
2
+ README.rdoc
3
+ ChangeLog.rdoc
4
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ html/
3
+ pkg/
4
+ vendor/cache/*.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/ChangeLog.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ === 0.1.0 / 2012-12-03
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Otherinbox
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.md ADDED
@@ -0,0 +1,221 @@
1
+ # Luggage
2
+
3
+ * [Homepage](https://github.com/otherinbox/luggage#readme)
4
+ * [Issues](https://github.com/otherinbox/luggage/issues)
5
+
6
+ DSL for interacting with Imap accounts
7
+
8
+ ## Install
9
+
10
+ Nothing fancy - in you Gemfile;
11
+
12
+ ``` ruby
13
+ gem 'luggage'
14
+ ```
15
+
16
+ That was easy, right?
17
+
18
+
19
+ ## Before we get started...
20
+
21
+ Many of the following examples use a DSL/block style syntax. Most of the object
22
+ initializers accept a block and do an `instance_eval` on the new object
23
+ if a block is passed. These three examples are equivalent:
24
+
25
+ ``` ruby
26
+ Luggage.new :connection => c do
27
+ mailboxes "INBOX" do
28
+ message do
29
+ template "path/to/foo.eml"
30
+ end.save!
31
+ end
32
+ end
33
+ ```
34
+
35
+ ``` ruby
36
+ f = Luggage.new(:connection => c)
37
+ mb = f.maiboxes["INBOX"]
38
+ m = mb.message(:template => "path/to/foo.eml")
39
+ m.save!
40
+ ```
41
+
42
+ ``` ruby
43
+ Luggage.new(:connection => c).mailboxes["INBOX"].message(:template => "path/to/foo.eml").save!
44
+ ```
45
+
46
+
47
+ ## Creating a factory
48
+
49
+ Factories provide the top-level interface for interacting with IMAP servers.
50
+ All factories require an authenticated `Net::IMAP` instance, however there
51
+ are multiple ways to get there:
52
+
53
+ ### Using an existing Connection
54
+
55
+ If you have an instance of `Net::IMAP` you can pass it to the constructor as
56
+ `:connection` and the factory will use it.
57
+
58
+ ``` ruby
59
+ f = Luggage.new(:connection => connection)
60
+ ```
61
+
62
+ Keep in mind that the connection needs to be authenticated
63
+
64
+ ### Using an authentication string
65
+
66
+ `Net::IMAP` natively supports `LOGIN` and `CRAM-MD5` authentication schemes,
67
+ if you want to use either of these you can pass in `:server` and `:authenticate`,
68
+ the contents of `:authenticate` will be passed to `Net::IMAP#authenticate`.
69
+
70
+ ``` ruby
71
+ f = Luggage.new(:server => 'imap.aol.com', :authenticate => 'LOGIN user password')
72
+ f = Luggage.new(:server => ['imap.aol.com' 993, true], :authenticate => 'LOGIN user password')
73
+ ```
74
+
75
+ Notice that the value of `:server` will be passed to `Net::IMAP#new`, so the full
76
+ syntax of the initializer is available. See [the Ruby docs](http://rubydoc.info/stdlib/net/Net/IMAP)
77
+ for more details on auth and intialization
78
+
79
+ ### Using XOauth
80
+
81
+ Google has implemented XOauth for their IMAP connections. To use this pass in
82
+ `:server` as before and a token as `:xoauth`
83
+
84
+ ``` ruby
85
+ f = Luggage.new(:server => ['imap.gmail.com', 993, true], :xoauth => token)
86
+ ```
87
+
88
+ See the documentation for you service provider for details on generating that token.
89
+
90
+
91
+ ## Working with mailboxes
92
+
93
+ `Luggage#mailboxes` provides an interface to the different mailboxes on your
94
+ remote server. To access existing mailboxes you can use a couple syntaxes:
95
+
96
+ ``` ruby
97
+ Luggage.new(:connection => c) do
98
+ mailboxes["SPAM"] # => #<Luggage::Mailbox server: "imap.gmail.com", name: "SPAM">
99
+ mailboxes("SPAM") # => #<Luggage::Mailbox server: "imap.gmail.com", name: "SPAM">
100
+ mailboxes[:inbox] # => #<Luggage::Mailbox server: "imap.gmail.com", name: "INBOX">
101
+ mailboxes[:g_all] # => #<Luggage::Mailbox server: "imap.gmail.com", name: "[Gmail]/All Mail">
102
+ mailboxes[0] # => #<Luggage::Mailbox server: "imap.gmail.com", name: "INBOX">
103
+ mailboxes(0) # => #<Luggage::Mailbox server: "imap.gmail.com", name: "INBOX">
104
+ mailboxes.first # => #<Luggage::Mailbox server: "imap.gmail.com", name: "INBOX">
105
+ mailboxes # => [<Luggage::Mailbox server: "imap.gmail.com", name: "SPAM">...]
106
+ mailboxes[0..10] # => [<Luggage::Mailbox server: "imap.gmail.com", name: "INBOX">...]
107
+ end
108
+ ```
109
+
110
+ In most cases you can use method call and array/hash index syntax interchangeably.
111
+ Mailboxes come with a couple useful helper methods:
112
+
113
+ ``` ruby
114
+ Luggage.new(:connection => c) do
115
+ mailboxes["New mailbox"].save! # Creates the remote mailbox
116
+ mailboxes["Old and busted"].delete! # Deletes the remote mailbox
117
+ mailboxes["Cheshire"].exists? # Tells you if it exists remotely
118
+ mailboxes["INBOX"].expunge! # Permanently deletes any messages marked for deletion
119
+ end
120
+ ```
121
+
122
+ ## Querying messages
123
+
124
+ You can access the messages in a given mailbox through a couple helpers
125
+
126
+ ``` ruby
127
+ Luggage.new(:connection => c) do
128
+ mailboxes "INBOX" do
129
+ all # Returns an array of all the messages in the mailbox
130
+ first # Returns the first message (sorted by oldest first)
131
+ where("SINCE", 5.days.ago) # Executes Net::IMAP#search - see Ruby docs for mor info on search params
132
+ where(:subject => "FI!") # Shortcut for 'SUBJECT'
133
+ end
134
+ end
135
+ ```
136
+
137
+ Querying works somewhat like ActiveRecord scopes, in that you can chain calls to `where`
138
+ to build up a compound query, which is only executed once you attempt to inspect the results.
139
+ Keep in mind that compound queries are generated by appending each key/value pair into
140
+ big string and sending it to the IMAP server - this isn't SQL
141
+
142
+ Messages are retrieved somewhat lazily. The `Message-ID` and `uid` fields are always fetched,
143
+ but the full body isn't fetched until you try to access a field like `subject` or `body`.
144
+ You can inspect retrieved messages using the same syntax as the [Mail](https://github.com/mikel/mail)
145
+ gem, for instance:
146
+
147
+ ``` ruby
148
+ Luggage.new(:connection => c) do
149
+ mailboxes "INBOX" do
150
+ first.subject # The email subject
151
+ first.to # TO: field
152
+ first.headers['Return-Path'] # Random headers
153
+ first.body # Decoded body
154
+ first.multipart? # Is this a multi-part email?
155
+
156
+ first.flags # The flags set on the remote message
157
+ end
158
+ end
159
+ ```
160
+
161
+ Messages are uniquely identified by their `Message-ID` header, so if you want to access a
162
+ remote email you can fetch its data by creating a new messge instance and passing in the
163
+ message id:
164
+
165
+ ``` ruby
166
+ Luggage.new(:connection => c) do
167
+ message(:message_id => message_id).reload.flags
168
+ end
169
+ ```
170
+
171
+ See the next section for more details on that `Luggage#message` method...
172
+
173
+ ## Working with messages
174
+
175
+ `Luggage#message` and `Mailbox#message` provide interfaces for creating messages.
176
+
177
+ * If you pass in a `:template` argument, the message will be created using the contents
178
+ of the file at that path.
179
+ * If you pass an array as `:flags`, the passed flags will be set for the message when
180
+ it's uploaded to the remote server.
181
+ * A date can be passed as `:date` and will be used as the recieved-at date when the message
182
+ is uploaded.
183
+ * Any other arguments will be interpreted as properties to be set on the new message.
184
+ These will be set after the template is read (if provided), allowing
185
+ you to tweak the templates if needed.
186
+
187
+ If using the `Luggage` version, a mailbox must be specified as the first argument.
188
+
189
+ ``` ruby
190
+ Luggage.new(:connection => c) do
191
+ message("INBOX", :template => 'path/to/email.eml').save!
192
+
193
+ mailboxes "INBOX" do
194
+ message(:template => 'path/to/email.eml').save!
195
+
196
+ message(:template => 'path/to/email.eml', :subject => "Custom subject").save!
197
+ message do
198
+ subject "Howdy"
199
+ body "Partner"
200
+ from "me@gmail.com"
201
+ to "you@gmail.com"
202
+ headers "DKIM-Signature" => "FAIL"
203
+ end.save!
204
+ end
205
+ end
206
+ ```
207
+
208
+ _Don't forget to save_. Cause otherwise it won't get uploaded.
209
+
210
+ You can also work with existing messages on the server, either by querying for
211
+ them or by instantiating them directly by their message id
212
+
213
+ ``` ruby
214
+ Luggage.new(:connection => c) do
215
+ mailboxes :g_all do
216
+ message(:message_id => "<foo@example.com>").exists? # Does a message with this Message-ID exist in this mailbox?
217
+ message(:message_id => "<foo@example.com>").reload # Re-download the content and flags of this message
218
+ first.delete! # Delete this message (add the 'Deleted' flag)
219
+ end
220
+ end
221
+ ```
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler'
7
+ rescue LoadError => e
8
+ warn e.message
9
+ warn "Run `gem install bundler` to install Bundler."
10
+ exit -1
11
+ end
12
+
13
+ begin
14
+ Bundler.setup(:development)
15
+ rescue Bundler::BundlerError => e
16
+ warn e.message
17
+ warn "Run `bundle install` to install missing gems."
18
+ exit e.status_code
19
+ end
20
+
21
+ require 'rake'
22
+
23
+ require 'rdoc/task'
24
+ RDoc::Task.new do |rdoc|
25
+ rdoc.title = "luggage"
26
+ end
27
+ task :doc => :rdoc
28
+
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new
31
+
32
+ task :test => :spec
33
+ task :default => :spec
34
+
35
+ require "bundler/gem_tasks"
@@ -0,0 +1,63 @@
1
+ module Luggage
2
+ class Factory
3
+ attr_reader :connection
4
+
5
+ # Factory
6
+ #
7
+ # Factories require an instance of Net::IMAP. Serveral methods are supported:
8
+ #
9
+ # Factory.new(:connection => connection)
10
+ # In this case, `connection` should be an authorized Net::IMAP instance
11
+ #
12
+ # Factory.new(:server => "imap.example.com", :authentication => "LOGIN username password")
13
+ # In this case, we'll build a Net::IMAP instance and attempt to authenticate with the
14
+ # value of `authentication`. Net::IMAP supports LOGIN and CRAM-MD5 natively - see below
15
+ # for xoauth
16
+ #
17
+ # Factory.new(:server => "imap.gmail.com", :xoauth => "xoauth token string")
18
+ # In this case we'll build a Net::IMAP instance and attempt to send a raw XOAUTH authentication
19
+ # request using the supplied token.
20
+ #
21
+ def initialize(args = {}, &block)
22
+ if args.has_key?(:connection)
23
+ @connection = args[:connection]
24
+ elsif args.has_key?(:server) && args.has_key?(:authenticate)
25
+ @connection = Net::IMAP.new(*Array(args[:server]))
26
+ @connection.authenticate(*args[:authenticate])
27
+ elsif args.has_key?(:server) && args.has_key?(:xoauth)
28
+ @connection = Net::IMAP.new(*Array(args[:server]))
29
+ @connection.send(:send_command, "AUTHENTICATE XOAUTH #{args[:xoauth]}")
30
+ else
31
+ raise ArgumentError, "Imap Connection required."
32
+ end
33
+
34
+ instance_eval &block if block_given?
35
+ end
36
+
37
+ # Factory#message
38
+ #
39
+ # Constructs an Message
40
+ #
41
+ # `mailbox` can be either a string describing the Imap mailbox the message belongs to
42
+ # or an instance of Mailbox.
43
+ #
44
+ # `args` will be passed to ImapFactorY::Message#new_local - see that method for details
45
+ #
46
+ def message(mailbox, args = {}, &block)
47
+ Message.new_local(connection, mailbox, args, &block)
48
+ end
49
+
50
+ def mailboxes(*args)
51
+ array = MailboxArray.new(connection)
52
+ args.empty? ? array : array[*args]
53
+ end
54
+
55
+ def inspect
56
+ "#<Luggage::Factory server: \"#{host}\">"
57
+ end
58
+
59
+ def host
60
+ connection.instance_variable_get(:@host)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,101 @@
1
+ module Luggage
2
+ class Mailbox
3
+ include Enumerable
4
+
5
+ attr_reader :connection, :name
6
+
7
+ # This provides an interface to a remote Imap mailbox
8
+ #
9
+ # `connection` should be an authenticated Net::IMAP
10
+ #
11
+ # `name` is the name of the remote mailbox
12
+ #
13
+ def initialize(connection, name, &block)
14
+ raise ArgumentError, "Net::IMAP connection required" unless connection.kind_of?(Net::IMAP)
15
+ raise ArgumentError, "name required" unless name.present?
16
+
17
+ @connection = connection
18
+ @name = name
19
+
20
+ instance_eval &block if block_given?
21
+ end
22
+
23
+ # Constructs an Message whose mailbox will be set to this instance
24
+ #
25
+ # `args` will be passed to ImapFactorY::Message#new_local - see that method for details
26
+ #
27
+ def message(args = {}, &block)
28
+ Message.new_local(connection, self, args, &block)
29
+ end
30
+
31
+ # Returns true if this mailbox exists on the remote server, false otherwise
32
+ #
33
+ def exists?
34
+ @exists ||= connection.list("", name).present?
35
+ end
36
+
37
+ # Deletes this mailbox on the remote server
38
+ #
39
+ def delete!
40
+ connection.delete(name)
41
+ @exists = false
42
+ end
43
+
44
+ # Selects this mailbox for future Imap commands.
45
+ #
46
+ def select!
47
+ connection.select(name)
48
+ end
49
+
50
+ # Creates the mailbox on the remote server if it doesn't exist already
51
+ #
52
+ def save!
53
+ unless exists?
54
+ connection.create(name)
55
+ end
56
+ end
57
+
58
+ # Removes 'deleted' messages on the remote server. Message#delete! marks
59
+ # messages with the 'Deleted' flag, but leaves them on the server. This removes
60
+ # them entirely
61
+ #
62
+ def expunge!
63
+ select!
64
+ connection.expunge()
65
+ end
66
+
67
+ # Returns an array of Message instances describing all emails in the remote mailbox
68
+ #
69
+ def all
70
+ MailboxQueryBuilder.new(self)
71
+ end
72
+
73
+ # Returns a Message instance describing the first email on the remote mailbox
74
+ #
75
+ def first
76
+ all.first
77
+ end
78
+
79
+ # Iterates over Mailbox#each
80
+ #
81
+ def each(&block)
82
+ all.each(&block)
83
+ end
84
+
85
+ # Filters emails on the remote server
86
+ #
87
+ # Returns a MailboxQueryBuilder - see MailboxQueryBuilder#where for usage details
88
+ #
89
+ def where(*args)
90
+ all.where(*args)
91
+ end
92
+
93
+ def inspect
94
+ "#<Luggage::Mailbox server: \"#{host}\", name: \"#{name}\">"
95
+ end
96
+
97
+ def host
98
+ connection.instance_variable_get(:@host)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,58 @@
1
+ module Luggage
2
+ class MailboxArray
3
+ attr_reader :connection
4
+
5
+ def initialize(connection)
6
+ @connection = connection
7
+ end
8
+
9
+ def [](*args, &block)
10
+ case args.first
11
+ when String
12
+ mailbox(args.first, &block)
13
+ when :inbox, :spam, :sent, :trash
14
+ mailbox(args.first.to_s.upcase, &block)
15
+ when :g_all
16
+ mailbox("[Gmail]/All Mail", &block)
17
+ when :g_sent
18
+ mailbox("[Gmail]/Sent", &block)
19
+ when :g_trash
20
+ mailbox("[Gmail]/Trash", &block)
21
+ when Symbol
22
+ mailbox(args.first, &block)
23
+ when nil
24
+ mailboxes
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def method_missing(meth, *args, &block)
31
+ mailboxes.send(meth, *args, &block)
32
+ end
33
+
34
+ def inspect
35
+ mailboxes.inspect
36
+ end
37
+
38
+ def host
39
+ connection.instance_variable_get(:@host)
40
+ end
41
+
42
+ private
43
+
44
+ # Cosntructs a Mailbox
45
+ #
46
+ # `name` should be a string describing the Imap mailbox's name
47
+ #
48
+ def mailbox(name, &block)
49
+ Mailbox.new(connection, name, &block)
50
+ end
51
+
52
+ def mailboxes
53
+ connection.list("", "*").map do |result|
54
+ Mailbox.new(connection, result.name)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ module Luggage
2
+ class MailboxQueryBuilder
3
+ include Enumerable
4
+
5
+ attr_reader :connection, :mailbox, :query
6
+
7
+ # Provides an ActiveRecord-style query interface to emails on the remote server
8
+ #
9
+ # `mailbox` should be a Mailbox instance describing the remote mailbox to be queried
10
+ #
11
+ def initialize(mailbox)
12
+ raise ArgumentError, "Luggage::Mailbox required" unless mailbox.kind_of?(Mailbox)
13
+
14
+ @mailbox = mailbox
15
+ @connection = mailbox.connection
16
+ @query = []
17
+ end
18
+
19
+ # Executes the query and yields to each returned result
20
+ #
21
+ def each(&block)
22
+ messages.each(&block)
23
+ end
24
+
25
+ # Builds an Imap search query from the passed `args` hash. Each key is treated as
26
+ # a search key, each value is treated as a search value. Key/value pairs are appended
27
+ # to an array which will be passed to Net::IMAP#search. For more details on search
28
+ # syntax see Ruby std lib docs for Net::IMAP
29
+ #
30
+ def where(args = {})
31
+ @message_ids = nil
32
+ @messages = nil
33
+
34
+ args.each do |key, value|
35
+ case key.to_sym
36
+ when :body, :subject, :to, :cc, :from
37
+ @query += [key.to_s.upcase, value]
38
+ else
39
+ @query += [key, value]
40
+ end
41
+ end
42
+ self
43
+ end
44
+
45
+ # Executes the query and returns a slice of the resulting array of messages
46
+ #
47
+ def [](*args)
48
+ messages.[](*args)
49
+ end
50
+
51
+ def messages
52
+ @messages ||= message_ids.map {|message_id| Message.new(connection, mailbox, :message_id => message_id)}
53
+ end
54
+
55
+ def inspect
56
+ "#<Luggage::MailboxQueryBuilder server: \"#{host}\", mailbox: \"#{mailbox.name}\", query: #{query}>"
57
+ end
58
+
59
+ def host
60
+ connection.instance_variable_get(:@host)
61
+ end
62
+
63
+ private
64
+
65
+ def uids
66
+ mailbox.select!
67
+ connection.uid_search(@query.empty? ? "ALL" : @query)
68
+ end
69
+
70
+ def message_ids
71
+ field = "BODY[HEADER.FIELDS (Message-ID)]"
72
+ @message_ids ||= connection.uid_fetch(uids, field).map do |resp|
73
+ $1 if resp[:attr][field] =~ /(<\S*@\S*>)/
74
+ end.compact
75
+ end
76
+ end
77
+ end