Skip to content

Commit

Permalink
Merge branch 'bot-refactored' into prune-thread
Browse files Browse the repository at this point in the history
  • Loading branch information
missytake authored Jul 25, 2022
2 parents b0e876f + ccca381 commit 53b4b68
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 64 deletions.
14 changes: 14 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,17 @@ setup & usage
-------------

See the `docs/index.rst` file or https://mailadm.readthedocs.io for more info.

Setup Development Environment
-----------------------------

```
git clone https://github.com/deltachat/mailadm
python3 -m venv venv
. venv/bin/activate
pip install pytest tox
pip install .
sudo apt install postfix # choose "No configuration" when asked
sudo systemctl disable postfix
sudo touch /etc/postfix/main.cf
```
49 changes: 34 additions & 15 deletions install_mailadm.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,29 @@ if [[ $EUID -ne 0 ]]; then
exit 1
fi

set -xe

# modify the following variables
export MAIL_DOMAIN=example.org

export VMAIL_USER=vmail
export VMAIL_HOME=/home/vmail
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
# load variables from config file
if [ ! -f $SCRIPTPATH/.env ]; then
echo "export MAIL_DOMAIN=example.org" > $SCRIPTPATH/.env
echo "" >> $SCRIPTPATH/.env
echo "export VMAIL_USER=vmail" >> $SCRIPTPATH/.env
echo "export VMAIL_HOME=/home/vmail" >> $SCRIPTPATH/.env
echo "export MAILADM_USER=mailadm" >> $SCRIPTPATH/.env
echo "export MAILADM_HOME=/var/lib/mailadm" >> $SCRIPTPATH/.env
echo "" >> $SCRIPTPATH/.env
echo "export WEB_ENDPOINT=https://example.org/new_email" >> $SCRIPTPATH/.env
echo "export LOCALHOST_WEB_PORT=3691" >> $SCRIPTPATH/.env
echo "" >> $SCRIPTPATH/.env
echo "export [email protected]" >> $SCRIPTPATH/.env
echo "export BOT_PASSWORD=p4ssw0rd" >> $SCRIPTPATH/.env

echo "Can't get settings from $SCRIPTPATH/.env, please set environment variables there."
exit 1
else
. ./.env
fi

export MAILADM_USER=mailadm
export MAILADM_HOME=/var/lib/mailadm
export WEB_ENDPOINT=https://example.org/new_email
export LOCALHOST_WEB_PORT=3691
set -xe

# check if vmail user exists
if ! getent passwd $VMAIL_USER > /dev/null 2>&1; then
Expand Down Expand Up @@ -65,7 +76,15 @@ $MAILADM_HOME/venv/bin/mailadm gen-sysconfig \
--localhost-web-port=$LOCALHOST_WEB_PORT \
--mailadm-user $MAILADM_USER


systemctl daemon-reload
systemctl enable mailadm-web mailadm-prune
systemctl restart mailadm-web mailadm-prune
$MAILADM_HOME/venv/bin/mailadm setup-bot \
--email $BOT_EMAIL \
--password $BOT_PASSWORD \
--db $MAILADM_HOME/admbot.sqlite

# don't enable services if running in docker
if [ $(cat /proc/1/comm) = "systemd" ]
then
systemctl daemon-reload
systemctl enable mailadm-web mailadm-prune
systemctl restart mailadm-web mailadm-prune
fi
84 changes: 84 additions & 0 deletions src/mailadm/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import deltachat
from deltachat import account_hookimpl, run_cmdline
from mailadm.db import DB
from mailadm.commands import add_user, add_token, list_tokens
import os
from threading import Event


class SetupPlugin:
def __init__(self, admingrpid):
self.member_added = Event()
self.admingrpid = admingrpid
self.message_sent = Event()

@account_hookimpl
def ac_member_added(self, chat: deltachat.Chat, contact, actor, message):
assert chat.num_contacts() == 2
if chat.id == self.admingrpid:
self.member_added.set()

@account_hookimpl
def ac_message_delivered(self, message: deltachat.Message):
if not message.is_system_message():
self.message_sent.set()


class AdmBot:
def __init__(self, db):
self.db = db
with self.db.read_connection() as conn:
config = conn.config()
self.admingrpid = config.admingrpid

@account_hookimpl
def ac_incoming_message(self, command: deltachat.Message):
print("process_incoming message:", command)
command.create_chat()
if not self.check_privileges(command):
command.chat.send_text("Sorry, I only take commands from the admin group.")

if command.text.strip() == "/help":
text = ("/add-token name expiry prefix token maxuse"
"/add-user addr password token"
"/list-tokens")
command.chat.send_text(text)

elif command.text.strip() == "/add-token":
arguments = command.text.split(" ")
text = add_token(self.db, arguments[0], arguments[1], arguments[2], arguments[3], arguments[4])
command.chat.send_text(text)

elif command.text.strip() == "/add-user":
arguments = command.text.split(" ")
text = add_user(self.db, arguments[0], arguments[1], arguments[2])
command.chat.send_text(text)

elif command.text.strip() == "/list-tokens":
command.chat.send_text(list_tokens(self.db))

def check_privileges(self, command):
"""
Checks whether the incoming message was in the admin group.
"""
if command.chat.is_group() and self.admingrpid == command.chat.id:
if command.chat.is_protected() and command.chat.is_encrypted() and int(command.chat.num_contacts) >= 2:
if command.message.get_sender_contact() in command.chat.get_contacts():
return True
else:
print("%s is not allowed to give commands to mailadm." % (command.message.get_sender_contact(),))
else:
print("admin chat is broken. Try `mailadm setup-bot`. Group ID:" + self.admingrpid)
raise ValueError
else:
# reply "This command needs to be sent to the admin group"
return False


def main(db, argv=None):
run_cmdline(argv=argv, account_plugins=[AdmBot(db)])


if __name__ == "__main__":
db = DB(os.getenv("MAILADM_DB"))
main(db)
132 changes: 92 additions & 40 deletions src/mailadm/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@

from __future__ import print_function

import mailadm
import mailadm.db
from .conn import DBError, UserInfo
from .mailcow import MailcowError
import mailadm.util
import time
import os
import sys
import click
from click import style
import qrcode

import mailadm
import mailadm.db
from .conn import DBError, UserInfo
from .mailcow import MailcowError
import mailadm.commands
import mailadm.util
from .conn import DBError
from .bot import SetupPlugin

from deltachat import Account, account_hookimpl


option_dryrun = click.option(
Expand Down Expand Up @@ -50,15 +59,80 @@ def get_mailadm_db(ctx, show=False, fail_missing_config=True):
return db


@click.command()
@click.option("--db", type=str, default=str(os.getenv("MAILADM_HOME")) + "/admbot.sqlite",
help="Delta Chat database for admbot account", required=True)
@click.option("--email", type=str, default=None, help="name of email")
@click.option("--password", type=str, default=None, help="name of password")
@click.pass_context
@account_hookimpl
def setup_bot(ctx, email, password, db):
"""initialize the deltachat bot as an alternative command interface.
:param ctx: the click object passing the CLI environment
:param email: the email account the deltachat bot will use for receiving commands
:param password: the password to the bot's email account
:param db: the path to the deltachat database of the bot - NOT the path to the mailadm database!
"""
ac = Account(db)

if not ac.is_configured():
assert email and password, (
"you must specify --email and --password once to configure this database/account"
)
ac.set_config("addr", email)
ac.set_config("mail_pw", password)
ac.set_config("mvbox_move", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
configtracker = ac.configure()
configtracker.wait_finish()

ac.start_io()

chat = ac.create_group_chat("Admin group on {}".format(os.environ["MAIL_DOMAIN"]), contacts=[], verified=True)

setupplugin = SetupPlugin(chat.id)
ac.add_account_plugin(setupplugin)

chatinvite = chat.get_join_qr()
qr = qrcode.QRCode()
qr.add_data(chatinvite)
print("\nPlease scan this qr code to join a verified admin group chat:\n\n")
qr.print_ascii(invert=True)
print("\nAlternatively, copy-paste this invite to your Delta Chat desktop client:", chatinvite)

print("\nWaiting until you join the chat")
sys.stdout.flush() # flush stdout to actually show the messages above
setupplugin.member_added.wait()
setupplugin.message_sent.clear()
chat.send_text("Welcome to the Admin group on %s! Type /help to get an overview over existing commands." %
(os.environ["MAIL_DOMAIN"],))

mailadmdb = get_mailadm_db(ctx)
with mailadmdb.read_connection() as rconn:
admingrpid_old = rconn.config.admingrpid
if admingrpid_old:
setupplugin.message_sent.wait()
setupplugin.message_sent.clear()
oldgroup = ac.get_chat_by_id(int(admingrpid_old))
oldgroup.send_text("Someone created a new admin group on the command line. This one is not valid anymore.")
setupplugin.message_sent.wait()
ac.shutdown()

with mailadmdb.write_transaction() as wconn:
wconn.set_config("admingrpid", chat.id)


@click.command()
@click.pass_context
def config(ctx):
"""show and manipulate config settings. """
db = get_mailadm_db(ctx)
with db.read_connection() as conn:
with db.read_connection() as rconn:
click.secho("** mailadm version: {}".format(mailadm.__version__))
click.secho("** mailadm database path: {}".format(db.path))
for name, val in conn.get_config_items():
for name, val in rconn.get_config_items():
click.secho("{:22s} {}".format(name, val))


Expand All @@ -67,10 +141,7 @@ def config(ctx):
def list_tokens(ctx):
"""list available tokens """
db = get_mailadm_db(ctx)
with db.read_connection() as conn:
for name in conn.get_token_list():
token_info = conn.get_tokeninfo_by_name(name)
dump_token_info(token_info)
click.secho(mailadm.commands.list_tokens(db))


@click.command()
Expand Down Expand Up @@ -105,19 +176,11 @@ def dump_token_info(token_info):
help="prefix for all e-mail addresses for this token")
@click.option("--token", type=str, default=None, help="name of token to be used")
@click.pass_context
def add_token(ctx, name, expiry, prefix, token, maxuse):
def add_token(ctx, name, expiry, maxuse, prefix, token):
"""add new token for generating new e-mail addresses
"""
from mailadm.util import get_human_readable_id

db = get_mailadm_db(ctx)
if token is None:
token = expiry + "_" + get_human_readable_id(len=15)
with db.write_transaction() as conn:
info = conn.add_token(name=name, token=token, expiry=expiry,
maxuse=maxuse, prefix=prefix)
tc = conn.get_tokeninfo_by_name(info.name)
dump_token_info(tc)
click.secho(mailadm.commands.add_token(db, name, expiry, maxuse, prefix, token))


@click.command()
Expand Down Expand Up @@ -206,26 +269,17 @@ def init(ctx, web_endpoint, mail_domain, mailcow_endpoint, mailcow_token):
help="if not specified, generate a random password")
@click.option("--token", type=str, default=None,
help="name of token. if not specified, automatically use first token matching addr")
@option_dryrun
@click.pass_context
def add_user(ctx, addr, password, token):
def add_user(ctx, addr, password, token, dryrun):
"""add user as a mailadm managed account.
"""
with get_mailadm_db(ctx).write_transaction() as conn:
if token is None:
if "@" not in addr:
ctx.fail("invalid email address: {}".format(addr))

token_info = conn.get_tokeninfo_by_addr(addr)
if token_info is None:
ctx.fail("could not determine token for addr: {!r}".format(addr))
else:
token_info = conn.get_tokeninfo_by_name(token)
if token_info is None:
ctx.fail("token does not exist: {!r}".format(token))
try:
conn.add_email_account_tries(token_info, addr=addr, password=password, tries=1)
except (DBError, MailcowError) as e:
ctx.fail("failed to add e-mail account %s: %s" % (addr, e))
db = get_mailadm_db(ctx)
result = mailadm.commands.add_user(db, token, addr, password, dryrun)
if result["status"] == "error":
ctx.fail(result["message"])
elif result["status"] == "success":
click.secho("Created {} with password: {}".format(result["message"].addr, result["message"].clear_pw))


@click.command()
Expand All @@ -234,10 +288,7 @@ def add_user(ctx, addr, password, token):
def del_user(ctx, addr):
"""remove e-mail address"""
with get_mailadm_db(ctx).write_transaction() as conn:
try:
conn.delete_email_account(addr)
except (DBError, MailcowError) as e:
ctx.fail("failed to delete e-mail account {}: {}".format(addr, e))
conn.del_user(addr=addr)


@click.command()
Expand Down Expand Up @@ -309,6 +360,7 @@ def migrate_db(ctx):
conn.execute(q, ("path_virtual_mailboxes",))


mailadm_main.add_command(setup_bot)
mailadm_main.add_command(init)
mailadm_main.add_command(config)
mailadm_main.add_command(list_tokens)
Expand Down
Loading

0 comments on commit 53b4b68

Please sign in to comment.