|
#!/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 */ |
|
} |