Merge pull request #56 from HaschekSolutions/htmx

Big overhaul of web and API. Breaking old API
This commit is contained in:
Christian Haschek 2023-11-11 17:26:44 +01:00 committed by GitHub
commit 492c64135e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 719 additions and 7830 deletions

View file

@ -7,7 +7,6 @@ on:
branches: [ master ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: $(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')
jobs:
@ -20,7 +19,8 @@ jobs:
- name: Prepare
id: prep
run: |
DOCKER_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
DOCKER_IMAGE="ghcr.io/${{ env.IMAGE_NAME }}"
DOCKER_IMAGE_HUB="docker.io/${{ env.IMAGE_NAME }}"
VERSION=latest
SHORTREF=${GITHUB_SHA::8}
@ -29,18 +29,23 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/v}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
TAGS_HUB="${DOCKER_IMAGE_HUB}:${VERSION},${DOCKER_IMAGE_HUB}:${SHORTREF}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
TAGS_HUB="$TAGS_HUB,${DOCKER_IMAGE_HUB}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=tags_hub::${TAGS_HUB}
echo ::set-output name=docker_image_hub::${DOCKER_IMAGE_HUB}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
@ -50,14 +55,21 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@master
- name: Log in to the Container registry
- name: Log in to Github Packages
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push to GHCR
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
@ -65,4 +77,14 @@ jobs:
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.prep.outputs.tags }}
tags: ${{ steps.prep.outputs.tags }}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.prep.outputs.tags_hub }}

View file

@ -12,7 +12,7 @@
![](https://img.shields.io/badge/php-7.1%2B-brightgreen.svg)
![](https://img.shields.io/badge/python-2.7%2B-brightgreen.svg)
[![](https://img.shields.io/docker/pulls/hascheksolutions/opentrashmail?color=brightgreen)](https://hub.docker.com/r/hascheksolutions/opentrashmail)
[![](https://img.shields.io/docker/cloud/build/hascheksolutions/opentrashmail?color=brightgreen)](https://hub.docker.com/r/hascheksolutions/opentrashmail/builds)
[![](https://github.com/hascheksolutions/opentrashmail/actions/workflows/build-docker.yml/badge.svg?color=brightgreen)](https://github.com/HaschekSolutions/opentrashmail/actions)
[![Apache License](https://img.shields.io/badge/license-Apache-blue.svg?style=flat)](https://github.com/HaschekSolutions/opentrashmail/blob/master/LICENSE)
[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FHaschekSolutions%2Fopentrashmail&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
[![](https://img.shields.io/github/stars/HaschekSolutions/opentrashmail.svg?label=Stars&style=social)](https://github.com/HaschekSolutions/opentrashmail)
@ -22,24 +22,31 @@
</div>
![Screenshot of Open Trashmail](https://pictshare.net/shz4tq.png)
![Screenshot of Open Trashmail](https://pictshare.net/9tim7k.png)
# Features
- Python-powered mail server that works out of the box for any domain you throw at it
- API for integrating it in your own projects. Can be used to give users individual email addresses and read what they send to it
- RSS feed for every email address
- JSON API for integrating it in your own projects. Can be used to automate 2fa emails
- Handles attachments
- Web interface to manage emails
- Generates random email addresses
- 100% file based, no database needed
- Can be used as Email Honeypot
# JSON API
| Endpoint | Explanation | Example output |
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|
| /json/`[email-address]` | Returns an array of received emails with links to the attachments and the parsed text based body of the email. If `ADMIN` email is entered, will return all emails of all accounts | [![](https://pictshare.net/100x100/sflw6t.png)](https://pictshare.net/sflw6t.png) |
| /json/`[email-address]/[id]` | To see all the data of a received email, take the ID from the previous call and poll this to get the raw and HTML body of the email. Can be huge since the body can contain all attachments in base64 | [![](https://pictshare.net/100x100/eltku4.png)](https://pictshare.net/eltku4.png) |
| /json/listaccounts | If `SHOW_ACCOUNT_LIST` is set to true in the config.ini, this endpoint will return an array of all email addresses which have received at least one email | [![](https://pictshare.net/100x100/u6agji.png)](https://pictshare.net/u6agji.png) |
## [INFO] March '23
Since Docker Hub won't allow team Organizations anymore, we moved our images to GitHub Container Registry. So if you want to use the latest version, please use the new image ghcr.io/hascheksolutions/opentrashmail instead of hascheksolutions/opentrashmail
# Configuration
Just edit the `config.ini` You can use the following settings
- `URL` -> The url under which the GUI will be hosted. No tailing slash! example: https://trashmail.mydomain.eu
- `DOMAINS` -> Comma separated list of domains this mail server will be receiving emails on. It's just so the web interface can generate random addresses
- `MAILPORT`-> The port the Python-powered SMTP server will listen on. `Default: 25`
- `ADMIN` -> An email address (doesn't have to exist, just has to be valid) that will list all emails of all addresses the server has received. Kind of a catch-all
@ -60,9 +67,9 @@ Just edit the `config.ini` You can use the following settings
- [x] Admin overview for all available email addresses
- [x] Option to show raw email
- [x] Delete messages
- [ ] Secure HTML, so no malicious things can be loaded
- [x] Make better theme
- [x] Secure HTML, so no malicious things can be loaded
- [ ] Display embedded images inline using Content-ID
- [ ] Make better theme
- [ ] Configurable settings
- [x] Choose domains for random generation
- [x] Choose if out-of-scope emails are discarded

View file

@ -4,27 +4,14 @@ server {
set $base /var/www/opentrashmail;
root /var/www/opentrashmail/web/;
index index.html;
index index.php;
client_max_body_size 10M;
location / {
try_files $uri $uri/ =404;
try_files $uri $uri/ /index.php;
}
location /rss {
index rss.php;
if (!-e $request_filename){
rewrite ^(.*)$ /rss.php?url=$1 last;
}
}
location /api {
index api.php;
if (!-e $request_filename){
rewrite ^(.*)$ /api.php?url=$1 last;
}
}
# logging
access_log /var/log/nginx/opentrashmail/web.access.log;

View file

@ -28,6 +28,13 @@ else
echo "DOMAINS=localhost" >> /var/www/opentrashmail/config.ini
fi
if [ "$URL" != "" ]; then
echo "URL=$URL" >> /var/www/opentrashmail/config.ini
echo " [i] URL of GUI is set to: $URL"
else
echo "URL=http://localhost:8080" >> /var/www/opentrashmail/config.ini
fi
if [ "$SHOW_ACCOUNT_LIST" != "" ]; then
echo "SHOW_ACCOUNT_LIST=$SHOW_ACCOUNT_LIST" >> /var/www/opentrashmail/config.ini
echo " [i] Set show account list to: $SHOW_ACCOUNT_LIST"

19
docs/Dev.md Normal file
View file

@ -0,0 +1,19 @@
# Quick testing
From the main directory run
```bash
docker build -f docker/Dockerfile -t opentrashmail . && docker run --rm -it --name trashmail -p 3000:80 -p 2525:25 opentrashmail
```
And check if it works on http://localhost:3000
## Sending debug emails from the command line
Using the text file `tools/testmail.txt` and the following line of bash you can send emails to your python mailserver and test if it's acceping emails like you want.
Note that if you change cour config.ini, the mail server needs to be restarted before it takes effect.
```bash
cat "tools/testmail.txt" | while read L; do sleep "0.2"; echo "$L"; done | "nc" -C -v "localhost" "2525"
```

View file

@ -1,9 +0,0 @@
# Quick testing
From the `docker` directory run
```bash
docker build -t opentrashmail . && docker run --rm -it --name trashmail -p 3000:80 -p 2525:25 opentrashmail
```
And check if it works on http://localhost:3000

View file

@ -5,6 +5,10 @@
; pro tip: Use a wildcard domain like *.yourdomain.com to auto-generate the subdomains (you'll need to add a wildcard dns record too)
DOMAINS=yourdomain,sub.yourdomain,*.mydom.com
; This variable needs to be set in order for RSS to work
; The URL of your webserver hosting the GUI. No trailing slash
URL="http://localhost:8080"
; Enable to show a list of all existing accounts with mail
;SHOW_ACCOUNT_LIST=true

2
python/mailserver.py Normal file → Executable file
View file

@ -171,6 +171,8 @@ if __name__ == '__main__':
DELETE_OLDER_THAN_DAYS = (Config.get("CLEANUP","DELETE_OLDER_THAN_DAYS").lower() == "true")
print "[i] Starting Mailserver on port",port
print "[i] Discard unknown domains:",DISCARD_UNKNOWN
print "[i] Listening for domains:",DOMAINS
server = CustomSMTPServer(('0.0.0.0', port), None) # use your public IP here
print "[i] Ready to receive Emails"

14
tools/testmail.txt Normal file
View file

@ -0,0 +1,14 @@
HELO localhost
MAIL FROM:<system@example.com>
RCPT TO:<random.user@rand.domain.tld>
DATA
Subject: Test Message
Hi there! This is supposed to be an email...
Have a good day!
-- System
.
QUIT

View file

@ -1,157 +0,0 @@
<?php
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__FILE__));
error_reporting(E_ALL || ~E_NOTICE);
ini_set('display_errors', 1);
include_once(ROOT.DS.'inc'.DS.'core.php');
if (PHP_SAPI === 'cli-server')
$_SERVER['SCRIPT_NAME'] = pathinfo(__FILE__, PATHINFO_BASENAME);
if($_GET['url'])
$url = explode('/',ltrim(parse_url($_GET['url'], PHP_URL_PATH),'/'));
else $url = array_filter(explode('/',ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),'/')));
$action = strtolower($_REQUEST['a']);
$email = strtolower($_REQUEST['email']);
// quick hack to get admin email working.
// by the time $email is checked its been over written.
// store a copy in $admincheck to compare later.
$admincheck = $email;
if(!empty($email)){
if(!filter_var($email, FILTER_VALIDATE_EMAIL)){
// email param provided, but invalid: skip action and show invalid email error
$o = array('status'=>'err','reason'=>'Invalid Email address');
unset($action);
}
$dir = getDirForEmail($email);
$email = basename($dir);
}
switch($action)
{
case 'del':
$id = intval($_REQUEST['mid']);
if(!is_dir($dir))
$o = array('status'=>'err','reason'=>'No emails received on this address');
else if(!is_numeric($id) || !emailIDExists($email,$id))
$o = array('status'=>'err','reason'=>'Invalid Email ID');
else
{
if(deleteEmail($email,$id))
$o = array('status'=>'ok');
else
$o = array('status'=>'err','reason'=>'Could not delete email. Permission problem?');
}
break;
case 'getdoms':
$settings = loadSettings();
if($settings['DOMAINS'])
$o = explode(',',$settings['DOMAINS']);
else $o = [];
break;
case 'attachment':
$id = intval($_REQUEST['id']);
$filename = basename($_REQUEST['filename']);
$filepath = $dir.DS.'attachments'.DS.$id.'-'.$filename;
if(!is_dir($dir))
$o = array('status'=>'err','reason'=>'No emails received on this address');
else if(!is_numeric($id) || !emailIDExists($email,$id))
$o = array('status'=>'err','reason'=>'Invalid Email ID');
else if(!file_exists($filepath))
$o = array('status'=>'err','reason'=>'File not found');
else
{
header('Content-Type: '.mime_content_type($filepath));
readfile($filepath);
exit();
}
break;
case 'load':
$id = intval($_REQUEST['id']);
if(empty($email))
$o = array('status'=>'err','reason'=>'No email address provided');
else if(!is_dir($dir))
$o = array('status'=>'err','reason'=>'No emails received on this address');
else if(!is_numeric($id) || !emailIDExists($email,$id))
$o = array('status'=>'err','reason'=>'Invalid Email ID');
else
{
$data = getEmail($email,$id);
if($_REQUEST['raw']=='true')
{
header('Content-Type: text/plain');
exit($data['raw']);
}
$o = array('status'=>'ok','data'=>$data);
}
break;
case 'list':
$settings = loadSettings();
/*
if($settings['ADMIN'] && $settings['ADMIN']==$admincheck)
{
$o['status'] = 'ok';
$o['type'] = 'admin';
$o['dateformat'] = $settings['DATEFORMAT'];
$emails = listEmailAdresses();
$emaillist = array();
if(count($emails)>0)
{
foreach($emails as $email)
{
$emaildata = getEmailsOfEmail($email);
foreach($emaildata as $time=>$d)
$emaillist[$time.'-'.$email]=$d;
}
if(is_array($emaillist))
krsort($emaillist);
$data = (count($emaillist)?$emaillist:array());
}
$o['emails']=$data;
}
else */
if(!is_dir($dir) && $settings['ADMIN']!=$admincheck)
$o = array('status'=>'ok','emails'=>[]);
else
{
if(!$email) $email = $admincheck;
$data = getEmailsOfEmail($email);
$lastid = $_REQUEST['lastid'];
if($lastid && is_numeric($lastid))
{
foreach($data as $time=>$d)
{
if($time>$lastid)
$emails[$time]=$d;
}
$data = (is_array($emails)?$emails:array());
}
$o = array('status'=>'ok','emails'=>$data);
}
break;
case 'show-list':
$settings = loadSettings();
$o = $settings['SHOW_ACCOUNT_LIST'];
break;
case 'list-addresses':
$settings = loadSettings();
$o = array('status'=>'ok','addresses'=>[]);
if ($settings['SHOW_ACCOUNT_LIST'])
$o['addresses'] = listEmailAdresses();
break;
}
echo json_encode($o);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
web/css/default.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\chris\Downloads\pico-1.5.10.zip

View file

@ -1,7 +1,35 @@
body {
padding-top: 5rem;
}
.starter-template {
padding: 3rem 1.5rem;
form {
margin: 0;
padding: 0;
display: inline;
}
tr.htmx-swapping td {
opacity: 0;
transition: opacity 1s ease-out;
}
.badge {
background-color: #999;
color: white;
padding: 4px 8px;
margin: 2px;
text-align: center;
}
border-radius: 5px;
}
.htmx-indicator{
display:none;
}
.text-center {
text-align: center;
}
/* pico css overrides */
:root {
--form-element-spacing-vertical: 0.15rem;
--form-element-spacing-horizontal: 1rem;
}

5
web/css/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,220 @@
<?php
class OpenTrashmailBackend{
private $url;
private $settings;
public function __construct($url){
$this->url = $url;
$this->settings = loadSettings();
}
public function run(){
// api calls
if($this->url[0]=='api')
{
switch($this->url[1]){
case 'address':
return $this->listAccount($_REQUEST['email']?:$this->url[2]);
case 'read':
return $this->readMail($_REQUEST['email']?:$this->url[2],$_REQUEST['id']?:$this->url[3]);
case 'listaccounts':
if($this->settings['SHOW_ACCOUNT_LIST'])
return $this->listAccounts();
else return '403 Forbidden';
case 'raw-html':
return $this->getRawMail($this->url[2],$this->url[3],true);
case 'raw':
return $this->getRawMail($this->url[2],$this->url[3]);
case 'attachment':
return $this->getAttachment($this->url[2],$this->url[3]);
case 'delete':
return $this->deleteMail($_REQUEST['email']?:$this->url[2],$_REQUEST['id']?:$this->url[3]);
case 'random':
$addr = generateRandomEmail();
//add header HX-Redirect
return $this->listAccount($addr);
case 'deleteaccount':
return $this->deleteAccount($_REQUEST['email']?:$this->url[2]);
default:
return false;
}
}
// rss feed
else if($this->url[0]=='rss')
{
header("Content-Type: application/rss+xml; charset=UTF8");
$email = $this->url[1];
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(404);
exit('Error: Email not found');
}
return $this->renderTemplate('rss.xml',[
'email'=>$email,
'emaildata'=>getEmailsOfEmail($email),
'url'=>$this->settings['URL'],
]);
}
//json api
else if($this->url[0]=='json')
{
header("Content-Type: application/json; charset=UTF8");
if($this->url[1]=='listaccounts')
{
if($this->settings['SHOW_ACCOUNT_LIST'])
return json_encode(listEmailAdresses());
else exit(json_encode(['error'=>'403 Forbidden']));
}
$email = $this->url[1];
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(404);
exit(json_encode(['error'=>'Email not found']));
}
$id = $this->url[2];
if($id) //user wants a specific email ID
{
if(!emailIDExists($email,$id))
{
http_response_code(404);
exit(json_encode(['error'=>'Email ID not found']));
}
else if(!ctype_digit($id))
{
http_response_code(400);
exit(json_encode(['error'=>'Invalid ID']));
}
else
return json_encode(getEmail($email,$id));
}
else
return json_encode(getEmailsOfEmail($email,true,true));
}
else return false;
}
function deleteAccount($email)
{
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
$path = getDirForEmail($email);
if(is_dir($path))
delTree($path);
}
function listAccounts()
{
$accounts = listEmailAdresses();
return $this->renderTemplate('account-list.html',[
'emails'=>$accounts,
'dateformat'=>$this->settings['DATEFORMAT']
]);
}
function deleteMail($email,$id)
{
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
else if(!ctype_digit($id))
return $this->error('Invalid id');
else if(!emailIDExists($email,$id))
return $this->error('Email not found');
deleteEmail($email,$id);
return '';
}
function getRawMail($email,$id,$htmlbody=false)
{
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
else if(!ctype_digit($id))
return $this->error('Invalid id');
else if(!emailIDExists($email,$id))
return $this->error('Email not found');
$emaildata = getEmail($email,$id);
if($htmlbody)
exit($emaildata['parsed']['htmlbody']);
header('Content-Type: text/plain');
echo $emaildata['raw'];
exit;
}
function getAttachment($email,$attachment)
{
$id = substr($attachment,0,13);
$attachment = substr($attachment,14);
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
else if(!ctype_digit($id))
return $this->error('Invalid id');
else if(!emailIDExists($email,$id))
return $this->error('Email not found');
else if(!attachmentExists($email,$id,$attachment))
return $this->error('Attachment not found');
$dir = getDirForEmail($email);
$file = $dir.DS.'attachments'.DS.$id.'-'.$attachment;
$mime = mime_content_type($file);
header('Content-Type: '.$mime);
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
function readMail($email,$id)
{
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
else if(!ctype_digit($id))
return $this->error('Invalid id');
else if(!emailIDExists($email,$id))
return $this->error('Email not found');
$emaildata = getEmail($email,$id);
//$email['raw'] = file_get_contents(getDirForEmail($email['email']).DS.$email['id'].'.json');
//$email['parsed'] = json_decode($email['raw'],true);
//var_dump($emaildata);
return $this->renderTemplate('email.html',[
'emaildata'=>$emaildata,
'email'=>$email,
'mailid'=>$id,
'dateformat'=>$this->settings['DATEFORMAT']
]);
}
public function listAccount($email)
{
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
return $this->error('Invalid email address');
$emails = getEmailsOfEmail($email);
//var_dump($emails);
return $this->renderTemplate('email-table.html',[
'email'=>$email,
'emails'=>$emails,
'dateformat'=>$this->settings['DATEFORMAT']
]);
}
public function error($text)
{
return '<h1>'.$text.'</h1>';
}
public function renderTemplate($templatename,$variables=[])
{
ob_start();
if(is_array($variables))
extract($variables);
if(file_exists(ROOT.DS.'templates'.DS.$templatename.'.php'))
include(ROOT.DS.'templates'.DS.$templatename.'.php');
else if(file_exists(ROOT.DS.'templates'.DS.$templatename))
include(ROOT.DS.'templates'.DS.$templatename);
$rendered = ob_get_contents();
ob_end_clean();
return $rendered;
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,48 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="shortcut icon" href="favicon.ico">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/opentrashmail.css">
<link rel="stylesheet" href="css/fontawesome.min.css">
<title>Open Trashmail</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="?"><img src="imgs/logo_300_light.png" width="50px" /> Open Trashmail </a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navi" aria-controls="navi" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navi">
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" id="email" type="email" placeholder="email address" aria-label="email address">
<button onClick="accessAccount()" class="btn btn-secondary my-2 my-sm-0"><i class="fas fa-arrow-left"></i><i class="fas fa-envelope"></i> Access account</button> &nbsp;
<button onClick="generateAccount()" id="btn-gen-random" class="btn btn-secondary my-2 my-sm-0"><i class="fas fa-random"></i> Generate random</button> &nbsp;
<button onClick="listAddresses(event)" id="btn-list-addresses" class="btn btn-secondary my-2 my-sm-0" style="display:none;"><i class="fas fa-list"></i> List accounts</button>
</form>
</div>
</nav>
<main role="main" class="container" id="main">
<div class="starter-template">
<h1>Welcome to Open Trashmail</h1>
<p class="lead">Access an email address or generate a new one.</p>
</div>
</main><!-- /.container -->
<script src="js/jquery-3.4.1.min.js"></script>
<script src="js/namegenerator.js"></script>
<script src="js/opentrashmail.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/moment-with-locales.min.js"></script>
</body>
</html>

33
web/index.php Normal file
View file

@ -0,0 +1,33 @@
<?php
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__FILE__));
include_once(ROOT.DS.'inc'.DS.'OpenTrashmailBackend.class.php');
include_once(ROOT.DS.'inc'.DS.'core.php');
$url = array_filter(explode('/',ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),'/')));
$backend = new OpenTrashmailBackend($url);
if($_SERVER['HTTP_HX_REQUEST']!='true')
{
if(count($url)==0 || !file_exists(ROOT.DS.implode('/', $url)))
if($url[0]!='api' && $url[0]!='rss' && $url[0]!='json')
exit($backend->renderTemplate('index.html',[
'url'=>implode('/', $url),
'settings'=>loadSettings(),
]));
}
else if(count($url)==1 && $url[0] == 'api') {
exit($backend->renderTemplate('intro.html'));
}
$answer = $backend->run();
if($answer === false)
return false;
else
echo $answer;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
web/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,101 +0,0 @@
<?php
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__FILE__));
define('DOMAIN', $_SERVER['HTTP_HOST']);
error_reporting(E_ALL || ~E_NOTICE);
ini_set('display_errors', 1);
include_once(ROOT . DS . 'inc' . DS . 'core.php');
header("Content-Type: application/rss+xml; charset=UTF8");
if (PHP_SAPI === 'cli-server')
$_SERVER['SCRIPT_NAME'] = pathinfo(__FILE__, PATHINFO_BASENAME);
if ($_GET['url'])
$url = explode('/', ltrim(parse_url($_GET['url'], PHP_URL_PATH), '/'));
else $url = array_filter(explode('/', ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/')));
array_shift($url);
$email = $url[0];
if (!$email) {
http_response_code(404);
exit('Error: Email not found');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) exit();
$rss = '<?xml version="1.0" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="https://' . DOMAIN . '/rss.php" rel="self" type="application/rss+xml" />
<title>RSS for ' . $email . '</title>
<link>https://' . DOMAIN . '/#' . $email . '</link>
<description>RSS Feed for email address ' . $email . '</description>
<lastBuildDate>' . date(DateTime::RFC2822, time()) . '</lastBuildDate>
<image>
<title>RSS for ' . $email . '</title>
<url>https://raw.githubusercontent.com/HaschekSolutions/opentrashmail/master/web/imgs/logo_300.png</url>
<link>https://github.com/HaschekSolutions/opentrashmail</link>
</image>';
$emaildata = getEmailsOfEmail($email);
foreach ($emaildata as $id => $d) {
$data = getEmail($email, $id);
//var_dump($data);
$time = substr($id, 0, -3);
$date = date("Y-m-d H:i", $time);
$att_text = array();
$encl = array();
if (is_array($data['parsed']['attachments']))
foreach ($data['parsed']['attachments'] as $filename) {
$filepath = ROOT . DS . '..' . DS . 'data' . DS . $email . DS . 'attachments' . DS . $filename;
$parts = explode('-', $filename);
$fid = $parts[0];
$fn = $parts[1];
$url = 'https://' . DOMAIN . '/api.php?a=attachment&email=' . $email . '&id=' . $fid . '&filename=' . $fn;
//$encl[] = '<enclosure url="'.rawurlencode($url).'" length="'.filesize($filepath).'" type="'.mime_content_type($filepath).'" />';
$att_text[] = "<a href='$url' target='_blank'>$fn</a>";
}
$rss .= '
<item>
<title><![CDATA[' . $data['parsed']['subject'] . ']]></title>
<pubDate>' . date(DateTime::RFC2822, $time) . '</pubDate>
<link>https://' . DOMAIN . '/#' . $email . '</link>
<guid>https://' . DOMAIN . '/api.php?a=load&email=' . $email . '&id=' . $id . '&raw=true</guid>
<description>
<![CDATA[
Email from: ' . htmlentities($data['from']) . '<br/>
Email to: ' . (is_array($data['rcpts']) ? htmlentities(implode(',', $data['rcpts'])) : htmlentities($email)) . '<br/>
' . ((count($att_text) > 0) ? 'Attachments:<br/>' . array2ul($att_text) . '<br/>' : '') . '
<a href="https://' . DOMAIN . '/api.php?a=load&email=' . $email . '&id=' . $id . '&raw=true">View raw email</a> <br/>
<br/>---------<br/><br/>
' . ($data['parsed']['htmlbody'] ? $data['parsed']['htmlbody'] : nl2br(htmlentities($data['parsed']['body']))) . '
]]>
</description>
' ./*((count($encl)>0)?implode('<br/>',$encl):'').*/ '
</item>';
//if (++$i > 5) break;
}
$rss .= '</channel>
</rss> ';
echo $rss;
function array2ul($array)
{
$out = "<ul>";
foreach ($array as $key => $elem) {
$out .= "<li>$elem</li>";
}
$out .= "</ul>";
return $out;
}

View file

@ -0,0 +1,29 @@
<div>
<a role="button" class="outline" href="/json/listaccounts" target="_blank"><i class="fas fa-file-code"></i> JSON API</a>
</div>
<table>
<thead>
<tr>
<th scope="col">Email Addess</th>
<th>Emails in Inbox</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach($emails as $email): ?>
<tr>
<td>
<a href="/address/<?= $email; ?>" hx-get="/api/address/<?= $email; ?>" hx-push-url="/address/<?= $email; ?>" hx-target="#main">
<?= escape($email) ?>
</a>
</td>
<td><?= countEmailsOfAddress($email); ?></td>
<td>
<a href="/address/<?= $email; ?>" hx-get="/api/address/<?= $email; ?>" hx-push-url="/address/<?= $email; ?>" hx-target="#main" role="button" >Show</a>
<a href="#" role="button" hx-get="/api/deleteaccount/<?= $email ?>" hx-confirm="Are you sure to delete this account and all its emails?" hx-target="closest tr" hx-swap="outerHTML swap:1s">Delete</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

View file

@ -0,0 +1,53 @@
<nav aria-label="breadcrumb">
<ul>
<li><?= escape($email) ?></li>
<li></li>
<li></li>
</ul>
</nav>
<div>
<a role="button" class="outline" href="#" id="copyemailbtn" onclick="copyEmailToClipboard();return false;"><i class="far fa-clipboard"></i> Copy address to clipboard</a>
<a role="button" class="outline" href="/rss/<?= $email ?>" target="_blank"><i class="fas fa-rss"></i> RSS Feed</a>
<a role="button" class="outline" href="/json/<?= $email ?>" target="_blank"><i class="fas fa-file-code"></i> JSON API</a>
</div>
<table role="grid">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Date</th>
<th scope="col">From</th>
<th scope="col">Subject</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php if(count($emails)==0): ?>
<tr>
<td colspan="5"><center>No emails received on this address (yet..)</center></td>
</tr>
<?php endif; ?>
<?php foreach($emails as $unixtime => $ed): ?>
<tr>
<th scope="row"><?= ++$i; ?></th>
<td id="date-td-<?= $i ?>"><script>document.getElementById('date-td-<?= $i ?>').innerHTML = moment.unix(parseInt(<?=$unixtime?>/1000)).format('<?= $dateformat; ?>');</script></td>
<td><?= escape($ed['from']) ?></td>
<td><?= escape($ed['subject']) ?></td>
<td>
<a href="/read/<?= $email ?>/<?= $ed['id'] ?>" hx-get="/api/read/<?= $email ?>/<?= $ed['id'] ?>" hx-push-url="/read/<?= $email ?>/<?= $ed['id'] ?>" hx-target="#main" role="button">Open</a>
<a href="#" hx-get="/api/delete/<?= $email ?>/<?= $ed['id'] ?>" hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML swap:1s" role="button">Delete</a>
</td>
</tr>
<?php endforeach; ?>
</table>
<script>history.pushState({urlpath:"/address/<?= $email ?>"}, "", "/address/<?= $email ?>");</script>
<script>
function copyEmailToClipboard(){
navigator.clipboard.writeText("<?= $email ?>");
document.getElementById('copyemailbtn').innerHTML = '<i class="fas fa-check-circle" style="color: green;"></i> Copied!';
}
</script>

View file

@ -0,0 +1,52 @@
<nav aria-label="breadcrumb">
<ul>
<li><a href="/address/<?= $email ?>" hx-get="/api/address/<?= $email ?>" hx-target="#main"><?= escape($email) ?></a></li>
<li><?= escape($emaildata['parsed']['subject']) ?></li>
</ul>
</nav>
<article>
<header>
<p>Subject: <?= escape($emaildata['parsed']['subject']) ?></p>
<p>Received: <span id="date2-<?= $mailid ?>"><script>document.getElementById('date2-<?= $mailid ?>').innerHTML = moment.unix(parseInt(<?=$mailid?>/1000)).format('<?= $dateformat; ?>');</script></span></p>
<p>
Reciepients:
<?php foreach ($emaildata['rcpts'] as $to) : ?>
<small class="badge"><?= escape($to) ?></small>
<?php endforeach; ?>
</p>
</header>
<div id="emailbody">
<?php if($emaildata['parsed']['htmlbody']): ?>
<a href="#" hx-confirm="Warning: HTML may contain tracking functionality or scripts. Do you want to proceed?" hx-get="/api/raw-html/<?= $email ?>/<?= $mailid ?>" hx-target="#emailbody" role="button" class="secondary outline">Render email in HTML</a>
<?php endif; ?>
<hr>
<pre><?= nl2br(escape($emaildata['parsed']['body'])) ?></pre>
</div>
<footer>
Attachments
<div>
<?php if (count($emaildata['parsed']['attachments']) == 0) : ?>
<small class="secondary">No attachments</small>
<?php endif; ?>
<ul>
<?php foreach ($emaildata['parsed']['attachments'] as $attachment) : ?>
<li>
<a target="_blank" href="/api/attachment/<?= $email ?>/<?= $attachment ?>"><?= escape($attachment) ?></a>
</li>
<?php endforeach; ?>
</ul>
</div>
</footer>
</article>
<article>
<header>Raw email</header>
<a href="/api/raw/<?= $email ?>/<?= $mailid ?>" target="_blank">Open in new Window</a>
<pre><button hx-get="/api/raw/<?= $email ?>/<?= $mailid ?>" hx-swap="outerHTML">Load Raw Email</button></pre>
</article>
<!--
<script>history.pushState({email:"<?= $email ?>",id:"<?= $mailid ?>"}, "", "/read/<?= $email ?>/<?= $mailid ?>");</script> -->

View file

@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/pico.min.css">
<link rel="stylesheet" href="/css/fontawesome.min.css">
<link rel="stylesheet" href="/css/opentrashmail.css">
<title>Open Trashmail</title>
</head>
<body>
<div class="container-fluid">
<nav>
<ul>
<li><img src="/imgs/logo_300_light.png" width="50px" /> Open Trashmail</li>
<li><input id="email" hx-post="/api/address" hx-target="#main" name="email" type="email" hx-trigger="input changed delay:500ms" placeholder="email address" aria-label="email address"></li>
<li><button hx-get="/api/random" hx-target="#main"><i class="fas fa-random"></i> Generate random</button></li>
<?php if($settings['SHOW_ACCOUNT_LIST']): ?><li><button hx-get="/api/listaccounts" hx-target="#main" hx-push-url="/listaccounts"><i class="fas fa-list"></i> List accounts</button></li><?php endif; ?>
</ul>
</nav>
</div>
<button class="htmx-indicator" aria-busy="true">Loading…</button>
<main id="main" class="container" hx-get="/api/<?= $url ?>" hx-trigger="load">
</main>
<script src="/js/htmx.min.js"></script>
<script src="/js/namegenerator.js"></script>
<!-- <script src="/js/opentrashmail.js"></script> -->
<script src="/js/moment-with-locales.min.js"></script>
</body>
</html>

6
web/templates/intro.html Normal file
View file

@ -0,0 +1,6 @@
<div class="text-center">
<img src="/imgs/logo_300_roundbg.png" alt="OpenTrashmail Logo">
<h1>Welcome to OpenTrashmail</h1>
<p><a href="https://github.com/HaschekSolutions/opentrashmail" target="_blank" role="button" class="secondary"><i class="fab fa-github"></i> View on Github</a></p>
<p>OpenTrashmail is an open source, selfhostable, disposable email service that helps you protect your privacy online. With OpenTrashmail, you can create your own trashmail addresses using your own domain, subdomain or wildcard domain</p>
</div>

45
web/templates/rss.xml.php Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="<?= $url ?>/rss/<?= $email ?>" rel="self" type="application/rss+xml" />
<title>RSS for <?= $email ?></title>
<link><?= $url ?>/eml/<?= $email ?></link>
<description>RSS Feed for email address <?= $email ?></description>
<lastBuildDate><?= date(DateTime::RFC2822, time()) ?></lastBuildDate>
<image>
<title>RSS for <?= $email ?></title>
<url>https://raw.githubusercontent.com/HaschekSolutions/opentrashmail/master/web/imgs/logo_300.png</url>
<link>https://github.com/HaschekSolutions/opentrashmail</link>
</image>
<?php foreach ($emaildata as $id => $d):
$data = getEmail($email, $id);
$time = substr($id, 0, -3);
$att_text = [];
if (is_array($data['parsed']['attachments']))
foreach ($data['parsed']['attachments'] as $filename) {
$filepath = ROOT . DS . '..' . DS . 'data' . DS . $email . DS . 'attachments' . DS . $filename;
$parts = explode('-', $filename);
$fid = $parts[0];
$fn = $parts[1];
$att_url = $url . '/api/attachment/' . $email . '/' . $filename;
$att_text[] = "<a href='$att_url' target='_blank'>$fn</a>";
}
?>
<item>
<title><![CDATA[<?= $data['parsed']['subject'] ?>]]></title>
<pubDate><?= date(DateTime::RFC2822, $time) ?></pubDate>
<link><?= $url ?>/eml/<?= $email ?>/<?= $id ?></link>
<description>
<![CDATA[
Email from: <?= escape($d['from']) ?><br/>
Email to: <?= escape(implode(';',$data['rcpts'])) ?><br/>
<?= ((count($att_text) > 0) ? 'Attachments:<br/>' . array2ul($att_text) . '<br/>' : '') ?>
<a href="<?= $url ?>api/raw/test@0xv.eu/1699459401553">View raw email</a> <br/>
<br/>---------<br/><br/>
<?= ($data['parsed']['htmlbody'] ? $data['parsed']['htmlbody'] : nl2br(htmlentities($data['parsed']['body']))) ?>
]]>
</description>
</item>
<?php endforeach; ?>
</channel>
</rss>