mailcatcher2 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +176 -0
- data/bin/catchmail +72 -0
- data/bin/mailcatcher +6 -0
- data/lib/mail_catcher/bus.rb +7 -0
- data/lib/mail_catcher/mail.rb +183 -0
- data/lib/mail_catcher/smtp.rb +68 -0
- data/lib/mail_catcher/version.rb +5 -0
- data/lib/mail_catcher/web/application.rb +200 -0
- data/lib/mail_catcher/web.rb +29 -0
- data/lib/mail_catcher.rb +253 -0
- data/lib/mailcatcher.rb +5 -0
- data/public/favicon.ico +0 -0
- data/views/404.erb +6 -0
- data/views/index.erb +70 -0
- metadata +360 -0
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,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
|