Compare commits

...
15 Commits
18 changed files with 274 additions and 103 deletions
+3 -1
View File
@@ -142,8 +142,10 @@ dmypy.json
cython_debug/ cython_debug/
# Project specific # Project specific
message.txt message*.txt
config.py config.py
inventory.txt inventory.txt
ansible/host_vars/* ansible/host_vars/*
!ansible/host_vars/example.tld.yml
!.keep !.keep
TODO.md
+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
+4 -12
View File
@@ -5,17 +5,13 @@
git: git:
repo: https://src.mehl.mx/mxmehl/autoreply-editor.git repo: https://src.mehl.mx/mxmehl/autoreply-editor.git
dest: autoreply-editor dest: autoreply-editor
notify: restart supervisord
- name: Deploy config file - name: Deploy config file
template: template:
src: config.py.j2 src: config.py.j2
dest: autoreply-editor/config.py dest: autoreply-editor/config.py
notify: restart supervisord
- name: Create empty message file if it does not exist
copy:
content: ""
dest: "autoreply-editor/{{ message_file }}"
force: no
- name: Install specified python requirements - name: Install specified python requirements
pip: pip:
@@ -45,12 +41,6 @@
path: autoresponders path: autoresponders
state: directory state: directory
- name: Create autoreply mailbot filter
template:
src: filter-autoreply.j2
dest: .filter-autoreply
mode: 0600
handlers: handlers:
- name: reread supervisord - name: reread supervisord
command: supervisorctl reread command: supervisorctl reread
@@ -58,3 +48,5 @@
- name: update supervisord - name: update supervisord
command: supervisorctl update command: supervisorctl update
listen: reload supervisord listen: reload supervisord
- name: restart supervisord
command: supervisorctl restart autoreply-editor
+1 -1
View File
@@ -1,4 +1,4 @@
[program:autoreply-editor] [program:autoreply-editor]
directory=/home/{{ ansible_user_id }}/autoreply-editor directory=/home/{{ ansible_user_id }}/autoreply-editor
command=gunicorn --bind 0.0.0.0:{{ port }} wsgi:app command=gunicorn --bind 0.0.0.0:{{ port }} wsgi:app
startsecs=60 startsecs=5
+5 -1
View File
@@ -1,3 +1,7 @@
MESSAGE_FILE = "{{ message_file }}" """Configuration file"""
# Username and password for logging in to the site
BASIC_AUTH_USERNAME = "{{ auth_user }}" BASIC_AUTH_USERNAME = "{{ auth_user }}"
BASIC_AUTH_PASSWORD = "{{ auth_pass }}" 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 %}]
-5
View File
@@ -1,5 +0,0 @@
logfile "$HOME/mailfilter-autoreply.log"
FROM="{{ mail_from }}"
to "| mailbot -T reply -t $HOME/autoreply-editor/{{ message_file }} -d $HOME/autoresponders/autoresponsedb -N -A 'From: $FROM' /var/qmail/bin/qmail-inject -f ''"
+2 -1
View File
@@ -1,8 +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['BASIC_AUTH_FORCE'] = True
app.config.from_pyfile("../config.py") app.config.from_pyfile("../config.py")
+155 -32
View File
@@ -1,77 +1,200 @@
"""Provide basic settings and views""" """Provide basic settings and views"""
import fileinput import fileinput
import os
import re import re
import shutil
from pathlib import Path from pathlib import Path
from flask import request, render_template 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)
qmail_prefix = f"{str(Path.home())}/.qmail-"
maildrop_line = "|maildrop $HOME/.filter-autoreply"
def qmail_status(user): def qmail_status(user):
"""Find out whether the filter is currently activated for the given mail user""" """Find out whether the filter is currently activated for the given mail user"""
with open(f"{qmail_prefix}{user}", encoding="utf8") as dotqmail: with open(get_qmailfile(user), encoding="utf8") as qmailfile:
# TODO: RE not necessary here # trigger to check whether we have to re-open the file for initialisation
if not re.search( initialise = False
r"^\|maildrop \$HOME/\.filter-autoreply$", dotqmail.read(), re.MULTILINE
# 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 return False
# The maildrop line does not exist in the expected form
else: else:
return True 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", encoding="utf-8") as messagefile: """Index page listing available users"""
message = messagefile.read()
return render_template( return render_template(
"index.html", "index.html",
message=message, users=app.config.get("USERS"),
qmail_status=qmail_status(app.config.get("MAIL_USER")),
) )
@app.route("/", methods=["POST"]) @app.route("/user/<user>")
def index_post(): def user_status(user):
if request.method == "POST": """Status for specific user"""
# Check if user is valid
if not check_user(user):
abort(404)
with open(get_messagefile(user), "r", encoding="utf-8") as messagefile:
message = messagefile.read()
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": if request.form["action"] == "message":
input_message = request.form["message"] input_message = request.form["message"]
with open( with open(get_messagefile(user), "w", encoding="utf-8") as messagefile:
app.config.get("MESSAGE_FILE"), "w", encoding="utf-8"
) as messagefile:
messagefile.write(str(input_message)) messagefile.write(str(input_message))
result = "Success: The autoreply message has been updated!" result = "Success: The autoreply message has been updated!"
if request.form["action"] == "qmail": if request.form["action"] == "qmail":
# define whether to set a comment # define whether to set a comment
if request.form["status"] == "on": if request.form["status"] == "on":
preis = "#" state_current = get_maildropline(user, active=False)
preshould = "" state_desired = get_maildropline(user)
else: else:
preis = "" state_current = get_maildropline(user)
preshould = "#" state_desired = get_maildropline(user, active=False)
with fileinput.FileInput( with fileinput.FileInput(
f"{qmail_prefix}{app.config.get('MAIL_USER')}", get_qmailfile(user),
inplace=True, inplace=True,
backup=".bak", ) as qmailfile:
) as dotqmail: for line in qmailfile:
for line in dotqmail:
print( print(
line.replace( line.replace(state_current, state_desired),
f"{preis}{maildrop_line}", f"{preshould}{maildrop_line}"
),
end="", end="",
) )
result = f"Success: the autoreply is now {request.form['status']}." result = f"Success: the autoreply is now {request.form['status']}."
try: try:
return render_template("result.html", result=result) return render_template("update.html", user=user, result=result)
except UnboundLocalError: except UnboundLocalError:
return render_template("result.html", result="Something went terribly wrong!") 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 ''"
+6 -26
View File
@@ -6,33 +6,13 @@
<body> <body>
<h1>Autoreply Editor</h1> <h1>Autoreply Editor</h1>
<h2>Toggle autoreply message</h2> <h2>Configurable users</h2>
<p> <ul>
The autoreply is <strong>{%if qmail_status%}active{%else%}inactive{%endif%}</strong> {% for user in users %}
</p> <li><a href="/user/{{ user[1].split('@')[0] }}">{{ user[0] }} {{"<"}}{{ user[1] }}{{">"}}</a></li>
<form method="POST"> {% endfor %}
<input type="hidden" name="action" value="qmail" /> </ul>
{% 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> </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__":