Skip to content

Commit

Permalink
v1.2.0 closes #63
Browse files Browse the repository at this point in the history
  • Loading branch information
geek-at committed Nov 22, 2023
1 parent bb76133 commit 87404cc
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 10 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Changelog

## V1.1.6
- Reworked the navbar header to look better on smaller screens
## V1.2.0
- Implemented IP/Subnet filter using the config option `ALLOWED_IPS`
- Implemented Password authentication of the site and API using config option `PASSWORD`
- Implemented max attachment size as mentioned in [#63](https://github.com/HaschekSolutions/opentrashmail/issues/63)
- Reworked the navbar header to look better on smaller screens

## V1.1.5
- Added support for plaintext file attachments
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
- Web interface to manage emails
- Generates random email addresses
- 100% file based, no database needed
- Can be used as Email Honeypot
- Can be used as Email Honeypot or to programmatically solve 2fa emails

# General API calls and functions

Expand Down Expand Up @@ -64,6 +64,9 @@ Just edit the `config.ini` You can use the following settings
- `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
- `DATEFORMAT` -> How should timestamps be shown on the web interface ([moment.js syntax](https://momentjs.com/docs/#/displaying/))
- `PASSWORD` -> If configured, site and API can't be used without providing it via form, POST/GET variable `password` or http header `PWD` (eg: `curl -H "PWD: 123456" http://localhost:8080/json...`)
- `ALLOWED_IPS` -> Comma separated list of IPv4 or IPv6 CIDR addresses that are allowed to use the web UI or API
- `ATTACHMENTS_MAX_SIZE` -> Max size for each individual attachment of an email in Bytes

## Docker env vars
In Docker you can use the following environment variables:
Expand All @@ -77,7 +80,9 @@ In Docker you can use the following environment variables:
| ADMIN | If set to a valid email address and this address is entered in the API or webinterface, will show all emails of all accounts. Kind-of catch-all | test@test.com
| DATEFORMAT | Will format the received date in the web interface based on [moment.js](https://momentjs.com/) syntax | "MMMM Do YYYY, h:mm:ss a" |
| SKIP_FILEPERMISSIONS | If set to `true`, won't fix file permissions for the code data folder in the container. Useful for local dev. Default `false` | true,false |

| PASSWORD | If configured, site and API can't be used without providing it via form, POST/GET variable `password` or http header `PWD` | yousrstrongpassword |
| ALLOWED_IPS | Comma separated list of IPv4 or IPv6 CIDR addresses that are allowed to use the web UI or API | `192.168.5.0/24,2a02:ab:cd:ef::/60,172.16.0.0/16` |
| ATTACHMENTS_MAX_SIZE | Max size for each individual attachment of an email in Bytes | `2000000` = 2MB |

# Roadmap
- [x] Mail server
Expand All @@ -97,13 +102,13 @@ In Docker you can use the following environment variables:
- [x] Make better theme
- [x] Secure HTML, so no malicious things can be loaded
- [x] Display embedded images inline using Content-ID
- [ ] Configurable settings
- [x] Configurable settings
- [x] Choose domains for random generation
- [x] Choose if out-of-scope emails are discarded
- [x] Automated cleanup of old mails
- [ ] Honeypot mode where all emails are also saved for a catchall account
- [ ] Optionally secure whole site with a password
- [ ] Optionally allow site to be seen only from specific IP Range
- [x] Optionally secure whole site with a password
- [x] Optionally allow site to be seen only from specific IP Range
- [x] Honeypot mode where all emails are also saved for a catchall account (implemented with the ADMIN setting)

# Quick start

Expand Down
3 changes: 3 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ services:
- DISCARD_UNKNOWN=false
- SHOW_ACCOUNT_LIST=true
- SHOW_LOGS=true
# - PASSWORD=123456
# - ALLOWED_IPS=192.168.0.0/16,2a02:ab:cd:ef::/60
# - ATTACHMENTS_MAX_SIZE=10000000

ports:
- '2525:25'
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ services:
- DATEFORMAT=D.M.YYYY HH:mm
- SKIP_FILEPERMISSIONS=true
- DISCARD_UNKNOWN=false
# - PASSWORD=123456
# - ALLOWED_IPS=192.168.0.0/16,2a02:ab:cd:ef::/60
# - ATTACHMENTS_MAX_SIZE=10000000

ports:
- '2525:25'
Expand Down
3 changes: 3 additions & 0 deletions docker/rootfs/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ _buildConfig() {
echo "SHOW_ACCOUNT_LIST=${SHOW_ACCOUNT_LIST:-false}"
echo "ADMIN=${ADMIN:-}"
echo "SHOW_LOGS=${SHOW_LOGS:-false}"
echo "PASSWORD=${PASSWORD:-}"
echo "ALLOWED_IPS=${ALLOWED_IPS:-}"

echo "[MAILSERVER]"
echo "MAILPORT=${MAILPORT:-25}"
echo "DISCARD_UNKNOWN=${DISCARD_UNKNOWN:-true}"
echo "ATTACHMENTS_MAX_SIZE=${ATTACHMENTS_MAX_SIZE:-0}"

echo "[DATETIME]"
echo "DATEFORMAT=${DATEFORMAT:-D.M.YYYY HH:mm}"
Expand Down
11 changes: 11 additions & 0 deletions example.config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ URL="http://localhost:8080"
; Enable to show logs on the website
;SHOW_LOGS=false

; Password authentication for Web UI and API
; Passwords have to be sent via the HTTP header "PWD" or as a GET/Post parameter "password"
;PASSWORD=mystrongpassword

; If configured, only these IPs will be allowed to access the Web UI and API (but with no further authentication)
; Comma separated if multiple, can be IPv4 or IPv6
;ALLOWED_IPS=192.168.0.0/16,2a02:ab:cd:ef::/60

[MAILSERVER]
; Port that the Mailserver will run on (default 25 but that needs root)
MAILPORT=25
Expand All @@ -27,6 +35,9 @@ MAILPORT=25
; this greatly reduces the amount of spam you will receive
DISCARD_UNKNOWN=true

; Limits the size of each attachment in bytes. Leave empty to disable
;ATTACHMENTS_MAX_SIZE=2000000 ; 2MB

; Port number of the !! HIGHLY EXPERIMENTAL !! POP3 server
;POP3PORT=110

Expand Down
18 changes: 16 additions & 2 deletions python/mailserver3.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# globals for settings
DISCARD_UNKNOWN = False
DELETE_OLDER_THAN_DAYS = False
ATTACHMENTS_MAX_SIZE = 0
DOMAINS = []
LAST_CLEANUP = 0
URL = ""
Expand Down Expand Up @@ -50,13 +51,19 @@ async def handle_DATA(self, server, session, envelope):
if part.get_content_type() == 'text/plain':
#if it's a file
if part.get_filename() is not None:
attachments['file%d' % len(attachments)] = self.handleAttachment(part)
att = self.handleAttachment(part)
if(att == False):
return '500 Attachment too large. Max size: ' + str(ATTACHMENTS_MAX_SIZE/1000000)+"MB"
attachments['file%d' % len(attachments)] = att
else:
plaintext += part.get_payload()
elif part.get_content_type() == 'text/html':
html += part.get_payload()
else:
attachments['file%d' % len(attachments)] = self.handleAttachment(part)
att = self.handleAttachment(part)
if(att == False):
return '500 Attachment too large. Max size: ' + str(ATTACHMENTS_MAX_SIZE/1000000)+"MB"
attachments['file%d' % len(attachments)] = att

for em in rcpts:
em = em.lower()
Expand Down Expand Up @@ -132,6 +139,10 @@ def handleAttachment(self, part):
fid = hashlib.md5(filename.encode('utf-8')).hexdigest()+filename
logger.debug('Handling attachment: "%s" (ID: "%s") of type "%s" with CID "%s"',filename, fid,part.get_content_type(), cid)

if(ATTACHMENTS_MAX_SIZE > 0 and len(part.get_payload(decode=True)) > ATTACHMENTS_MAX_SIZE):
logger.info("Attachment too large: " + filename)
return False

return (filename,part.get_payload(decode=True),cid,fid)

def replace_cid_with_attachment_id(self, html_content, attachments,filenamebase,email):
Expand Down Expand Up @@ -192,12 +203,15 @@ async def run(port):
DISCARD_UNKNOWN = (Config.get("MAILSERVER", "DISCARD_UNKNOWN").lower() == "true")
DOMAINS = Config.get("GENERAL", "DOMAINS").lower().split(",")
URL = Config.get("GENERAL", "URL")
if("attachments_max_size" in Config.options("MAILSERVER")):
ATTACHMENTS_MAX_SIZE = int(Config.get("MAILSERVER", "ATTACHMENTS_MAX_SIZE"))

if("CLEANUP" in Config.sections() and "delete_older_than_days" in Config.options("CLEANUP")):
DELETE_OLDER_THAN_DAYS = (Config.get("CLEANUP", "DELETE_OLDER_THAN_DAYS").lower() == "true")

logger.info("[i] Starting Mailserver on port " + str(port))
logger.info("[i] Discard unknown domains: " + str(DISCARD_UNKNOWN))
logger.info("[i] Max size of attachments: " + str(ATTACHMENTS_MAX_SIZE))
logger.info("[i] Listening for domains: " + str(DOMAINS))

asyncio.run(run(port))
72 changes: 72 additions & 0 deletions web/inc/core.php

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions web/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@

$backend = new OpenTrashmailBackend($url);

$settings = loadSettings();

if($settings['ALLOWED_IPS'])
{
$ip = getUserIP();
if(!isIPInRange( $ip, $settings['ALLOWED_IPS'] ))
exit("Your IP ($ip) is not allowed to access this site.");
}

if($settings['PASSWORD']) //site requires a password
{
session_start();
$pw = $settings['PASSWORD'];
$auth = false;
//first check for auth header or POST/GET variable
if(isset($_SERVER['HTTP_PWD']) && $_SERVER['HTTP_PWD'] == $pw)
$auth = true;
else if(isset($_REQUEST['password']) && $_REQUEST['password'] == $pw)
$auth = true;
// if not, check for session
else if(isset($_SESSION['authenticated']) && $_SESSION['authenticated'] == true)
$auth = true;
// if user sent a pw but it's wrong, show error
else if($_REQUEST['password'] != $settings['PASSWORD'])
exit($backend->renderTemplate('password.html',[
'error'=>'Wrong password',
]));

if($auth===true)
$_SESSION['authenticated'] = true;
else
exit($backend->renderTemplate('password.html'));
}

if($_SERVER['HTTP_HX_REQUEST']!='true')
{
if(count($url)==0 || !file_exists(ROOT.DS.implode('/', $url)))
Expand Down
18 changes: 18 additions & 0 deletions web/templates/password.html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

<!DOCTYPE html>
<html>
<head>
<title>Password Form</title>
</head>
<body>
<h1>Enter Password</h1>
<form action="/" method="POST">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<br><br>
<input type="submit" value="Submit">
</form>

<h2><?=$error?></h2>
</body>
</html>

0 comments on commit 87404cc

Please sign in to comment.