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 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
@@ -0,0 +1,5 @@
1
+ # Change log
2
+
3
+ ## 1.0.0.rc1 (2021-03-20)
4
+
5
+ * Implement MailGrabber methods and functionality. See [README.md](https://github.com/MailToolbox/mail_grabber/blob/main/README.md)
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
+ [![Gem Version](https://badge.fury.io/rb/mail_grabber.svg)](https://badge.fury.io/rb/mail_grabber)
4
+ [![MIT license](https://img.shields.io/badge/license-MIT-brightgreen)](https://github.com/MailToolbox/mail_grabber/blob/main/LICENSE.txt)
5
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop-hq/rubocop)
6
+ [![Build Status](https://travis-ci.com/MailToolbox/mail_grabber.svg?branch=main)](https://travis-ci.com/MailToolbox/mail_grabber)
7
+ [![Maintainability](https://api.codeclimate.com/v1/badges/97deed5c1fbd003ca810/maintainability)](https://codeclimate.com/github/MailToolbox/mail_grabber/maintainability)
8
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/97deed5c1fbd003ca810/test_coverage)](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).
@@ -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