Compare commits

..

25 Commits

Author SHA1 Message Date
Martijn Alberts
4d37f44e32 Pull request #28: make username optional when replacing the signature
Merge in KSC/webapp-tools from fix-signature to master

* commit '001f3cd1456c16a82890bff48db3f48fd4867dcc':
  make username optional when replacing the signature
2021-01-28 14:33:23 +01:00
Robin van Genderen
829da666d1 Pull request #29: README.md move apostrophe to correct place
Merge in KSC/webapp-tools from malberts/READMEmd-1611392752255 to master

* commit '67cf6fe3587ec9dfdfdd6f34688108662c500e2e':
  README.md move apostrophe to correct place
2021-01-25 09:08:29 +01:00
Martijn Alberts
67cf6fe358 README.md move apostrophe to correct place 2021-01-23 10:06:32 +01:00
Robin van Genderen
8b1f21da6b Pull request #27: Add signature injection examples
Merge in KSC/webapp-tools from ~MALBERTS/webapp-tools-martijn:docu to master

* commit 'e588ef3992cbece93a47ac121c29334f23580390':
  Add signature injection examples
2021-01-22 15:38:35 +01:00
Martijn Alberts
e588ef3992 Add signature injection examples 2021-01-22 15:37:44 +01:00
rvangenderen
001f3cd145 make username optional when replacing the signature 2021-01-22 15:26:10 +01:00
Martijn Alberts
90471e39e0 Pull request #26: restore-signature not needed when replace-signature is used
Merge in KSC/webapp-tools from why-martijn-why to master

* commit '2a3a4157296115a1d0a98b22599f3d55db4ff48a':
  restore-signature not needed when replace-signature is used
2021-01-21 15:10:53 +01:00
rvangenderen
2a3a415729 restore-signature not needed when replace-signature is used 2021-01-21 15:08:12 +01:00
Martijn Alberts
54cf3a0678 Pull request #25: replace to replace-signature
Merge in KSC/webapp-tools from replace-signature to master

* commit 'af885f2c32437a491a6800b99d500f0079355b66':
  replace  to replace-signature
2021-01-21 14:47:11 +01:00
rvangenderen
af885f2c32 replace to replace-signature 2021-01-21 14:41:35 +01:00
Robin van Genderen
be1d9e2308 Pull request #24: add support for modifying shared stores
Merge in KSC/webapp-tools from open-shared-store to master

* commit 'c485022ffc4eae672e015f09bd4452a727c1b331':
  add support for modifying shared stores
2020-12-24 11:57:15 +01:00
rvangenderen
c485022ffc add support for modifying shared stores 2020-12-24 11:56:57 +01:00
rvangenderen
70bfdf098a add option to remove expired public S/MIME Certificates 2020-12-14 10:23:53 +01:00
Martijn Alberts
38fc9c6b64 Merge pull request #23 in KSC/webapp-tools from fix-mail-keyerror to master
* commit '713cfe225580746f4699a6e5130ad1392648c19b':
  create mail key if not exist
2020-07-09 10:21:17 +02:00
rvangenderen
713cfe2255 create mail key if not exist 2020-07-09 10:10:18 +02:00
Robin van Genderen
384cb70e4b Merge pull request #22 in KSC/webapp-tools from malberts/files_adminpy-1575907755669 to master
* commit '0bb89137943a7406064185c8f6299334cfa39a0a':
  Add check for settings key
2019-12-09 17:15:51 +01:00
Martijn Alberts
0bb8913794 Add check for settings key 2019-12-09 17:10:38 +01:00
Robin van Genderen
d515051244 Merge pull request #21 in KSC/webapp-tools from ~MALBERTS/webapp-tools-martijn:add_calendar_resolution to master
* commit 'a708768c7a1c248bbfd5fb8c7ee793ad113257f2':
  Add calendar resolution setting
2019-09-12 12:39:39 +02:00
Martyn Alberts
a708768c7a Add calendar resolution setting 2019-09-12 12:00:34 +02:00
Robin van Genderen
5048d33cf1 add hint that python3-pkg-resources is required for debian 10 2019-08-13 16:38:14 +02:00
Robin van Genderen
87a5ab0b07 create empty prop if not exist 2019-07-22 16:00:07 +02:00
Martijn Alberts
7a75569e5d Merge pull request #20 in KSC/webapp-tools from ~MALBERTS/webapp-tools-martijn:norecipient to master
* commit '7477f89a54af5b5fb10d1c46a9f81e88eb4d77df':
  Manage recipient: Display error when PR_EC_RECIPIENT_HISTORY_JSON_W does not exist
2019-07-18 13:56:21 +02:00
Martyn Alberts
7477f89a54 Manage recipient: Display error when PR_EC_RECIPIENT_HISTORY_JSON_W does not exist 2019-07-18 13:08:13 +02:00
Robin van Genderen
7b4845a877 Merge pull request #19 in KSC/webapp-tools from ~MALBERTS/webapp-tools-martijn:python3 to master
* commit '35b4bbeec1bde647cbf0da316ef0984f5a23bf00':
  Add brackets around prints
2019-07-18 12:40:29 +02:00
Martyn Alberts
35b4bbeec1 Add brackets around prints 2019-07-18 12:36:41 +02:00
4 changed files with 238 additions and 41 deletions

View File

@ -54,6 +54,14 @@ 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,8 +2,10 @@
#encoding: utf-8
import kopano
from kopano.errors import NotFoundError
from MAPI.Util import *
import json
import sys
def opt_args():
@ -24,14 +26,22 @@ 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)
webapp = user.store.prop(0X6773001F).value
try:
webapp = user.store.prop(0X6773001F).value
except NotFoundError:
webapp = dict(recipients=[])
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=(',', ': ')))
@ -50,8 +60,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:
@ -59,7 +69,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,6 +6,9 @@ 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
@ -15,6 +18,35 @@ 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
@ -23,6 +55,8 @@ If you want to make a change for all users pass the --all-users parameter. Examp
- 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,18 +2,15 @@
# 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('python3-kopano should be installed on your system')
print('python-kopano should be installed on your system')
sys.exit(1)
try:
from MAPI.Util import *
except ImportError:
print('python3-mapi should be installed on your system')
print('python-mapi should be installed on your system')
sys.exit(1)
import json
import base64
@ -21,11 +18,12 @@ try:
import OpenSSL.crypto
except ImportError:
pass
from datetime import datetime
from datetime import datetime, timedelta
from time import mktime
import getpass
import time
from optparse import OptionGroup
from tabulate import tabulate
try:
from dotty_dict import dotty
except ImportError:
@ -53,11 +51,20 @@ 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", dest="replace", action="store_true", help="Replace existing signature, file layout must be: username_signature-name_signatureid.html")
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("--default-signature", dest="default_signature", action="store_true", help="Set signature as default one")
parser.add_option_group(group)
@ -65,12 +72,13 @@ 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)
# 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")
@ -84,8 +92,9 @@ 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="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)")
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)")
parser.add_option_group(group)
# Advanced option group
@ -192,7 +201,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
@ -200,6 +209,73 @@ 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
@ -216,6 +292,8 @@ 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']
@ -232,23 +310,38 @@ 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)
@ -275,6 +368,7 @@ def restore_signature(user, filename, replace=None, default=None):
write_settings(user, json.dumps(settings))
"""
Export categories from users store
@ -308,7 +402,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:
@ -349,7 +443,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:
@ -392,7 +486,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()))
@ -410,7 +504,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)
@ -429,6 +523,27 @@ 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.
@ -438,6 +553,7 @@ 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:
@ -465,7 +581,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
@ -480,7 +596,7 @@ def advanced_inject(user, data, value_type='string'):
write_settings(user, json.dumps(new_settings))
"""
Main function run with arguments
"""
@ -508,22 +624,36 @@ 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, options.replace, options.default_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)
# Advanced injection option
if options.add_option:
@ -569,29 +699,44 @@ 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.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')
# 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))
# Polling interval
if options.pollinginterval:
if options.polling_interval:
try:
value = int(options.pollinginterval)
value = int(options.polling_interval)
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.pollinginterval)
setting = 'settings.zarafa.v1.main.reminder.polling_interval = {}'.format(options.polling_interval)
advanced_inject(user, setting)
print('Polling interval changed to', '{}'.format(options.pollinginterval))
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))
# Always at last!!!
if options.reset: