move newmail script to dovecot tasks; add changepw script; fix quota-warning script
authorRalf Jung <post@ralfj.de>
Sun, 20 May 2018 22:00:28 +0000 (00:00 +0200)
committerRalf Jung <post@ralfj.de>
Sun, 20 May 2018 22:26:03 +0000 (00:26 +0200)
host_vars/template.yml
roles/email/tasks/dovecot.yml
roles/email/tasks/postfix.yml
roles/email/templates/changepw [new file with mode: 0644]
roles/email/templates/dovecot/quota-warning.sh

index dbc47ef..d5e5971 100644 (file)
@@ -56,6 +56,8 @@ postfix:
     quota:
       general: 1G
       trash: +10M
+    # optional: Where to generate a CGI script that users can use to change their password
+    changepw_cgi: /srv/mail.example.org/cgi/changepw
   # optional: Hostnames and SHA1 certificate hashes that are allowed to relay email via this host.
   relay_client_cert_whitelist:
     - hostname: other.example.org
index f4929eb..aa59821 100644 (file)
     dest: /etc/dovecot/quota-warning.sh
     src: templates/dovecot/quota-warning.sh
     mode: +x
+# scripts
+- name: create newmail dir
+  file: path=/root/newmail state=directory
+- name: install newmail script
+  copy:
+    dest: /root/newmail/newmail
+    src: files/newmail/newmail
+    mode: u=rwx,g=rx,o=rx
+- name: install newmail templates
+  copy:
+    dest: /root/newmail/templates.py
+    src: files/newmail/templates.py
+- name: install changepw script
+  when: postfix.dovecot.changepw_cgi is defined
+  template:
+    dest: "{{postfix.dovecot.changepw_cgi}}"
+    src: templates/changepw
+    mode: u=rwx,g=rx,o=
index fb8f703..67e459d 100644 (file)
     dest: /etc/cron.daily/check-for-local-mail
     src: files/check-for-local-mail
     mode: u=rwx,g=rx,o=rx
-# tools
-- block:
-  - name: create newmail dir
-    file: path=/root/newmail state=directory
-  - name: install newmail script
-    copy:
-      dest: /root/newmail/newmail
-      src: files/newmail/newmail
-      mode: u=rwx,g=rx,o=rx
-  - name: install newmail templates
-    copy:
-      dest: /root/newmail/templates.py
-      src: files/newmail/templates.py
-  when: postfix.dovecot is defined
diff --git a/roles/email/templates/changepw b/roles/email/templates/changepw
new file mode 100644 (file)
index 0000000..96eef60
--- /dev/null
@@ -0,0 +1,102 @@
+#!/usr/bin/env python2
+from __future__ import print_function
+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>")
index 00c09f8..f834d1c 100644 (file)
@@ -2,7 +2,7 @@
 set -e
 
 PERCENT=$1
-FROM="{{postfix.dovecot.postmaster}}"
+FROM="{{postfix.postmaster}}"
 
 msg="From: $FROM
 To: $USER