mailcatcher2 0.10.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
+ SHA256:
3
+ metadata.gz: 04d793d679abdc3e5c257613b3c48471d4e31376cc1d6ce3f14f397b4bc0aff6
4
+ data.tar.gz: 7bbbc5203aafd9e2b28c6b93f612d950fdc3dc08e9885d7bcd89783d3cd380b4
5
+ SHA512:
6
+ metadata.gz: 4eaf5d819d3663b4b7e60f2a079791686013a75a446ca7362a372189e5bdf52a8e9334c62e6c806c428f39f5789aa98d163f122106bd8b4813844c38fc2590dd
7
+ data.tar.gz: b1bc32114c69fc3c8b0de5ac063b133c8e33e523f714c84398ed2d5f23193287696746218819597d4bcad88056cea8e28e0fda59365bfbc256a525714f0f6df4
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2011 Samuel Cochran
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,176 @@
1
+ # MailCatcher
2
+
3
+ Catches mail and serves it through a dream.
4
+
5
+ MailCatcher runs a super simple SMTP server which catches any message sent to it to display in a web interface. Run mailcatcher, set your favourite app to deliver to smtp://127.0.0.1:1025 instead of your default SMTP server, then check out http://127.0.0.1:1080 to see the mail that's arrived so far.
6
+
7
+ ![MailCatcher screenshot](https://cloud.githubusercontent.com/assets/14028/14093249/4100f904-f598-11e5-936b-e6a396f18e39.png)
8
+
9
+ ## Features
10
+
11
+ * Catches all mail and stores it for display.
12
+ * Shows HTML, Plain Text and Source version of messages, as applicable.
13
+ * Rewrites HTML enabling display of embedded, inline images/etc and opens links in a new window.
14
+ * Lists attachments and allows separate downloading of parts.
15
+ * Download original email to view in your native mail client(s).
16
+ * Command line options to override the default SMTP/HTTP IP and port settings.
17
+ * Mail appears instantly if your browser supports [WebSockets][websockets], otherwise updates every thirty seconds.
18
+ * Runs as a daemon in the background, optionally in foreground.
19
+ * Sendmail-analogue command, `catchmail`, makes using mailcatcher from PHP a lot easier.
20
+ * Keyboard navigation between messages
21
+
22
+ ## How
23
+
24
+ 1. `gem install mailcatcher`
25
+ 2. `mailcatcher`
26
+ 3. Go to http://127.0.0.1:1080/
27
+ 4. Send mail through smtp://127.0.0.1:1025
28
+
29
+ ### Command Line Options
30
+
31
+ Use `mailcatcher --help` to see the command line options.
32
+
33
+ ```
34
+ Usage: mailcatcher [options]
35
+
36
+ MailCatcher v0.8.0
37
+
38
+ --ip IP Set the ip address of both servers
39
+ --smtp-ip IP Set the ip address of the smtp server
40
+ --smtp-port PORT Set the port of the smtp server
41
+ --http-ip IP Set the ip address of the http server
42
+ --http-port PORT Set the port address of the http server
43
+ --messages-limit COUNT Only keep up to COUNT most recent messages
44
+ --http-path PATH Add a prefix to all HTTP paths
45
+ --no-quit Don't allow quitting the process
46
+ -f, --foreground Run in the foreground
47
+ -b, --browse Open web browser
48
+ -v, --verbose Be more verbose
49
+ -h, --help Display this help information
50
+ --version Display the current version
51
+ ```
52
+
53
+ ### Upgrading
54
+
55
+ Upgrading works the same as installation:
56
+
57
+ ```
58
+ gem install mailcatcher
59
+ ```
60
+
61
+ ### Ruby
62
+
63
+ If you have trouble with the setup commands, make sure you have [Ruby installed](https://www.ruby-lang.org/en/documentation/installation/):
64
+
65
+ ```
66
+ ruby -v
67
+ gem environment
68
+ ```
69
+
70
+ You might need to install build tools for some of the gem dependencies. On Debian or Ubuntu, `apt install build-essential`. On macOS, `xcode-select --install`.
71
+
72
+ If you encounter issues installing [thin](https://rubygems.org/gems/thin), try:
73
+
74
+ ```
75
+ gem install thin -v 1.5.1 -- --with-cflags="-Wno-error=implicit-function-declaration"
76
+ ```
77
+
78
+ ### Bundler
79
+
80
+ Please don't put mailcatcher into your Gemfile. It will conflict with your application's gems at some point.
81
+
82
+ Instead, pop a note in your README stating you use mailcatcher, and to run `gem install mailcatcher` then `mailcatcher` to get started.
83
+
84
+ ### RVM
85
+
86
+ Under RVM your mailcatcher command may only be available under the ruby you install mailcatcher into. To prevent this, and to prevent gem conflicts, install mailcatcher into a dedicated gemset with a wrapper script:
87
+
88
+ rvm default@mailcatcher --create do gem install mailcatcher
89
+ ln -s "$(rvm default@mailcatcher do rvm wrapper show mailcatcher)" "$rvm_bin_path/"
90
+
91
+ ### Rails
92
+
93
+ To set up your rails app, I recommend adding this to your `environments/development.rb`:
94
+
95
+ config.action_mailer.delivery_method = :smtp
96
+ config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 }
97
+ config.action_mailer.raise_delivery_errors = false
98
+
99
+ ### PHP
100
+
101
+ For projects using PHP, or PHP frameworks and application platforms like Drupal, you can set [PHP's mail configuration](https://www.php.net/manual/en/mail.configuration.php) in your [php.ini](https://www.php.net/manual/en/configuration.file.php) to send via MailCatcher with:
102
+
103
+ sendmail_path = /usr/bin/env catchmail -f some@from.address
104
+
105
+ You can do this in your [Apache configuration](https://www.php.net/manual/en/configuration.changes.php) like so:
106
+
107
+ php_admin_value sendmail_path "/usr/bin/env catchmail -f some@from.address"
108
+
109
+ If you've installed via RVM this probably won't work unless you've manually added your RVM bin paths to your system environment's PATH. In that case, run `which catchmail` and put that path into the `sendmail_path` directive above instead of `/usr/bin/env catchmail`.
110
+
111
+ If starting `mailcatcher` on alternative SMTP IP and/or port with parameters like `--smtp-ip 192.168.0.1 --smtp-port 10025`, add the same parameters to your `catchmail` command:
112
+
113
+ sendmail_path = /usr/bin/env catchmail --smtp-ip 192.160.0.1 --smtp-port 10025 -f some@from.address
114
+
115
+ ### Django
116
+
117
+ For use in Django, add the following configuration to your projects' settings.py
118
+
119
+ ```python
120
+ if DEBUG:
121
+ EMAIL_HOST = '127.0.0.1'
122
+ EMAIL_HOST_USER = ''
123
+ EMAIL_HOST_PASSWORD = ''
124
+ EMAIL_PORT = 1025
125
+ EMAIL_USE_TLS = False
126
+ ```
127
+
128
+ ### Docker
129
+
130
+ There is a Docker image available [on Docker Hub](https://hub.docker.com/r/sj26/mailcatcher):
131
+
132
+ ```
133
+ $ docker run -p 1080 -p 1025 sj26/mailcatcher
134
+ Unable to find image 'sj26/mailcatcher:latest' locally
135
+ latest: Pulling from sj26/mailcatcher
136
+ 8c6d1654570f: Already exists
137
+ f5649d186f41: Already exists
138
+ b850834ea1df: Already exists
139
+ d6ac1a07fd46: Pull complete
140
+ b609298bc3c9: Pull complete
141
+ ab05825ece51: Pull complete
142
+ Digest: sha256:b17c45de08a0a82b012d90d4bd048620952c475f5655c61eef373318de6c0855
143
+ Status: Downloaded newer image for sj26/mailcatcher:latest
144
+ Starting MailCatcher v0.9.0
145
+ ==> smtp://0.0.0.0:1025
146
+ ==> http://0.0.0.0:1080
147
+ ```
148
+
149
+ How those ports appear and can be accessed may vary based on your Docker configuration. For example, your may need to use `http://127.0.0.1:1080` etc instead of the listed address. But MailCatcher will run and listen to those ports on all IPs it can from within the Docker container.
150
+
151
+ ### API
152
+
153
+ A fairly RESTful URL schema means you can download a list of messages in JSON from `/messages`, each message's metadata with `/messages/:id.json`, and then the pertinent parts with `/messages/:id.html` and `/messages/:id.plain` for the default HTML and plain text version, `/messages/:id/parts/:cid` for individual attachments by CID, or the whole message with `/messages/:id.source`.
154
+
155
+ ## Caveats
156
+
157
+ * Mail processing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or [file an issue][mailcatcher-issues] and let me know. Include the whole message you're having problems with.
158
+ * Encodings are difficult. MailCatcher does not completely support utf-8 straight over the wire, you must use a mail library which encodes things properly based on SMTP server capabilities.
159
+
160
+ ## Thanks
161
+
162
+ MailCatcher is just a mishmash of other people's hard work. Thank you so much to the people who have built the wonderful guts on which this project relies.
163
+
164
+ ## Donations
165
+
166
+ I work on MailCatcher mostly in my own spare time. If you've found Mailcatcher useful and would like to help feed me and fund continued development and new features, please [donate via PayPal][donate]. If you'd like a specific feature added to MailCatcher and are willing to pay for it, please [email me](mailto:sj26@sj26.com).
167
+
168
+ ## License
169
+
170
+ Copyright © 2010-2019 Samuel Cochran (sj26@sj26.com). Released under the MIT License, see [LICENSE][license] for details.
171
+
172
+ [donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=522WUPLRWUSKE
173
+ [license]: https://github.com/sj26/mailcatcher/blob/master/LICENSE
174
+ [mailcatcher-github]: https://github.com/sj26/mailcatcher
175
+ [mailcatcher-issues]: https://github.com/sj26/mailcatcher/issues
176
+ [websockets]: https://tools.ietf.org/html/rfc6455
data/bin/catchmail ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require 'mail'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'mail'
9
+ end
10
+
11
+ require 'optparse'
12
+
13
+ options = {:smtp_ip => '127.0.0.1', :smtp_port => 1025}
14
+
15
+ OptionParser.new do |parser|
16
+ parser.banner = <<-BANNER.gsub /^ +/, ""
17
+ Usage: catchmail [options] [recipient ...]
18
+ sendmail-like interface to forward mail to MailCatcher.
19
+ BANNER
20
+
21
+ parser.on('--ip IP') do |ip|
22
+ options[:smtp_ip] = ip
23
+ end
24
+
25
+ parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
26
+ options[:smtp_ip] = ip
27
+ end
28
+
29
+ parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
30
+ options[:smtp_port] = port
31
+ end
32
+
33
+ parser.on('-f FROM', 'Set the sending address') do |from|
34
+ options[:from] = from
35
+ end
36
+
37
+ parser.on('-oi', 'Ignored option -oi') do |ignored|
38
+ end
39
+ parser.on('-t', 'Ignored option -t') do |ignored|
40
+ end
41
+ parser.on('-q', 'Ignored option -q') do |ignored|
42
+ end
43
+
44
+ parser.on('-x', '--no-exit', 'Can\'t exit from the application') do
45
+ options[:no_exit] = true
46
+ end
47
+
48
+ parser.on('-h', '--help', 'Display this help information') do
49
+ puts parser
50
+ exit!
51
+ end
52
+ end.parse!
53
+
54
+ Mail.defaults do
55
+ delivery_method :smtp,
56
+ :address => options[:smtp_ip],
57
+ :port => options[:smtp_port]
58
+ end
59
+
60
+ message = Mail.new($stdin.read)
61
+
62
+ message.return_path = options[:from] if options[:from]
63
+
64
+ ARGV.each do |recipient|
65
+ if message.to.nil?
66
+ message.to = recipient
67
+ else
68
+ message.to << recipient
69
+ end
70
+ end
71
+
72
+ message.deliver
data/bin/mailcatcher ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'mail_catcher'
5
+
6
+ MailCatcher.run!
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventmachine"
4
+
5
+ module MailCatcher
6
+ Bus = EventMachine::Channel.new
7
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventmachine"
4
+ require "json"
5
+ require "mail"
6
+ require "sqlite3"
7
+
8
+ module MailCatcher::Mail extend self
9
+ def db
10
+ @__db ||= begin
11
+ SQLite3::Database.new("/srv/mailcatcher.db", :type_translation => true).tap do |db|
12
+ db.execute(<<-SQL)
13
+ CREATE TABLE message (
14
+ id INTEGER PRIMARY KEY ASC,
15
+ sender TEXT,
16
+ recipients TEXT,
17
+ subject TEXT,
18
+ source BLOB,
19
+ size TEXT,
20
+ type TEXT,
21
+ created_at DATETIME DEFAULT CURRENT_DATETIME
22
+ )
23
+ SQL
24
+ db.execute(<<-SQL)
25
+ CREATE TABLE message_part (
26
+ id INTEGER PRIMARY KEY ASC,
27
+ message_id INTEGER NOT NULL,
28
+ cid TEXT,
29
+ type TEXT,
30
+ is_attachment INTEGER,
31
+ filename TEXT,
32
+ charset TEXT,
33
+ body BLOB,
34
+ size INTEGER,
35
+ created_at DATETIME DEFAULT CURRENT_DATETIME,
36
+ FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
37
+ )
38
+ SQL
39
+ db.execute("PRAGMA foreign_keys = ON")
40
+ end
41
+ end
42
+ end
43
+
44
+ def add_message(message)
45
+ @add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
46
+
47
+ mail = Mail.new(message[:source])
48
+ @add_message_query.execute(message[:sender], JSON.generate(message[:recipients]), mail.subject, message[:source], mail.mime_type || "text/plain", message[:source].length)
49
+ message_id = db.last_insert_row_id
50
+ parts = mail.all_parts
51
+ parts = [mail] if parts.empty?
52
+ parts.each do |part|
53
+ body = part.body.to_s
54
+ # Only parts have CIDs, not mail
55
+ cid = part.cid if part.respond_to? :cid
56
+ add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
57
+ end
58
+
59
+ EventMachine.next_tick do
60
+ message = MailCatcher::Mail.message message_id
61
+ MailCatcher::Bus.push(type: "add", message: message)
62
+ end
63
+ end
64
+
65
+ def add_message_part(*args)
66
+ @add_message_part_query ||= db.prepare "INSERT INTO message_part (message_id, cid, type, is_attachment, filename, charset, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))"
67
+ @add_message_part_query.execute(*args)
68
+ end
69
+
70
+ def latest_created_at
71
+ @latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
72
+ @latest_created_at_query.execute.next
73
+ end
74
+
75
+ def messages
76
+ @messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at, id ASC"
77
+ @messages_query.execute.map do |row|
78
+ Hash[row.fields.zip(row)].tap do |message|
79
+ message["recipients"] &&= JSON.parse(message["recipients"])
80
+ end
81
+ end
82
+ end
83
+
84
+ def message(id)
85
+ @message_query ||= db.prepare "SELECT id, sender, recipients, subject, size, type, created_at FROM message WHERE id = ? LIMIT 1"
86
+ row = @message_query.execute(id).next
87
+ row && Hash[row.fields.zip(row)].tap do |message|
88
+ message["recipients"] &&= JSON.parse(message["recipients"])
89
+ end
90
+ end
91
+
92
+ def message_source(id)
93
+ @message_source_query ||= db.prepare "SELECT source FROM message WHERE id = ? LIMIT 1"
94
+ row = @message_source_query.execute(id).next
95
+ row && row.first
96
+ end
97
+
98
+ def message_has_html?(id)
99
+ @message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type IN ('application/xhtml+xml', 'text/html') LIMIT 1"
100
+ (!!@message_has_html_query.execute(id).next) || ["text/html", "application/xhtml+xml"].include?(message(id)["type"])
101
+ end
102
+
103
+ def message_has_plain?(id)
104
+ @message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
105
+ (!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
106
+ end
107
+
108
+ def message_parts(id)
109
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
110
+ @message_parts_query.execute(id).map do |row|
111
+ Hash[row.fields.zip(row)]
112
+ end
113
+ end
114
+
115
+ def message_attachments(id)
116
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
117
+ @message_parts_query.execute(id).map do |row|
118
+ Hash[row.fields.zip(row)]
119
+ end
120
+ end
121
+
122
+ def message_part(message_id, part_id)
123
+ @message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
124
+ row = @message_part_query.execute(message_id, part_id).next
125
+ row && Hash[row.fields.zip(row)]
126
+ end
127
+
128
+ def message_part_type(message_id, part_type)
129
+ @message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
130
+ row = @message_part_type_query.execute(message_id, part_type).next
131
+ row && Hash[row.fields.zip(row)]
132
+ end
133
+
134
+ def message_part_html(message_id)
135
+ part = message_part_type(message_id, "text/html")
136
+ part ||= message_part_type(message_id, "application/xhtml+xml")
137
+ part ||= begin
138
+ message = message(message_id)
139
+ message if message and ["text/html", "application/xhtml+xml"].include? message["type"]
140
+ end
141
+ end
142
+
143
+ def message_part_plain(message_id)
144
+ message_part_type message_id, "text/plain"
145
+ end
146
+
147
+ def message_part_cid(message_id, cid)
148
+ @message_part_cid_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ?"
149
+ @message_part_cid_query.execute(message_id).map do |row|
150
+ Hash[row.fields.zip(row)]
151
+ end.find do |part|
152
+ part["cid"] == cid
153
+ end
154
+ end
155
+
156
+ def delete!
157
+ @delete_all_messages_query ||= db.prepare "DELETE FROM message"
158
+ @delete_all_messages_query.execute
159
+
160
+ EventMachine.next_tick do
161
+ MailCatcher::Bus.push(type: "clear")
162
+ end
163
+ end
164
+
165
+ def delete_message!(message_id)
166
+ @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?"
167
+ @delete_messages_query.execute(message_id)
168
+
169
+ EventMachine.next_tick do
170
+ MailCatcher::Bus.push(type: "remove", id: message_id)
171
+ end
172
+ end
173
+
174
+ def delete_older_messages!(count = MailCatcher.options[:messages_limit])
175
+ return if count.nil?
176
+ @older_messages_query ||= db.prepare "SELECT id FROM message WHERE id NOT IN (SELECT id FROM message ORDER BY created_at DESC LIMIT ?)"
177
+ @older_messages_query.execute(count).map do |row|
178
+ Hash[row.fields.zip(row)]
179
+ end.each do |message|
180
+ delete_message!(message["id"])
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventmachine"
4
+
5
+ require "mail_catcher/mail"
6
+
7
+ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
8
+ # We override EM's mail from processing to allow multiple mail-from commands
9
+ # per [RFC 2821](https://tools.ietf.org/html/rfc2821#section-4.1.1.2)
10
+ def process_mail_from sender
11
+ if @state.include? :mail_from
12
+ @state -= [:mail_from, :rcpt, :data]
13
+
14
+ receive_reset
15
+ end
16
+
17
+ super
18
+ end
19
+
20
+ def current_message
21
+ @current_message ||= {}
22
+ end
23
+
24
+ def receive_reset
25
+ @current_message = nil
26
+
27
+ true
28
+ end
29
+
30
+ def receive_sender(sender)
31
+ # EventMachine SMTP advertises size extensions [https://tools.ietf.org/html/rfc1870]
32
+ # so strip potential " SIZE=..." suffixes from senders
33
+ sender = $` if sender =~ / SIZE=\d+\z/
34
+
35
+ current_message[:sender] = sender
36
+
37
+ true
38
+ end
39
+
40
+ def receive_recipient(recipient)
41
+ current_message[:recipients] ||= []
42
+ current_message[:recipients] << recipient
43
+
44
+ true
45
+ end
46
+
47
+ def receive_data_chunk(lines)
48
+ current_message[:source] ||= +""
49
+
50
+ lines.each do |line|
51
+ current_message[:source] << line << "\r\n"
52
+ end
53
+
54
+ true
55
+ end
56
+
57
+ def receive_message
58
+ MailCatcher::Mail.add_message current_message
59
+ MailCatcher::Mail.delete_older_messages!
60
+ puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
61
+ true
62
+ rescue => exception
63
+ MailCatcher.log_exception("Error receiving message", @current_message, exception)
64
+ false
65
+ ensure
66
+ @current_message = nil
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailCatcher
4
+ VERSION = "0.10.0"
5
+ end