mail_grabber 1.0.0.rc1

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
+ 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