Compare commits

...
25 Commits
Author SHA1 Message Date
mxmehl 701da16aaf make sure .filter are 0600 2021-12-23 14:57:52 +01:00
mxmehl 57de8f47d0 make path to message file absolute 2021-12-23 14:48:51 +01:00
mxmehl 671af52b15 do not create backup files for .qmail 2021-12-23 14:20:47 +01:00
mxmehl ef9cc1aab3 fix pylint and black 2021-12-23 13:57:13 +01:00
mxmehl 0a3fc13f1e minor service improvements 2021-12-23 13:56:58 +01:00
mxmehl 9c5b6dce9c adapt playbook to more functions of the python tool 2021-12-23 13:48:48 +01:00
mxmehl de2bd45ae4 update sample config 2021-12-23 13:32:58 +01:00
mxmehl da6f92f5d3 get rid of unnecessary output argument 2021-12-23 13:32:46 +01:00
mxmehl 98d19f1091 better navigation on templates 2021-12-23 13:28:19 +01:00
mxmehl ee5d179922 integrate multi-user approach, based on pre-defined list of tuples 2021-12-23 13:25:17 +01:00
mxmehl 67b72d935d disable unused if clause 2021-12-23 12:37:02 +01:00
mxmehl 5df9c0de93 embed active toggle get_maildropline 2021-12-23 12:36:15 +01:00
mxmehl 6cbdfcecaa embed RE escape in get_maildropline 2021-12-23 12:10:36 +01:00
mxmehl 002512cf20 create .filter file from template, and make maildrop command in .qmail variable depending on user 2021-12-23 12:05:55 +01:00
mxmehl 61548211ed create .qmail and message file if needed 2021-12-20 18:46:26 +01:00
mxmehl 3c1fbf2cf2 force basic auth on all pages 2021-12-19 22:52:12 +01:00
mxmehl 724c4da9fa do not fail when rc == 0 2021-12-19 22:45:07 +01:00
mxmehl 973f7a087b extend prototype for qmail toggling 2021-12-19 22:25:09 +01:00
mxmehl 065def33fc prototype qmail toggling 2021-12-19 22:00:41 +01:00
mxmehl 28064e8144 steps for autoresponder mailbot 2021-12-19 21:05:18 +01:00
mxmehl fbebdad8fe fix wrong dir name 2021-12-19 21:05:05 +01:00
mxmehl d5ea0d0856 create empty message file if necessary 2021-12-19 20:58:24 +01:00
mxmehl d06c80e218 further ansible tasks 2021-12-19 20:51:04 +01:00
mxmehl 979039094e fix wrong command 2021-12-19 20:50:48 +01:00
mxmehl 85db8c7cff first steps for ansible deployment 2021-12-19 20:14:23 +01:00
21 changed files with 357 additions and 33 deletions
+6 -1
View File
@@ -142,5 +142,10 @@ dmypy.json
cython_debug/ cython_debug/
# Project specific # Project specific
message.txt message*.txt
config.py config.py
inventory.txt
ansible/host_vars/*
!ansible/host_vars/example.tld.yml
!.keep
TODO.md
+2 -2
View File
@@ -8,7 +8,7 @@ used in Uberspace.de environments.
1. Clone Repo, e.g. to `/home/YOURUSERNAME/autoreply-editor` 1. Clone Repo, e.g. to `/home/YOURUSERNAME/autoreply-editor`
2. Create `config.py` and `message.txt` from their sample files 2. Create `config.py` and `message.txt` from their sample files
3. Install requirements: `pip3 install -r requirements.txt --user` 3. Install requirements: `pip3 install -r requirements.txt --user`
4. Create web backend on uberspace: `uberspace web backend app.example.com --http --port 5000` 4. Create web backend on uberspace: `uberspace web backend set app.example.com --http --port 5000`
5. Create a service in `~/etc/services.d/autoreply-editor.ini`: 5. Create a service in `~/etc/services.d/autoreply-editor.ini`:
``` ```
@@ -36,7 +36,7 @@ to "| mailbot -T reply -t $HOME/autoreply-editor/message.txt -d $HOME/autorespon
Make sure that the file permissions are `0600`! Make sure that the file permissions are `0600`!
Afterwards, also create the directory `~/autoreply` if necessary. Afterwards, also create the directory `~/autoresponders` if necessary.
### .qmail file ### .qmail file
+3
View File
@@ -0,0 +1,3 @@
[defaults]
interpreter_python = python3
inventory = inventory.txt
+3
View File
@@ -0,0 +1,3 @@
---
message_file: message.txt
port: 5000
View File
+10
View File
@@ -0,0 +1,10 @@
---
auth_user: exampleuser
auth_pass: changemenow
domain: autoreply.example.tld
users:
- name: Foobar
email: foobar@example.com
- name: Barbaz
email: barbaz@example.net
+2
View File
@@ -0,0 +1,2 @@
[autoreply_editor]
example.tld
+52
View File
@@ -0,0 +1,52 @@
- hosts: autoreply_editor
gather_facts: true
tasks:
- name: Get newest version of autoreply-editor
git:
repo: https://src.mehl.mx/mxmehl/autoreply-editor.git
dest: autoreply-editor
notify: restart supervisord
- name: Deploy config file
template:
src: config.py.j2
dest: autoreply-editor/config.py
notify: restart supervisord
- name: Install specified python requirements
pip:
requirements: requirements.txt
chdir: autoreply-editor
extra_args: --user
- name: Register domain for service
command: "uberspace web domain add {{ domain }}"
register: cmdrtrn
failed_when:
# Adding an already registered domain fails, but we ignore this specific error
- cmdrtrn.rc != 0
- "'It is already configured for your Uberspace account' not in cmdrtrn.stderr"
- name: Create web backend for service
command: "uberspace web backend set {{ domain }} --http --port {{ port }}"
- name: Deploy supervisord service
template:
src: autoreply-editor.ini.j2
dest: etc/services.d/autoreply-editor.ini
notify: reload supervisord
- name: Create autoresponders directory
file:
path: autoresponders
state: directory
handlers:
- name: reread supervisord
command: supervisorctl reread
listen: reload supervisord
- name: update supervisord
command: supervisorctl update
listen: reload supervisord
- name: restart supervisord
command: supervisorctl restart autoreply-editor
@@ -0,0 +1,4 @@
[program:autoreply-editor]
directory=/home/{{ ansible_user_id }}/autoreply-editor
command=gunicorn --bind 0.0.0.0:{{ port }} wsgi:app
startsecs=5
+7
View File
@@ -0,0 +1,7 @@
"""Configuration file"""
# Username and password for logging in to the site
BASIC_AUTH_USERNAME = "{{ auth_user }}"
BASIC_AUTH_PASSWORD = "{{ auth_pass }}"
# List of users. First value is the full name, second the email address
USERS = [{% for user in users %}("{{ user.name }}", "{{ user.email }}"),{% endfor %}]
+3 -1
View File
@@ -1,7 +1,9 @@
"""Initialise flask app"""
from flask import Flask from flask import Flask
app = Flask(__name__) app = Flask(__name__)
import autoreply_editor.main import autoreply_editor.main #pylint: disable=wrong-import-position
app.config['BASIC_AUTH_FORCE'] = True
app.config.from_pyfile("../config.py") app.config.from_pyfile("../config.py")
+186 -11
View File
@@ -1,25 +1,200 @@
"""Provide basic settings and views""" """Provide basic settings and views"""
from flask import request, render_template, redirect import fileinput
import os
import re
import shutil
from pathlib import Path
from flask import abort, request, render_template
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
from jinja2 import Template
from autoreply_editor import app from autoreply_editor import app
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
def qmail_status(user):
"""Find out whether the filter is currently activated for the given mail user"""
with open(get_qmailfile(user), encoding="utf8") as qmailfile:
# trigger to check whether we have to re-open the file for initialisation
initialise = False
# try to find active filter
if re.search(
rf"{get_maildropline(user, active=True, regex=True)}",
qmailfile.read(),
re.MULTILINE,
):
return True
else:
# jump back to top of file because we've read it in the "if"
qmailfile.seek(0)
# filter is deactivated (commented)
if re.search(
rf"{get_maildropline(user, active=False, regex=True)}",
qmailfile.read(),
re.MULTILINE,
):
return False
# The maildrop line does not exist in the expected form
else:
initialise = True
# Append a commented filter command to the file
if initialise:
with open(get_qmailfile(user), mode="a", encoding="utf8") as qmailfile:
qmailfile.write(f"{get_maildropline(user, active=False)}")
return False
def check_user(user):
"""Check if user exists"""
fulluser = [
item for item in app.config.get("USERS") if item[1].split("@")[0] == user
]
if not fulluser:
return False
else:
return fulluser[0]
def get_messagefile(user):
"""Return message file of user, and create if necessary"""
# Path of the project
gitpath = str(Path(app.root_path).parent)
# File name of the message
filename = f"message-{user}.txt"
# Combine path
filepath = os.path.join(gitpath, filename)
if not os.path.isfile(filepath):
Path(filepath).touch()
return filepath
def get_qmailfile(user):
"""Return qmail file of user, and create if necessary based on default qmail"""
qmail_base = f"{str(Path.home())}/.qmail"
filepath = f"{qmail_base}-{user}"
if not os.path.isfile(filepath):
shutil.copy(f"{qmail_base}-default", filepath)
return filepath
def get_filterfile(user):
"""Return .filter-autoreply file of user, and create if necessary"""
filter_base = f"{str(Path.home())}/.filter-autoreply"
filepath = f"{filter_base}-{user}"
if not os.path.isfile(filepath):
with open(
os.path.join(app.root_path, "templates/filterfile.j2"), encoding="utf-8"
) as template:
filterconfig = Template(template.read()).render(
email=check_user(user)[1],
user=user,
name=check_user(user)[0],
messagefile=get_messagefile(user),
)
with open(filepath, "w", encoding="utf-8") as filterfile:
filterfile.write(filterconfig)
# Change permissions to 0600
os.chmod(filepath, 0o600)
return filepath
def get_maildropline(user, active=True, regex=False):
"""
Define the line calling maildrop, with options to have the line disabled or
in Regex format
"""
base = f"|maildrop {get_filterfile(user)}"
# If the filter should be inactive, we comment it
if not active:
base = f"#{base}"
# depending on whether it should be RE escaped or not, return the line
if regex:
return f"^{re.escape(base)}$"
else:
return base
@app.route("/") @app.route("/")
def index(): def index():
with open(app.config.get("MESSAGE_FILE"), "r") as f: """Index page listing available users"""
message = f.read() return render_template(
"index.html",
users=app.config.get("USERS"),
)
return render_template("index.html", message=message)
@app.route('/', methods=['POST']) @app.route("/user/<user>")
def index_post(): def user_status(user):
input_message = request.form['message'] """Status for specific user"""
# Check if user is valid
if not check_user(user):
abort(404)
if request.method == 'POST': with open(get_messagefile(user), "r", encoding="utf-8") as messagefile:
with open(app.config.get("MESSAGE_FILE"), 'w') as f: message = messagefile.read()
f.write(str(input_message))
return render_template(
"user.html",
message=message,
qmail_status=qmail_status(user),
)
@app.route("/user/<user>", methods=["POST"])
def user_update(user):
"""Update user with POST request"""
# Check if user is valid
if not check_user(user):
abort(404)
if request.form["action"] == "message":
input_message = request.form["message"]
with open(get_messagefile(user), "w", encoding="utf-8") as messagefile:
messagefile.write(str(input_message))
result = "Success: The autoreply message has been updated!" result = "Success: The autoreply message has been updated!"
return render_template("result.html", result=result) if request.form["action"] == "qmail":
# define whether to set a comment
if request.form["status"] == "on":
state_current = get_maildropline(user, active=False)
state_desired = get_maildropline(user)
else:
state_current = get_maildropline(user)
state_desired = get_maildropline(user, active=False)
with fileinput.FileInput(
get_qmailfile(user),
inplace=True,
) as qmailfile:
for line in qmailfile:
print(
line.replace(state_current, state_desired),
end="",
)
result = f"Success: the autoreply is now {request.form['status']}."
try:
return render_template("update.html", user=user, result=result)
except UnboundLocalError:
return render_template(
"update.html", user=user, result="Something went terribly wrong!"
)
@app.errorhandler(404)
def page_not_found(error):
"""Error for 404"""
return render_template("error.html", error=error), 404
@@ -6,7 +6,7 @@
<body> <body>
<h1>Autoreply Editor</h1> <h1>Autoreply Editor</h1>
<p>{{ result }}</p> <p>{{ error }}</p>
<p><a href="/">Go back</a></p> <p><a href="/">Go back</a></p>
+5
View File
@@ -0,0 +1,5 @@
logfile "$HOME/mailfilter-autoreply-{{ user }}.log"
FROM="{{ name }} <{{ email }}>"
to "| mailbot -T reply -t {{ messagefile }} -d $HOME/autoresponders/autoresponsedb-{{ user }} -N -A 'From: $FROM' /var/qmail/bin/qmail-inject -f ''"
+8 -9
View File
@@ -6,14 +6,13 @@
<body> <body>
<h1>Autoreply Editor</h1> <h1>Autoreply Editor</h1>
<p> <h2>Configurable users</h2>
Below you can see the current text of your autoreply message. You
can edit and update it directly below: <ul>
</p> {% for user in users %}
<form action="" method="POST"> <li><a href="/user/{{ user[1].split('@')[0] }}">{{ user[0] }} {{"<"}}{{ user[1] }}{{">"}}</a></li>
<textarea name="message" cols="120" rows="20">{{ message }}</textarea> {% endfor %}
<br /> </ul>
<input type="submit" value="Update">
</form>
</body> </body>
</html> </html>
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<head>
<title>Autoreply Editor</title>
</head>
<body>
<h1>Autoreply Editor</h1>
<p>{{ result }}</p>
<p><a href="/user/{{ user }}">Go back</a></p>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<head>
<title>Autoreply Editor</title>
</head>
<body>
<h1>Autoreply Editor</h1>
<p>
<a href="/">↩ Back to user overview</a>
</p>
<h2>Toggle autoreply message</h2>
<p>
The autoreply is <strong>{%if qmail_status%}active{%else%}inactive{%endif%}</strong>
</p>
<form method="POST">
<input type="hidden" name="action" value="qmail" />
{% if qmail_status %}
<input type="hidden" name="status" value="off" />
<input type="submit" value="Deactivate">
{% else %}
<input type="hidden" name="status" value="on" />
<input type="submit" value="Activate">
{% endif %}
</form>
<h2>Change autoreply message</h2>
<p>
Below you can see the current text of your autoreply message. You
can edit and update it directly below:
</p>
<form method="POST">
<input type="hidden" name="action" value="message" />
<textarea name="message" cols="120" rows="20">{{ message }}</textarea>
<br />
<input type="submit" value="Update">
</form>
</body>
</html>
+7 -2
View File
@@ -1,6 +1,11 @@
"""Configuration file""" """Configuration file"""
MESSAGE_FILE = "message.txt" # Username and password for logging in to the site
BASIC_AUTH_FORCE = True
BASIC_AUTH_USERNAME = "username" BASIC_AUTH_USERNAME = "username"
BASIC_AUTH_PASSWORD = "password" BASIC_AUTH_PASSWORD = "password"
# List of users. First value is the full name, second the email address
USERS = [
("Foobar", "foobar@example.com"),
("Barbaz", "baz@example.net")
]
-6
View File
@@ -1,6 +0,0 @@
Hello,
This is my autoreply message.
Best,
Me
+1
View File
@@ -1,3 +1,4 @@
Flask Flask
Flask-BasicAuth Flask-BasicAuth
gunicorn gunicorn
jinja2
+1
View File
@@ -1,3 +1,4 @@
"""WSGI for Flask app"""
from autoreply_editor import app from autoreply_editor import app
if __name__ == "__main__": if __name__ == "__main__":