Skip to content

Instantly share code, notes, and snippets.

@metacritical
Last active January 6, 2024 12:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save metacritical/4540736752dbf97b07084b7ee640c934 to your computer and use it in GitHub Desktop.
Save metacritical/4540736752dbf97b07084b7ee640c934 to your computer and use it in GitHub Desktop.
Simple Mail Catcher for rails.

Update your ./bin/dev script with the following code.

__gems=("foreman" "sinatra" "gserver" "sinatra-contrib" "byebug")

for gem in "${__gems[@]}"; do
    if ! gem list "$gem" -i --silent; then
        echo "Installing $gem..."
        gem install "$gem"
    fi
done

# Check for 'mail:' directive in Procfile.dev
if ! grep -q "^mail:" Procfile.dev; then
    echo "mail: export RACK_ENV=development && ruby lib/fakemail.rb" >> Procfile.dev
    echo "Added 'mail:' directive to Procfile.dev."
fi

if [ ! -f lib/fakemail.rb ]; then
    # Download fakemail.rb to the lib/ directory
    curl "https://gist.githubusercontent.com/metacritical/4540736752dbf97b07084b7ee640c934/raw/3a90e7e48dd0bc6151f01c0f12495fa8a2b1d88a/fakemail.rb" -o lib/fakemail.rb
    echo "Downloaded fakemail.rb to lib/ directory."
fi

if [ ! -f bin/connect_byebug ]; then
    # Download connect_byebug to the bin/ directory
    curl "https://gist.githubusercontent.com/metacritical/4540736752dbf97b07084b7ee640c934/raw/3a90e7e48dd0bc6151f01c0f12495fa8a2b1d88a/connect_byebug" -o bin/connect_byebug
    chmod +x bin/connect_byebug
    echo "Downloaded conect_byebug to bin/ directory."
fi

exec foreman start -f Procfile.dev "$@"

Update config/environments/development.rb

 config.action_mailer.delivery_method = :smtp
 config.action_mailer.smtp_settings = { address: '127.0.0.1', port: 2525 }

As soon as you will run the ./bin/dev script it will automatically download and install gems and download the fakemail.rb to lib. Even update the Procfile.dev you only need to configure the above two files thats it.

The debugger is attached on 8989 which can be connected to using the bin/connect_byebug script given here.

#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <port>"
exit 1
fi
port="$1"
byebug -R localhost:$port
#!/usr/bin/env ruby
require 'sinatra'
require 'gserver'
require 'mail'
require 'fileutils'
require 'logger'
require 'sinatra/reloader' if development?
set :environment, :development
logger = Logger.new(STDOUT)
if ENV.fetch('RACK_ENV') == 'development'
require 'byebug'
require 'byebug/core'
begin
Byebug.start_server 'localhost', ENV.fetch('BYEBUG_SERVER_PORT', 8989).to_i
logger.info "Byebug server started on PORT: 8989"
rescue Errno::EADDRINUSE
logger.warn 'Byebug server already running'
end
end
module EmailStorage
@emails = []
@trash = []
def self.add_email(email)
@emails << email
end
def self.emails
@emails
end
def self.trash
@trash
end
def self.delete_email(index)
@trash.push(@emails.delete_at(index))
end
def self.delete_trash(index)
@trash.delete_at(index)
end
end
#PID Cleanup
%x[rm -rf tmp/pids/*.pid]
PID_FILE = 'tmp/pids/smtp_server.pid'
# Simple SMTP Server class
class SimpleSMTPServer < GServer
def start
super
write_pid_file
end
def shutdown
super
delete_pid_file
end
def serve(io)
io.print "220 Welcome to SimpleSMTPServer\r\n"
helo = io.gets
io.print "250 Great, let's start\r\n"
from = io.gets
io.print "250 Ok\r\n"
to = io.gets
io.print "250 Ok\r\n"
io.print "354 End data with <CR><LF>.<CR><LF>\r\n"
email_data = ""
loop do
line = io.gets
break if line == ".\r\n"
email_data += line
end
email = Mail.read_from_string(email_data)
EmailStorage.add_email({
from: email.from,
to: email.to,
subject: email.subject,
body: email.body.decoded,
date: email.date
})
io.print "250 Ok: queued\r\n"
io.print "221 Bye\r\n"
end
private
def write_pid_file
FileUtils.mkdir_p(File.dirname(PID_FILE))
File.write(PID_FILE, Process.pid.to_s)
end
def delete_pid_file
File.delete(PID_FILE) if File.exist?(PID_FILE)
end
end
def server_running?
return false unless File.exist?(PID_FILE)
pid = File.read(PID_FILE).to_i
Process.kill(0, pid)
true
rescue Errno::ESRCH
false
end
Signal.trap('INT') { smtp_server.shutdown }
Signal.trap('TERM') { smtp_server.shutdown }
if server_running?
puts "Server is already running. Exiting."
exit(1)
else
# Start SMTP Server in a separate thread
sleep 1
Thread.new do
smtp_server = SimpleSMTPServer.new(2525)
logger.info "Simple SMTP Server listenining on localhost @ 2525 "
smtp_server.start
smtp_server.join
end
end
# Sinatra routes
get '/' do
@emails = EmailStorage.emails
@trash = EmailStorage.trash
erb :index, layout: :app_layout
end
get '/emails/:id' do
@emails = EmailStorage.emails
@trash = EmailStorage.trash
@email = @emails[params[:id].to_i]
erb :show_email, layout: :app_layout
end
post '/delete/' do
params[:email_id].keys.each do |id|
EmailStorage.delete_email(id.to_i)
end
redirect '/'
end
get '/trash' do
@emails = EmailStorage.emails
@trash = EmailStorage.trash
erb :trash, layout: :app_layout
end
post '/delete_trash/' do
EmailStorage.delete_trash(params[:id].to_i)
redirect '/trash'
end
post '/empty_trash' do
EmailStorage.trash.clear
redirect '/'
end
get '/custom.css' do
erb :custom_css
end
__END__
@@ index
<table class="table table-striped table-hover">
<tbody>
<!-- inbox header -->
<tr>
<td>
<button class="btn btn-outline-primary">
<input type="checkbox" class="all" title="select all" onclick="toggleCheckboxes(this)"> All
</button> </td>
<td></td>
<td></td>
<td></td>
</tr>
<!-- inbox item -->
<form action="/delete/" method="post">
<% @emails.reverse.each_with_index do |email, index| %>
<tr>
<td>
<label>
<input type="checkbox" name="email_id[<%= index %>]" >
</label> <span class="name text-truncate"><%= email[:from][0] %></span>
</td>
<td>
<a class="row clickable-row" href="/emails/<%= index %>">
<span class="subject"><%= email[:subject] %></span>
</a>
</td>
<td>
<span class="badge"><%= email[:date].to_date %></span>
</td>
<td>
<button type="submit" class="btn btn-sm" name="email_id[<%= index %>]">🗑️ </button>
</td>
<span class="float-right fa fa-paperclip"></span></td>
</tr>
<% end %>
</form>
<!-- inbox item end-->
</tbody>
</table>
@@ trash
<table class="table table-striped table-hover">
<tbody>
<!-- inbox header -->
<tr>
<td>
<btn class="btn btn-outline-primary">
<input type="checkbox" class="all" title="select all" onclick="toggleCheckboxes(this)"> All
</btn>
</td>
<td></td>
<td></td>
<td></td>
</tr>
<!-- inbox item -->
<form action="/delete_trash/" method="post">
<% @trash.reverse.each_with_index do |email, index| %>
<tr>
<td>
<label>
<input type="checkbox" name="email_id[<%= index %>]">
</label> <span class="name text-truncate"><%= email[:from][0] %></span>
</td>
<td>
<a class="row clickable-row" href="/emails/<%= index %>">
<span class="subject"><%= email[:subject] %></span>
</a>
</td>
<td>
<span class="badge"><%= email[:date].to_date %></span>
</td>
<td>
<button type="submit" class="btn btn-sm">🗑️ </button>
</td>
<span class="float-right fa fa-paperclip"></span></td>
</tr>
<% end %>
<!-- inbox item end-->
</form>
</tbody>
</table>
@@ show_email
<p>From: <%= @email[:from] %></p>
<p>To: <%= @email[:to] %></p>
<p>Subject: <%= @email[:subject] %></p>
<p><%= @email[:body] %></p>
<a href="/">Back to Inbox</a>
@@ show_trash
<div class="main-content">
<h3>Trash</h3>
<% @trash.reverse.each_with_index do |email, index| %>
<div class="email-item">
<strong>From:</strong> <%= email[:from][0] %><br>
<strong>Subject:</strong><%= email[:subject] %><br>
<p> <a href="/emails/<%= index %>">View</a> </p>
<form action="/delete_trash/<%= index %>" method="post">
<button type="submit">Delete</button>
</form>
</div>
<% end %>
<a href="/trash">Back to Trash</a>
</div>
@@ app_layout
<head>
<title>FakeMail</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="/custom.css">
</head>
<body>
<script>
function toggleCheckboxes(allCheckbox) {
const isChecked = allCheckbox.checked;
document.querySelectorAll('input[name^="email_id"]').forEach(checkbox => checkbox.checked = isChecked);
}
</script>
<!------ START ----->
<div class="container-fluid py-3">
<div class="row">
<div class="col-md-2">
<div class="btn-group w-100">
<h1><span class="badge bg-warning">📩 MailCapture </span></h1>
</div>
</div>
<div class="col-md-10">
</div>
</div>
<hr>
<div class="row">
<!--left-->
<aside class="col-sm-3 col-md-2 pb-3">
<form action="/empty_trash" method="post">
<button type="submit" class="btn btn-danger btn-sm btn-block">
<i class="fa fa-edit"></i> Empty Trash
</button>
</form>
<hr>
<ul class="nav nav-pills flex-column">
<li class="active"><a href="/">
<span class="badge badge-primary float-right">
<%= @emails.count %>
</span>📥 Inbox
</a>
</li>
<li>
<a href="/trash">
<span class="badge badge-primary float-right">
<%= @trash.count %>
</span>🗑 Trash
</a>
</li>
</ul>
<hr>
</aside>
<!--main-->
<div class="col-sm-9 col-md-10">
<!-- tabs -->
<ul class="nav nav-tabs border-0">
<li class="nav-item">
<a class="nav-link active" href="#inbox" data-toggle="tab">
<i class="fa fa-inbox mr-1"></i> <%= request.path_info == '/' ? 'Inbox' : 'Trash' %>
</a>
</li>
</ul>
<!-- tab panes -->
<div class="tab-content py-4">
<div class="tab-pane in active" id="inbox">
<%= yield %>
</div>
</div>
<div class="row-md-12">
<div class="card card-body text-right">
<small>Last updated: 5/01/2024: 3:02 PM</small>
</div>
</div>
</div>
</div>
</div>
<!-------- END --------->
</body>
@@ custom_css
.clickable-row {
display: block;
color: inherit; /* Ensures text color is not changed */
text-decoration: none; /* Removes underline from links */
}
.clickable-row:hover {
text-decoration: none; /* Optional: Removes underline on hover */
background-color: #f5f5f5; /* Optional: Change background on hover */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment