#!/usr/bin/env python3

import os, cgi, MySQLdb, subprocess, time

# settings
DB_NAME = 'vmail'
DB_USER = 'vmail'
DB_PW   = '{{postfix.dovecot.mysql_password}}'

# DB stuff
db = MySQLdb.connect(user=DB_USER, passwd=DB_PW, db=DB_NAME, charset='utf8')

def db_execute(query, args):
   cursor = db.cursor(MySQLdb.cursors.DictCursor)
   if len(args) == 1 and isinstance(args[0], dict): args = args[0] # einzelnes dict weiterreichen
   cursor.execute(query, args)
   return cursor

def db_fetch_one_row(query, *args):
   cursor = db_execute(query, args)
   result = cursor.fetchone()
   if cursor.fetchone() is not None:
      raise Exception("Found more than one result, only one was expected")
   cursor.close()
   return result

def db_run(query, *args):
   cursor = db_execute(query, args)
   cursor.close()

def get_user(name):
   return db_fetch_one_row('SELECT * FROM users WHERE username=%s', name)

def change_user_pw(name, hash):
   db_run('UPDATE users SET password=%s WHERE username=%s', hash, name)

def change_user_active(name, active):
   db_run('UPDATE users SET active=%s WHERE username=%s', int(active), name)

# interaction with dbadm pw
def compare_pw(hash, plain):
   try:
      subprocess.check_output(["doveadm", "pw", "-t", hash, "-p", plain])
      return True
   except subprocess.CalledProcessError:
      return False

def gen_hash(plain):
   return subprocess.check_output(["doveadm", "pw", "-s", "SHA512-CRYPT", "-r", str(64*1024), "-p", plain]).decode('utf-8').strip()

# the core action
def act(user, oldpw, newpw1, newpw2):
   if user is None and oldpw is None and newpw1 is None and newpw2 is None:
      return
   if user is None or oldpw is None or newpw1 is None or newpw2 is None:
      return "<b>Error</b>: You have to fill all the fields."
   curdata = get_user(user)
   if curdata is None:
      return "<b>Error</b>: User not found."
   if len(newpw1) < 8:
      return "<b>Error</b>: Password must be at least 8 characters long."
   if newpw1 != newpw2:
      return "<b>Error</b>: New passwords do not match."
   # slow down brute-force attacks
   time.sleep(2.5)
   # now go on
   if not compare_pw(curdata['password'], oldpw):
      return "<b>Error</b>: Old PW is wrong."
   new_hash = gen_hash(newpw1)
   change_user_pw(user, new_hash)
   # potentially enable this user
   if curdata['active'] < 0:
      change_user_active(user, -curdata['active'])
   return "Password successfully changed."

# print headers
print("Content-Type: text/html")
print()

# print document header
print("<!DOCTYPE html>")
print("<html><head>")
print("  <meta charset='utf-8'>")
print("  <title>User PW change</title>")
print("  <style type='text/css'> th { text-align: right; } </style>")
print("</head><body>")

# do stuff
form = cgi.FieldStorage()
msg = act(form.getfirst('user'), form.getfirst('oldpw'), form.getfirst('newpw1'), form.getfirst('newpw2'))
if msg is not None:
   print("<p>{}</p>".format(msg))

# print form
print("  <form action='changepw' method='post'><table>")
print("    <tr><th>Username: </th><td><input type='text' name='user' placeholder='user@domain'></td>")
print("    <tr><th>Old Password: </th><td><input type='password' name='oldpw'></td>")
print("    <tr><th>New Password: </th><td><input type='password' name='newpw1'></td>")
print("    <tr><th>New Password (confirmation): </th><td><input type='password' name='newpw2'></td>")
print("    <tr><th></th><td><input type='submit' value='Change Password'></td></tr>")
print("  </table></form>")
print("</body></html>")
