luggage 1.0.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.
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