|
|
|
@ -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:
|
|
|
|
|