Compare commits

..

1 Commits

Author SHA1 Message Date
nvanschoote
fa4dddd153 Enforce the use of python3 when running this script 2019-07-12 10:24:32 +02:00
4 changed files with 41 additions and 238 deletions

View File

@ -54,14 +54,6 @@ def files(options):
files = options.file.split(',')
for file in files:
configfile = ConfigObj(file)
# Check if the settings section key is present in the file
try:
value = configfile['setting']
except KeyError:
print('Setting does not exist in', file)
continue
if configfile['setting'].as_bool('use_zarafa_credentials'):
username = options.user
else:

View File

@ -2,10 +2,8 @@
#encoding: utf-8
import kopano
from kopano.errors import NotFoundError
from MAPI.Util import *
import json
import sys
def opt_args():
@ -26,22 +24,14 @@ def main():
options, args = opt_args()
if not options.user:
print('Please use:\n %s --user <username>' % (sys.argv[0]))
print 'Please use:\n %s --user <username>' % (sys.argv[0])
sys.exit(0)
user = kopano.Server(options).user(options.user)
try:
webapp = user.store.prop(0X6773001F).value
except NotFoundError:
webapp = dict(recipients=[])
webapp = user.store.prop(0X6773001F).value
webapp = json.loads(webapp)
if options.backup:
if len(webapp['recipients']) == 0:
print('Property PR_EC_RECIPIENT_HISTORY_JSON_W not found . User might have never used recipient history before.', file=sys.stderr)
sys.exit(1)
f = open('%s.json' % user.name, 'w')
f.write(json.dumps(webapp, sort_keys=True,
indent=4, separators=(',', ': ')))
@ -60,8 +50,8 @@ def main():
sys.exit(0)
if options.list:
print(json.dumps(webapp, sort_keys=True,
indent=4, separators=(',', ': ')))
print json.dumps(webapp, sort_keys=True,
indent=4, separators=(',', ': '))
sys.exit(0)
if options.remove:
@ -69,7 +59,7 @@ def main():
for rec in webapp['recipients']:
if options.remove in rec['display_name'] or options.remove in rec['smtp_address'] \
or options.remove in rec['email_address']:
print('removing contact %s [%s]' % (rec['display_name'], rec['smtp_address']))
print 'removing contact %s [%s]' % (rec['display_name'], rec['smtp_address'])
else:
newlist['recipients'].append(rec)

View File

@ -6,9 +6,6 @@ WebApp admin is a command-line interface to modify, inject and export WebApp set
# Example Usage
Overview of all options:
> python3 webapp_admin -h
Reset WebApp settings
> python3 webapp_admin -u john --reset
@ -18,35 +15,6 @@ Change free/busy to 36 months
If you want to make a change for all users pass the --all-users parameter. Example:
> python3 webapp_admin --all-users --icons Breeze
## Signatures
To restore, replace and backup signatures we need a two part, underscore separated filename consisting of a `name` and `id`.\
Example single user: `this-is-my-signature_1234.html`\
---
**Note**\
The hypens in the filename will be displayed as spaces in WebApp\
The username can also be part of the .html file, but is then ignored by the script.
In WebApp the ID is created based on the unix time, so the ID can be anything
---
Examples
Backup signature for user `henk`
> python3 webapp_admin -u henk --backup-signature
Restore signature for user `henk`
> python3 webapp_admin -u henk --restore-signature my-cool-signature_1615141312112.html
Replace signature for user `henk`
> python3 webapp_admin -u henk --replace-signature my-cool-signature_1615141312112.html
Restore signatures for all users
> python3 webapp_admin --all-users --restore-signature mycompany-signature_1412130992124.html
# Dependencies
- python3
@ -55,8 +23,6 @@ Restore signatures for all users
- OpenSSL
- dotty_dict
For debian 10 python3-pkg-resources is required
# License
licensed under GNU Affero General Public License v3.
licensed under GNU Affero General Public License v3.

View File

@ -2,15 +2,18 @@
# encoding: utf-8
from pkg_resources import parse_version
import sys
if sys.version_info[0] < 3:
print('This tool works with Python3. Not Python 2')
sys.exit(1)
try:
import kopano
except ImportError:
print('python-kopano should be installed on your system')
print('python3-kopano should be installed on your system')
sys.exit(1)
try:
from MAPI.Util import *
except ImportError:
print('python-mapi should be installed on your system')
print('python3-mapi should be installed on your system')
sys.exit(1)
import json
import base64
@ -18,12 +21,11 @@ try:
import OpenSSL.crypto
except ImportError:
pass
from datetime import datetime, timedelta
from datetime import datetime
from time import mktime
import getpass
import time
from optparse import OptionGroup
from tabulate import tabulate
try:
from dotty_dict import dotty
except ImportError:
@ -51,20 +53,11 @@ def opt_args(print_help=None):
group.add_option("--reset", dest="reset", action="store_true", help="Reset WebApp settings")
parser.add_option_group(group)
# Addionals stores group
group = OptionGroup(parser, "Store", "")
group.add_option("--add-store", dest="add_store", action="store", help="Add shared store")
group.add_option("--del-store", dest="del_store", action="store", help="Delete shared store")
group.add_option("--folder-type", dest="folder_type", action="store", help="Folder to add")
group.add_option("--subfolder", dest="sub_folder", action="store_true", help="Add subfolders")
group.add_option("--list-stores", dest="list_stores", action="store_true", help="List shared stores")
parser.add_option_group(group)
# Signature option group
group = OptionGroup(parser, "Signature", "")
group.add_option("--backup-signature", dest="backup_signature", action="store_true", help="Backup signature")
group.add_option("--restore-signature", dest="restore_signature", action="store", help="Restore signature (need file name)")
group.add_option("--replace-signature", dest="replace_signature", action="store", help="Replace existing signature, file layout must be: username_signature-name_signatureid.html or signature-name_signatureid.html ")
group.add_option("--replace", dest="replace", action="store_true", help="Replace existing signature, file layout must be: username_signature-name_signatureid.html")
group.add_option("--default-signature", dest="default_signature", action="store_true", help="Set signature as default one")
parser.add_option_group(group)
@ -72,13 +65,12 @@ def opt_args(print_help=None):
group = OptionGroup(parser, "Categories", "")
group.add_option("--export-categories", dest="export_categories", action="store_true", help="Export Categories (name and color)")
group.add_option("--import-categories", dest="import_categories", action="store_true", help="Import Categories (name and color)")
parser.add_option_group(group)
parser.add_option_group(group)
# S/MIME option group
group = OptionGroup(parser, "S/MIME", "")
group.add_option("--export-smime", dest="export_smime", action="store_true", help="Export private S/MIME certificate")
group.add_option("--import-smime", dest="import_smime", action="store", help="Import private S/MIME certificate")
group.add_option("--remove-expired", dest="remove_expired", action="store_true", help="Remove expired public S/MIME certificates")
group.add_option("--public", dest="public_smime", action="store_true", help="Export/Import public S/MIME certificate")
group.add_option("--password", dest="password", action="store", help="set password")
group.add_option("--ask-password", dest="ask_password", action="store_true", help="ask for password if needed")
@ -92,9 +84,8 @@ def opt_args(print_help=None):
group.add_option("--icons", dest="icons", action="store", help="Change icons (e.g. breeze)")
group.add_option("--htmleditor", dest="htmleditor", action="store", help="Change the HTML editor (e.g. full_tinymce)")
group.add_option("--remove-state", dest="remove_state", action="store_true", help="Remove all the state settings")
group.add_option("--add-safesender", dest="add_sender", action="store", help="Add domain to safe sender list")
group.add_option("--polling-interval", dest="polling_interval", action="store", help="Change the polling interval (seconds)")
group.add_option("--calendar-resolution", dest="calendar_resolution", action="store", help="Change the calendar resolution (minutes)")
group.add_option("--add-safesender", dest="addsender", action="store", help="Add domain to safe sender list")
group.add_option("--polling-interval", dest="pollinginterval", action="store", help="Change the polling interval (seconds)")
parser.add_option_group(group)
# Advanced option group
@ -201,7 +192,7 @@ def language(user, language):
except:
print('User language is not defined using en_GB as fallback')
language = 'en_GB'
if not settings['settings']['zarafa']['v1'].get('main'):
settings['settings']['zarafa']['v1']['main'] = {}
settings['settings']['zarafa']['v1']['main']['language'] = language
@ -209,73 +200,6 @@ def language(user, language):
write_settings(user, json.dumps(settings))
"""
Add shared store
"""
def add_store(user, user_to_add, folder_type, subfolder=False):
allowed_folder_types = ["all", "inbox", "calendar", "contact", "note", "task"]
if folder_type not in allowed_folder_types:
print("Unknown folder type allowed: {}".format(','.join(allowed_folder_types)))
sys.exit(1)
settings = read_settings(user)
if not settings['settings']['zarafa']['v1']['contexts'].get('hierarchy') or not settings['settings']['zarafa']['v1']['contexts']['hierarchy'].get('shared_stores'):
settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores'] = {}
settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores'][user_to_add]= {folder_type : {'folder_type': folder_type, 'show_subfolders': subfolder}}
print("Saving settings")
write_settings(user, json.dumps(settings))
"""
Delete shared store
"""
def del_store(user, user_to_del, folder_type=None):
if folder_type:
allowed_folder_types = ["all", "inbox", "calendar", "contact", "note", "task"]
if folder_type not in allowed_folder_types:
print("Unknown folder type allowed: {}".format(','.join(allowed_folder_types)))
sys.exit(1)
settings = read_settings(user)
if not settings['settings']['zarafa']['v1']['contexts'].get('hierarchy') or not settings['settings']['zarafa']['v1']['contexts']['hierarchy'].get('shared_stores'):
print("No additional stores found")
return
shared_store = settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores'].get(user_to_del)
if not shared_store:
print("No additional stores found")
return
try:
if not folder_type:
settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores'].pop(user_to_del)
else:
settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores'][user_to_del].pop(folder_type)
except KeyError:
pass
print("Saving settings")
write_settings(user, json.dumps(settings))
"""
List all added stores
"""
def list_stores(user):
settings = read_settings(user)
try:
stores = settings['settings']['zarafa']['v1']['contexts']['hierarchy']['shared_stores']
except KeyError:
print("No additional stores found")
return
table_header = ["User", 'Folder type', 'Show subfolders']
table_data =[]
for user in stores:
for folder in stores[user]:
table_data.append([user, folder, stores[user][folder]['show_subfolders']])
print(tabulate(table_data, headers=table_header,tablefmt="grid"))
"""
Backup signature from the users store
@ -292,8 +216,6 @@ def backup_signature(user, location=None):
except Exception as e:
print('Could not load WebApp settings for user {} (Error: {})'.format(user.name, repr(e)))
sys.exit(1)
if not settings['settings']['zarafa']['v1']['contexts'].get("mail"):
settings['settings']['zarafa']['v1']['contexts']['mail'] = {}
if settings['settings']['zarafa']['v1']['contexts']['mail'].get('signatures'):
for item in settings['settings']['zarafa']['v1']['contexts']['mail']['signatures']['all']:
name = settings['settings']['zarafa']['v1']['contexts']['mail']['signatures']['all'][item]['name']
@ -310,38 +232,23 @@ def backup_signature(user, location=None):
Restore signature into the users store
:param user: The user
:param filename: The filename of the signature
:param filename: The filename of the signature
:param replace: Remove all existing signatures for the restore signature
:param default: Set the signature as default for new mail and replies
"""
def restore_signature(user, filename, replace=None, default=None):
restorefile = filename
filename_split = filename.split('_')
if len(filename_split) == 2:
signaturename = filename_split[0].replace('-',' ')
if replace:
signatureid = filename_split[1].split('.')[0]
elif len(filename_split) == 3:
signaturename = filename_split[1].replace('-',' ')
if replace:
signatureid = filename_split[2].split('.')[0]
else:
if replace:
print('File format is not supported')
sys.exit(1)
with open(restorefile, 'r') as sigfile:
signaturehtml = sigfile.read()
if replace:
signatureid = filename.split('_')[2].split('.')[0]
action = 'Replacing'
else:
signaturename = filename.split('.')[0]
signatureid = int(time.time())
action = 'Adding'
signaturename = filename.split('_')[1].replace('-',' ')
signaturecontent = dict(
{u'name': signaturename, u'content': signaturehtml, u'isHTML': True})
settings = read_settings(user)
@ -368,7 +275,6 @@ def restore_signature(user, filename, replace=None, default=None):
write_settings(user, json.dumps(settings))
"""
Export categories from users store
@ -402,7 +308,7 @@ def export_categories(user, location=None):
Import categories from users store
:param user: The user
:param filename: The filename of the signature
:param filename: The filename of the signature
"""
def import_categories(user, filename=None):
if filename:
@ -443,7 +349,7 @@ def export_smime(user, location=None, public=None):
return
for cert in certificates:
if public and cert.prop(PR_MESSAGE_CLASS_W).value == 'WebApp.Security.Public':
if public and cert.prop(PR_MESSAGE_CLASS_w).value == 'WebApp.Security.Public':
extension = 'pub'
body = cert.text
else:
@ -486,7 +392,7 @@ def import_smime(user, cert_file, passwd, ask_password=None, public=None):
except Exception as e:
print(e)
sys.exit(1)
certificate = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, p12.get_certificate())
cert_data = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, certificate)
date_before = MAPI.Time.unixtime(mktime(datetime.strptime(cert_data.get_notBefore().decode('utf-8'), "%Y%m%d%H%M%SZ" ).timetuple()))
@ -504,7 +410,7 @@ def import_smime(user, cert_file, passwd, ask_password=None, public=None):
email = dict_issued_to[key].decode('utf-8')
else:
issued_to += "%s=%s\n" % (key, dict_issued_to[key])
if user.email == email:
item = assoc.mapiobj.CreateMessage(None, MAPI_ASSOCIATED)
@ -523,27 +429,6 @@ def import_smime(user, cert_file, passwd, ask_password=None, public=None):
else:
print('Email address doesn\'t match')
"""
Remove expired S/MIME Public certificates
:param user: The user
"""
def remove_expired_smime(user):
# unable to loop over the associated items so getting the items in a list instead
certificates =list(user.store.root.associated.items())
if len(certificates) == 0:
print('No certificates found')
return
now = datetime.now()
for cert in certificates:
# We only want to remove the public certificate
if cert.prop(PR_MESSAGE_CLASS_W).value == 'WebApp.Security.Public':
if cert.prop(PR_MESSAGE_DELIVERY_TIME).value < now:
print('deleting public certificate {} ({})'.format(cert.subject, cert.prop(PR_MESSAGE_DELIVERY_TIME).value))
user.store.root.associated.delete(cert)
"""
Custom function to merge two dictionaries.
@ -553,7 +438,6 @@ but this function caused undesired behavior
:param dict1: The first dictionary
:param dict2: The second dictionary
"""
def mergedicts(dict1, dict2):
for k in set(dict1.keys()).union(dict2.keys()):
if k in dict1 and k in dict2:
@ -581,7 +465,7 @@ def advanced_inject(user, data, value_type='string'):
split_data = data.split('=')
value = split_data[1].lstrip().rstrip()
if value.lower() == 'true':
if value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
@ -596,7 +480,7 @@ def advanced_inject(user, data, value_type='string'):
write_settings(user, json.dumps(new_settings))
"""
Main function run with arguments
"""
@ -624,36 +508,22 @@ def main():
if options.language:
language(user, options.language)
if options.add_store:
add_store(user, options.add_store, options.folder_type, options.sub_folder)
if options.del_store:
del_store(user, options.del_store, options.folder_type)
if options.list_stores:
list_stores(user)
#Categories
if options.export_categories:
export_categories(user, options.file)
if options.import_categories:
import_categories(user, options.file)
# S/MIME import/export
if options.export_smime:
export_smime(user, options.location, options.public_smime)
if options.import_smime:
import_smime(user, options.import_smime, options.password, options.ask_password, options.public_smime)
if options.remove_expired:
remove_expired_smime(user)
# Signature
if options.backup_signature:
backup_signature(user, options.location)
if options.restore_signature:
restore_signature(user, options.restore_signature, False, options.default_signature)
if options.replace_signature:
restore_signature(user, options.replace_signature, True, options.default_signature)
restore_signature(user, options.restore_signature, options.replace, options.default_signature)
# Advanced injection option
if options.add_option:
@ -699,44 +569,29 @@ def main():
# State settings
if options.remove_state:
settings = read_settings(user)
settings['settings']['zarafa']['v1']['state'] = {}
write_settings(user, json.dumps(settings))
print('Removed state settings for {}'.format(user.name))
settings = read_settings(user)
settings['settings']['zarafa']['v1']['state'] = {}
write_settings(user, json.dumps(settings))
print('Removed state settings for {}'.format(user.name))
# Add sender to safe sender list
if options.add_sender:
settings = read_settings(user)
setting = 'settings.zarafa.v1.contexts.mail.safe_senders_list = {}'.format(options.add_sender)
advanced_inject(user, setting, 'list')
print('{}'.format(options.add_sender), 'Added to safe sender list for {}'.format(user.name))
# Add sender to safe sender list
if options.addsender:
settings = read_settings(user)
setting = 'settings.zarafa.v1.contexts.mail.safe_senders_list = {}'.format(options.addsender)
advanced_inject(user, setting, 'list')
print('{}'.format(options.addsender), 'Added to safe sender list')
# Polling interval
if options.polling_interval:
if options.pollinginterval:
try:
value = int(options.polling_interval)
value = int(options.pollinginterval)
except ValueError:
print('Invalid number used. Please specify the value in seconds')
sys.exit(1)
settings = read_settings(user)
setting = 'settings.zarafa.v1.main.reminder.polling_interval = {}'.format(options.polling_interval)
setting = 'settings.zarafa.v1.main.reminder.polling_interval = {}'.format(options.pollinginterval)
advanced_inject(user, setting)
print('Polling interval changed to', '{}'.format(options.polling_interval), 'for {}'.format(user.name))
# Calendar resolution (zoom level)
if options.calendar_resolution:
try:
value = int(options.calendar_resolution)
except ValueError:
print('Invalid number used. Please specify the value in minutes')
sys.exit(1)
if value < 5 or value > 60:
print('Unsupported value used. Use a number between 5 and 60')
sys.exit(1)
settings = read_settings(user)
setting = 'settings.zarafa.v1.contexts.calendar.default_zoom_level = {}'.format(options.calendar_resolution)
advanced_inject(user, setting)
print('Calendar resolution changed to', '{}'.format(options.calendar_resolution), 'for {}'.format(user.name))
print('Polling interval changed to', '{}'.format(options.pollinginterval))
# Always at last!!!
if options.reset: