mail_grabber 1.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/lib/mail_grabber.rb +8 -0
- data/lib/mail_grabber/database_helper.rb +250 -0
- data/lib/mail_grabber/database_queries.rb +102 -0
- data/lib/mail_grabber/delivery_method.rb +28 -0
- data/lib/mail_grabber/error.rb +11 -0
- data/lib/mail_grabber/railtie.rb +14 -0
- data/lib/mail_grabber/version.rb +5 -0
- data/lib/mail_grabber/web.rb +36 -0
- data/lib/mail_grabber/web/application.rb +135 -0
- data/lib/mail_grabber/web/application_helper.rb +14 -0
- data/lib/mail_grabber/web/application_router.rb +79 -0
- data/lib/mail_grabber/web/assets/images/mail_grabber515x500.png +0 -0
- data/lib/mail_grabber/web/assets/javascripts/application.js +539 -0
- data/lib/mail_grabber/web/assets/stylesheets/application.css +159 -0
- data/lib/mail_grabber/web/views/_message_attachment_template.html.erb +7 -0
- data/lib/mail_grabber/web/views/_message_content_template.html.erb +53 -0
- data/lib/mail_grabber/web/views/_message_list_template.html.erb +14 -0
- data/lib/mail_grabber/web/views/index.html.erb +46 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d564e5c8027b8dd25676e1076cddefda9c055cf694df0fb8824ef836c0c8c31d
|
4
|
+
data.tar.gz: 4599ee2a8e4491b26df2d44005eeb11594993b889bdc3a1e277c284b6a8422bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e58191d8045ef116faffeb4574f5d875e0cf0c9a1387354d151226981d318752bc9ba675e0d8ade11629f45f69ac247f65d6bc71724f4655b00612d199c52e9
|
7
|
+
data.tar.gz: 3b07b4809a2d9326ff95790f02c0d2ef89252f688e9d7968784aae2d37a69bffc431c74a5495f187faafe1f5b5d3538d29f476e0cda58b95bcaf816d39b4a693
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Norbert Szivós
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# M<img src="https://raw.githubusercontent.com/MailToolbox/mail_grabber/main/images/mail_grabber515x500.png" height="22" />ilGrabber
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/mail_grabber)
|
4
|
+
[](https://github.com/MailToolbox/mail_grabber/blob/main/LICENSE.txt)
|
5
|
+
[](https://github.com/rubocop-hq/rubocop)
|
6
|
+
[](https://travis-ci.com/MailToolbox/mail_grabber)
|
7
|
+
[](https://codeclimate.com/github/MailToolbox/mail_grabber/maintainability)
|
8
|
+
[](https://codeclimate.com/github/MailToolbox/mail_grabber/test_coverage)
|
9
|
+
|
10
|
+
**MailGrabber** is yet another solution to inspect sent emails.
|
11
|
+
|
12
|
+
It has two part:
|
13
|
+
- delivery method to grab emails and store into a database
|
14
|
+
- simple rack web interface to check those emails
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'mail_grabber'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle install
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install mail_grabber
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
- [How to use MailGrabber in a Ruby script or IRB console](https://github.com/MailToolbox/mail_grabber/blob/main/docs/usage_in_script_or_console.md)
|
35
|
+
- [How to use MailGrabber in Ruby on Rails](https://github.com/MailToolbox/mail_grabber/blob/main/docs/usage_in_ruby_on_rails.md)
|
36
|
+
|
37
|
+
## Development
|
38
|
+
|
39
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
40
|
+
|
41
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
42
|
+
|
43
|
+
To release a new version:
|
44
|
+
|
45
|
+
- Update [CHANGELOG.md](https://github.com/MailToolbox/mail_grabber/blob/main/CHANGELOG.md)
|
46
|
+
- Update the version number in `version.rb` manually or use `gem-release` gem and run `gem bump -v major|minor|patch|rc|beta`.
|
47
|
+
- Build gem with `bundle exec rake build`.
|
48
|
+
- Run `bundle install` to update gemfiles and commit the changes.
|
49
|
+
- Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
50
|
+
|
51
|
+
## Contributing
|
52
|
+
|
53
|
+
Bug reports and pull requests are welcome. Please read [CONTRIBUTING.md](https://github.com/MailToolbox/mail_grabber/blob/main/CONTRIBUTING.md) if you would like to contribute to this project.
|
54
|
+
|
55
|
+
## Inspiration
|
56
|
+
|
57
|
+
- [MailCatcher](https://github.com/sj26/mailcatcher)
|
58
|
+
- [letter_opener_web](https://github.com/fgrehm/letter_opener_web)
|
59
|
+
- [Rack](https://github.com/rack/rack)
|
60
|
+
- [Rack::Router](https://github.com/pjb3/rack-router)
|
61
|
+
- [Sidekiq](https://github.com/mperham/sidekiq)
|
62
|
+
- and other solutions regarding in this topic
|
63
|
+
|
64
|
+
## License
|
65
|
+
|
66
|
+
The gem is available as open source under the terms of the [MIT License](https://github.com/MailToolbox/mail_grabber/blob/main/LICENSE.txt).
|
data/lib/mail_grabber.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mail_grabber/error'
|
4
|
+
require 'mail_grabber/database_helper'
|
5
|
+
require 'mail_grabber/delivery_method'
|
6
|
+
# If we are using this gem outside of Rails then do not load this code.
|
7
|
+
require 'mail_grabber/railtie' if defined?(Rails)
|
8
|
+
require 'mail_grabber/version'
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'sqlite3'
|
5
|
+
|
6
|
+
require 'mail_grabber/database_queries'
|
7
|
+
|
8
|
+
module MailGrabber
|
9
|
+
module DatabaseHelper
|
10
|
+
include DatabaseQueries
|
11
|
+
|
12
|
+
DATABASE = {
|
13
|
+
folder: 'tmp',
|
14
|
+
filename: 'mail_grabber.sqlite3',
|
15
|
+
params: {
|
16
|
+
type_translation: true,
|
17
|
+
results_as_hash: true
|
18
|
+
}
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Create connection to the SQLite3 database. Use foreign_keys pragmas that
|
22
|
+
# we can use DELETE CASCADE option. It accepts block to execute queries.
|
23
|
+
# If something goes wrong then it raise database helper error.
|
24
|
+
# Also ensure to close the database (important to close database if we are
|
25
|
+
# don't want to see database busy errors).
|
26
|
+
def connection
|
27
|
+
database = open_database
|
28
|
+
database.foreign_keys = 'ON'
|
29
|
+
|
30
|
+
yield database
|
31
|
+
rescue SQLite3::Exception => e
|
32
|
+
raise Error::DatabaseHelperError, e
|
33
|
+
ensure
|
34
|
+
database&.close
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create connection and execute a query.
|
38
|
+
#
|
39
|
+
# @param [String] query which query we would like to execute
|
40
|
+
# @param [Array] args any arguments which we will use in the query
|
41
|
+
def connection_execute(query, *args)
|
42
|
+
connection { |db| db.execute(query, *args) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create connection and execute many queries in transaction. It accepts
|
46
|
+
# block to execute queries. If something goes wrong it rolls back the
|
47
|
+
# changes and raise database helper error.
|
48
|
+
def connection_execute_transaction
|
49
|
+
connection do |db|
|
50
|
+
db.transaction
|
51
|
+
|
52
|
+
yield db
|
53
|
+
|
54
|
+
db.commit
|
55
|
+
rescue SQLite3::Exception => e
|
56
|
+
db.rollback
|
57
|
+
|
58
|
+
raise Error::DatabaseHelperError, e
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Helper method to delete all messages.
|
63
|
+
def delete_all_messages
|
64
|
+
connection_execute('DELETE FROM mail')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper method to delete a message.
|
68
|
+
#
|
69
|
+
# @param [String/Integer] id the identifier of the message
|
70
|
+
def delete_message_by(id)
|
71
|
+
connection_execute('DELETE FROM mail WHERE id = ?', id.to_i)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Helper method to get all messages.
|
75
|
+
def select_all_messages
|
76
|
+
connection_execute('SELECT * FROM mail ORDER BY id DESC, created_at DESC')
|
77
|
+
end
|
78
|
+
|
79
|
+
# Helper method to get a message.
|
80
|
+
#
|
81
|
+
# @param [String/Integer] id the identifier of the message
|
82
|
+
def select_message_by(id)
|
83
|
+
connection_execute('SELECT * FROM mail WHERE id = ?', id.to_i).first
|
84
|
+
end
|
85
|
+
|
86
|
+
# Helper method to get a message part.
|
87
|
+
#
|
88
|
+
# @param [String/Integer] id the identifier of the message part
|
89
|
+
def select_message_parts_by(id)
|
90
|
+
connection_execute('SELECT * FROM mail_part WHERE mail_id = ?', id.to_i)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Helper method to get a specific number of messages. We can specify which
|
94
|
+
# part of the table we need and how many messages want to see.
|
95
|
+
#
|
96
|
+
# @param [String/Integer] page which part of the table want to see
|
97
|
+
# @param [String/Integer] per_page how many messages gives back
|
98
|
+
def select_messages_by(page, per_page)
|
99
|
+
page = page.to_i
|
100
|
+
per_page = per_page.to_i
|
101
|
+
|
102
|
+
connection_execute(
|
103
|
+
select_messages_with_pagination_query,
|
104
|
+
per_page * (page - 1),
|
105
|
+
per_page
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Helper method to store a message in the database.
|
110
|
+
#
|
111
|
+
# @param [Mail::Message] message which we would like to store
|
112
|
+
def store_mail(message)
|
113
|
+
connection_execute_transaction do |db|
|
114
|
+
insert_into_mail(db, message)
|
115
|
+
|
116
|
+
insert_into_mail_part(db, message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# Check that the given Mail::Part is an attachment or not.
|
123
|
+
#
|
124
|
+
# @param [Mail::Part] object
|
125
|
+
#
|
126
|
+
# @return [Integer] 1 if it is an attachment, else 0
|
127
|
+
def attachment?(object)
|
128
|
+
object.attachment? ? 1 : 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Convert the given message or body to utf8 string. Needs this that we can
|
132
|
+
# send it as JSON.
|
133
|
+
#
|
134
|
+
# @param [Mail::Message/Mail::Body] object
|
135
|
+
#
|
136
|
+
# @return [String] which we can store in the database and send as JSON
|
137
|
+
def convert_to_utf8_string(object)
|
138
|
+
object.to_s.force_encoding('UTF-8')
|
139
|
+
end
|
140
|
+
|
141
|
+
# Encode the given decoded body with base64 encoding if Mail::Part is an
|
142
|
+
# attachment.
|
143
|
+
#
|
144
|
+
# @param [Mail::Part] object
|
145
|
+
# @param [String] string the decoded body of the Mail::Part
|
146
|
+
#
|
147
|
+
# @return [String] with the encoded or the original body
|
148
|
+
def encode_if_attachment(object, string)
|
149
|
+
object.attachment? ? Base64.encode64(string) : string
|
150
|
+
end
|
151
|
+
|
152
|
+
# Extract cid value from the Mail::Part.
|
153
|
+
#
|
154
|
+
# @param [Mail::Part] object
|
155
|
+
#
|
156
|
+
# @return [String] the cid value
|
157
|
+
def extract_cid(object)
|
158
|
+
object.cid if object.respond_to?(:cid)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Extract all parts from the Mail::Message object. If it is not multipart
|
162
|
+
# then it returns back with original object in an Array.
|
163
|
+
#
|
164
|
+
# @param [Mail::Message] message
|
165
|
+
#
|
166
|
+
# @return [Array] with all parts of the message or an Array with the message
|
167
|
+
def extract_mail_parts(message)
|
168
|
+
message.multipart? ? message.all_parts : [message]
|
169
|
+
end
|
170
|
+
|
171
|
+
# Extract MIME type of the Mail::Part object. If it is nil then it returns
|
172
|
+
# with text/plain value.
|
173
|
+
#
|
174
|
+
# @param [Mail::Part] object
|
175
|
+
#
|
176
|
+
# @return [String] with MIME type of the part
|
177
|
+
def extract_mime_type(object)
|
178
|
+
object.mime_type || 'text/plain'
|
179
|
+
end
|
180
|
+
|
181
|
+
# Check that the given Mail::Part is an inline attachment or not.
|
182
|
+
#
|
183
|
+
# @param [Mail::Part] object
|
184
|
+
#
|
185
|
+
# @return [Integer] 1 if it is an inline attachment, else 0
|
186
|
+
def inline?(object)
|
187
|
+
object.respond_to?(:inline?) && object.inline? ? 1 : 0
|
188
|
+
end
|
189
|
+
|
190
|
+
# Store Mail::Message in the database.
|
191
|
+
#
|
192
|
+
# @param [SQLite::Database] db
|
193
|
+
# @param [Mail::Message] message
|
194
|
+
def insert_into_mail(db, message)
|
195
|
+
db.execute(
|
196
|
+
insert_into_mail_query,
|
197
|
+
message.subject,
|
198
|
+
message.from&.join(', '),
|
199
|
+
message.to&.join(', '),
|
200
|
+
message.cc&.join(', '),
|
201
|
+
message.bcc&.join(', '),
|
202
|
+
convert_to_utf8_string(message)
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Store Mail::Part in the database.
|
207
|
+
#
|
208
|
+
# @param [SQLite::Database] db
|
209
|
+
# @param [Mail::Message] message
|
210
|
+
def insert_into_mail_part(db, message)
|
211
|
+
mail_id = db.last_insert_row_id
|
212
|
+
|
213
|
+
extract_mail_parts(message).each do |part|
|
214
|
+
body = part.decoded
|
215
|
+
|
216
|
+
db.execute(
|
217
|
+
insert_into_mail_part_query,
|
218
|
+
mail_id,
|
219
|
+
extract_cid(part),
|
220
|
+
extract_mime_type(part),
|
221
|
+
attachment?(part),
|
222
|
+
inline?(part),
|
223
|
+
part.filename,
|
224
|
+
part.charset,
|
225
|
+
encode_if_attachment(part, body),
|
226
|
+
body.length
|
227
|
+
)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Open a database connection with the database. Also it checks that the
|
232
|
+
# database is exist or not. If it does not exist then it creates a new one.
|
233
|
+
#
|
234
|
+
# @return [SQLite3::Database] a database object
|
235
|
+
def open_database
|
236
|
+
db_location = "#{DATABASE[:folder]}/#{DATABASE[:filename]}"
|
237
|
+
|
238
|
+
if File.exist?(db_location)
|
239
|
+
SQLite3::Database.new(db_location, **DATABASE[:params])
|
240
|
+
else
|
241
|
+
Dir.mkdir(DATABASE[:folder]) unless Dir.exist?(DATABASE[:folder])
|
242
|
+
|
243
|
+
SQLite3::Database.new(db_location, **DATABASE[:params]).tap do |db|
|
244
|
+
create_mail_table(db)
|
245
|
+
create_mail_part_table(db)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MailGrabber
|
4
|
+
module DatabaseQueries
|
5
|
+
# Create mail table if it is not exist.
|
6
|
+
#
|
7
|
+
# @param [SQLite3::Database] db to execute create table query
|
8
|
+
def create_mail_table(db)
|
9
|
+
db.execute(<<-SQL)
|
10
|
+
CREATE TABLE IF NOT EXISTS mail (
|
11
|
+
id INTEGER PRIMARY KEY,
|
12
|
+
subject TEXT,
|
13
|
+
senders TEXT,
|
14
|
+
recipients TEXT,
|
15
|
+
carbon_copy TEXT,
|
16
|
+
blind_carbon_copy TEXT,
|
17
|
+
raw BLOB,
|
18
|
+
created_at DATETIME DEFAULT CURRENT_DATETIME
|
19
|
+
)
|
20
|
+
SQL
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create mail part table if it is not exist.
|
24
|
+
#
|
25
|
+
# @param [SQLite3::Database] db to execute create table query
|
26
|
+
def create_mail_part_table(db)
|
27
|
+
db.execute(<<-SQL)
|
28
|
+
CREATE TABLE IF NOT EXISTS mail_part (
|
29
|
+
id INTEGER PRIMARY KEY,
|
30
|
+
mail_id INTEGER NOT NULL,
|
31
|
+
cid TEXT,
|
32
|
+
mime_type TEXT,
|
33
|
+
is_attachment INTEGER,
|
34
|
+
is_inline INTEGER,
|
35
|
+
filename TEXT,
|
36
|
+
charset TEXT,
|
37
|
+
body BLOB,
|
38
|
+
size INTEGER,
|
39
|
+
created_at DATETIME DEFAULT CURRENT_DATETIME,
|
40
|
+
FOREIGN KEY (mail_id) REFERENCES mail(id) ON DELETE CASCADE
|
41
|
+
)
|
42
|
+
SQL
|
43
|
+
end
|
44
|
+
|
45
|
+
# Insert mail query.
|
46
|
+
#
|
47
|
+
# @return [Srting] with the insert mail query
|
48
|
+
def insert_into_mail_query
|
49
|
+
<<-SQL
|
50
|
+
INSERT INTO mail (
|
51
|
+
subject,
|
52
|
+
senders,
|
53
|
+
recipients,
|
54
|
+
carbon_copy,
|
55
|
+
blind_carbon_copy,
|
56
|
+
raw,
|
57
|
+
created_at
|
58
|
+
)
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
60
|
+
SQL
|
61
|
+
end
|
62
|
+
|
63
|
+
# Insert mail part query.
|
64
|
+
#
|
65
|
+
# @return [Srting] with the insert mail part query
|
66
|
+
def insert_into_mail_part_query
|
67
|
+
<<-SQL
|
68
|
+
INSERT INTO mail_part (
|
69
|
+
mail_id,
|
70
|
+
cid,
|
71
|
+
mime_type,
|
72
|
+
is_attachment,
|
73
|
+
is_inline,
|
74
|
+
filename,
|
75
|
+
charset,
|
76
|
+
body,
|
77
|
+
size,
|
78
|
+
created_at
|
79
|
+
)
|
80
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
81
|
+
SQL
|
82
|
+
end
|
83
|
+
|
84
|
+
# Select messages with pagination query.
|
85
|
+
#
|
86
|
+
# @return [Srting] with the select messages query
|
87
|
+
def select_messages_with_pagination_query
|
88
|
+
<<-SQL
|
89
|
+
SELECT id, subject, senders, created_at
|
90
|
+
FROM mail
|
91
|
+
WHERE id NOT IN (
|
92
|
+
SELECT id
|
93
|
+
FROM mail
|
94
|
+
ORDER BY id DESC, created_at DESC
|
95
|
+
LIMIT ?
|
96
|
+
)
|
97
|
+
ORDER BY id DESC, created_at DESC
|
98
|
+
LIMIT ?
|
99
|
+
SQL
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|