Add files via upload

This commit is contained in:
Harold Finch
2023-04-10 07:22:09 +02:00
committed by GitHub
parent 12cd50b598
commit d1a4c3e77a
51 changed files with 13206 additions and 0 deletions

44
tools/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Cron Jobs
* dump_disclaimer.py
Dump per-domain disclaimer which stored in LDAP or SQL database.
It's safe to execute it manually.
* cleanup_amavisd_db.py
Cleanup old records from Amavisd database. It's safe to execute it manually.
* delete_mailboxes.py
Delete mailboxes which are scheduled to be removed. The schedule date
was set while you removed the mail account with iRedAdmin(-Pro).
# Utils
* upgrade_iredadmin.sh
Upgrade an old iRedAdmin-Pro or iRedAdmin open source edition to current
release.
* update_mailbox_quota.py
Update mailbox quota for one user (specified on command line) or bulk users
(read from a plain text file).
* notify_quarantined_recipients.py
Notify local recipients (via email) that they have emails quarantined on
server and not delivered to their mailbox.
* convert_ini_to_py.sh
Convert old iRedAdmin-Pro config file (.ini format) to the new one.
* migrate_cluebringer_wblist_to_amavisd.py
Migrate Cluebringer white/blacklists to Amavisd database, and, optionally,
delete them in Cluebringer database.
Note: Don't forget to enable iRedAPD plugin `amavisd_wblist` in
`/opt/iredapd/settings.py`.

0
tools/__init__.py Normal file
View File

264
tools/cleanup_amavisd_db.py Normal file
View File

@@ -0,0 +1,264 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Remove old records in Amavisd database.
# USAGE:
#
# 1: Make sure you have correct database settings in iRedAdmin config file
# 'settings.py' for Amavisd.
#
# 2: Make sure you have proper values for below two parameters:
#
# AMAVISD_REMOVE_MAILLOG_IN_DAYS = 7
# AMAVISD_REMOVE_QUARANTINED_IN_DAYS = 7
#
# Default values is defined in libs/default_settings.py, you can override
# them in settings.py. WARNING: DO NOT MODIFY libs/default_settings.py.
#
# 3: Test this script in command line directly, make sure no errors in output
# message.
#
# # python cleanup_amavisd_db.py
#
# 4: Setup a daily cron job to execute this script. For example: execute
# it daily at 1:30AM.
#
# 30 1 * * * python /path/to/cleanup_amavisd_db.py >/dev/null
#
# That's all.
import os
import sys
import time
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from libs import iredutils
from tools import ira_tool_lib
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
if not (settings.amavisd_enable_logging or settings.amavisd_enable_quarantine):
sys.exit("Amavisd is not enabled. SKIP.")
backend = settings.backend
logger.info('Backend: %s' % backend)
logger.info('SQL server: %s:%d' % (settings.amavisd_db_host, int(settings.amavisd_db_port)))
db_settings = iredutils.get_settings_from_db(params=['amavisd_remove_quarantined_in_days', 'amavisd_remove_maillog_in_days'])
keep_quar_days = db_settings['amavisd_remove_quarantined_in_days']
keep_inout_days = db_settings['amavisd_remove_maillog_in_days']
query_size_limit = settings.AMAVISD_CLEANUP_QUERY_SIZE_LIMIT
# SQL records in `quarantine` table reference to `msgs`.
if keep_quar_days > keep_inout_days:
keep_inout_days = keep_quar_days
conn_amavisd = ira_tool_lib.get_db_conn('amavisd')
if settings.backend in ['mysql', 'ldap']:
# Querying (SELECT) without locking. Require MySQL 5.0+ and InnoDB.
#
# Since we're dealing with sql records created days ago, no new records
# will be inserted with that date, it's safe to use dirty read.
logger.info('Enable dirty read for querying without locking SQL tables.')
try:
conn_amavisd.query('SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED')
except Exception as e:
logger.error('Cannot enable dirty read: %s' % repr(e))
# Removing records from single table.
def remove_from_one_table(sql_table, index_column, removed_values):
total = len(removed_values)
# Delete how many records each time
offset = query_size_limit
if total:
loop_times = total / offset
if total % offset:
loop_times += 1
for i in range(int(loop_times)):
removing_values = removed_values[offset * i: offset * (i + 1)]
logger.info(
'\t[-] Deleting records: %d - %d (%s)' % (i * offset, i * offset + len(removing_values), time.ctime()))
conn_amavisd.delete(sql_table,
vars={'ids': removing_values},
where='%s IN $ids' % index_column)
# Delete old quarantined mails from table 'msgs'. It will also
# delete records in table 'quarantine'.
logger.info('Delete quarantined mails which older than %d days' % keep_quar_days)
_now = int(time.time())
_expire_seconds = _now - (keep_quar_days * 86400)
sql_where = """time_num < %d AND quar_type='Q'""" % _expire_seconds
counter_msgs = 0
while True:
qr = conn_amavisd.select('msgs',
what='mail_id',
where=sql_where,
limit=query_size_limit)
if qr:
ids = [r.mail_id for r in qr]
_total = len(ids)
logger.info('\t[-] Deleting records: %d - %d (%s)' % (counter_msgs + 1, counter_msgs + _total, time.ctime()))
conn_amavisd.delete('msgs', vars={'ids': ids}, where='mail_id IN $ids')
conn_amavisd.delete('msgrcpt', vars={'ids': ids}, where='mail_id IN $ids')
counter_msgs += len(ids)
else:
break
logger.info('Delete incoming/outgoing emails which older than %d days' % keep_inout_days)
_now = int(time.time())
_expire_seconds = _now - (keep_inout_days * 86400)
sql_where = """time_num < %d AND (quar_type <> 'Q' OR quar_type IS NULL)""" % _expire_seconds
# We experienced an issue with PostgreSQL, it always return an non-existing
# SQL record, and it causes endless loop. As a hack, we store all removed
# `mail_id` and compare new `mail_id` with this list.
_removed_ids = set()
counter_msgrcpt = 0
while True:
qr = conn_amavisd.select('msgs',
what='mail_id',
where=sql_where,
limit=query_size_limit)
if qr:
ids = [iredutils.bytes2str(r.mail_id) for r in qr]
_total = len(ids)
_removing_ids = list(set(ids) - set(_removed_ids))
if not _removing_ids:
break
logger.info(
'\t[-] Deleting records: %d - %d (%s)' % (counter_msgrcpt + 1, counter_msgrcpt + _total, time.ctime()))
conn_amavisd.delete('msgs', vars={'ids': _removing_ids}, where='mail_id IN $ids')
conn_amavisd.delete('msgrcpt', vars={'ids': _removing_ids}, where='mail_id IN $ids')
counter_msgrcpt += _total
_removed_ids.update(ids)
else:
break
# delete unreferenced records from tables msgrcpt, quarantine and maddr
logger.info('Delete unreferenced records from table `msgrcpt`.')
conn_amavisd.query('''
DELETE FROM msgrcpt
WHERE NOT EXISTS (SELECT 1 FROM msgs WHERE mail_id=msgrcpt.mail_id)
''')
#
# Delete unreferenced records from table `quarantine`.
#
logger.info('Delete unreferenced records from table `quarantine`.')
msgs_mail_ids = set()
maddr_ids_in_use = set()
quar_mail_ids = set()
qr = conn_amavisd.select('msgs', what='mail_id, sid')
for i in qr:
msgs_mail_ids.add(i.mail_id)
maddr_ids_in_use.add(i.sid)
qr = conn_amavisd.select('quarantine', what='mail_id')
for i in qr:
quar_mail_ids.add(i.mail_id)
invalid_quar_mail_ids = [i for i in quar_mail_ids if i not in msgs_mail_ids]
remove_from_one_table(sql_table='quarantine',
index_column='mail_id',
removed_values=invalid_quar_mail_ids)
#
# Delete unreferenced records from table `maddr`.
#
logger.info('Delete unreferenced records from table `maddr`.')
# Get all maddr.id
maddr_ids = set()
qr = conn_amavisd.select('maddr', what='id')
for i in qr:
maddr_ids.add(i.id)
qr = conn_amavisd.select('msgrcpt', what='rid')
for i in qr:
maddr_ids_in_use.add(i.rid)
invalid_maddr_ids = [i for i in maddr_ids if i not in maddr_ids_in_use]
remove_from_one_table(sql_table='maddr',
index_column='id',
removed_values=invalid_maddr_ids)
#
# Delete unreferenced records from table `mailaddr`.
#
logger.info('Delete unreferenced records from table `mailaddr`.')
# Get all `mailaddr.id`
mailaddr_ids = set()
qr = conn_amavisd.select('mailaddr', what='id')
for i in qr:
mailaddr_ids.add(i.id)
# Get all `wblist.sid` and `outbound_wblist.rid` (both refer to `mailaddr.id`)
wblist_ids = set()
qr = conn_amavisd.select('wblist', what='sid')
for i in qr:
wblist_ids.add(i.sid)
try:
qr = conn_amavisd.select('outbound_wblist', what='rid')
for i in qr:
wblist_ids.add(i.rid)
except:
# No outbound_wblist table
pass
invalid_mailaddr_ids = [i for i in mailaddr_ids if i not in wblist_ids]
remove_from_one_table(sql_table='mailaddr',
index_column='id',
removed_values=invalid_mailaddr_ids)
logger.info('')
logger.info('Remained records:')
logger.info('')
logger.info(' `msgs`: %-7.d' % len(msgs_mail_ids))
logger.info('`quarantine`: %-7.d' % (len(quar_mail_ids) - len(invalid_quar_mail_ids)))
logger.info(' `maddr`: %-7.d' % (len(maddr_ids) - len(invalid_maddr_ids)))
logger.info(' `mailaddr`: %-7.d' % (len(mailaddr_ids) - len(invalid_mailaddr_ids)))
if counter_msgs \
or counter_msgrcpt \
or invalid_quar_mail_ids \
or invalid_maddr_ids \
or invalid_mailaddr_ids:
msg = 'Removed records: '
msg += '%d in msgs, ' % counter_msgs
msg += '%d in msgrcpt, ' % counter_msgrcpt
msg += '%d in quarantine, ' % len(invalid_quar_mail_ids)
msg += '%d in maddr, ' % len(invalid_maddr_ids)
msg += '%d in mailaddr.' % len(invalid_mailaddr_ids)
ira_tool_lib.log_to_iredadmin(msg, admin='cleanup_amavisd_db', event='cleanup_db')
logger.info('Log cleanup status.')

91
tools/cleanup_db.py Normal file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Remove old records in iRedAdmin SQL database.
# USAGE:
#
# 1: Make sure you have proper values for below two parameters:
#
# IREDADMIN_LOG_KEPT_DAYS = 30
#
# Default values is defined in libs/default_settings.py, you can override
# them in settings.py. WARNING: DO NOT MODIFY libs/default_settings.py.
#
# 2: Test this script in command line directly, make sure no errors in output
# message.
#
# # python cleanup_db.py
#
# 3: Setup a daily cron job to execute this script. For example: execute
# it daily at 1:30AM.
#
# 30 1 * * * python /path/to/cleanup_db.py >/dev/null
#
# That's all.
import os
import sys
import time
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools.ira_tool_lib import debug, logger, sql_dbn, get_db_conn, sql_count_id
web.config.debug = debug
backend = settings.backend
logger.info('Backend: %s' % backend)
logger.info('SQL server: %s:%d' % (settings.iredadmin_db_host, int(settings.iredadmin_db_port)))
query_size_limit = 100
conn_iredadmin = get_db_conn('iredadmin')
#
# iredadmin.log
#
_days = settings.IREDADMIN_LOG_KEPT_DAYS
logger.info('Delete old admin activity log (> %d days)' % _days)
if sql_dbn == 'mysql':
sql_where = """timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)""" % _days
elif sql_dbn == 'postgres':
sql_where = """timestamp < CURRENT_TIMESTAMP - INTERVAL '%d DAYS'""" % _days
else:
logger.error('Invalid SQL backend: %s' % sql_dbn)
sys.exit()
total_before = sql_count_id(conn_iredadmin, 'log')
conn_iredadmin.delete('log', where=sql_where)
total_after = sql_count_id(conn_iredadmin, 'log')
logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after))
#
# iredadmin.domain_ownership
#
_days = settings.DOMAIN_OWNERSHIP_EXPIRE_DAYS
logger.info('Delete old domain ownership verification records (> %d days)' % _days)
total_before = sql_count_id(conn_iredadmin, 'domain_ownership')
conn_iredadmin.delete('domain_ownership', where="expire > %d" % (_days * 24 * 60 * 60))
total_after = sql_count_id(conn_iredadmin, 'domain_ownership')
logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after))
#
# iredadmin.newsletter_subunsub_confirms
#
now = int(time.time())
_hours = settings.NEWSLETTER_SUBSCRIPTION_REQUEST_KEEP_HOURS
logger.info('Delete expired newsletter subscription confirm tokens (> %d hours)' % _hours)
total_before = sql_count_id(conn_iredadmin, 'newsletter_subunsub_confirms')
_expired = now - (_hours * 60 * 60)
conn_iredadmin.delete('newsletter_subunsub_confirms', where="expired <= %d" % _expired)
total_after = sql_count_id(conn_iredadmin, 'newsletter_subunsub_confirms')
logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after))

229
tools/delete_mailboxes.py Normal file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Delete mailboxes which are scheduled to be removed.
#
# Notes: iRedAdmin will store maildir path of removed mail users in SQL table
# `iredadmin.deleted_mailboxes` (LDAP backends) or
# `vmail.deleted_mailboxes` (SQL backends).
#
# Usage: Either run this script manually, or run it with a daily cron job.
#
# # python3 delete_mailboxes.py
#
# Available arguments:
#
# * --delete-without-timestamp:
#
# [RISKY] If no timestamp string in maildir path, continue to delete it.
#
# With default iRedMail settings, maildir path will contain a timestamp
# like this: <domain.com>/u/s/e/username-2016.08.17.09.53.03/
# (2016.08.17.09.53.03 is the timestamp), this way all created maildir
# paths are unique, even if you removed the user and recreate it with
# same mail address.
#
# Without timestamp in maildir path (e.g. <domain.com>/u/s/e/username/),
# if you removed a user and recreate it someday, this user will see old
# emails in old mailbox (because maildir path is same as old user's). So
# it becomes RISKY to remove the mailbox if no timestamp in maildir path.
#
# * --delete-null-date:
#
# Delete mailbox if SQL column `deleted_mailboxes.delete_date` is null.
#
# * --debug: print additional log
import os
import sys
import time
import logging
import shutil
import pwd
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
from libs import iredutils
from tools import ira_tool_lib
import settings
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
if '--debug' in sys.argv:
logger.setLevel(logging.DEBUG)
# Delete if `deleted_mailboxes.delete_date` is null.
delete_null_date = False
if '--delete-null-date' in sys.argv:
delete_null_date = True
# Make sure there's a timestamp (yyyy.mm.dd.hh.mm.ss) in maildir path,
# otherwise it's too risky to remove this mailbox -- because the maildir
# could be reused by another user after old account was removed.
#
# - Safe to remove: <domain.com>/u/s/e/username-<timestamp>/
# - Dangerous to remove: <domain.com>/u/s/e/username/
delete_without_timestamp = False
if '--delete-without-timestamp' in sys.argv:
delete_without_timestamp = True
def delete_record(conn_deleted_mailboxes, rid):
try:
conn_deleted_mailboxes.delete('deleted_mailboxes',
vars={'id': rid},
where='id=$id')
return True,
except Exception as e:
return False, repr(e)
def delete_mailbox(conn_deleted_mailboxes,
record,
all_maildirs=None):
rid = record.id
username = str(record.username).lower()
timestamp = str(record.timestamp)
delete_date = record.delete_date
maildir = record.maildir
maildir = maildir.replace('//', '/') # Remove duplicate '/'
if delete_without_timestamp:
# Make sure no other mailbox is stored under the maildir.
if all_maildirs:
if not maildir.endswith('/'):
maildir += '/'
for mdir in all_maildirs:
if mdir.startswith(maildir) or (mdir == maildir):
logger.error("<<< ABORT, CRITICAL >>> Trying to remove mailbox ({}) owned by user ({}), but there is another mailbox ({}) stored under this directory. Aborted.".format(maildir, username, mdir))
return False
else:
_dir = maildir.rstrip('/')
if len(_dir) <= 21:
# Why 21 chars:
# - 20 chars: "-<timestamp>". e.g. "-2014.03.26.15.07.25"
# - username contains at least 1 char
logger.error("<<< SKIP >>> Seems no timestamp in maildir path (%s), too risky to remove this mailbox." % maildir)
return False
try:
# Extract timestamp string, make sure it's a valid time format.
ts = _dir[-19:]
time.strptime(ts, '%Y.%m.%d.%H.%M.%S')
except Exception as e:
logger.debug("<<< WARNING >>> Invalid or missing timestamp in maildir path (%s), skip." % maildir)
logger.debug("<<< WARNING >>> Error message: %s." % repr(e))
return False
# check maildir path
if os.path.isdir(maildir):
# Make sure directory is owned by vmail:vmail
_dir_stat = os.stat(maildir)
_dir_uid = _dir_stat.st_uid
# Get uid/gid of vmail user
owner = pwd.getpwuid(_dir_uid).pw_name
if owner != 'vmail':
logger.error('<<< ERROR >> Directory is not owned by `vmail` user: uid -> {}, user -> {}.'.format(_dir_uid, owner))
return False
try:
msg = '[{}] {}.'.format(username, maildir)
msg += ' Account was deleted at {}.'.format(timestamp)
if delete_date:
msg += ' Mailbox was scheduled to be removed on {}.'.format(delete_date)
else:
msg += ' Mailbox was scheduled to be removed as soon as possible.'
logger.info(msg)
logger.info("Removing mailbox: {}".format(maildir))
# Delete mailbox
shutil.rmtree(maildir)
# Log this deletion.
ira_tool_lib.log_to_iredadmin(msg,
admin='cron_delete_mailboxes',
username=username,
event='delete_mailboxes')
except Exception as e:
logger.error('<<< ERROR >> while deleting mailbox ({} -> {}): {}'.format(username, maildir, repr(e)))
# Delete record.
delete_record(conn_deleted_mailboxes=conn_deleted_mailboxes, rid=rid)
# Establish SQL connection.
try:
if settings.backend == 'ldap':
conn_deleted_mailboxes = ira_tool_lib.get_db_conn('iredadmin')
from libs.ldaplib.core import LDAPWrap
_wrap = LDAPWrap()
conn_vmail = _wrap.conn
else:
conn_deleted_mailboxes = ira_tool_lib.get_db_conn('vmail')
conn_vmail = conn_deleted_mailboxes
except Exception as e:
sys.exit('<<< ERROR >>> Cannot connect to SQL database, aborted. Error: %s' % repr(e))
# Get paths of all maildirs.
sql_where = 'delete_date <= %s' % web.sqlquote(web.sqlliteral('NOW()'))
if delete_null_date:
sql_where = '(delete_date <= %s) OR (delete_date IS NULL)' % web.sqlquote(web.sqlliteral('NOW()'))
qr_mailboxes = conn_deleted_mailboxes.select('deleted_mailboxes', where=sql_where)
if not qr_mailboxes:
logger.debug('No mailbox is scheduled to be removed.')
if not delete_null_date:
logger.debug("To remove mailboxes without schedule date, please run this script with argument '--delete-null-date'.")
if not delete_without_timestamp:
logger.debug("To remove mailboxes without timesamp in maildir path, please run this script with argument '--delete-without-timestamp'. [WARNING] It's RISKY.")
sys.exit()
# Get all maildir paths used by active mail users.
#
# To delete mailbox without timestamp in maildir path, we must make sure:
# - maildir is not used by some active user
# - no other mailbox is stored under this maildir path
#
# Q: Why query all maildir paths instead of querying SQL/LDAP directly?
# A:
# 1. LDAP attribute `homeDirectory` doesn't support `sub` (substring) index.
# 2. if maildir path contains duplicate '/', the validation will fail (not
# equal).
all_maildirs = []
if delete_without_timestamp:
if settings.backend == 'ldap':
_qr = conn_vmail.search_s(settings.ldap_basedn,
2, # ldap.SCOPE_SUBTREE
"(objectClass=mailUser)",
['homeDirectory'])
for (_dn, _ldif) in _qr:
_ldif = iredutils.bytes2str(_ldif)
if 'homeDirectory' in _ldif:
_dir = _ldif['homeDirectory'][0].lower().replace('//', '/')
all_maildirs.append(_dir)
elif settings.backend in ['mysql', 'pgsql']:
# WARNING: always append '/' in returned maildir path.
_qr = conn_vmail.select('mailbox',
what="LOWER(CONCAT(storagebasedirectory, '/', storagenode, '/', maildir, '/')) AS maildir")
all_maildirs = [str(i.maildir).replace('//', '/') for i in _qr]
for r in list(qr_mailboxes):
delete_mailbox(conn_deleted_mailboxes=conn_deleted_mailboxes,
record=r,
all_maildirs=all_maildirs)

24
tools/delete_sessions.py Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Delete all records in SQL table "iredadmin.sessions" to force
# all admins to re-login.
import os
import sys
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
from tools import ira_tool_lib
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
conn = ira_tool_lib.get_db_conn('iredadmin')
logger.info('Delete all existing sessions to force all admins to re-login.')
conn.query('DELETE FROM sessions')

190
tools/dump_disclaimer.py Normal file
View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Updated: 2012.07.01
# Purpose: Dump disclaimer text from OpenLDAP directory server or SQL servers.
# Requirements: iRedMail-0.5.0 or later releases
#
# Shipped within iRedAdmin-Pro: http://www.iredmail.org/admin_panel.html
# USAGE:
#
# - Make sure you have correct backend related settings in iRedAdmin config
# file, settings.ini.
#
# - Test this script in command line directly, make sure no errors in output
# message.
#
# # python /path/to/dump_disclaimer.py /etc/postfix/disclaimer/
#
# - Setup a cron job to execute this script daily. For example: execute
# this script at 2:01AM every day.
#
# 1 2 * * * python /path/to/dump_disclaimer.py /etc/postfix/disclaimer/
#
# That's all.
import os
import sys
import web
web.config.debug = False
# Directory used to store disclaimer files.
# Default directory is /etc/postfix/disclaimer/.
# Default disclaimer file name is [domain_name].txt
if len(sys.argv) != 2:
sys.exit('Error: Please specify a directory used to store disclaimer, default is /etc/postfix/disclaimer/')
else:
DISCLAIMER_DIR = sys.argv[1]
DISCLAIMER_FILE_EXT = '.txt'
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from libs import iredutils
from tools import ira_tool_lib
logger = ira_tool_lib.logger
if settings.backend == 'ldap':
import ldap
elif settings.backend == 'mysql':
sql_dbn = 'mysql'
elif settings.backend == 'pgsql':
sql_dbn = 'postgres'
def write_disclaimer(text, filename, file_type='txt'):
# Write plain text
try:
f = open(filename, 'w')
if file_type == 'html':
html = """<div id="disclaimer_separator"><p>----------</p><br /></div>"""
html += """<div id="disclaimer_text"><p>""" + text + """</p></div>"""
f.write('\n' + html + '\n')
else:
f.write('\n---------\n' + text + '\n')
logger.info(" + %s" % filename)
f.close()
except Exception as e:
logger.info('<<< ERROR >>> %s' % str(e))
def handle_disclaimer(domain, disclaimer_text):
"""Dump or remove disclaimer text."""
txt = os.path.join(DISCLAIMER_DIR, domain + '.txt')
html = os.path.join(DISCLAIMER_DIR, domain + '.html')
if disclaimer_text:
write_disclaimer(text=disclaimer_text,
filename=txt,
file_type='txt')
write_disclaimer(text=disclaimer_text,
filename=html,
file_type='html')
else:
# Remove old disclaimer file if no disclaimer setting
try:
for f in [txt, html]:
if os.path.isfile(f):
os.remove(f)
logger.info(" - Remove %s." % f)
except OSError:
pass
except Exception as e:
# Other errors.
logger.info("<<< ERROR >>> {}: {}.".format(domain, str(e)))
def dump_from_ldap():
"""Dump disclaimer text from LDAP server."""
logger.info('Connecting to LDAP server')
conn = ldap.initialize(uri=settings.ldap_uri,
trace_level=0,
bytes_strictness='silent')
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
logger.info('Binding with dn: %s' % settings.ldap_basedn)
conn.bind_s(settings.ldap_bind_dn, settings.ldap_bind_password)
# Search and get disclaimer.
logger.info('Searching all domains')
qr = conn.search_s(
settings.ldap_basedn,
ldap.SCOPE_ONELEVEL,
'(objectClass=mailDomain)',
['domainName', 'domainAliasName', 'disclaimer'],
)
logger.info('Dumping ...')
for (_dn, _ldif) in qr:
_ldif = iredutils.bytes2str(_ldif)
# Get domain names.
_domains = _ldif['domainName']
_alias_domains = _ldif.get('domainAliasName', [])
disclaimer_text = _ldif.get('disclaimer', [''])[0]
domains = _domains + _alias_domains
for domain in domains:
handle_disclaimer(domain, disclaimer_text)
conn.unbind()
logger.info('Connection closed.')
def dump_from_sql():
"""Dump disclaimer text from MySQL or PostgreSQL server."""
logger.info("Connecting to SQL server '%s:%d' as user '%s' ..." % (settings.vmail_db_host,
int(settings.vmail_db_port),
settings.vmail_db_user))
conn = web.database(dbn=sql_dbn,
host=settings.vmail_db_host,
port=int(settings.vmail_db_port),
db=settings.vmail_db_name,
user=settings.vmail_db_user,
pw=settings.vmail_db_password)
logger.info('Get all alias domains')
qr = conn.select('alias_domain', what='alias_domain, target_domain')
alias_domains = {}
for i in qr:
_alias_domain = str(i.alias_domain).lower()
_target_domain = str(i.target_domain).lower()
if _target_domain in alias_domains:
alias_domains[_target_domain].append(_alias_domain)
else:
alias_domains[_target_domain] = [_alias_domain]
# Search and get disclaimer.
logger.info('Get all primary domains')
qr = conn.select('domain', what='domain, disclaimer')
# Dump disclaimer for every domain.
logger.info('Dumping...')
for r in qr:
domain = str(r.domain).lower()
disclaimer_text = r.disclaimer
domains = [domain] + alias_domains.get(domain, [])
logger.info(domain)
for domain in domains:
handle_disclaimer(domain, disclaimer_text)
logger.info('Completed.')
if settings.backend == 'ldap':
dump_from_ldap()
elif settings.backend in ['mysql', 'pgsql']:
dump_from_sql()

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Dump quarantined emails to given directory (specified on command line).
#
# Usage:
#
# python dump_quarantined_mail.py /path/to/dir
import os
import sys
import time
import web
output_dir = sys.argv[1]
if not os.path.isdir(output_dir):
sys.exit("Output directory doesn't exist: %s" % output_dir)
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
from tools.ira_tool_lib import debug, get_db_conn
web.config.debug = debug
now = int(time.time())
conn_amavisd = get_db_conn('amavisd')
conn_iredadmin = get_db_conn('iredadmin')
# Get last time
last_time = 0
try:
qr = conn_iredadmin.select('tracking', what='v', where="k='dump_quarantined_mail'", limit=1)
if qr:
last_time = int(qr[0].v)
except:
pass
# Get value of all `quarantine.mail_id`.
try:
qr = conn_amavisd.select(['msgs', 'quarantine'],
what='msgs.mail_id AS mail_id',
where='msgs.mail_id=quarantine.mail_id AND msgs.time_num >= %d' % last_time,
group='msgs.mail_id')
except Exception as e:
print('<<< ERROR >>> {}'.format(repr(e)))
sys.exit()
total = len(qr)
print("* Found {} quarantined emails in SQL db.".format(total))
counter = 1
for r in qr:
mail_id = str(r.mail_id)
try:
records = conn_amavisd.select('quarantine',
what='mail_text',
where='mail_id = %s' % web.sqlquote(mail_id),
order='chunk_ind ASC')
if not records:
continue
# Combine mail_text as RAW mail message.
message = ''
for i in list(records):
for j in i.mail_text:
message += j
# Write message to file
try:
eml_path = os.path.join(output_dir, 'spam-' + mail_id)
print("[{}/{}] Dumping email to file: {}".format(counter, total, eml_path))
f = open(eml_path, 'w')
f.write(message)
f.close()
except Exception as e:
print('<<< ERROR >>> cannot write file {}'.format(repr(e)))
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))
counter += 1
# Log last time.
conn_iredadmin.delete('tracking', where="k='dump_quarantined_mail'")
conn_iredadmin.insert('tracking', k='dump_quarantined_mail', v=now)

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Query user last login info from (My)SQL database and display it in a more
readable format (plain text or html).
Note: You need to follow this tutorial to enable last_login plugin in Dovecot:
https://docs.iredmail.org/track.user.last.login.html
Usage:
python3 export_last_login.py # in plain text format
python3 export_last_login.py html > export.html # in html format
"""
import os
import sys
import time
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools import ira_tool_lib
from libs.iredutils import epoch_seconds_to_gmt
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
if settings.backend == 'ldap':
conn = ira_tool_lib.get_db_conn('iredadmin')
else:
conn = ira_tool_lib.get_db_conn('vmail')
# Get output format
try:
export_format = sys.argv[1]
except:
export_format = 'text'
try:
qr = conn.select('last_login',
order='last_login DESC')
except Exception as e:
sys.exit("Query failed: {}".format(e))
if export_format == 'html':
_now = time.strftime('%Y-%d-%m %H:%M:%S')
html = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<style type="text/css">
.th_size, .th_date, .td_size, .td_date {{ white-space: nowrap; }}
.tr_date {{ background-color: #DDDDDD; }}
.text_align_left {{ text-align: left; }}
</style>
</head>
<body>
<h3>User Last Login Time ({0})</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Email</th>
<th>Time (GMT)</th>
</tr>
</thead>
<tbody>
""".format(_now)
counter = 1
for row in qr:
username = row.username
seconds = row.last_login
last_login = epoch_seconds_to_gmt(seconds)
if export_format == 'html':
html += """
<tr>
<td>{}</td>
<td>{}</td>
<td>{}</td>
</tr>
""".format(counter, username, last_login)
else:
print("{:6} | {:30} | {}".format(counter, username, last_login))
counter += 1
if export_format == 'html':
html += """</tbody></table></body></html>"""
print(html)

216
tools/import_users.py Normal file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# Purpose: Read mail accounts from given plain text file (in specified format),
# then create them with iRedAdmin-Pro RESTful API interface.
#
# Usage:
#
# - Make sure your iRedAdmin-Pro has RESTful API interface enabled by
# following our tutorial:
# https://docs.iredmail.org/iredadmin-pro.restful.api.html#enable-restful-api
#
# - Generate file /opt/users.list which contains the mail accounts you want
# to import, one account per line, with account info stored in few fields:
#
# 1: [REQUIRED] user's full email address.
# 2: [REQUIRED] plain text or password hash which starts with the password
# scheme name. For example, "{SSHA}xxx", "{SSHA512}xxx".
# 3: [optional] mailbox quota in MB. Must be an integer number.
# 4: [optional] full display name.
# 5: [optional] list of mailing list addresses. If not empty, user will be
# assigned to given mailing lists as a member.
#
# Notes:
#
# - Multiple addresses must be separated by ":".
# - If mailing list doesn't exist, it will not be created automatically.
# 6: [optional] employeeid: employee id.
#
# NOTE: the separator "," for ending EMPTY optional fields is not required.
#
# Samples:
#
# user@domain.com, plain_password
# user@domain.com, plain_password, 1024, Zhang Huangbin, list1@domain.com:list2@domain.com
# user@domain.com, plain_password, , , list1@domain.com:list2@domain.com
# user@domain.com, plain_password, 1024, Zhang Huangbin
#
# - Update 3 parameters in this file:
#
# api_endpoint = ''
# verify_cert = True
# admin = 'postmaster@a.io'
# pw = 'password'
#
# - "api_endpoint" is the endpoint of iRedAdmin-Pro RESTful API.
# - With "verify_cert = True", a valid ssl cert is required on API
# server (https://). If you don't have a valid ssl cert yet, please set
# it to False.
# - "admin" is the email address of domain admin which has privilege to
# manage the email domain which you're going to import users to.
# - "pw" is plain password of domain admin.
#
# - Run commands below to create users listed in the "/opt/users.list" file:
#
# python import_users.py /opt/users.list
import os
import sys
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# Endpoint of iRedAdmin-Pro RESTful API
api_endpoint = 'http://127.0.0.1:8080/api'
# Verify SSL cert of API server.
# If you don't have a valid SSL cert yet, please set it to False.
verify_cert = True
# Domain admin email address and password
admin = 'postmaster@a.io'
pw = 'www'
# Define the order of fields in each line. Fields must be separated by comma.
#
#
# WARNING: For empty optional fields, a comma is still required as placeholder.
#
# Samples:
#
# user@domain.com, plain_password, , ,
# user@domain.com, plain_password, 1024, Zhang Huangbin, list1@domain.com:list2@domain.com,
# user@domain.com, plain_password, , , list1@domain.com:list2@domain.com,
# user@domain.com, plain_password, 1024, Zhang Huangbin,,
#
field_map = ['mail', 'password', 'quota', 'name', 'groups', 'employeeid']
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
from libs import iredutils
def __get(url, data=None):
_url = api_endpoint + url
r = requests.get(_url, data=data, cookies=cookies, verify=verify_cert)
return r.json()
def __post(url, data=None):
_url = api_endpoint + url
r = requests.post(_url, data=data, cookies=cookies, verify=verify_cert)
return r.json()
def __put(url, data=None):
_url = api_endpoint + url
r = requests.put(_url, data=data, cookies=cookies, verify=verify_cert)
return r.json()
def __delete(url, data=None):
_url = api_endpoint + url
r = requests.delete(_url, data=data, cookies=cookies, verify=verify_cert)
return r.json()
def usage():
pass
if len(sys.argv) != 2 or len(sys.argv) > 2:
print("Usage: $ python bulk_import.py /path/to/file")
usage()
sys.exit()
else:
file = sys.argv[1]
if not os.path.exists(file):
print("<<< ERROR >>> file does not exist: {}".format(file))
sys.exit()
#
# Login
#
r = requests.post(api_endpoint + '/login',
data={'username': admin, 'password': pw},
verify=verify_cert)
# Get returned JSON data
res = r.json()
if not res['_success']:
sys.exit('Login failed')
cookies = r.cookies
# Read user list.
f = open(file, 'rb')
for line in f.readlines():
line = iredutils.bytes2str(line.strip())
fields = line.split(',')
try:
d = {}
for (k, v) in zip(field_map, fields):
d[k] = v
except:
sys.exit("<<< ERROR >>> line has invalid format:\n{}".format(line))
# Get user mail address
mail = d.pop('mail')
mail.lower()
if not iredutils.is_email(mail):
sys.exit("<<< ERROR >>> line has invalid user email address: {}\nLine: {}".format(mail, line))
password = d.pop('password')
name = d.pop("name", mail.split("@", 1)[0])
quota = d.pop("quota", "0")
# Get mail address(es) of assigned mailing list(s)
groups = d.pop('groups', "")
groups.lower()
groups = [addr.lower().strip() for addr in groups.split(':') if iredutils.is_email(addr)]
# Create user
res = __post('/user/' + mail,
data={'name': name,
'password': password.strip(),
'quota': quota})
if res['_success']:
print("[OK] Created user: {}".format(mail))
else:
if res['_msg'] == 'ALREADY_EXISTS':
print("[SKIP] Account already exists: {}.".format(mail))
continue
else:
sys.exit('<<< ERROR >>> failed to create user: {}'.format(res))
if password.startswith('{'):
res = __put('/user/' + mail,
data={'password_hash': password})
if res['_success']:
print(" |- [OK] Updated user password (hash): {}".format(mail))
else:
sys.exit('<<< ERROR >>> failed to updated user password (hash): {}, error: {}'.format(mail, res))
if groups:
for group in groups:
res = __put('/ml/' + group,
data={'add_subscribers': mail,
'require_confirm': 'no'})
if res['_success']:
print(" |- [OK] Subscribed user to mailing list: {} -> {}".format(mail, group))
else:
print('<<< WARNING >>> failed to subscribe user to mailing list: {} -> {}, error: {}'.format(mail, group, res))
employeeid = d.pop("employeeid", "")
if employeeid:
res = __put('/user/' + mail,
data={'employeeid': employeeid})
if res['_success']:
print(" |- [OK] Updated employeeid: {}".format(mail))
else:
sys.exit('<<< ERROR >>> failed to updated employeeid: {}, error: {}'.format(mail, res))

99
tools/ira_tool_lib.py Normal file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Library used by other scripts under tools/ directory.
import os
import sys
import logging
import web
debug = False
# Set True to print SQL queries.
web.config.debug = debug
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from libs import iredutils
backend = settings.backend
if backend in ['ldap', 'mysql']:
sql_dbn = 'mysql'
elif backend in ['pgsql']:
sql_dbn = 'postgres'
else:
sys.exit('Error: Unsupported backend (%s).' % backend)
# logging
logger = logging.getLogger('iredadmin')
_ch = logging.StreamHandler(sys.stdout)
_formatter = logging.Formatter('* %(message)s')
_ch.setFormatter(_formatter)
logger.addHandler(_ch)
logger.setLevel(logging.INFO)
def get_db_conn(db_name):
if backend == 'ldap' and db_name in ['ldap', 'vmail']:
logger.error("""Please use code below to get LDAP connection cursor:\n
from libs.ldaplib.core import LDAPWrap\n
_wrap = LDAPWrap()\n
conn = _wrap.conn\n""")
return None
try:
conn = web.database(
dbn=sql_dbn,
host=settings.__dict__[db_name + '_db_host'],
port=int(settings.__dict__[db_name + '_db_port']),
db=settings.__dict__[db_name + '_db_name'],
user=settings.__dict__[db_name + '_db_user'],
pw=settings.__dict__[db_name + '_db_password'],
)
conn.supports_multiple_insert = True
return conn
except Exception as e:
logger.error(e)
return None
# Log in `iredadmin.log`
def log_to_iredadmin(msg, event, admin='', username='', loglevel='info'):
conn = get_db_conn('iredadmin')
try:
conn.insert('log',
admin=admin,
username=username,
event=event,
loglevel=loglevel,
msg=str(msg),
ip='127.0.0.1',
timestamp=iredutils.get_gmttime())
except:
pass
return None
def sql_count_id(conn, table, column='id', where=None):
if where:
qr = conn.select(table,
what='count(%s) as total' % column,
where=where)
else:
qr = conn.select(table,
what='count(%s) as total' % column)
if qr:
total = qr[0].total
else:
total = 0
return total

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Migrate Cluebringer white/blacklist to Amavisd database.
#
# Note: it's safe to execute this script as many times as you want, it won't
# generate duplicate records.
import os
import sys
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from libs.iredutils import is_valid_amavisd_address
from libs.amavisd import wblist
from tools import ira_tool_lib
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
# Check database name to make sure it's Cluebringer
if settings.policyd_db_name != 'cluebringer':
sys.exit('Error: not a Cluebringer database.')
logger.info('Establish SQL connection.')
conn = ira_tool_lib.get_db_conn('policyd')
logger.info('Query white/blacklist info.')
# Converted wblist
wl = []
bl = []
# value of sql column: policy_groups.id
wl_id = None
bl_id = None
wb_ids = []
# query whitelist and/or blacklist. possible values: 'wl', 'bl'.
query_lists = []
# get policy_groups.id
qr = conn.select('policy_groups', what='id,name', where="name IN ('whitelists', 'blacklists')")
if qr:
for r in qr:
if r.name == 'whitelists':
wl_id = r.id
elif r.name == 'blacklists':
bl_id = r.id
if wl_id:
logger.info('policy_groups.id: %d -> whitelists' % wl_id)
query_lists.append('wl')
wb_ids.append(wl_id)
if bl_id:
logger.info('policy_groups.id: %d -> blacklists' % bl_id)
query_lists.append('bl')
wb_ids.append(bl_id)
else:
logger.info('No whitelist/blacklist found. Exit.')
sys.exit()
logger.info('Query all whitelists and blacklists.')
qr = conn.select('policy_group_members',
vars={'wb_ids': wb_ids},
what='policygroupid, member',
where='policygroupid IN $wb_ids AND disabled=0')
if qr:
logger.info('Convert Cluebringer white/blacklists to Amavisd syntax format.')
for r in qr:
# Single IP Address: 192.168.2.10
# CIDR formatted range of IP addresses: 192.168.2.10/31
# Single user: user@example.com
# Entire domain: @example.com
# All sub-domains: .example.com
value = None
if is_valid_amavisd_address(r.member):
value = r.member
else:
# Convert from different syntax format
if r.member.startswith('.'):
tmp = '@' + r.member
if is_valid_amavisd_address(tmp):
value = tmp
else:
logger.info('[?] Discard record in improper format: %s, cannot convert.' % r.member)
elif '/' in r.member:
logger.info('[?] Discard record in improper format: %s. CIDR IP range is not supported.' % r.member)
if value:
if r.policygroupid == wl_id:
wl.append(value)
else:
bl.append(value)
if wl:
logger.info('Converted whitelisted: %d total' % len(wl))
else:
logger.info('No whitelists found.')
if bl:
logger.info('Converted blacklisted: %d total' % len(bl))
else:
logger.info('No blacklists found.')
confirm = input('Migrate converted white/blacklists to Amavisd database right now? [y|N]')
if confirm not in ['y', 'Y', 'yes', 'YES']:
logger.info('Exit without migrating to Amavisd database.')
sys.exit()
# Import to Amavisd database.
try:
logger.info('Migrating, please wait ...')
wblist.add_wblist(account='@.',
wl_senders=wl,
bl_senders=bl,
flush_before_import=False)
logger.info("Don't forget to enable iRedAPD plugin 'amavisd_wblist' in /opt/iredapd/settings.py.")
except Exception as e:
logger.info(str(e))
# Ask to delete wblist in cluebringer
confirm = input('Delete all white/blacklists stored in Cluebringer database? [y|N]')
if confirm not in ['y', 'Y', 'yes', 'YES']:
logger.info('Exit without deleting Cluebringer white/blacklists.')
sys.exit()
conn.delete('policy_group_members', vars={'wb_ids': wb_ids}, where='policygroupid IN $wb_ids')
conn.delete('policy_groups', vars={'wb_ids': wb_ids}, where='id IN $wb_ids')
conn.delete('policy_members', where="destination='%%internal_domains' AND source IN ('%%whitelists', '%%blacklists')")
# Get policies.id
qr = conn.select('policies', what='id', where="name IN ('whitelists', 'blacklists')")
if qr:
pids = [r.id for r in qr]
conn.delete('access_control', vars={'pids': pids}, where='policyid IN $pids')
conn.delete('policies', vars={'pids': pids}, where='id IN $pids')
logger.info('DONE')

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<style type="text/css">
.th_size, .th_date, .td_size, .td_date { white-space: nowrap; }
.tr_date { background-color: #DDDDDD; }
.text_align_left { text-align: left; }
</style>
</head>
<body>
<p>Quarantined mails will be kept for %(quar_keep_days)d days, please login to self-service site to manage them: <a href="%(iredadmin_url)s" target="_blank" rel='noopener'>%(iredadmin_url)s</a>.</p>
<p>Date and time are in time zone: %(timezone)s.</p>
<table cellpadding="2px">
<thead>
<th class="th_subject">Subject</th>
<th class="th_sender">Sender</th>
<th class="th_spam_level">Spam Level</th>
<th class="th_date">Time</th>
</thead>
<tbody>
<!-- Info of quarantined mails -->
%(quar_mail_info)s
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,369 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Notify local recipients (via email) that they have emails
# quarantined on server and not delivered to their mailbox.
# Usage:
#
# - Set a correct URL in iRedAdmin-Pro config file `settings.py`, so that
# users can manage quarantined email within received notification email:
#
# # URL of your iRedAdmin-Pro login page which will be shown in notification
# # email, so that user can login to manage quarantined emails.
# # Sample: 'https://your_server.com/iredadmin/'
# #
# # Note: mail domain must have self-service enabled, otherwise normal
# # mail user cannot login to iRedAdmin-Pro for self-service.
# NOTIFICATION_URL_SELF_SERVICE = 'https://[your_server]/iredadmin/'
#
# - Setup a cron job to run this script every 6 or 12, 24 hours, it's up to
# you. Sample cron job (every 12 hours):
#
# 1 */12 * * * python /path/to/notify_quarantined_recipients.py >/dev/null
#
# Available arguments:
#
# --force-all:
# Send notification to all users (who have email quarantined).
#
# --force-all-time:
# Notify users for their all quarantined emails instead of just new
# ones since last notification.
#
# --notify-backupmx
# Send notification to all recipients under backup mx domain
#
# - Also, it's ok to run this script manually:
#
# # python notify_quarantined_recipients.py [arg1 arg2 arg3 ...]
# Customization
#
# - This script sends email via /usr/sbin/sendmail command by default, it
# should work quite well and has better performance. if you still prefer
# to send notification email via smtp, please set proper smtp server and
# account info in iRedAdmin-Pro config file `settings.py`:
#
# NOTIFICATION_SMTP_SERVER = 'localhost'
# NOTIFICATION_SMTP_PORT = 587
# NOTIFICATION_SMTP_STARTTLS = True
# NOTIFICATION_SMTP_USER = ''
# NOTIFICATION_SMTP_PASSWORD = ''
#
# - To custom mail subject of notification email, please define below
# variable in iRedAdmin-Pro config file `settings.py`:
#
# # Subject of notification email.
# NOTIFICATION_QUARANTINE_MAIL_SUBJECT = '[Attention] You have emails quarantined and not delivered to mailbox'
#
# - To custom HTML template file, please create your own template file with
# correct name in either place:
#
# - `/opt/iredmail/custom/iredadmin/notify_quarantined_recipients.html`
#
# This file is used if your iRedMail server was deployed with the
# iRedMail Easy platform (https://www.iredmail.org/easy.html), easy
# for iRedAdmin-Pro upgrade.
#
# - `tools/notify_quarantined_recipients.html.custom` under iRedAdmin-Pro
# directory.
#
# General use. Note: there's a `.custom` suffix in file name.
#
# If no custom file, `tools/notify_quarantined_recipients.html` will be used.
#
# How it works:
#
# - Mail user login to iRedAdmin-Pro (self-service) and choose to receive
# notification email when there's email quarantined.
#
# - OpenLDAP: user will be assigned `enabledService=quar_notify`.
# - SQL backends: column `mailbox.settings` contains `quar_notify:yes`.
#
# - This script queries SQL/LDAP database to see who are willing to receive
# a notification email.
#
# - This script checks Amavisd database to get info of quarantined mails
# for these users.
import os
import sys
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
import web
os.environ['LC_ALL'] = 'C'
script_dir = os.path.abspath(os.path.dirname(__file__))
rootdir = script_dir + '/../'
sys.path.insert(0, rootdir)
now = int(time.time())
import settings
from libs import iredutils
from libs.ireddate import utc_to_timezone
from tools import ira_tool_lib
web.config.debug = ira_tool_lib.debug
logger = ira_tool_lib.logger
backend = settings.backend
# Read template HTML file.
custom_easy_tmpl = "/opt/iredmail/custom/iredadmin/notify_quarantined_recipients.html"
custom_tmpl = os.path.join(rootdir, 'tools', 'notify_quarantined_recipients.html.custom')
default_tmpl = os.path.join(rootdir, 'tools', 'notify_quarantined_recipients.html')
if os.path.isfile(custom_easy_tmpl):
html_tmpl = custom_easy_tmpl
elif os.path.isfile(custom_tmpl):
html_tmpl = custom_tmpl
else:
html_tmpl = default_tmpl
# Info used in notification email.
mail_subject = settings.NOTIFICATION_QUARANTINE_MAIL_SUBJECT
smtp_user = settings.NOTIFICATION_SMTP_USER
iredadmin_url = settings.NOTIFICATION_URL_SELF_SERVICE
# Use '--force-all' option to notify all mail users.
force_all_users = '--force-all' in sys.argv or False
force_all_time = '--force-all-time' in sys.argv or False
notify_backupmx = '--notify-backupmx' in sys.argv or False
# Backup MX domains.
# We may not have any accounts under backup mx domain, so if sys admin chooses
# to notify recipients in backup mx domain, we send the notification also.
backupmx_domains = []
# List of target users' email address.
target_users = []
# Get list of users (email) who asked to receive notification email.
if settings.backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
_wrap = LDAPWrap()
conn_ldap = _wrap.conn
# Get users who ask to get a notification email under each domain.
if force_all_users:
q_filter = '(&(objectClass=mailUser)(accountStatus=active))'
else:
q_filter = '(&(objectClass=mailUser)(accountStatus=active)(enabledService=quar_notify))'
try:
qr = conn_ldap.search_s(settings.ldap_basedn,
2, # ldap.SCOPE_SUBTREE,
q_filter,
['mail'])
for (_dn, _ldif) in qr:
_ldif = iredutils.bytes2str(_ldif)
target_users += _ldif.get('mail', [])
except Exception as e:
logger.info('<< ERROR >> Error while querying mail users: %s' % repr(e))
if notify_backupmx:
# Query all backup mx domains
q_filter = '(&(objectClass=mailDomain)(accountStatus=active)(domainBackupMX=yes)(mtaTransport=relay:*))'
try:
qr = conn_ldap.search_s(settings.ldap_basedn,
1, # ldap.SCOPE_ONELEVEL,
q_filter,
['domainName', 'domainAliasName'])
for (_dn, _ldif) in qr:
_ldif = iredutils.bytes2str(_ldif)
backupmx_domains += _ldif.get('domainName', [])
backupmx_domains += _ldif.get('domainAliasName', [])
except Exception as e:
logger.info('<< ERROR >> Error while querying backup MX domains: %s' % repr(e))
elif settings.backend in ['mysql', 'pgsql']:
conn_vmaildb = ira_tool_lib.get_db_conn('vmail')
# Get all users who asked to receive notification email.
if force_all_users:
sql_where = 'active=1'
else:
sql_where = 'settings LIKE %s AND active=1' % web.sqlquote('%' + 'quar_notify:' + '%')
qr = conn_vmaildb.select('mailbox',
what='username',
where=sql_where)
for r in qr:
target_users.append(r.username)
if notify_backupmx:
# Get all backup mx domains
qr = conn_vmaildb.select('domain',
what='domain',
where='backupmx=1 AND active=1')
for i in qr:
backupmx_domains += [str(i.domain).lower()]
if backupmx_domains:
# Get all alias domains
qr = conn_vmaildb.select('alias_domain',
vars={'domains': backupmx_domains},
what='alias_domain',
where='target_domain IN $domains')
for i in qr:
backupmx_domains += [str(i.alias_domain).lower()]
if not (target_users or backupmx_domains):
logger.debug('No user asks to receive notification email. Exit.')
sys.exit()
mail_body_template = open(html_tmpl).read()
conn_amavisd = ira_tool_lib.get_db_conn('amavisd')
conn_iredadmin = ira_tool_lib.get_db_conn('iredadmin')
reversed_backupmx_domains = []
target_backupmx_users = []
if backupmx_domains:
for d in backupmx_domains:
rd = d.split('.')
rd.reverse()
rd = '.'.join(rd)
reversed_backupmx_domains.append(rd)
qr = conn_amavisd.select('maddr',
vars={'rcpt': reversed_backupmx_domains},
what='email',
where='domain IN $rcpt')
for i in qr:
_email = iredutils.bytes2str(i.email)
target_backupmx_users.append(_email)
logger.info('%d backup MX domains (%d users) will receive notification email.' % (len(backupmx_domains), len(target_backupmx_users)))
logger.debug('%d users are willing to receive notification email.' % len(target_users))
target_users += target_backupmx_users
# Notify users.
for user in target_users:
# Get maddr.id of recipient
qr = conn_amavisd.select('maddr',
vars={'rcpt': user},
what='id',
where='email=$rcpt',
limit=1)
if qr:
rid = qr[0].id
else:
logger.debug('[SKIP] No log of user: ' + user)
continue
# Get info of quarantined mails
sql_what = 'msgrcpt.rid AS rid,' \
+ 'msgs.mail_id AS mail_id,' \
+ 'msgs.subject AS subject,' \
+ 'msgs.from_addr AS from_addr,' \
+ 'msgs.spam_level AS spam_level,' \
+ 'msgs.time_num'
sql_where = """msgrcpt.rid=$rid AND msgs.mail_id=msgrcpt.mail_id AND msgs.quar_type='Q'"""
last_notify_time = 0
if not force_all_time:
# Get last time
try:
qr = conn_iredadmin.select('tracking', what='v', where="k='quarantine_notify_time'", limit=1)
if qr:
last_notify_time = int(qr[0].v) or 0
except:
pass
if last_notify_time:
sql_where += """ AND msgs.time_num >= %s""" % last_notify_time
qr = conn_amavisd.select(['msgs', 'msgrcpt'],
vars={'rid': rid},
what=sql_what,
where=sql_where,
order='msgs.time_num DESC')
if not qr:
logger.debug('[SKIP] No quarantined emails for %s.' % user)
continue
total = len(qr)
# Group messages by date.
info_by_date = {}
quar_mail_info = '\n'
# Create a HTML table to present quarantined emails.
for rcd in qr:
# time format: Apr 4, 2015
dt = iredutils.epoch_seconds_to_gmt(iredutils.bytes2str(rcd.time_num))
time_with_tz = utc_to_timezone(dt=dt, timezone=settings.LOCAL_TIMEZONE)
try:
time_tuple = time_with_tz.timetuple()
except:
time_tuple = time.strptime(time_with_tz, '%Y-%m-%d %H:%M:%S')
mail_date = time.strftime('%b %d, %Y', time_tuple)
mail_time = time.strftime('%H:%M:%S', time_tuple)
info = '<tr>' + '\n'
info += '<td class="td_subject">' + iredutils.bytes2str(rcd.subject) + '</td>' + '\n'
info += '<td class="td_sender">' + iredutils.bytes2str(rcd.from_addr) + '</td>' + '\n'
info += '<td class="td_spam_level">' + iredutils.bytes2str(rcd.spam_level) + '</td>' + '\n'
info += '<td class="td_date">' + mail_time + '</td>' + '\n'
info += '</tr>' + '\n\n'
if mail_date not in info_by_date:
info_by_date[mail_date] = []
info_by_date[mail_date].append(info)
for _date in sorted(list(info_by_date.keys()), reverse=True):
quar_mail_info += '<tr class="tr_date"><td colspan="4">' + _date + '</td></tr>' + '\n'
for r in info_by_date[_date]:
quar_mail_info += r
msg = MIMEMultipart('alternative')
msg['Subject'] = Header(mail_subject % {'total': total}, 'utf-8')
msg['To'] = user
if settings.NOTIFICATION_SENDER_NAME:
msg['From'] = '{} <{}>'.format(Header(settings.NOTIFICATION_SENDER_NAME, 'utf-8'), smtp_user)
else:
msg['From'] = Header(smtp_user, 'utf-8')
mail_body = mail_body_template % {'quar_mail_info': quar_mail_info,
'quar_keep_days': settings.AMAVISD_REMOVE_QUARANTINED_IN_DAYS,
'iredadmin_url': iredadmin_url,
'timezone': settings.LOCAL_TIMEZONE}
# HTML email must contain text and html part with same content, otherwise
# it will be considered as not well-formated email.
body_part_plain = MIMEText(mail_body, 'plain', 'utf-8')
msg.attach(body_part_plain)
body_part_html = MIMEText(mail_body, 'html', 'utf-8')
msg.attach(body_part_html)
msg_string = msg.as_string()
ret = iredutils.sendmail(recipients=user, message_text=msg_string)
if ret[0]:
logger.info('+ %s: %d mails.' % (user, total))
else:
logger.info('+ << ERROR >> Error while sending notification email to {}: {}'.format(user, ret[1]))
# Log last notify time.
conn_iredadmin.delete('tracking', where="k='quarantine_notify_time'")
conn_iredadmin.insert('tracking', k='quarantine_notify_time', v=now)

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Promote given user to be a global admin.
# FYI https://docs.iredmail.org/promote.user.to.be.global.admin.html
# Usage:
# python3 promote_to_global_admin.py <user-email>
def usage():
print("""Usage: Run this script with user email address:
# python3 promote_to_global_admin.py user@domain.com
""")
import os
import sys
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools.ira_tool_lib import debug, get_db_conn
from libs.iredutils import is_email
backend = settings.backend
web.config.debug = debug
# Check arguments
if len(sys.argv) == 2:
email = sys.argv[1]
if not is_email(email):
usage()
sys.exit()
else:
usage()
sys.exit()
if backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
from libs.ldaplib import ldaputils
_wrap = LDAPWrap()
conn = _wrap.conn
dn = ldaputils.rdn_value_to_user_dn(email)
mod_attrs = ldaputils.attr_ldif(attr="enabledService", value="domainadmin", mode="add")
mod_attrs += ldaputils.attr_ldif(attr="domainGlobalAdmin", value="yes", mode="add")
try:
conn.modify_s(dn, mod_attrs)
print("User {} is now a global admin.".format(email))
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))
elif backend in ['mysql', 'pgsql']:
conn = get_db_conn('vmail')
try:
conn.update("mailbox",
isadmin=1,
isglobaladmin=1,
where="username='{}'".format(email))
conn.insert("domain_admins",
username=email,
domain="ALL")
print("User {} is now a global admin.".format(email))
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Update user password.
# Usage:
# python reset_user_password.py <email> <new_password>
def usage():
print("""Usage: Run this script with user email address and new plain password:
# python3 reset_user_password.py user@domain.com 123456
""")
import os
import sys
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools.ira_tool_lib import debug, get_db_conn
from libs.iredutils import is_email
from libs.iredpwd import generate_password_hash
backend = settings.backend
web.config.debug = debug
# Check arguments
if len(sys.argv) == 3:
email = sys.argv[1]
pw = sys.argv[2]
if not is_email(email):
usage()
sys.exit()
else:
usage()
sys.exit()
pw_hash = generate_password_hash(pw)
if backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
from libs.ldaplib import ldaputils
_wrap = LDAPWrap()
conn = _wrap.conn
dn = ldaputils.rdn_value_to_user_dn(email)
mod_attrs = ldaputils.mod_replace('userPassword', pw_hash)
mod_attrs += ldaputils.mod_replace('shadowLastChange', ldaputils.get_days_of_shadow_last_change())
try:
conn.modify_s(dn, mod_attrs)
print("[{}] Password has been reset.".format(email))
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))
elif backend in ['mysql', 'pgsql']:
conn = get_db_conn('vmail')
try:
conn.update('mailbox',
password=pw_hash,
passwordlastchange=web.sqlliteral('NOW()'),
where="username='{}'".format(email))
print("[{}] Password has been reset.".format(email))
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Update mailbox quota for one user or multiple users.
# Note: Mailbox quota size unit is bytes. for example, size `104857600` is 100 MB.
def usage():
print("""Usage:
1) Update mailbox quota for one user.
To simply update one user's quota, run this script with user's email
address and new quota size (in bytes). For example:
# python3 update_mailbox_quota.py user@domain.com 2048576000
2) Update mailbox quota for multiple users.
- Create text file "new_quota.txt", each line contains one email address
and the new quota size (in bytes).
user1@domain.com 20480000
user2@domain.com 102400000
user3@domain.com 409600000
- Run this script with this file:
# python3 update_mailbox_quota.py new_quota.txt
""")
import os
import sys
import web
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools.ira_tool_lib import debug, logger, get_db_conn
from libs.iredutils import is_email
backend = settings.backend
logger.info('Backend: {}'.format(backend))
web.config.debug = debug
# List of (email, quota) tuples.
users = []
# Check arguments
if len(sys.argv) == 2:
# bulk update
text_file = sys.argv[1]
if not os.path.isfile(text_file):
sys.exit('<<< ERROR>>> Not a regular file: %s' % text_file)
# Get all (email, quota) tuples.
f = open(text_file)
for _line in f.readlines():
(_email, _quota) = _line.strip().split(' ', 1)
if is_email(_email) and _quota.isdigit():
users += [(_email, _quota)]
else:
print("[SKIP] no valid email address or quota: {}".format(_line))
elif len(sys.argv) == 3:
# update single user
_email = sys.argv[1]
_quota = sys.argv[2]
if is_email(_email):
users += [(_email, _quota)]
else:
sys.exit('<<< ERROR >>> Not an valid email address: %s' % _email)
else:
usage()
total = len(users)
logger.info('{} users in total.'.format(total))
count = 1
if backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
from libs.ldaplib.ldaputils import rdn_value_to_user_dn, mod_replace
_wrap = LDAPWrap()
conn = _wrap.conn
for (_email, _quota) in users:
logger.info('(%d/%d) Updating %s -> %s' % (count, total, _email, _quota))
dn = rdn_value_to_user_dn(_email)
mod_attrs = mod_replace('mailQuota', _quota)
try:
conn.modify_s(dn, mod_attrs)
except Exception as e:
print("<<< ERROR >>> {}".format(e))
elif backend in ['mysql', 'pgsql']:
conn = get_db_conn('vmail')
for (_email, _quota) in users:
logger.info('(%d/%d) Updating %s -> %s' % (count, total, _email, _quota))
conn.update('mailbox',
quota=int(_quota),
where="username='%s'" % _email)

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# Author: Zhang Huangbin <zhb@iredmail.org>
# Purpose: Update user passwords from records in a CSV file.
import os
import sys
import web
def usage():
print("""Usage:
- Store the email address and new password in a plain text file, e.g.
'passwords.csv'. format is:
<email> <new_password>
Samples:
user1@domain.com pF4mTq4jaRzDLlWl
user2@domain.com SPhkTUlZs1TBxvmJ
user3@domain.com 8deNR8IBLycRujDN
- Run this script with this file:
python3 update_password_in_csv.py passwords.csv
""")
os.environ['LC_ALL'] = 'C'
rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../'
sys.path.insert(0, rootdir)
import settings
from tools.ira_tool_lib import debug, logger, get_db_conn
from libs.iredutils import is_email
from libs.iredpwd import generate_password_hash
backend = settings.backend
logger.info('Backend: %s' % backend)
web.config.debug = debug
logger.info('Parsing command line arguments.')
# File which stores email and quota.
text_file = ''
# The separator
column_separator = ' '
# List of (email, quota) tuples.
users = []
# Check arguments
if len(sys.argv) == 2:
text_file = sys.argv[1]
if not os.path.isfile(text_file):
sys.exit('<<< ERROR>>> Not a regular file: %s' % text_file)
# Get all (email, password) tuples.
f = open(text_file)
line_num = 0
for _line in f.readlines():
line_num += 1
(_email, _pw) = _line.split(column_separator, 1)
if is_email(_email):
users += [(_email, _pw)]
else:
print("[SKIP] line {}: no valid email address: {}".format(line_num, _line))
f.close()
else:
usage()
total = len(users)
logger.info('%d users in total.' % total)
count = 1
if backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
from libs.ldaplib.ldaputils import rdn_value_to_user_dn, mod_replace
_wrap = LDAPWrap()
conn = _wrap.conn
for (_email, _pw) in users:
logger.info('(%d/%d) Updating %s' % (count, total, _email))
dn = rdn_value_to_user_dn(_email)
pw_hash = generate_password_hash(_pw)
mod_attrs = mod_replace('userPassword', pw_hash)
try:
conn.modify_s(dn, mod_attrs)
except Exception as e:
print("<<< ERROR >>> {}".format(repr(e)))
elif backend in ['mysql', 'pgsql']:
conn = get_db_conn('vmail')
for (_email, _pw) in users:
logger.info('(%d/%d) Updating %s' % (count, total, _email))
pw_hash = generate_password_hash(_pw)
conn.update('mailbox',
password=pw_hash,
where="username='%s'" % _email)

963
tools/upgrade_iredadmin.sh Normal file
View File

@@ -0,0 +1,963 @@
#!/usr/bin/env bash
# Purpose: Upgrade iRedAdmin from old release.
# Works with both iRedAdmin open source edition or iRedAdmin-Pro.
# USAGE:
#
# # cd /path/to/iRedAdmin-xxx/tools/
# # bash upgrade_iredadmin.sh
#
# Notes:
#
# * it uses sql username 'root' by default to connect to sql database. If you
# are using a remote SQL database which you don't have root privilege,
# please specify the sql username on command line with 'SQL_IREDADMIN_USER'
# parameter like this:
#
# SQL_IREDADMIN_USER='iredadmin' bash upgrade_iredadmin.sh
#
# * it reads sql password for given sql user from /root/.my.cnf by default.
# if you use a different file, please specify the file on command line with
# 'MY_CNF' parameter like this:
#
# MY_CNF='/root/.my.cnf-iredadmin' SQL_IREDADMIN_USER='iredadmin' bash upgrade_iredadmin.sh
export LC_ALL='C'
export IRA_HTTPD_USER='iredadmin'
export IRA_HTTPD_GROUP='iredadmin'
export SYS_ROOT_USER='root'
# If you don't have root privilege, use another sql user instead.
export SQL_IREDADMIN_USER="${SQL_IREDADMIN_USER:=root}"
export MY_CNF="${MY_CNF:=/root/.my.cnf}"
export CMD_MYSQL="mysql --defaults-file=${MY_CNF} -u ${SQL_IREDADMIN_USER}"
# Check OS to detect some necessary info.
export KERNEL_NAME="$(uname -s | tr '[a-z]' '[A-Z]')"
export NGINX_PID_FILE='/var/run/nginx.pid'
export NGINX_SNIPPET_CONF='/etc/nginx/templates/iredadmin.tmpl'
export NGINX_SNIPPET_CONF2='/etc/nginx/templates/iredadmin-subdomain.tmpl'
# iRedMail-0.9.7
export NGINX_SNIPPET_CONF3='/etc/nginx/conf.d/default.conf'
export USE_SYSTEMD='NO'
if which systemctl &>/dev/null; then
export USE_SYSTEMD='YES'
export SYSTEMD_SERVICE_DIR='/lib/systemd/system'
export SYSTEMD_SERVICE_DIR2='/etc/systemd/system'
export SYSTEMD_SERVICE_USER_DIR='/etc/systemd/system/multi-user.target.wants/'
fi
# Python.
export CMD_PYTHON3='/usr/bin/python3'
export CMD_PIP3='/usr/bin/pip3'
# uwsgi
export CMD_UWSGI='/usr/bin/uwsgi'
# If uwsgi is installed with pip, plugins are compiled into core binary
# directly, not plugins are installed separately.
# Mainly used on RHEL/CentOS/Rocky/Alma.
export UWSGI_HAS_PLUGINS="YES"
if [ X"${KERNEL_NAME}" == X"LINUX" ]; then
# Note: RHEL has minor version number in VERSION_ID.
export DISTRO_VERSION=$(awk -F'"' '/^VERSION_ID=/ {print $2}' /etc/os-release | awk -F'.' '{print $1}')
if [ -f /etc/redhat-release ]; then
# RHEL/CentOS
export DISTRO='RHEL'
# Installed with pip.
export CMD_UWSGI='/usr/sbin/uwsgi'
if [[ -x "/usr/local/bin/uwsgi" ]]; then
export CMD_UWSGI='/usr/local/bin/uwsgi'
export UWSGI_HAS_PLUGINS="NO"
fi
export HTTPD_RC_SCRIPT_NAME='httpd'
export CRON_SPOOL_DIR='/var/spool/cron'
if [[ -L /opt/www/iredadmin ]]; then
export HTTPD_SERVERROOT='/opt/www'
else
export HTTPD_SERVERROOT='/var/www'
fi
elif [ -f /etc/lsb-release ]; then
# Ubuntu
export DISTRO='UBUNTU'
export HTTPD_RC_SCRIPT_NAME='apache2'
export CRON_SPOOL_DIR='/var/spool/cron/crontabs'
if [ -L /opt/www/iredadmin ]; then
export HTTPD_SERVERROOT='/opt/www'
else
export HTTPD_SERVERROOT='/usr/share/apache2'
fi
elif [ -f /etc/debian_version ]; then
# Debian
export DISTRO='DEBIAN'
export HTTPD_RC_SCRIPT_NAME='apache2'
export CRON_SPOOL_DIR='/var/spool/cron/crontabs'
if [ -L /opt/www/iredadmin ]; then
export HTTPD_SERVERROOT='/opt/www'
else
export HTTPD_SERVERROOT='/usr/share/apache2'
fi
elif [ -f /etc/SuSE-release ]; then
# openSUSE
export DISTRO='SUSE'
export HTTPD_SERVERROOT='/srv/www'
export HTTPD_RC_SCRIPT_NAME='apache2'
export CRON_SPOOL_DIR='/var/spool/cron'
else
echo "<<< ERROR >>> Cannot detect Linux distribution name. Exit."
echo "Please contact support@iredmail.org to solve it."
exit 255
fi
elif [ X"${KERNEL_NAME}" == X'FREEBSD' ]; then
export DISTRO='FREEBSD'
export SYSRC='/usr/sbin/sysrc'
export CMD_PYTHON3='/usr/local/bin/python3'
export CMD_UWSGI='/usr/local/bin/uwsgi'
[ -x /usr/local/bin/pip-3.8 ] && export CMD_PIP3='/usr/local/bin/pip-3.8'
[ -x /usr/local/bin/pip3 ] && export CMD_PIP3='/usr/local/bin/pip3'
[ -x /usr/local/bin/pip ] && export CMD_PIP3='/usr/local/bin/pip'
export CRON_SPOOL_DIR='/var/cron/tabs'
export NGINX_SNIPPET_CONF='/usr/local/etc/nginx/templates/iredadmin.tmpl'
export NGINX_SNIPPET_CONF2='/usr/local/etc/nginx/templates/iredadmin-subdomain.tmpl'
export NGINX_SNIPPET_CONF3='/usr/local/etc/nginx/conf.d/default.conf'
if [ -L /opt/www/iredadmin ]; then
export HTTPD_SERVERROOT='/opt/www'
else
export HTTPD_SERVERROOT='/usr/local/www'
fi
if [ -f /usr/local/etc/rc.d/apache24 ]; then
export HTTPD_RC_SCRIPT_NAME='apache24'
else
export HTTPD_RC_SCRIPT_NAME='apache22'
fi
elif [ X"${KERNEL_NAME}" == X'OPENBSD' ]; then
export CMD_PYTHON3='/usr/local/bin/python3'
export CMD_PIP3='/usr/local/bin/pip3'
export CMD_UWSGI='/usr/local/bin/uwsgi'
export DISTRO='OPENBSD'
export CRON_SPOOL_DIR='/var/cron/tabs'
if [[ -h /opt/www/iredadmin ]]; then
export HTTPD_SERVERROOT='/opt/www'
else
export HTTPD_SERVERROOT='/var/www'
fi
else
echo "Cannot detect Linux/BSD distribution. Exit."
echo "Please contact author iRedMail team <support@iredmail.org> to solve it."
exit 255
fi
export CRON_FILE_ROOT="${CRON_SPOOL_DIR}/${SYS_ROOT_USER}"
# Optional argument to set the directory which stores iRedAdmin.
if [ $# -gt 0 ]; then
if [ -d ${1} ]; then
export HTTPD_SERVERROOT="${1}"
fi
if echo ${HTTPD_SERVERROOT} | grep '/iredadmin/*$' > /dev/null; then
export HTTPD_SERVERROOT="$(dirname ${HTTPD_SERVERROOT})"
fi
fi
# iRedAdmin directory and config file.
export IRA_ROOT_DIR="${HTTPD_SERVERROOT}/iredadmin"
export IRA_CONF_PY="${IRA_ROOT_DIR}/settings.py"
export IRA_CUSTOM_CONF_PY="${IRA_ROOT_DIR}/custom_settings.py"
enable_service() {
srv="$1"
echo "* Enable service: ${srv}"
if [ X"${DISTRO}" == X'RHEL' ]; then
if [ X"${USE_SYSTEMD}" == X'YES' ]; then
systemctl enable $srv
else
chkconfig --level 345 $srv on
fi
elif [ X"${DISTRO}" == X'DEBIAN' -o X"${DISTRO}" == X'UBUNTU' ]; then
if [ X"${USE_SYSTEMD}" == X'YES' ]; then
systemctl enable $srv
else
update-rc.d $srv defaults
fi
elif [ X"${DISTRO}" == X'FREEBSD' ]; then
${SYSRC} -f /etc/rc.conf.local ${srv}_enable=YES
elif [ X"${DISTRO}" == X'OPENBSD' ]; then
rcctl enable $srv
fi
}
restart_service() {
srv="$1"
if [ X"${KERNEL_NAME}" == X'LINUX' ]; then
if [ X"${USE_SYSTEMD}" == X'YES' ]; then
systemctl restart ${srv}
else
service ${srv} restart
fi
elif [ X"${KERNEL_NAME}" == X'FREEBSD' ]; then
service ${srv} restart
elif [ X"${KERNEL_NAME}" == X'OPENBSD' ]; then
rcctl restart ${srv}
fi
if [ X"$?" != X'0' ]; then
echo "Failed, please restart service manually and check its log file."
fi
}
restart_web_service()
{
export web_service="${HTTPD_RC_SCRIPT_NAME}"
if [ -f ${NGINX_PID_FILE} ]; then
if [ -n "$(cat ${NGINX_PID_FILE})" ]; then
export web_service="iredadmin"
fi
fi
echo "* Restarting ${web_service} service."
if [ X"${KERNEL_NAME}" == X'LINUX' ]; then
# The uwsgi script on CentOS 6 has problem with 'restart' action,
# 'stop' with few seconds sleep fixes it.
if [ X"${DISTRO}" == X'RHEL' -a X"${web_service}" == X'uwsgi' ]; then
service ${web_service} stop
sleep 5
service ${web_service} start
else
service ${web_service} restart
fi
elif [ X"${KERNEL_NAME}" == X'FREEBSD' ]; then
service ${web_service} restart
elif [ X"${KERNEL_NAME}" == X'OPENBSD' ]; then
rcctl restart ${web_service}
fi
if [ X"$?" != X'0' ]; then
echo "Failed, please restart Apache web server or 'iredadmin' (if you're running Nginx as web server) manually."
fi
}
check_mlmmjadmin_installation()
{
if [ ! -e /opt/mlmmjadmin ]; then
echo "<<< ERROR >>> No mlmmjadmin installation found (/opt/mlmmjadmin)."
echo "<<< ERROR >>> Please follow iRedMail upgrade tutorials to the latest"
echo "<<< ERROR >>> stable release first, then come back to upgrade iRedAdmin-Pro."
echo "<<< ERROR >>> mlmmj and mlmmjadmin was first introduced in iRedMail-0.9.8."
echo "<<< ERROR >>> https://docs.iredmail.org/iredmail.releases.html"
exit 255
fi
}
remove_pkg() {
echo "Remove package(s): $@"
if [ X"${DISTRO}" == X'RHEL' ]; then
yum remove -y $@
fi
}
install_pkg()
{
echo "Install package(s): $@"
if [ X"${DISTRO}" == X'RHEL' ]; then
yum -y install $@
elif [ X"${DISTRO}" == X'DEBIAN' -o X"${DISTRO}" == X'UBUNTU' ]; then
apt-get install -y $@
elif [ X"${DISTRO}" == X'FREEBSD' ]; then
cd /usr/ports/$@ && make install clean
elif [ X"${DISTRO}" == X'OPENBSD' ]; then
pkg_add -r $@
else
echo "<< ERROR >> Please install package(s) manually: $@"
fi
}
has_python_module()
{
mod="$1"
${CMD_PYTHON3} -c "import $mod" &>/dev/null
if [ X"$?" == X'0' ]; then
echo 'YES'
else
echo 'NO'
fi
}
add_missing_parameter()
{
# Usage: add_missing_parameter VARIABLE DEFAULT_VALUE [COMMENT]
var="${1}"
value="${2}"
shift 2
comment="$@"
if ! grep "^${var}" ${IRA_CONF_PY} &>/dev/null; then
if [ ! -z "${comment}" ]; then
echo "# ${comment}" >> ${IRA_CONF_PY}
fi
if [ X"${value}" == X'True' -o X"${value}" == X'False' ]; then
echo "${var} = ${value}" >> ${IRA_CONF_PY}
else
# Value must be quoted as string.
echo "${var} = '${value}'" >> ${IRA_CONF_PY}
fi
fi
}
# Remove all single quote and double quotes in string.
strip_quotes()
{
# Read input from stdin
str="$(cat <&0)"
value="$(echo ${str} | tr -d '"' | tr -d "'")"
echo "${value}"
}
get_iredadmin_setting()
{
var="${1}"
value="$(grep "^${var}" ${IRA_CONF_PY} | awk '{print $NF}' | strip_quotes)"
echo "${value}"
}
check_dot_my_cnf()
{
if egrep '^backend.*(mysql|ldap)' ${IRA_CONF_PY} &>/dev/null; then
if [ ! -f ${MY_CNF} ]; then
echo "<<< ERROR >>> File ${MY_CNF} not found."
echo "<<< ERROR >>> Please add mysql root user and password in it like below, then run this script again."
cat <<EOF
[client]
host=127.0.0.1
port=3306
user=${SQL_IREDADMIN_USER}
password="plain_password"
EOF
exit 255
fi
# Check MySQL connection
${CMD_MYSQL} -e "SHOW DATABASES" &>/dev/null
if [ X"$?" != X'0' ]; then
echo "<<< ERROR >>> MySQL user name '${SQL_IREDADMIN_USER}' or password is incorrect in ${MY_CNF}, please double check."
exit 255
fi
fi
}
check_mlmmjadmin_installation
check_dot_my_cnf
echo "* Detected Linux/BSD distribution: ${DISTRO}"
echo "* HTTP server root: ${HTTPD_SERVERROOT}"
if [ -L ${IRA_ROOT_DIR} ]; then
export IRA_ROOT_REAL_DIR="$(readlink ${IRA_ROOT_DIR})"
echo "* Found iRedAdmin directory: ${IRA_ROOT_DIR}, symbol link of ${IRA_ROOT_REAL_DIR}"
else
echo "<<< ERROR >>> Directory (${IRA_ROOT_DIR}) is not a symbol link created by iRedMail. Exit."
exit 255
fi
# Copy config file
if [ -f ${IRA_CONF_PY} ]; then
echo "* Found iRedAdmin config file: ${IRA_CONF_PY}"
else
echo "<<< ERROR >>> Cannot find a valid config file (settings.py)."
exit 255
fi
# Check whether current directory is iRedAdmin
PWD="$(pwd)"
if ! echo ${PWD} | grep 'iRedAdmin-.*/tools' >/dev/null; then
echo "<<< ERROR >>> Cannot find new version of iRedAdmin in current directory. Exit."
exit 255
fi
# Copy current directory to Apache server root
dir_new_version="$(dirname ${PWD})"
name_new_version="$(basename ${dir_new_version})"
NEW_IRA_ROOT_DIR="${HTTPD_SERVERROOT}/${name_new_version}"
if [ -d ${NEW_IRA_ROOT_DIR} ]; then
COPY_FILES="${dir_new_version}/*"
COPY_DEST_DIR="${NEW_IRA_ROOT_DIR}"
#echo "<<< ERROR >>> Directory exist: ${NEW_IRA_ROOT_DIR}. Exit."
#exit 255
else
COPY_FILES="${dir_new_version}"
COPY_DEST_DIR="${HTTPD_SERVERROOT}"
fi
echo "* Copying new version to ${NEW_IRA_ROOT_DIR}"
cp -rf ${COPY_FILES} ${COPY_DEST_DIR}
# Copy old config files
echo "* Copy ${IRA_CONF_PY}."
cp -p ${IRA_CONF_PY} ${NEW_IRA_ROOT_DIR}/
if [ -f ${IRA_CUSTOM_CONF_PY} ]; then
echo "* Copy ${IRA_CUSTOM_CONF_PY}."
cp -p ${IRA_CUSTOM_CONF_PY} ${NEW_IRA_ROOT_DIR}
fi
# Copy hooks.py. It's ok if missing.
if [ -f ${IRA_ROOT_DIR}/hooks.py ]; then
echo "* Copy ${IRA_ROOT_DIR}/hooks.py."
cp -p ${IRA_ROOT_DIR}/hooks.py ${NEW_IRA_ROOT_DIR}/ &>/dev/null
fi
# Copy custom files under 'tools/'. It's ok if missing.
cp -p ${IRA_ROOT_DIR}/tools/*.custom ${NEW_IRA_ROOT_DIR}/tools/ &>/dev/null
cp -p ${IRA_ROOT_DIR}/tools/*.last-time ${NEW_IRA_ROOT_DIR}/tools/ &>/dev/null
# Template file renamed
if [ -f "${IRA_ROOT_DIR}/tools/notify_quarantined_recipients.custom.html" ]; then
echo "* Copy ${IRA_ROOT_DIR}/tools/notify_quarantined_recipients.custom.html"
cp -f ${IRA_ROOT_DIR}/tools/notify_quarantined_recipients.custom.html \
${NEW_IRA_ROOT_DIR}/tools/notify_quarantined_recipients.html.custom
fi
# Copy favicon.ico and brand logo image.
for var in 'BRAND_FAVICON' 'BRAND_LOGO'; do
if grep "^${var}\>" ${IRA_CONF_PY} &>/dev/null; then
_file=$(grep "^${var}\>" ${IRA_CONF_PY} | awk '{print $NF}' | tr -d '"' | tr -d "'")
echo "* Copy file ${IRA_ROOT_DIR}/static/${_file}"
cp -f ${IRA_ROOT_DIR}/static/${_file} ${NEW_IRA_ROOT_DIR}/static/
fi
done
# iredadmin is now ran as a standalone uwsgi instance, we don't need uwsgi
# daemon service anymore.
_uwsgi_confs='
/etc/uwsgi.d/iredadmin.ini
/etc/uwsgi-available/iredadmin.ini
/etc/uwsgi/apps-enabled/iredadmin.ini &>/dev/null
/etc/uwsgi/apps-available/iredadmin.ini &>/dev/null
/usr/local/etc/uwsgi/iredadmin.ini
/etc/uwsgi-enabled/iredadmin.ini &>/dev/null
/etc/uwsgi-available/iredadmin.ini &>/dev/null
'
for f in ${_uwsgi_confs}; do
rm -f ${f} &>/dev/null
done
# Remove 'uwsgi_XXX' from /etc/rc.conf on FreeBSD.
if [[ X"${DISTRO}" == X'FREEBSD' ]]; then
${SYSRC} -x uwsgi_enable &>/dev/null
${SYSRC} -x uwsgi_profiles &>/dev/null
${SYSRC} -x uwsgi_iredadmin_flags &>/dev/null
fi
# Update Nginx template file
export _restart_nginx='NO'
for f in ${NGINX_SNIPPET_CONF} ${NGINX_SNIPPET_CONF2} ${NGINX_SNIPPET_CONF3}; do
if [[ -f ${f} ]]; then
if grep 'unix:.*iredadmin.socket' ${f} &>/dev/null; then
export _restart_nginx='YES'
perl -pi -e 's#uwsgi_pass unix:.*iredadmin.socket;#uwsgi_pass 127.0.0.1:7791;#g' ${f}
fi
fi
done
if [[ X"${_restart_nginx}" == X'YES' ]]; then
restart_service nginx
fi
# Update uwsgi ini config file
if [ -d ${NEW_IRA_ROOT_DIR}/rc_scripts/uwsgi ]; then
perl -pi -e 's#^chdir = (.*)#chdir = $ENV{HTTPD_SERVERROOT}/iredadmin#g' ${NEW_IRA_ROOT_DIR}/rc_scripts/uwsgi/*.ini
fi
# Copy rc script or systemd service file
if [ X"${USE_SYSTEMD}" == X'YES' ]; then
echo "* Remove existing systemd service files."
rm -f ${SYSTEMD_SERVICE_DIR}/iredadmin.service &>/dev/null
rm -f ${SYSTEMD_SERVICE_DIR2}/iredadmin.service &>/dev/null
rm -f ${SYSTEMD_SERVICE_USER_DIR}/iredadmin.service &>/dev/null
echo "* Copy systemd service file: ${SYSTEMD_SERVICE_DIR}/iredadmin.service."
if [ X"${DISTRO}" == X'RHEL' ]; then
cp -f ${NEW_IRA_ROOT_DIR}/rc_scripts/systemd/rhel${DISTRO_VERSION}.service ${SYSTEMD_SERVICE_DIR}/iredadmin.service
perl -pi -e 's#/opt/www#$ENV{HTTPD_SERVERROOT}#g' ${SYSTEMD_SERVICE_DIR}/iredadmin.service
perl -pi -e 's#/usr/local/bin/uwsgi#$ENV{CMD_UWSGI}#g' ${SYSTEMD_SERVICE_DIR}/iredadmin.service
if [[ X"${UWSGI_HAS_PLUGINS}" == X"NO" ]]; then
_ini_file="${NEW_IRA_ROOT_DIR}/rc_scripts/uwsgi/rhel${DISTRO_VERSION}.ini"
if [[ -f ${_ini_file} ]]; then
perl -pi -e 's#^(plugins.*)##g' ${_ini_file}
fi
fi
elif [ X"${DISTRO}" == X'DEBIAN' -o X"${DISTRO}" == X'UBUNTU' ]; then
cp -f ${NEW_IRA_ROOT_DIR}/rc_scripts/systemd/debian.service ${SYSTEMD_SERVICE_DIR}/iredadmin.service
perl -pi -e 's#/opt/www#$ENV{HTTPD_SERVERROOT}#g' ${SYSTEMD_SERVICE_DIR}/iredadmin.service
fi
chmod -R 0644 ${SYSTEMD_SERVICE_DIR}/iredadmin.service
systemctl daemon-reload &>/dev/null
else
if [ X"${DISTRO}" == X"FREEBSD" ]; then
cp ${NEW_IRA_ROOT_DIR}/rc_scripts/iredadmin.freebsd /usr/local/etc/rc.d/iredadmin
perl -pi -e 's#/opt/www#$ENV{HTTPD_SERVERROOT}#g' /usr/local/etc/rc.d/iredadmin
# Remove 'uwsgi_iredadmin_flags=' in /etc/rc.conf.local
if [ -f /etc/rc.conf.local ]; then
perl -pi -e 's#^uwsgi_iredadminflags=.*##g' /etc/rc.conf.local
fi
elif [ X"${DISTRO}" == X'OPENBSD' ]; then
cp ${NEW_IRA_ROOT_DIR}/rc_scripts/iredadmin.openbsd ${DIR_RC_SCRIPTS}/iredadmin
perl -pi -e 's#/opt/www#$ENV{HTTPD_SERVERROOT}#g' /etc/rc.d/iredadmin
cp -f ${NEW_IRA_ROOT_DIR}/rc_scripts/iredadmin.openbsd /etc/rc.d/iredadmin
chmod 0755 /etc/rc.d/iredadmin
# Remove 'uwsgi_flags=' in /etc/rc.conf.local
if [ -f /etc/rc.conf.local ]; then
perl -pi -e 's#^uwsgi_flags=.*iredadmin.*##g' /etc/rc.conf.local
fi
fi
fi
# Set owner and permission.
chown -R ${IRA_HTTPD_USER}:${IRA_HTTPD_GROUP} ${NEW_IRA_ROOT_DIR}
chmod -R 0555 ${NEW_IRA_ROOT_DIR}
chmod 0400 ${NEW_IRA_ROOT_DIR}/settings.py
echo "* Removing old symbol link ${IRA_ROOT_DIR}"
rm -f ${IRA_ROOT_DIR}
echo "* Creating symbol link ${IRA_ROOT_DIR} to ${NEW_IRA_ROOT_DIR}"
cd ${HTTPD_SERVERROOT}
ln -s ${name_new_version} iredadmin
# Add missing setting parameters.
if grep 'amavisd_enable_logging.*True.*' ${IRA_CONF_PY} &>/dev/null; then
add_missing_parameter 'amavisd_enable_policy_lookup' True 'Enable per-recipient spam policy, white/blacklist.'
else
add_missing_parameter 'amavisd_enable_policy_lookup' False 'Enable per-recipient spam policy, white/blacklist.'
fi
if ! grep '^iredapd_' ${IRA_CONF_PY} &>/dev/null; then
add_missing_parameter 'iredapd_enabled' True 'Enable iRedAPD integration.'
# Get iredapd db password from /opt/iredapd/settings.py.
if [ -f /opt/iredapd/settings.py ]; then
grep '^iredapd_db_' /opt/iredapd/settings.py >> ${IRA_CONF_PY}
perl -pi -e 's#iredapd_db_server#iredapd_db_host#g' ${IRA_CONF_PY}
else
# Check backend.
if egrep '^backend.*pgsql' ${IRA_CONF_PY} &>/dev/null; then
export IREDAPD_DB_PORT='5432'
else
export IREDAPD_DB_PORT='3306'
fi
add_missing_parameter 'iredapd_db_host' '127.0.0.1'
add_missing_parameter 'iredapd_db_port' ${IREDAPD_DB_PORT}
add_missing_parameter 'iredapd_db_name' 'iredapd'
add_missing_parameter 'iredapd_db_user' 'iredapd'
add_missing_parameter 'iredapd_db_password' 'password'
fi
fi
perl -pi -e 's#iredapd_db_server#iredapd_db_host#g' ${IRA_CONF_PY}
if ! grep '^fail2ban_' ${IRA_CONF_PY} &>/dev/null; then
# Try to get password of SQL user `fail2ban`.
if egrep '^backend.*(mysql|ldap)' ${IRA_CONF_PY} &>/dev/null; then
_my_cnf='/root/.my.cnf-fail2ban'
if [ -f ${_my_cnf} ]; then
_host="$(grep '^host=' ${_my_cnf} | awk -F'host=' '{print $2}' | strip_quotes)"
_port="$(grep '^port=' ${_my_cnf} | awk -F'port=' '{print $2}' | strip_quotes)"
_user="$(grep '^user=' ${_my_cnf} | awk -F'user=' '{print $2}' | strip_quotes)"
_password="$(grep '^password=' ${_my_cnf} | awk -F'password=' '{print $2}' | strip_quotes)"
[ X"${_host}" == X'' ] && _host='127.0.0.1'
[ X"${_port}" == X'' ] && _port='3306'
fi
elif egrep '^backend.*pgsql' ${IRA_CONF_PY} &>/dev/null; then
# Absolute path to ~/.pgpass
# - RHEL: /var/lib/pgsql/.pgpass
# - Debian/Ubuntu: /var/lib/postgresql/.pgpass
# - FreeBSD: /var/db/postgres/.pgpass
# - OpenBSD: /var/postgresql/.pgpass
for dir in \
/var/lib/pgsql \
/var/lib/postgresql \
/var/db/postgres \
/var/postgresql; do
_pgpass="${dir}/.pgpass"
if [ -f ${_pgpass} ]; then
if grep ':fail2ban:' ${_pgpass} &>/dev/null; then
_host="127.0.0.1"
_port="5432"
_user="fail2ban"
_password="$(grep ':fail2ban:' ${_pgpass} | awk -F':' '{print $NF}')"
break
fi
fi
done
fi
if [ X"${_host}" != X'' ] && \
[ X"${_port}" != X'' ] && \
[ X"${_user}" != X'' ] && \
[ X"${_password}" != X'' ]; then
echo "* Enable Fail2ban SQL integration."
add_missing_parameter 'fail2ban_enabled' 'True'
add_missing_parameter 'fail2ban_db_host' "${_host}"
add_missing_parameter 'fail2ban_db_port' "${_port}"
add_missing_parameter 'fail2ban_db_name' "fail2ban"
add_missing_parameter 'fail2ban_db_user' "${_user}"
add_missing_parameter 'fail2ban_db_password' "${_password}"
fi
fi
# FreeBSD uses /var/run/log for syslog.
if [ X"${DISTRO}" == X'FREEBSD' ]; then
add_missing_parameter 'SYSLOG_SERVER' '/var/run/log'
fi
#
# Enable mlmmj integration
#
if [ -e /opt/mlmmjadmin ]; then
echo "* Enable mlmmj integration."
# Force to use backend `bk_none`.
perl -pi -e 's#^(backend_api).*#${1} = "bk_none"#g' /opt/mlmmjadmin/settings.py
if egrep '^backend.*(ldap)' ${IRA_CONF_PY} &>/dev/null; then
perl -pi -e 's#^(backend_cli).*#${1} = "bk_iredmail_ldap"#g' /opt/mlmmjadmin/settings.py
else
perl -pi -e 's#^(backend_cli).*#${1} = "bk_iredmail_sql"#g' /opt/mlmmjadmin/settings.py
fi
# Add parameter `mlmmjadmin_api_auth_token` if missing
if ! grep '^mlmmjadmin_api_auth_token' ${IRA_CONF_PY} >/dev/null; then
# Get first api auth token
token=$(grep '^api_auth_tokens' /opt/mlmmjadmin/settings.py | awk -F"[=\']" '{print $3}' | tr -d '\n')
echo -e "\nmlmmjadmin_api_auth_token = '${token}'" >> ${IRA_CONF_PY}
fi
echo "* Restarting service: mlmmjadmin."
restart_service mlmmjadmin
fi
# Change old parameter names to the new ones:
#
# - ADDITION_USER_SERVICES -> ADDITIONAL_ENABLED_USER_SERVICES
# - LDAP_SERVER_NAME -> LDAP_SERVER_PRODUCT_NAME
perl -pi -e 's#ADDITION_USER_SERVICES#ADDITIONAL_ENABLED_USER_SERVICES#g' ${IRA_CONF_PY}
perl -pi -e 's#LDAP_SERVER_NAME#LDAP_SERVER_PRODUCT_NAME#g' ${IRA_CONF_PY}
# Remove deprecated setting: ENABLE_SELF_SERVICE, it's now a per-domain setting.
perl -pi -e 's#^(ENABLE_SELF_SERVICE.*)##g' ${IRA_CONF_PY}
# Dependent packages.
export REQUIRED_PKGS=""
export PIP3_MODS=""
# Python modules.
export PKG_PY_PIP='python3-pip'
export PKG_PY_LDAP='python3-ldap'
export PKG_PY_MYSQL='python3-pymysql'
export PKG_PY_PGSQL='python3-psycopg2'
export PKG_PY_JSON='python3-simplejson'
export PKG_PY_DNS='python3-dnspython'
export PKG_PY_REQUESTS='python3-requests'
export PKG_PY_JINJA='python3-jinja2'
# Python modules installed with pip3: uwsgi.
if [ X"${DISTRO}" == X'RHEL' ]; then
if [ X"${DISTRO_VERSION}" == X'7' ]; then
export PKG_PY_MYSQL='python36-PyMySQL'
export PKG_PY_JSON='python36-simplejson'
export PKG_PY_JINJA='python36-jinja2'
export REQUIRED_PKGS="${REQUIRED_PKGS} uwsgi uwsgi-plugin-python36 uwsgi-plugin-syslog"
if rpm -q mod_wsgi &>/dev/null; then
remove_pkg mod_wsgi
export REQUIRED_PKGS="${REQUIRED_PKGS} python3-mod_wsgi"
fi
else
if [ ! -x ${CMD_UWSGI} ]; then
# gcc is required to install uwsgi.
export REQUIRED_PKGS="${REQUIRED_PKGS} python3-devel python3-pip gcc"
export PIP3_MODS="${PIP3_MODS} uwsgi"
fi
fi
export PKG_PY_DNS='python3-dns'
elif [ X"${DISTRO}" == X'DEBIAN' -o X"${DISTRO}" == X'UBUNTU' ]; then
export REQUIRED_PKGS="${REQUIRED_PKGS} uwsgi-core uwsgi-plugin-python3"
if [ X"${DISTRO_VERSION}" == X'9' ]; then
export PKG_PY_LDAP='python3-pyldap'
else
export PKG_PY_LDAP='python3-ldap'
fi
elif [ X"${DISTRO}" == X'OPENBSD' ]; then
export PKG_PY_PIP='py3-pip'
export PKG_PY_JSON='py3-simplejson'
export PKG_PY_DNS='py3-dnspython'
export PKG_PY_REQUESTS='py3-requests'
export PKG_PY_JINJA='py3-jinja2'
if [ X"${DISTRO_VERSION}" == X'6.6' -o X"${DISTRO_VERSION}" == X'6.7' ]; then
export PKG_PY_MYSQL='py3-mysqlclient'
else
export PKG_PY_MYSQL='py3-pymysql'
fi
if [ ! -x ${CMD_UWSGI} ]; then
export PIP3_MODS="${PIP3_MODS} uwsgi"
fi
elif [ X"${DISTRO}" == X'FREEBSD' ]; then
export PKG_PY_PIP='devel/py-pip'
export PKG_UWSGI="www/uwsgi"
export PKG_PY_JSON='devel/py-simplejson'
export PKG_PY_DNS='dns/py-dnspython'
export PKG_PY_REQUESTS='www/py-requests'
export PKG_PY_JINJA='devel/py-Jinja2'
if [ ! -x ${CMD_UWSGI} ]; then
export REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_UWSGI}"
fi
fi
echo "* Check and install required packages."
if egrep '^backend.*ldap' ${IRA_CONF_PY} &>/dev/null; then
[ X"$(has_python_module ldap)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_LDAP}"
[ X"$(has_python_module pymysql)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_MYSQL}"
elif egrep '^backend.*mysql' ${IRA_CONF_PY} &>/dev/null; then
[ X"$(has_python_module pymysql)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_MYSQL}"
elif egrep '^backend.*pgsql' ${IRA_CONF_PY} &>/dev/null; then
[ X"$(has_python_module psycopg2)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_PGSQL}"
fi
[ X"$(has_python_module pip)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_PIP}"
[ X"$(has_python_module simplejson)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_JSON}"
[ X"$(has_python_module dns)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_DNS}"
[ X"$(has_python_module requests)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_REQUESTS}"
if [ X"$(has_python_module web)" == X'NO' ]; then
PIP3_MODS="${PIP3_MODS} web.py>=0.61"
else # Verify module version.
_webpy_ver=$(${CMD_PYTHON3} -c "import web; print(web.__version__)")
if echo ${_webpy_ver} | grep '^0\.[45]' &>/dev/null; then
PIP3_MODS="${PIP3_MODS} web.py>=0.61"
fi
fi
[ X"$(has_python_module jinja2)" == X'NO' ] && REQUIRED_PKGS="${REQUIRED_PKGS} ${PKG_PY_JINJA}"
if [ X"${REQUIRED_PKGS}" != X'' ]; then
install_pkg ${REQUIRED_PKGS}
if [ X"$?" != X'0' ]; then
echo "Package installation failed, please check console output and fix it manually."
exist 255
fi
fi
if [ X"${PIP3_MODS}" != X'' ]; then
${CMD_PIP3} install -U ${PIP3_MODS}
if [ X"$?" != X'0' ]; then
echo "Package installation failed, please check console output and fix it manually."
exist 255
fi
fi
#------------------------------------------
# Add new SQL tables, drop deprecated ones.
#
export ira_db_host="$(get_iredadmin_setting 'iredadmin_db_host')"
export ira_db_port="$(get_iredadmin_setting 'iredadmin_db_port')"
export ira_db_name="$(get_iredadmin_setting 'iredadmin_db_name')"
export ira_db_user="$(get_iredadmin_setting 'iredadmin_db_user')"
export ira_db_password="$(get_iredadmin_setting 'iredadmin_db_password')"
#
# Update sql tables
#
psql_conn="psql -h ${ira_db_host} \
-p ${ira_db_port} \
-U ${ira_db_user} \
-d ${ira_db_name}"
if egrep '^backend.*(mysql|ldap)' ${IRA_CONF_PY} &>/dev/null; then
echo "* Check SQL tables, and add missed ones - if there's any"
${CMD_MYSQL} ${ira_db_name} -e "SOURCE ${IRA_ROOT_DIR}/SQL/iredadmin.mysql"
${CMD_MYSQL} ${ira_db_name} -e "ALTER TABLE log MODIFY COLUMN msg TEXT;"
# Add column `tracking.id`.
${CMD_MYSQL} ${ira_db_name} -e "DESC tracking \G" | grep 'Field: id' &>/dev/null
if [ X"$?" != X'0' ]; then
${CMD_MYSQL} ${ira_db_name} -e "ALTER TABLE tracking ADD COLUMN id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY;"
fi
# Set column `id` to `PRIMARY KEY`
_tables='deleted_mailboxes updatelog log tracking'
for _table in ${_tables}; do
${CMD_MYSQL} ${ira_db_name} -e "DESC ${_table}" | grep '^id.*PRI.*auto_increment' &>/dev/null
if [ X"$?" != X'0' ]; then
${CMD_MYSQL} ${ira_db_name} -e "ALTER TABLE ${_table} ADD PRIMARY KEY (id)"
fi
done
elif egrep '^backend.*pgsql' ${IRA_CONF_PY} &>/dev/null; then
export PGPASSWORD="${ira_db_password}"
# Allow log.msg to store long text.
${psql_conn} <<EOF
ALTER TABLE log ALTER COLUMN msg TYPE TEXT;
EOF
# SQL table: tracking.
${psql_conn} -c '\d' | grep '\<tracking\>' &>/dev/null
if [ X"$?" != X'0' ]; then
echo "* [SQL] Add new table: iredadmin.tracking."
${psql_conn} <<EOF
CREATE TABLE tracking (
id SERIAL PRIMARY KEY,
k VARCHAR(50) NOT NULL,
v TEXT,
time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX idx_tracking_k ON tracking (k);
EOF
fi
# Set column `tracking.id` to `PRIMARY KEY`
# SQL table: domain_ownership.
${psql_conn} -c '\d' | grep '\<domain_ownership\>' &>/dev/null
if [ X"$?" != X'0' ]; then
echo "* [SQL] Add new table: iredadmin.domain_ownership."
${psql_conn} <<EOF
CREATE TABLE domain_ownership (
id SERIAL PRIMARY KEY,
admin VARCHAR(255) NOT NULL DEFAULT '',
domain VARCHAR(255) NOT NULL DEFAULT '',
alias_domain VARCHAR(255) NOT NULL DEFAULT '',
verify_code VARCHAR(100) NOT NULL DEFAULT '',
verified INT2 NOT NULL DEFAULT 0,
message TEXT,
last_verify TIMESTAMP NULL DEFAULT NULL,
expire INT DEFAULT 0
);
CREATE UNIQUE INDEX idx_ownership_1 ON domain_ownership (admin, domain, alias_domain);
CREATE INDEX idx_ownership_2 ON domain_ownership (verified);
EOF
fi
# SQL table: newsletter_subunsub_confirms.
${psql_conn} -c '\d' | grep '\<newsletter_subunsub_confirms\>' &>/dev/null
if [ X"$?" != X'0' ]; then
echo "* [SQL] Add new table: iredadmin.newsletter_subunsub_confirms."
_sql="$(cat ${IRA_ROOT_DIR}/SQL/snippets/newsletter_subunsub_confirms.pgsql)"
${psql_conn} <<EOF
${_sql}
EOF
unset _sql
fi
# SQL table: settings.
${psql_conn} -c '\d' | grep '\<settings\>' &>/dev/null
if [ X"$?" != X'0' ]; then
echo "* [SQL] Add new table: iredadmin.settings."
_sql="$(cat ${IRA_ROOT_DIR}/SQL/snippets/settings.pgsql)"
${psql_conn} <<EOF
${_sql}
EOF
unset _sql
fi
fi
#------------------------------
# Cron job.
#
[[ -d ${CRON_SPOOL_DIR} ]] || mkdir -p ${CRON_SPOOL_DIR} &>/dev/null
if [[ ! -f ${CRON_FILE_ROOT} ]]; then
touch ${CRON_FILE_ROOT} &>/dev/null
chmod 0600 ${CRON_FILE_ROOT} &>/dev/null
fi
# cron job: clean up database.
if ! grep 'iredadmin/tools/cleanup_db.py' ${CRON_FILE_ROOT} &>/dev/null; then
cat >> ${CRON_FILE_ROOT} <<EOF
# iRedAdmin: Clean up sql database.
1 * * * * ${CMD_PYTHON3} ${IRA_ROOT_DIR}/tools/cleanup_db.py &>/dev/null
EOF
fi
# cron job: clean up database.
if ! grep 'iredadmin/tools/delete_mailboxes.py' ${CRON_FILE_ROOT} &>/dev/null; then
cat >> ${CRON_FILE_ROOT} <<EOF
# iRedAdmin: Remove mailboxes which are scheduled to be removed.
1 3 * * * ${CMD_PYTHON3} ${IRA_ROOT_DIR}/tools/delete_mailboxes.py
EOF
fi
echo "* Replace py2 by py3 in cron jobs."
perl -pi -e 's#(.*) python (.*/iredadmin/tools/.*)#${1} $ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
perl -pi -e 's#(.*) python2 (.*/iredadmin/tools/.*)#${1} $ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
perl -pi -e 's#(.*)/usr/bin/python (.*/iredadmin/tools/.*)#${1}$ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
perl -pi -e 's#(.*)/usr/bin/python2 (.*/iredadmin/tools/.*)#${1}$ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
perl -pi -e 's#(.*)/usr/local/bin/python (.*/iredadmin/tools/.*)#${1}$ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
perl -pi -e 's#(.*)/usr/local/bin/python2 (.*/iredadmin/tools/.*)#${1}$ENV{CMD_PYTHON3} ${2}#' ${CRON_FILE_ROOT}
echo "* Clean up."
cd ${NEW_IRA_ROOT_DIR}/
rm -f settings.pyc settings.pyo tools/settings.py
if [[ -f ${NEW_IRA_ROOT_DIR}/libs/form_utils.py ]]; then
# Not a trial license.
cd ${NEW_IRA_ROOT_DIR}
find . -name '*.so' | xargs rm -f {}
cd - &>/dev/null
fi
# Delete all sessions to force admins to re-login.
cd ${NEW_IRA_ROOT_DIR}/tools/
${CMD_PYTHON3} delete_sessions.py
echo "* iRedAdmin has been successfully upgraded."
restart_web_service
# Enable and restart service
enable_service iredadmin
restart_service iredadmin
echo "* Upgrading completed."
cat <<EOF
<<< NOTE >>> If iRedAdmin doesn't work as expected, please post your issue in
<<< NOTE >>> our online support forum: http://www.iredmail.org/forum/
EOF