Compare commits
25
Commits
2d62258008
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701da16aaf
|
||
|
|
57de8f47d0
|
||
|
|
671af52b15
|
||
|
|
ef9cc1aab3
|
||
|
|
0a3fc13f1e
|
||
|
|
9c5b6dce9c
|
||
|
|
de2bd45ae4
|
||
|
|
da6f92f5d3
|
||
|
|
98d19f1091
|
||
|
|
ee5d179922
|
||
|
|
67b72d935d
|
||
|
|
5df9c0de93
|
||
|
|
6cbdfcecaa
|
||
|
|
002512cf20
|
||
|
|
61548211ed
|
||
|
|
3c1fbf2cf2
|
||
|
|
724c4da9fa
|
||
|
|
973f7a087b
|
||
|
|
065def33fc
|
||
|
|
28064e8144
|
||
|
|
fbebdad8fe
|
||
|
|
d5ea0d0856
|
||
|
|
d06c80e218
|
||
|
|
979039094e
|
||
|
|
85db8c7cff
|
+6
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[defaults]
|
||||||
|
interpreter_python = python3
|
||||||
|
inventory = inventory.txt
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
message_file: message.txt
|
||||||
|
port: 5000
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[autoreply_editor]
|
||||||
|
example.tld
|
||||||
@@ -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
|
||||||
@@ -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 %}]
|
||||||
@@ -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
@@ -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>
|
||||||
|
|
||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -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")
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
Hello,
|
|
||||||
|
|
||||||
This is my autoreply message.
|
|
||||||
|
|
||||||
Best,
|
|
||||||
Me
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
Flask
|
Flask
|
||||||
Flask-BasicAuth
|
Flask-BasicAuth
|
||||||
gunicorn
|
gunicorn
|
||||||
|
jinja2
|
||||||
|
|||||||
Reference in New Issue
Block a user