v1.2.0 closes #63

This commit is contained in:
Chris 2023-11-22 12:26:09 +01:00
parent bb7613359a
commit 87404cc3d7
10 changed files with 176 additions and 10 deletions

View file

@ -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

View file

@ -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
@ -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:
@ -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
@ -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

View file

@ -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'

View file

@ -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'

View file

@ -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}"

View file

@ -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
@ -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

View file

@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
# globals for settings
DISCARD_UNKNOWN = False
DELETE_OLDER_THAN_DAYS = False
ATTACHMENTS_MAX_SIZE = 0
DOMAINS = []
LAST_CLEANUP = 0
URL = ""
@ -50,13 +51,19 @@ class CustomHandler:
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()
@ -132,6 +139,10 @@ class CustomHandler:
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):
@ -192,12 +203,15 @@ if __name__ == '__main__':
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))

File diff suppressed because one or more lines are too long

View file

@ -9,6 +9,40 @@ $url = array_filter(explode('/',ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL
$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)))

View file

@ -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>