├── .hgignore ├── AUTHORS ├── ChangeLog ├── LICENSE ├── README.md ├── SQL ├── iredadmin.mysql ├── iredadmin.pgsql └── snippets │ ├── newsletter_subunsub_confirms.mysql │ ├── newsletter_subunsub_confirms.pgsql │ └── settings.pgsql ├── controllers ├── __init__.py ├── decorators.py ├── ldap │ ├── __init__.py │ ├── admin.py │ ├── basic.py │ ├── domain.py │ ├── urls.py │ ├── user.py │ └── utils.py ├── panel │ ├── __init__.py │ ├── log.py │ └── urls.py ├── sql │ ├── __init__.py │ ├── admin.py │ ├── basic.py │ ├── domain.py │ ├── urls.py │ ├── user.py │ └── utils.py └── utils.py ├── docs ├── README.customization └── tests.md ├── i18n ├── babel.cfg ├── bg_BG │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── cs_CZ │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── da_DK │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── de_DE │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── en_US │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── es_ES │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── fi_FI │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── fr_FR │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── hu_HU │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── iredadmin.po ├── it_IT │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── ja_JP │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── ko_KR │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── lv_LV │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── nl_NL │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── pl_PL │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── pt_BR │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── ro_RO │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── ru_RU │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── sl_SI │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── sr │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── sv_SE │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po ├── translation.sh ├── zh_CN │ └── LC_MESSAGES │ │ ├── iredadmin.mo │ │ └── iredadmin.po └── zh_TW │ └── LC_MESSAGES │ ├── iredadmin.mo │ └── iredadmin.po ├── iredadmin.py ├── libs ├── __init__.py ├── default_settings.py ├── form_utils.py ├── hooks.py ├── iredbase.py ├── ireddate.py ├── iredpwd.py ├── iredutils.py ├── jinja_filters.py ├── l10n.py ├── ldaplib │ ├── __init__.py │ ├── admin.py │ ├── attrs.py │ ├── auth.py │ ├── core.py │ ├── decorators.py │ ├── domain.py │ ├── general.py │ ├── iredldif.py │ ├── ldaputils.py │ └── user.py ├── logger.py ├── mailparser.py ├── panel │ ├── __init__.py │ └── log.py ├── regxes.py ├── sqllib │ ├── __init__.py │ ├── admin.py │ ├── auth.py │ ├── decorators.py │ ├── domain.py │ ├── general.py │ ├── sqlutils.py │ ├── user.py │ └── utils.py └── sysinfo.py ├── rc_scripts ├── iredadmin.debian ├── iredadmin.freebsd ├── iredadmin.openbsd ├── iredadmin.rhel ├── systemd │ ├── debian.service │ ├── rhel7.service │ ├── rhel8.service │ └── rhel9.service └── uwsgi │ ├── debian.ini │ ├── freebsd.ini │ ├── openbsd.ini │ ├── rhel7.ini │ ├── rhel8.ini │ └── rhel9.ini ├── requirements.txt ├── settings.py.ldap.sample ├── settings.py.mysql.sample ├── settings.py.pgsql.sample ├── static ├── .htaccess ├── default │ ├── css │ │ ├── fancybox.css │ │ ├── reset.css │ │ ├── screen.css │ │ ├── spectre-icons.min.css │ │ └── spectre.min.css │ └── images │ │ ├── arrow_left_off.png │ │ ├── arrow_left_ovr.png │ │ ├── arrow_leftend_off.png │ │ ├── arrow_leftend_ovr.png │ │ ├── arrow_right_off.png │ │ ├── arrow_right_ovr.png │ │ ├── arrow_rightend_off.png │ │ ├── arrow_rightend_ovr.png │ │ ├── arrow_sm_black.gif │ │ ├── arrow_sm_grey.gif │ │ ├── ball_grey_16.png │ │ ├── ball_yellow_13.png │ │ ├── bck_black_10.png │ │ ├── bck_black_5.png │ │ ├── bck_black_70.png │ │ ├── bck_header.png │ │ ├── bck_main.png │ │ ├── bck_white_10.png │ │ ├── bck_white_50.png │ │ ├── bck_white_75.png │ │ ├── bck_white_90.png │ │ ├── but_slide.png │ │ ├── button_glas1_ovr.png │ │ ├── button_glas2.png │ │ ├── fancybox │ │ ├── blank.gif │ │ ├── fancybox-x.png │ │ ├── fancybox-y.png │ │ ├── fancybox.blank.gif │ │ └── fancybox.png │ │ ├── gear.png │ │ ├── graph_16.png │ │ ├── header.png │ │ ├── ico_close_off.png │ │ ├── ico_close_ovr.png │ │ ├── line.gif │ │ ├── login.jpg │ │ ├── login_header.png │ │ ├── members.png │ │ ├── page_active.gif │ │ ├── rule.gif │ │ ├── rule2.gif │ │ └── tablesorter │ │ ├── asc.gif │ │ ├── bg.gif │ │ └── desc.gif ├── favicon.ico ├── fontawesome │ ├── css │ │ └── fontawesome-all.min.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 ├── js │ ├── jquery-3.6.4.min.js │ ├── jquery.fancybox.js │ ├── jquery.idtabs.js │ ├── jquery.quickfilter.js │ ├── jquery.tooltip.js │ └── stupidtable.min.js └── logo.png ├── templates └── default │ ├── dashboard.html │ ├── error_csrf.html │ ├── error_without_login.html │ ├── layout.html │ ├── ldap │ ├── admin │ │ ├── create.html │ │ ├── list.html │ │ └── profile.html │ ├── domain │ │ ├── create.html │ │ ├── list.html │ │ └── profile.html │ ├── ldif.html │ └── user │ │ ├── create.html │ │ ├── list.html │ │ └── profile.html │ ├── login.html │ ├── macros │ ├── form_inputs.html │ ├── general.html │ ├── ldap.html │ └── msg_handlers.html │ ├── panel │ └── log.html │ └── sql │ ├── admin │ ├── create.html │ ├── list.html │ └── profile.html │ ├── domain │ ├── create.html │ ├── list.html │ └── profile.html │ └── user │ ├── create.html │ ├── list.html │ └── profile.html ├── tools ├── README.md ├── __init__.py ├── cleanup_amavisd_db.py ├── cleanup_db.py ├── delete_mailboxes.py ├── delete_sessions.py ├── dump_disclaimer.py ├── dump_quarantined_mails.py ├── ira_tool_lib.py ├── promote_user_to_global_admin.py ├── reset_user_password.py ├── update_mailbox_quota.py ├── update_password_in_csv.py └── upgrade_iredadmin.sh └── web ├── __init__.py ├── application.py ├── browser.py ├── contrib ├── __init__.py └── template.py ├── db.py ├── debugerror.py ├── form.py ├── http.py ├── httpserver.py ├── net.py ├── py3helpers.py ├── session.py ├── template.py ├── test.py ├── utils.py ├── webapi.py └── wsgi.py /.hgignore: -------------------------------------------------------------------------------- 1 | #use glob syntax. 2 | syntax: glob 3 | *.py[co] 4 | *.svn 5 | settings.ini 6 | settings.ini.bak 7 | .project 8 | .pydevproject 9 | .settings 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors: 2 | * Zhang Huangbin 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Don't forget to check out our lightweight email archiving software: https://spiderd.io/ 2 | 3 | * Download packed stable release tarballs: 4 | https://dl.iredmail.org/yum/misc/ 5 | 6 | * Installation Guide, Release Notes, Upgrade Tutorials: 7 | https://docs.iredmail.org/iredadmin-pro.releases.html 8 | 9 | * Please report bugs in our forum: 10 | https://forum.iredmail.org/ 11 | -------------------------------------------------------------------------------- /SQL/iredadmin.pgsql: -------------------------------------------------------------------------------- 1 | -- CREATE DATABASE iredadmin WITH TEMPLATE template0 ENCODING 'UTF8'; 2 | -- CREATE ROLE iredadmin WITH LOGIN ENCRYPTED PASSWORD 'plain_password' NOSUPERUSER NOCREATEDB NOCREATEROLE; 3 | -- \c iredadmin; 4 | 5 | -- Session table required by webpy session module. 6 | CREATE TABLE sessions ( 7 | session_id CHAR(128) UNIQUE NOT NULL, 8 | atime TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | data TEXT 10 | ); 11 | 12 | -- Store all admin operations. 13 | CREATE TABLE log ( 14 | id SERIAL PRIMARY KEY, 15 | admin VARCHAR(255) NOT NULL, 16 | timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | ip VARCHAR(40) NOT NULL, 18 | domain VARCHAR(255) NOT NULL DEFAULT '', 19 | username VARCHAR(255) NOT NULL DEFAULT '', 20 | event VARCHAR(20) NOT NULL DEFAULT '', 21 | loglevel VARCHAR(10) NOT NULL DEFAULT 'info', 22 | msg TEXT 23 | ); 24 | 25 | CREATE INDEX idx_log_timestamp ON log (timestamp); 26 | CREATE INDEX idx_log_ip ON log (ip); 27 | CREATE INDEX idx_log_domain ON log (domain); 28 | CREATE INDEX idx_log_username ON log (username); 29 | CREATE INDEX idx_log_event ON log (event); 30 | CREATE INDEX idx_log_loglevel ON log (loglevel); 31 | 32 | CREATE TABLE updatelog ( 33 | date DATE NOT NULL, 34 | PRIMARY KEY (date) 35 | ); 36 | 37 | -- GRANT INSERT,UPDATE,DELETE,SELECT on sessions,log,updatelog to iredadmin; 38 | -- GRANT UPDATE,USAGE,SELECT ON log_id_seq TO iredadmin; 39 | 40 | -- Key-value store. 41 | CREATE TABLE tracking ( 42 | id SERIAL PRIMARY KEY, 43 | k VARCHAR(255) NOT NULL, 44 | v TEXT, 45 | time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP 46 | ); 47 | CREATE UNIQUE INDEX idx_tracking_k ON tracking (k); 48 | 49 | -- Store <-> domain <-> verify_code used to verify domain ownership 50 | CREATE TABLE domain_ownership ( 51 | id SERIAL PRIMARY KEY, 52 | -- the admin who added this domain with iRedAdmin. Required if domain was 53 | -- added by a normal domain admin. 54 | admin VARCHAR(255) NOT NULL DEFAULT '', 55 | -- the domain we're going to verify. If we're going to verifying an alias 56 | -- domain, it stores primary domain. 57 | domain VARCHAR(255) NOT NULL DEFAULT '', 58 | -- if we're verifying an alias domain: 59 | -- - store primary domain in `domain` 60 | -- - store alias domain in `alias_domain` 61 | alias_domain VARCHAR(255) NOT NULL DEFAULT '', 62 | -- a unique string which domain admin should put in TXT type DNS record 63 | -- or as a web file on web server 64 | verify_code VARCHAR(100) NOT NULL DEFAULT '', 65 | -- store the verify status 66 | verified INT2 NOT NULL DEFAULT 0, 67 | -- store error message if any returned while verifying, so that domain 68 | -- admin can fix it 69 | message TEXT, 70 | -- the last time we verify it. If it's verified, this record will be 71 | -- removed in 1 month. 72 | last_verify TIMESTAMP NULL DEFAULT NULL, 73 | -- expire time. cron job `tools/cleanup_db.py` will remove verified or 74 | -- unverified domains regularly. e.g. one month. 75 | -- Note: stores seconds since Unix epoch 76 | expire INT DEFAULT 0 77 | ); 78 | CREATE UNIQUE INDEX idx_ownership_1 ON domain_ownership (admin, domain, alias_domain); 79 | CREATE INDEX idx_ownership_2 ON domain_ownership (verified); 80 | 81 | -- mailing list subscription/unsubscription confirms. 82 | CREATE TABLE newsletter_subunsub_confirms ( 83 | id SERIAL PRIMARY KEY, 84 | -- email of mailing list 85 | mail VARCHAR(255) NOT NULL DEFAULT '', 86 | -- unique server wide id 87 | mlid VARCHAR(255) NOT NULL DEFAULT '', 88 | -- email of subscriber 89 | subscriber VARCHAR(255) NOT NULL DEFAULT '', 90 | -- kinds of 'subscribe', 'unsubscribe' 91 | kind VARCHAR(20) NOT NULL DEFAULT '', 92 | -- unique server-wide id as confirm token 93 | token VARCHAR(255) NOT NULL DEFAULT '', 94 | expired INT DEFAULT 0 95 | ); 96 | CREATE UNIQUE INDEX idx_subunsub_confirms_1 ON newsletter_subunsub_confirms (mlid, subscriber, kind); 97 | CREATE INDEX idx_subunsub_confirms_2 ON newsletter_subunsub_confirms (mail); 98 | CREATE INDEX idx_subunsub_confirms_3 ON newsletter_subunsub_confirms (token); 99 | CREATE INDEX idx_subunsub_confirms_4 ON newsletter_subunsub_confirms (expired); 100 | 101 | -- Key-value store for settings. 102 | -- `k` is the (unique) parameter name. 103 | -- `v` must be a valid JSON string with only one key: "value". Its value will 104 | -- be converted to Python native format (string, list, integer). 105 | -- Samples: 106 | -- {"value": 20} 107 | -- {"value": "a-string"} 108 | -- {"value": [v1, v2, v3, ...]} 109 | -- {"value": true} 110 | CREATE TABLE settings ( 111 | id SERIAL PRIMARY KEY, 112 | account VARCHAR(255) NOT NULL DEFAULT 'global', 113 | k VARCHAR(255) NOT NULL, 114 | v TEXT 115 | ); 116 | CREATE UNIQUE INDEX idx_settings_account_k ON settings (account, k); 117 | -------------------------------------------------------------------------------- /SQL/snippets/newsletter_subunsub_confirms.mysql: -------------------------------------------------------------------------------- 1 | -- mailing list subscription/unsubscription confirms. 2 | CREATE TABLE IF NOT EXISTS `newsletter_subunsub_confirms` ( 3 | `id` BIGINT(20) UNSIGNED AUTO_INCREMENT, 4 | -- email of mailing list 5 | `mail` VARCHAR(255) NOT NULL DEFAULT '', 6 | -- unique server wide id 7 | `mlid` VARCHAR(255) NOT NULL DEFAULT '', 8 | -- email of subscriber 9 | `subscriber` VARCHAR(255) NOT NULL DEFAULT '', 10 | -- kinds of 'subscribe', 'unsubscribe' 11 | `kind` VARCHAR(20) NOT NULL DEFAULT '', 12 | -- unique server-wide id as confirm token 13 | `token` VARCHAR(255) NOT NULL DEFAULT '', 14 | `expired` INT UNSIGNED DEFAULT 0, 15 | PRIMARY KEY (id), 16 | INDEX (mail), 17 | UNIQUE INDEX (mlid, subscriber, kind), 18 | INDEX (token), 19 | INDEX (expired) 20 | ) ENGINE=InnoDB; 21 | -------------------------------------------------------------------------------- /SQL/snippets/newsletter_subunsub_confirms.pgsql: -------------------------------------------------------------------------------- 1 | -- mailing list subscription/unsubscription confirms. 2 | CREATE TABLE newsletter_subunsub_confirms ( 3 | id SERIAL PRIMARY KEY, 4 | -- email of mailing list 5 | mail VARCHAR(255) NOT NULL DEFAULT '', 6 | -- unique server wide id 7 | mlid VARCHAR(255) NOT NULL DEFAULT '', 8 | -- email of subscriber 9 | subscriber VARCHAR(255) NOT NULL DEFAULT '', 10 | -- kinds of 'subscribe', 'unsubscribe' 11 | kind VARCHAR(20) NOT NULL DEFAULT '', 12 | -- unique server-wide id as confirm token 13 | token VARCHAR(255) NOT NULL DEFAULT '', 14 | expired INT DEFAULT 0 15 | ); 16 | CREATE UNIQUE INDEX idx_subunsub_confirms_1 ON newsletter_subunsub_confirms (mlid, subscriber, kind); 17 | CREATE INDEX idx_subunsub_confirms_2 ON newsletter_subunsub_confirms (mail); 18 | CREATE INDEX idx_subunsub_confirms_3 ON newsletter_subunsub_confirms (token); 19 | CREATE INDEX idx_subunsub_confirms_4 ON newsletter_subunsub_confirms (expired); 20 | -------------------------------------------------------------------------------- /SQL/snippets/settings.pgsql: -------------------------------------------------------------------------------- 1 | CREATE TABLE settings ( 2 | id SERIAL PRIMARY KEY, 3 | account VARCHAR(255) NOT NULL DEFAULT 'global', 4 | k VARCHAR(255) NOT NULL, 5 | v TEXT 6 | ); 7 | CREATE UNIQUE INDEX idx_settings_account_k ON settings (account, k); 8 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/controllers/__init__.py -------------------------------------------------------------------------------- /controllers/decorators.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | from libs import iredutils 5 | 6 | session = web.config.get("_session") 7 | 8 | 9 | def require_login(func): 10 | def proxyfunc(*args, **kw): 11 | if session.get("logged") is True: 12 | return func(*args, **kw) 13 | else: 14 | session.kill() 15 | raise web.seeother("/login?msg=LOGIN_REQUIRED") 16 | 17 | return proxyfunc 18 | 19 | 20 | def require_global_admin(func): 21 | def proxyfunc(*args, **kw): 22 | if session.get("is_global_admin"): 23 | return func(*args, **kw) 24 | else: 25 | if session.get("logged"): 26 | raise web.seeother("/domains?msg=PERMISSION_DENIED") 27 | else: 28 | raise web.seeother("/login?msg=LOGIN_REQUIRED") 29 | 30 | return proxyfunc 31 | 32 | 33 | def csrf_protected(f): 34 | def decorated(*args, **kw): 35 | form = web.input() 36 | 37 | if "csrf_token" not in form: 38 | return web.render("error_csrf.html") 39 | 40 | if not session.get("csrf_token"): 41 | session["csrf_token"] = iredutils.generate_random_strings(32) 42 | 43 | if form["csrf_token"] != session["csrf_token"]: 44 | return web.render("error_csrf.html") 45 | 46 | return f(*args, **kw) 47 | 48 | return decorated 49 | -------------------------------------------------------------------------------- /controllers/ldap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/controllers/ldap/__init__.py -------------------------------------------------------------------------------- /controllers/ldap/urls.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | from libs.regxes import email as e, domain as d 4 | 5 | # fmt: off 6 | urls = [ 7 | # Make url ending with or without '/' going to the same class. 8 | '/(.*)/', 'controllers.utils.Redirect', 9 | 10 | '/', 'controllers.ldap.basic.Login', 11 | '/login', 'controllers.ldap.basic.Login', 12 | '/logout', 'controllers.ldap.basic.Logout', 13 | '/dashboard', 'controllers.ldap.basic.Dashboard', 14 | 15 | # Perform some operations from search page. 16 | '/action/(domain)', 'controllers.ldap.basic.OperationsFromSearchPage', 17 | '/action/(admin)', 'controllers.ldap.basic.OperationsFromSearchPage', 18 | '/action/(user)', 'controllers.ldap.basic.OperationsFromSearchPage', 19 | 20 | # Domain related. 21 | '/domains', 'controllers.ldap.domain.List', 22 | r'/domains/page/(\d+)', 'controllers.ldap.domain.List', 23 | # List disabled accounts. 24 | '/domains/disabled', 'controllers.ldap.domain.ListDisabled', 25 | r'/domains/disabled/page/(\d+)', 'controllers.ldap.domain.ListDisabled', 26 | # Profiles 27 | '/profile/domain/(general)/(%s$)' % d, 'controllers.ldap.domain.Profile', 28 | '/profile/domain/(admins)/(%s$)' % d, 'controllers.ldap.domain.Profile', 29 | '/profile/domain/(advanced)/(%s$)' % d, 'controllers.ldap.domain.Profile', 30 | '/profile/domain/(%s)' % d, 'controllers.ldap.domain.Profile', 31 | '/create/domain', 'controllers.ldap.domain.Create', 32 | 33 | # Admin related. 34 | '/admins', 'controllers.ldap.admin.List', 35 | r'/admins/page/(\d+)', 'controllers.ldap.admin.List', 36 | '/profile/admin/(general)/(%s$)' % e, 'controllers.ldap.admin.Profile', 37 | '/profile/admin/(password)/(%s$)' % e, 'controllers.ldap.admin.Profile', 38 | '/create/admin', 'controllers.ldap.admin.Create', 39 | 40 | '/create/(user)', 'controllers.ldap.utils.CreateDispatcher', 41 | 42 | # User related 43 | # List users, delete users under same domain. 44 | '/users/(%s$)' % d, 'controllers.ldap.user.Users', 45 | r'/users/(%s)/page/(\d+)' % d, 'controllers.ldap.user.Users', 46 | # List disabled accounts. 47 | '/users/(%s)/disabled' % d, 'controllers.ldap.user.DisabledUsers', 48 | r'/users/(%s)/disabled/page/(\d+)' % d, 'controllers.ldap.user.DisabledUsers', 49 | # Create user. 50 | '/create/user/(%s$)' % d, 'controllers.ldap.user.Create', 51 | # Profile pages. 52 | '/profile/user/(general)/(%s$)' % e, 'controllers.ldap.user.Profile', 53 | '/profile/user/(password)/(%s$)' % e, 'controllers.ldap.user.Profile', 54 | '/profile/user/(advanced)/(%s$)' % e, 'controllers.ldap.user.Profile', 55 | 56 | # Internal domain admins 57 | '/admins/(%s$)' % d, 'controllers.ldap.user.Admin', 58 | r'/admins/(%s)/page/(\d+)' % d, 'controllers.ldap.user.Admin', 59 | ] 60 | -------------------------------------------------------------------------------- /controllers/ldap/utils.py: -------------------------------------------------------------------------------- 1 | import web 2 | from controllers import decorators 3 | from libs.ldaplib import domain as ldap_lib_domain 4 | 5 | 6 | # Get all domains, select the first one. 7 | class CreateDispatcher: 8 | @decorators.require_global_admin 9 | def GET(self, account_type): 10 | qr = ldap_lib_domain.list_accounts(attributes=['domainName'], names_only=True, conn=None) 11 | if qr[0]: 12 | all_domains = qr[1] 13 | 14 | if all_domains: 15 | # Create new account under first domain, so that we 16 | # can get per-domain account settings, such as number of 17 | # account limit, password length control, etc. 18 | raise web.seeother('/create/{}/{}'.format(account_type, all_domains[0])) 19 | else: 20 | raise web.seeother('/domains?msg=NO_DOMAIN_AVAILABLE') 21 | else: 22 | raise web.seeother('/domains?msg=' + web.urlquote(qr[1])) 23 | -------------------------------------------------------------------------------- /controllers/panel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/controllers/panel/__init__.py -------------------------------------------------------------------------------- /controllers/panel/log.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | import settings 5 | from controllers import decorators 6 | from libs import iredutils 7 | from libs.panel import LOG_EVENTS, log as loglib 8 | 9 | session = web.config.get('_session') 10 | 11 | if settings.backend == 'ldap': 12 | from libs.ldaplib.core import LDAPWrap 13 | from libs.ldaplib import admin as ldap_lib_admin 14 | elif settings.backend in ['mysql', 'pgsql']: 15 | from libs.sqllib import SQLWrap, admin as sql_lib_admin 16 | 17 | 18 | class Log: 19 | @decorators.require_global_admin 20 | def GET(self): 21 | form = web.input(_unicode=False) 22 | 23 | # Get queries. 24 | form_event = web.safestr(form.get('event', 'all')) 25 | form_domain = web.safestr(form.get('domain', 'all')) 26 | form_admin = web.safestr(form.get('admin', 'all')) 27 | form_cur_page = web.safestr(form.get('page', '1')) 28 | 29 | # Verify input data. 30 | if form_event not in LOG_EVENTS: 31 | form_event = "all" 32 | 33 | if not iredutils.is_domain(form_domain): 34 | form_domain = "" 35 | 36 | if not iredutils.is_email(form_admin): 37 | form_admin = "" 38 | 39 | if not form_cur_page.isdigit() or form_cur_page == '0': 40 | form_cur_page = 1 41 | else: 42 | form_cur_page = int(form_cur_page) 43 | 44 | total, entries = loglib.list_logs(event=form_event, 45 | domain=form_domain, 46 | admin=form_admin, 47 | cur_page=form_cur_page) 48 | 49 | # Pre-defined 50 | all_domains = [] 51 | all_admins = [] 52 | 53 | if settings.backend == 'ldap': 54 | _wrap = LDAPWrap() 55 | conn = _wrap.conn 56 | 57 | # Get all managed domains under control. 58 | qr = ldap_lib_admin.get_managed_domains(admin=session.get('username'), conn=conn) 59 | if qr[0] is True: 60 | all_domains = qr[1] 61 | 62 | # Get all admins. 63 | if session.get('is_global_admin') is True: 64 | result = ldap_lib_admin.list_accounts(attributes=['mail'], conn=conn) 65 | if result[0] is not False: 66 | all_admins = [v[1]['mail'][0] for v in result[1]] 67 | else: 68 | all_admins = [form_admin] 69 | 70 | elif settings.backend in ['mysql', 'pgsql']: 71 | # Get all managed domains under control. 72 | _wrap = SQLWrap() 73 | conn = _wrap.conn 74 | qr = sql_lib_admin.get_managed_domains(conn=conn, 75 | admin=session.get('username'), 76 | domain_name_only=True) 77 | if qr[0] is True: 78 | all_domains = qr[1] 79 | 80 | # Get all admins. 81 | if session.get('is_global_admin') is True: 82 | qr = sql_lib_admin.get_all_admins(columns=['username'], email_only=True, conn=conn) 83 | if qr[0]: 84 | all_admins = qr[1] 85 | else: 86 | all_admins = [form_admin] 87 | 88 | return web.render('panel/log.html', 89 | event=form_event, 90 | domain=form_domain, 91 | admin=form_admin, 92 | log_events=LOG_EVENTS, 93 | cur_page=form_cur_page, 94 | total=total, 95 | entries=entries, 96 | all_domains=all_domains, 97 | all_admins=all_admins, 98 | msg=form.get('msg')) 99 | 100 | @decorators.require_global_admin 101 | @decorators.csrf_protected 102 | @decorators.require_global_admin 103 | def POST(self): 104 | form = web.input(_unicode=False, id=[]) 105 | action = form.get('action', 'delete') 106 | 107 | delete_all = False 108 | if action == 'deleteAll': 109 | delete_all = True 110 | 111 | qr = loglib.delete_logs(form=form, delete_all=delete_all) 112 | if qr[0]: 113 | # Keep the log filter. 114 | form_domain = web.safestr(form.get('domain')) 115 | form_admin = web.safestr(form.get('admin')) 116 | form_event = web.safestr(form.get('event')) 117 | url = 'domain={}&admin={}&event={}'.format(form_domain, form_admin, form_event) 118 | 119 | raise web.seeother('/activities/admins?%s&msg=DELETED' % url) 120 | else: 121 | raise web.seeother('/activities/admins?msg=%s' % web.urlquote(qr[1])) 122 | -------------------------------------------------------------------------------- /controllers/panel/urls.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | # fmt: off 4 | urls = [ 5 | '/system', 'controllers.panel.log.Log', 6 | '/activities/admins', 'controllers.panel.log.Log', 7 | ] 8 | # fmt: on 9 | -------------------------------------------------------------------------------- /controllers/sql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/controllers/sql/__init__.py -------------------------------------------------------------------------------- /controllers/sql/basic.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | import settings 5 | 6 | from libs import __version__ 7 | from libs import iredutils, sysinfo 8 | from libs.logger import log_activity 9 | 10 | from libs.sqllib import SQLWrap, auth, decorators 11 | from libs.sqllib import admin as sql_lib_admin 12 | 13 | session = web.config.get('_session') 14 | 15 | 16 | class Login: 17 | def GET(self): 18 | if not session.get('logged'): 19 | form = web.input(_unicode=False) 20 | 21 | if not iredutils.is_allowed_admin_login_ip(client_ip=web.ctx.ip): 22 | return web.render('error_without_login.html', 23 | error='NOT_ALLOWED_IP') 24 | 25 | # Show login page. 26 | return web.render('login.html', 27 | languagemaps=iredutils.get_language_maps(), 28 | msg=form.get('msg')) 29 | else: 30 | if settings.REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN: 31 | raise web.seeother('/domains') 32 | else: 33 | raise web.seeother('/dashboard') 34 | 35 | def POST(self): 36 | # Get username, password. 37 | form = web.input(_unicode=False) 38 | 39 | username = form.get('username', '').strip().lower() 40 | password = str(form.get('password', '').strip()) 41 | 42 | # Auth as domain admin 43 | _wrap = SQLWrap() 44 | conn = _wrap.conn 45 | 46 | auth_result = auth.auth(conn=conn, 47 | username=username, 48 | password=password, 49 | account_type='admin') 50 | 51 | if auth_result[0] is True: 52 | log_activity(msg="Admin login success", event='login') 53 | 54 | # Save selected language 55 | selected_language = str(form.get('lang', '')).strip() 56 | if selected_language != web.ctx.lang and \ 57 | selected_language in iredutils.get_language_maps(): 58 | session['lang'] = selected_language 59 | 60 | account_settings = auth_result[1].get('account_settings', {}) 61 | if (not session.get('is_global_admin')) and 'create_new_domains' in account_settings: 62 | session['create_new_domains'] = True 63 | 64 | for k in ['disable_viewing_mail_log', 65 | 'disable_managing_quarantined_mails']: 66 | if account_settings.get(k) == 'yes': 67 | session[k] = True 68 | 69 | if settings.REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN: 70 | raise web.seeother('/domains') 71 | else: 72 | raise web.seeother('/dashboard?checknew') 73 | else: 74 | raise web.seeother('/login?msg=INVALID_CREDENTIALS') 75 | 76 | 77 | class Logout: 78 | def GET(self): 79 | try: 80 | session.kill() 81 | except: 82 | pass 83 | 84 | raise web.seeother('/login') 85 | 86 | 87 | class Dashboard: 88 | @decorators.require_global_admin 89 | def GET(self): 90 | form = web.input(_unicode=False) 91 | _check_new_version = ('checknew' in form) 92 | 93 | # Check new version. 94 | if session.get('is_global_admin') and _check_new_version: 95 | (_status, _info) = sysinfo.check_new_version() 96 | session['new_version_available'] = _status 97 | if _status: 98 | session['new_version'] = _info 99 | else: 100 | session['new_version_check_error'] = _info 101 | 102 | # Get numbers of domains, users, aliases. 103 | num_existing_domains = 0 104 | num_existing_users = 0 105 | 106 | _wrap = SQLWrap() 107 | conn = _wrap.conn 108 | 109 | try: 110 | num_existing_domains = sql_lib_admin.num_managed_domains(conn=conn) 111 | num_existing_users = sql_lib_admin.num_managed_users(conn=conn) 112 | except: 113 | pass 114 | 115 | # Get numbers of existing messages and quota bytes. 116 | # Set None as default, so that it's easy to detect them in Jinja2 template. 117 | total_messages = None 118 | total_bytes = None 119 | if session.get('is_global_admin'): 120 | if settings.SHOW_USED_QUOTA: 121 | try: 122 | qr = sql_lib_admin.sum_all_used_quota(conn=conn) 123 | total_messages = qr['messages'] 124 | total_bytes = qr['bytes'] 125 | except: 126 | pass 127 | 128 | return web.render( 129 | 'dashboard.html', 130 | version=__version__, 131 | iredmail_version=sysinfo.get_iredmail_version(), 132 | hostname=sysinfo.get_hostname(), 133 | uptime=sysinfo.get_server_uptime(), 134 | loadavg=sysinfo.get_system_load_average(), 135 | netif_data=sysinfo.get_nic_info(), 136 | # number of existing accounts 137 | num_existing_domains=num_existing_domains, 138 | num_existing_users=num_existing_users, 139 | total_messages=total_messages, 140 | total_bytes=total_bytes, 141 | ) 142 | -------------------------------------------------------------------------------- /controllers/sql/urls.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | from libs.regxes import email as e, domain as d 4 | 5 | # fmt: off 6 | urls = [ 7 | # Make url ending with or without '/' going to the same class. 8 | '/(.*)/', 'controllers.utils.Redirect', 9 | 10 | '/', 'controllers.sql.basic.Login', 11 | '/login', 'controllers.sql.basic.Login', 12 | '/logout', 'controllers.sql.basic.Logout', 13 | '/dashboard', 'controllers.sql.basic.Dashboard', 14 | 15 | # Domain related. 16 | '/domains', 'controllers.sql.domain.List', 17 | r'/domains/page/(\d+)', 'controllers.sql.domain.List', 18 | # List disabled accounts. 19 | '/domains/disabled', 'controllers.sql.domain.ListDisabled', 20 | r'/domains/disabled/page/(\d+)', 'controllers.sql.domain.ListDisabled', 21 | # Domain profiles 22 | '/profile/domain/(general)/(%s$)' % d, 'controllers.sql.domain.Profile', 23 | '/profile/domain/(advanced)/(%s$)' % d, 'controllers.sql.domain.Profile', 24 | '/profile/domain/(%s)' % d, 'controllers.sql.domain.Profile', 25 | '/create/domain', 'controllers.sql.domain.Create', 26 | 27 | # Admin related. 28 | '/admins', 'controllers.sql.admin.List', 29 | r'/admins/page/(\d+)', 'controllers.sql.admin.List', 30 | '/profile/admin/(general)/(%s$)' % e, 'controllers.sql.admin.Profile', 31 | '/profile/admin/(password)/(%s$)' % e, 'controllers.sql.admin.Profile', 32 | '/create/admin', 'controllers.sql.admin.Create', 33 | 34 | # Redirect to first mail domain. 35 | '/create/(user)', 'controllers.sql.utils.CreateDispatcher', 36 | 37 | # User related. 38 | '/users/(%s$)' % d, 'controllers.sql.user.List', 39 | r'/users/(%s)/page/(\d+)' % d, 'controllers.sql.user.List', 40 | # List disabled accounts. 41 | '/users/(%s)/disabled' % d, 'controllers.sql.user.ListDisabled', 42 | r'/users/(%s)/disabled/page/(\d+)' % d, 'controllers.sql.user.ListDisabled', 43 | # Create user. 44 | '/create/user/(%s$)' % d, 'controllers.sql.user.Create', 45 | # Profile pages. 46 | '/profile/user/(general)/(%s$)' % e, 'controllers.sql.user.Profile', 47 | '/profile/user/(password)/(%s$)' % e, 'controllers.sql.user.Profile', 48 | '/profile/user/(advanced)/(%s$)' % e, 'controllers.sql.user.Profile', 49 | ] 50 | -------------------------------------------------------------------------------- /controllers/sql/utils.py: -------------------------------------------------------------------------------- 1 | import web 2 | from controllers import decorators 3 | from libs.sqllib import SQLWrap 4 | from libs.sqllib import domain as sql_lib_domain 5 | 6 | 7 | # Get all domains, select the first one. 8 | class CreateDispatcher: 9 | @decorators.require_global_admin 10 | def GET(self, account_type): 11 | _wrap = SQLWrap() 12 | conn = _wrap.conn 13 | 14 | qr = sql_lib_domain.get_all_domains(conn=conn, name_only=True) 15 | 16 | if qr[0] is True: 17 | all_domains = qr[1] 18 | 19 | # Go to first available domain. 20 | if all_domains: 21 | raise web.seeother('/create/{}/{}'.format(account_type, all_domains[0])) 22 | else: 23 | raise web.seeother('/domains?msg=NO_DOMAIN_AVAILABLE') 24 | else: 25 | raise web.seeother('/domains?msg=' + web.urlquote(qr[1])) 26 | -------------------------------------------------------------------------------- /controllers/utils.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import simplejson as json 4 | import web 5 | 6 | 7 | class Redirect: 8 | """Make url ending with or without '/' going to the same class.""" 9 | 10 | def GET(self, path): 11 | raise web.seeother("/" + str(path)) 12 | -------------------------------------------------------------------------------- /docs/README.customization: -------------------------------------------------------------------------------- 1 | * Logo image, favicon.ico, brand name and short description can be defined in 2 | config file (settings.py): 3 | 4 | ``` 5 | BRAND_LOGO = 'logo.png' # load file 'static/logo.png' 6 | BRAND_FAVICON = 'favicon.ico' # load file 'static/favicon.ico' 7 | BRAND_NAME = 'iRedAdmin' 8 | BRAND_DESC = 'iRedMail Admin Panel' 9 | ``` 10 | 11 | * Link of support page on page footer can be defined in config file (settings.py): 12 | 13 | ``` 14 | URL_SUPPORT = 'http://www.iredmail.org/support.html' 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | # Perform unit tests 2 | 3 | ## docstring tests 4 | 5 | ``` 6 | python3 -m doctest path/to/file.py 7 | ``` 8 | -------------------------------------------------------------------------------- /i18n/babel.cfg: -------------------------------------------------------------------------------- 1 | [jinja2: templates/**.html] 2 | encoding = utf-8 3 | line_statement_prefix = % 4 | -------------------------------------------------------------------------------- /i18n/bg_BG/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/bg_BG/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/cs_CZ/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/cs_CZ/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/da_DK/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/da_DK/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/de_DE/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/de_DE/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/en_US/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/en_US/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/es_ES/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/es_ES/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/fi_FI/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/fi_FI/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/fr_FR/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/fr_FR/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/hu_HU/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/hu_HU/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/it_IT/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/it_IT/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/ja_JP/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/ja_JP/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/ko_KR/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/ko_KR/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/lv_LV/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/lv_LV/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/nl_NL/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/nl_NL/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/pl_PL/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/pl_PL/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/pt_BR/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/pt_BR/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/ro_RO/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/ro_RO/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/ru_RU/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/ru_RU/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/sl_SI/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/sl_SI/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/sr/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/sr/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/sv_SE/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/sv_SE/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/translation.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Author: Zhang Huangbin (zhb _at_ iredmail.org) 4 | 5 | #--------------------------------------------------------------------- 6 | # This file is part of iRedAdmin-Pro, which is official web-based admin 7 | # panel (Full-Featured Edition) for iRedMail. 8 | # 9 | # ---- Restrictions ---- 10 | # * Source code is only available after you purchase it, so that you can 11 | # modify it to fit your need, but it is NOT allowed to redistribute 12 | # and sell iRedAdmin and the one you modified based on iRedAdmin. 13 | # 14 | # * We will do our best to solve all bugs found in official iRedAdmin, 15 | # but we are not guarantee to solve bugs occured in your modified copy. 16 | # 17 | # * It is NOT allowed to deployed on more than 1 server. 18 | # 19 | #--------------------------------------------------------------------- 20 | 21 | # Available actions: [all, LANG]. 22 | ACTIONORLANG="$1" 23 | 24 | if [ -z "${ACTIONORLANG}" ]; then 25 | cat </dev/null 58 | } 59 | 60 | update_po() 61 | { 62 | # Update PO files. 63 | echo "* Updating existing translations ..." 64 | 65 | for lang in ${LANGUAGES} 66 | do 67 | [ -d ${lang}/LC_MESSAGES/ ] || mkdir -p ${lang}/LC_MESSAGES/ 68 | pybabel update --ignore-obsolete\ 69 | -i ${POFILE} \ 70 | -D ${DOMAIN} \ 71 | -d . \ 72 | -l ${lang} 73 | 74 | # Remove 'fuzzy' tag. 75 | perl -pi -e 's/#, fuzzy//' ${lang}/LC_MESSAGES/${DOMAIN}.po 76 | 77 | # Comment ', python-format'. 78 | perl -pi -e 's/^(, python-format.*)/#${1}/' ${lang}/LC_MESSAGES/${DOMAIN}.po 79 | done 80 | } 81 | 82 | convert_po_to_mo() 83 | { 84 | for lang in ${LANGUAGES}; do 85 | echo " + Converting ${lang} ..." 86 | msgfmt --statistics --check-format ${lang}/LC_MESSAGES/${DOMAIN}.po -o ${lang}/LC_MESSAGES/${DOMAIN}.mo 87 | done 88 | } 89 | 90 | if [ X"${ACTIONORLANG}" == X"all" -o X"${ACTIONORLANG}" == X"" ]; then 91 | export LANGUAGES="${AVAILABLE_LANGS}" 92 | else 93 | export LANGUAGES="$(basename ${ACTIONORLANG})" 94 | fi 95 | 96 | extract_latest && \ 97 | update_po && \ 98 | convert_po_to_mo 99 | -------------------------------------------------------------------------------- /i18n/zh_CN/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/zh_CN/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /i18n/zh_TW/LC_MESSAGES/iredadmin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/i18n/zh_TW/LC_MESSAGES/iredadmin.mo -------------------------------------------------------------------------------- /iredadmin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Author: Zhang Huangbin 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 8 | from libs import iredbase 9 | 10 | # Initialize webpy app. 11 | app = iredbase.app 12 | 13 | if __name__ == "__main__": 14 | # Starting webpy builtin http server. 15 | # WARNING: this should not be used for production. 16 | app.run() 17 | else: 18 | # Run as a WSGI application 19 | application = app.wsgifunc() 20 | -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Zhang Huangbin" 2 | __author_mail__ = "zhb@iredmail.org" 3 | __version__ = "2.6" 4 | -------------------------------------------------------------------------------- /libs/hooks.py: -------------------------------------------------------------------------------- 1 | import web 2 | 3 | 4 | def hook_set_language(): 5 | # parameter `lang` in URI. e.g. https://xxx/?lang=en_US 6 | _lang = web.input(lang=None, _method="GET").get("lang") 7 | 8 | # parameter `lang` in session. 9 | if not _lang: 10 | _lang = web.config.get("_session", {}).get("lang") 11 | 12 | web.ctx.lang = _lang or "en_US" 13 | -------------------------------------------------------------------------------- /libs/ireddate.py: -------------------------------------------------------------------------------- 1 | import time 2 | import re 3 | from datetime import tzinfo, timedelta, datetime 4 | 5 | from libs.l10n import TIMEZONE_OFFSETS 6 | import settings 7 | 8 | __timezone__ = None 9 | __local_timezone__ = None 10 | __timezones__ = {} 11 | 12 | DEFAULT_DATETIME_INPUT_FORMATS = ( 13 | "%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59' 14 | "%Y-%m-%d %H:%M", # '2006-10-25 14:30' 15 | "%Y-%m-%d", # '2006-10-25' 16 | "%Y/%m/%d %H:%M:%S", # '2006/10/25 14:30:59' 17 | "%Y/%m/%d %H:%M", # '2006/10/25 14:30' 18 | "%Y/%m/%d ", # '2006/10/25 ' 19 | "%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59' 20 | "%m/%d/%Y %H:%M", # '10/25/2006 14:30' 21 | "%m/%d/%Y", # '10/25/2006' 22 | "%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59' 23 | "%m/%d/%y %H:%M", # '10/25/06 14:30' 24 | "%m/%d/%y", # '10/25/06' 25 | "%H:%M:%S", # '14:30:59' 26 | "%H:%M", # '14:30' 27 | ) 28 | 29 | ZERO = timedelta(0) 30 | 31 | 32 | class UTCTimeZone(tzinfo): 33 | """UTC""" 34 | 35 | def utcoffset(self, dt): 36 | return ZERO 37 | 38 | def tzname(self, dt): 39 | return "UTC" 40 | 41 | def dst(self, dt): 42 | return ZERO 43 | 44 | def __repr__(self): 45 | return "" 46 | 47 | 48 | UTC = UTCTimeZone() 49 | 50 | 51 | class FixedOffset(tzinfo): 52 | """Fixed offset in minutes east from UTC.""" 53 | 54 | def __init__(self, offset, name): 55 | self.__offset = timedelta(minutes=offset) 56 | self.__name = name 57 | 58 | def utcoffset(self, dt): 59 | return self.__offset 60 | 61 | def tzname(self, dt): 62 | return self.__name 63 | 64 | def dst(self, dt): 65 | return ZERO 66 | 67 | 68 | for (tzname, offset) in list(TIMEZONE_OFFSETS.items()): 69 | __timezones__[tzname] = FixedOffset(offset, tzname) 70 | 71 | re_timezone = re.compile(r"GMT\s?([+-]?)(\d+):(\d\d)", re.IGNORECASE) 72 | 73 | 74 | def fix_gmt_timezone(tz): 75 | if isinstance(tz, str): 76 | b = re_timezone.match(tz) 77 | if b: 78 | sign = b.group(1) 79 | if not sign: 80 | sign = "+" 81 | 82 | hour = b.group(2) 83 | if hour in ["0", "00"]: 84 | return "UTC" 85 | 86 | minute = b.group(3) 87 | return "GMT" + sign + hour + ":" + minute 88 | return tz 89 | 90 | 91 | def set_local_timezone(tz): 92 | global __local_timezone__ 93 | __local_timezone__ = timezone(tz) 94 | 95 | 96 | def get_local_timezone(): 97 | return __local_timezone__ 98 | 99 | 100 | def timezone(tzname): 101 | # Validate tzname and return it 102 | if not tzname: 103 | return None 104 | 105 | if isinstance(tzname, str): 106 | # not pytz module imported, so just return None 107 | tzname = fix_gmt_timezone(tzname) 108 | tz = __timezones__.get(tzname, None) 109 | if not tz: 110 | tz = UTC 111 | return tz 112 | elif isinstance(tzname, tzinfo): 113 | return tzname 114 | else: 115 | return UTC 116 | 117 | 118 | def pick_timezone(*args): 119 | for x in args: 120 | tz = timezone(x) 121 | if tz: 122 | return tz 123 | 124 | 125 | def to_timezone(dt, tzinfo=None): 126 | """ 127 | Convert a datetime to timezone 128 | """ 129 | if not dt: 130 | return dt 131 | tz = pick_timezone(tzinfo, __timezone__) 132 | if not tz: 133 | return dt 134 | dttz = getattr(dt, "tzinfo", None) 135 | if not dttz: 136 | return dt.replace(tzinfo=tz) 137 | else: 138 | return dt.astimezone(tz) 139 | 140 | 141 | def to_datetime_with_tzinfo(dt, tzinfo=None, formatstr=None): 142 | """ 143 | Convert a date or time to datetime with tzinfo 144 | """ 145 | if not dt: 146 | return dt 147 | 148 | tz = pick_timezone(tzinfo, __timezone__) 149 | 150 | if isinstance(dt, str): 151 | if not formatstr: 152 | formats = DEFAULT_DATETIME_INPUT_FORMATS 153 | else: 154 | formats = list(formatstr) 155 | d = None 156 | for fmt in formats: 157 | try: 158 | d = datetime(*time.strptime(dt, fmt)[:6]) 159 | except ValueError: 160 | continue 161 | if not d: 162 | return None 163 | d = d.replace(tzinfo=tz) 164 | else: 165 | d = datetime( 166 | getattr(dt, "year", 1970), 167 | getattr(dt, "month", 1), 168 | getattr(dt, "day", 1), 169 | getattr(dt, "hour", 0), 170 | getattr(dt, "minute", 0), 171 | getattr(dt, "second", 0), 172 | getattr(dt, "microsecond", 0), 173 | ) 174 | 175 | if not getattr(dt, "tzinfo", None): 176 | d = d.replace(tzinfo=tz) 177 | else: 178 | d = d.replace(tzinfo=dt.tzinfo) 179 | return to_timezone(d, tzinfo) 180 | 181 | 182 | def utc_to_timezone(dt, timezone=None, formatstr="%Y-%m-%d %H:%M:%S"): 183 | if not timezone: 184 | timezone = settings.LOCAL_TIMEZONE 185 | 186 | # Convert original timestamp to new timestamp with UTC timezone. 187 | t = to_datetime_with_tzinfo(dt, tzinfo=UTC) 188 | 189 | # Convert original timestamp (with UTC timezone) to timestamp with 190 | # local timezone. 191 | ft = to_datetime_with_tzinfo(t, tzinfo=timezone) 192 | 193 | if ft: 194 | # Check 'daylight saving time' 195 | if time.localtime().tm_isdst: 196 | ft += timedelta(seconds=3600) 197 | 198 | return ft.strftime(formatstr) 199 | else: 200 | return "--" 201 | -------------------------------------------------------------------------------- /libs/jinja_filters.py: -------------------------------------------------------------------------------- 1 | """Custom Jinja2 filters.""" 2 | 3 | 4 | def file_size_format(value, base_mb=False): 5 | """Convert file size to a human-readable format, e.g. 20 MB, 1 GB, 2 TB. 6 | 7 | @value -- file size in KB 8 | @base_mb -- if True, @value is in MB. 9 | """ 10 | ret = "0" 11 | 12 | try: 13 | _bytes = float(value) 14 | except: 15 | return ret 16 | 17 | if base_mb: 18 | _bytes = _bytes * 1024 * 1024 19 | 20 | # byte 21 | base = 1024 22 | 23 | if _bytes == 0: 24 | return ret 25 | 26 | if _bytes < base: 27 | ret = "%d Bytes" % (_bytes) 28 | elif _bytes < base * base: 29 | ret = "%d KB" % (_bytes / base) 30 | elif _bytes < base * base * base: 31 | ret = "%d MB" % (_bytes / (base * base)) 32 | elif _bytes < base * base * base * base: 33 | if _bytes % (base * base * base) == 0: 34 | ret = "%d GB" % (_bytes / (base * base * base)) 35 | else: 36 | ret = "%.2f GB" % (_bytes / (base * base * base)) 37 | else: 38 | if _bytes % (base * base * base * base) == 0: 39 | ret = "%d TB" % (_bytes / (base * base * base * base)) 40 | else: 41 | ret = "%d GB" % (_bytes / (base * base * base)) 42 | 43 | return ret 44 | 45 | 46 | def cut_string(s, length=40): 47 | try: 48 | if len(s) != len(s.encode("utf-8", "replace")): 49 | length = length / 2 50 | 51 | if len(s) >= length: 52 | return s[:length] + "..." 53 | else: 54 | return s 55 | except UnicodeDecodeError: 56 | return str(s, encoding="utf-8", errors="replace") 57 | except: 58 | return s 59 | 60 | 61 | # Return value of percentage. 62 | def convert_to_percentage(current, total): 63 | try: 64 | current = int(current) 65 | total = int(total) 66 | except: 67 | return 0 68 | 69 | if current == 0 or total == 0: 70 | return 0 71 | else: 72 | percent = (current * 100) // total 73 | if percent < 0: 74 | return 0 75 | elif percent > 100: 76 | return 100 77 | else: 78 | return percent 79 | -------------------------------------------------------------------------------- /libs/ldaplib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/libs/ldaplib/__init__.py -------------------------------------------------------------------------------- /libs/ldaplib/auth.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | import ldap 5 | import settings 6 | from libs import iredutils, iredpwd 7 | from libs.l10n import TIMEZONES 8 | 9 | from libs.ldaplib.core import LDAPWrap 10 | from libs.ldaplib import ldaputils 11 | 12 | session = web.config.get('_session') 13 | 14 | 15 | # Verify bind dn/pw or return LDAP connection object 16 | # Return True if bind success, error message (string) if failed 17 | def verify_bind_dn_pw(dn, password, conn=None): 18 | dn = web.safestr(dn.strip()) 19 | password = password.strip() 20 | 21 | if not conn: 22 | _wrap = LDAPWrap() 23 | conn = _wrap.conn 24 | 25 | try: 26 | qr = conn.search_s(dn, 27 | ldap.SCOPE_BASE, 28 | '(objectClass=*)', 29 | ['userPassword']) 30 | if not qr: 31 | # No such account. 32 | return (False, 'INVALID_CREDENTIALS') 33 | 34 | _ldif = iredutils.bytes2str(qr[0][1]) 35 | pw = _ldif.get('userPassword', [''])[0] 36 | if iredpwd.verify_password_hash(pw, password): 37 | return (True, ) 38 | else: 39 | return (False, 'INVALID_CREDENTIALS') 40 | except Exception as e: 41 | return (False, repr(e)) 42 | 43 | 44 | # Used for user auth. 45 | def login_auth(username, 46 | password, 47 | account_type='user', 48 | conn=None): 49 | """Perform full login. 50 | 51 | @username -- full email address 52 | @password -- account password 53 | @account_type -- user, admin 54 | @conn -- ldap connection cursor 55 | """ 56 | if account_type == 'user': 57 | dn = ldaputils.rdn_value_to_user_dn(username) 58 | search_filter = '(&(accountStatus=active)(objectClass=mailUser))' 59 | elif account_type == 'admin': 60 | dn = ldaputils.rdn_value_to_admin_dn(username) 61 | search_filter = '(&(accountStatus=active)(objectClass=mailAdmin))' 62 | else: 63 | return (False, 'INVALID_CREDENTIALS') 64 | 65 | if not conn: 66 | _wrap = LDAPWrap() 67 | conn = _wrap.conn 68 | 69 | qr = verify_bind_dn_pw(dn=dn, password=password, conn=conn) 70 | if not qr[0]: 71 | return qr 72 | 73 | # Update session data to indicate this is an global admin, normal admin, 74 | # normal mail user (self-service). 75 | _attrs = ['objectClass', 'mail', 'domainGlobalAdmin', 76 | 'enabledService', 'disabledService', 'accountSetting'] 77 | qr = conn.search_s(dn, 78 | ldap.SCOPE_BASE, 79 | search_filter, 80 | _attrs) 81 | 82 | if not qr: 83 | # No such account. 84 | # WARN: Do not return message like 'INVALID USER', it will help 85 | # cracker to perdict user existence. 86 | return (False, 'INVALID_CREDENTIALS') 87 | 88 | (_dn, _ldif) = qr[0] 89 | _ldif = iredutils.bytes2str(_ldif) 90 | 91 | _object_classes = _ldif.get('objectClass', []) 92 | _disabled_services = _ldif.get('disabledService', []) 93 | 94 | if _ldif.get('domainGlobalAdmin', ['no'])[0].lower() == 'yes': 95 | session['is_global_admin'] = True 96 | 97 | if 'mailUser' in _object_classes: 98 | # Make sure user have 'domainGlobalAdmin=yes' for global 99 | # admin or 'enabledService=domainadmin' for domain admin. 100 | if session.get('is_global_admin'): 101 | session['admin_is_mail_user'] = True 102 | 103 | if session['is_global_admin']: 104 | if not iredutils.is_allowed_global_admin_login_ip(client_ip=web.ctx.ip): 105 | session.kill() 106 | raise web.seeother('/login?msg=NOT_ALLOWED_IP') 107 | 108 | # Language 109 | lang = _ldif.get('preferredLanguage', [settings.default_language])[0] 110 | session['lang'] = iredutils.bytes2str(lang) 111 | 112 | # 113 | # disabledService 114 | # 115 | if 'view_mail_log' in _disabled_services: 116 | session['disable_viewing_mail_log'] = True 117 | 118 | if 'manage_quarantined_mails' in _disabled_services: 119 | session['disable_managing_quarantined_mails'] = True 120 | 121 | # 122 | # accountSetting 123 | # 124 | _as = ldaputils.account_setting_list_to_dict(_ldif.get('accountSetting', [])) 125 | if 'create_new_domains' in _as: 126 | session['create_new_domains'] = True 127 | 128 | # per-account time zone 129 | tz_name = iredutils.bytes2str(_as.get('timezone')) 130 | if tz_name in TIMEZONES: 131 | timezone = TIMEZONES[tz_name] 132 | session['timezone'] = timezone 133 | 134 | return (True, ) 135 | -------------------------------------------------------------------------------- /libs/ldaplib/core.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import ldap 4 | import settings 5 | from libs.logger import logger 6 | 7 | 8 | class LDAPWrap: 9 | def __init__(self): 10 | # Initialize LDAP connection. 11 | self.conn = None 12 | 13 | uri = settings.ldap_uri 14 | 15 | # Detect STARTTLS support. 16 | starttls = False 17 | if uri.startswith('ldaps://'): 18 | starttls = True 19 | 20 | # Rebuild uri, use ldap:// + STARTTLS (with normal port 389) 21 | # instead of ldaps:// (port 636) for secure connection. 22 | uri = uri.replace('ldaps://', 'ldap://') 23 | 24 | # Don't check CA cert 25 | ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) 26 | 27 | self.conn = ldap.initialize(uri=uri) 28 | 29 | # Set LDAP protocol version: LDAP v3. 30 | self.conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) 31 | 32 | if starttls: 33 | self.conn.start_tls_s() 34 | 35 | try: 36 | # bind as vmailadmin 37 | self.conn.bind_s(settings.ldap_bind_dn, settings.ldap_bind_password) 38 | except Exception as e: 39 | logger.error('VMAILADMIN_INVALID_CREDENTIALS. Detail: %s' % repr(e)) 40 | 41 | def __del__(self): 42 | try: 43 | if self.conn: 44 | self.conn.unbind() 45 | except: 46 | pass 47 | -------------------------------------------------------------------------------- /libs/ldaplib/decorators.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | from controllers import decorators as base_decorators 4 | 5 | require_login = base_decorators.require_login 6 | require_global_admin = base_decorators.require_global_admin 7 | csrf_protected = base_decorators.csrf_protected 8 | -------------------------------------------------------------------------------- /libs/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import traceback 4 | from logging.handlers import SysLogHandler 5 | 6 | import web 7 | from libs import iredutils 8 | import settings 9 | 10 | session = web.config.get("_session") 11 | 12 | # Set application name. 13 | logger = logging.getLogger("iredadmin") 14 | 15 | # Set log level. 16 | _log_level = getattr(logging, str(settings.LOG_LEVEL).upper()) 17 | logger.setLevel(_log_level) 18 | 19 | if settings.LOG_TARGET == "stdout": 20 | _handler = logging.StreamHandler() 21 | _formatter = logging.Formatter("%(message)s (%(pathname)s, L%(lineno)d)") 22 | else: 23 | # Defaults to "syslog": 24 | _facility = getattr(SysLogHandler, "LOG_" + settings.SYSLOG_FACILITY.upper()) 25 | _formatter = logging.Formatter("%(name)s %(message)s (%(pathname)s, L%(lineno)d)") 26 | 27 | if settings.SYSLOG_SERVER.startswith("/"): 28 | # Log to a local socket 29 | _handler = SysLogHandler(address=settings.SYSLOG_SERVER, facility=_facility) 30 | else: 31 | # Log to a network address 32 | _server = (settings.SYSLOG_SERVER, settings.SYSLOG_PORT) 33 | _handler = SysLogHandler(address=_server, facility=_facility) 34 | 35 | _handler.setFormatter(_formatter) 36 | logger.addHandler(_handler) 37 | 38 | 39 | def log_traceback(): 40 | exc_type, exc_value, exc_traceback = sys.exc_info() 41 | msg = traceback.format_exception(exc_type, exc_value, exc_traceback) 42 | logger.error(msg) 43 | 44 | 45 | def log_activity(msg, admin="", domain="", username="", event="", loglevel="info"): 46 | try: 47 | if not admin: 48 | admin = session.get("username") 49 | 50 | msg = str(msg) 51 | 52 | # Prepend '[API]' in log message 53 | try: 54 | if web.ctx.fullpath.startswith("/api/"): 55 | msg = "[API] " + msg 56 | except: 57 | pass 58 | 59 | web.conn_iredadmin.insert( 60 | "log", 61 | admin=str(admin), 62 | domain=str(domain), 63 | username=str(username), 64 | loglevel=str(loglevel), 65 | event=str(event), 66 | msg=msg, 67 | ip=str(session.ip), 68 | timestamp=iredutils.get_gmttime(), 69 | ) 70 | 71 | if loglevel == "info": 72 | logger.info("{0} admin={1}, domain={2}, username={3}, event={4}, " 73 | "ip={5}".format(msg, admin, domain, username, event, session.ip)) 74 | elif loglevel == "error": 75 | logger.error("{0} admin={1}, domain={2}, username={3}, event={4}, " 76 | "ip={5}".format(msg, admin, domain, username, event, session.ip)) 77 | except: 78 | pass 79 | 80 | return None 81 | -------------------------------------------------------------------------------- /libs/mailparser.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import email 4 | from email.header import decode_header 5 | from libs.logger import log_traceback 6 | 7 | 8 | def __decode_headers(msg): 9 | """Decode message into list of {header: value}.""" 10 | 11 | # List of {header: value} pairs. 12 | headers = [] 13 | 14 | # header 'From: name ' 15 | header_from = [] 16 | 17 | for (header, value) in list(msg.items()): 18 | for (text, encoding) in decode_header(value): 19 | if not encoding: 20 | encoding = 'utf-8' 21 | 22 | try: 23 | value = str(text, encoding=encoding, errors='replace') 24 | except: 25 | continue 26 | 27 | if header == 'From': 28 | header_from.append(value) 29 | else: 30 | headers += [{header: value}] 31 | 32 | if header_from: 33 | headers = [{'From': ' '.join(header_from)}] + headers 34 | 35 | return headers 36 | 37 | 38 | def parse_raw_message(msg: bytes): 39 | """Read RAW message from string. Return tuple of: 40 | 41 | list of multiple mail headers: [[header: value], [header: value], ...] 42 | list of (multiple) body parts: [part1, part2, ...] 43 | list of attachment file names: [name1, name2, ...] 44 | """ 45 | 46 | # Get all mail headers. Sample: 47 | # [{'From': 'sender@xx.com'}, {'To': 'recipient@xx.net'}] 48 | headers = [] 49 | 50 | # Get decoded content parts of mail body. 51 | bodies = [] 52 | 53 | # Get list of attachment names. 54 | attachments = [] 55 | 56 | msg = email.message_from_bytes(msg) 57 | 58 | # Extract all headers. 59 | for i in __decode_headers(msg): 60 | for k in i: 61 | headers += [(k, i[k])] 62 | 63 | for part in msg.walk(): 64 | _content_type = part.get_content_maintype() 65 | 66 | # multipart/* is just a container 67 | if _content_type == 'multipart': 68 | continue 69 | 70 | # either a string or None. 71 | _filename = part.get_filename() 72 | if _filename: 73 | attachments += [_filename] 74 | 75 | if _content_type == 'text': 76 | # Plain text, not an attachment. 77 | try: 78 | if part.get_content_charset(): 79 | encoding = part.get_content_charset() 80 | elif part.get_charset(): 81 | encoding = part.get_charset() 82 | else: 83 | encoding = 'utf-8' 84 | 85 | text = str(part.get_payload(decode=True), 86 | encoding=encoding, 87 | errors='replace') 88 | 89 | text = text.strip() 90 | bodies.append(text) 91 | except: 92 | log_traceback() 93 | 94 | return (headers, bodies, attachments) 95 | -------------------------------------------------------------------------------- /libs/panel/__init__.py: -------------------------------------------------------------------------------- 1 | # Events in admin log. Detailed comments of event names are defined in 2 | # templates/default/macros/general.html 3 | LOG_EVENTS = [ 4 | 'all', 5 | 'login', 6 | 'user_login', 7 | 'active', 8 | 'disable', 9 | 'create', 10 | 'delete', 11 | 'update', 12 | 'grant', # Grant user as domain admin 13 | 'revoke', # Revoke admin privilege 14 | 'backup', 15 | 'delete_mailboxes', 16 | 'update_wblist', 17 | 'iredapd', # iRedAPD rejection. 18 | 'unban', # Unban IP address 19 | ] 20 | -------------------------------------------------------------------------------- /libs/panel/log.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | 5 | import settings 6 | from libs import iredutils 7 | from libs.panel import LOG_EVENTS 8 | 9 | if settings.backend == 'ldap': 10 | from libs.ldaplib.general import is_domain_admin 11 | else: 12 | from libs.sqllib.general import is_domain_admin 13 | 14 | session = web.config.get('_session') 15 | 16 | 17 | def list_logs(event='all', domain='all', admin='all', cur_page=1): 18 | event = web.safestr(event) 19 | domain = web.safestr(domain) 20 | admin = web.safestr(admin) 21 | cur_page = int(cur_page) 22 | 23 | sql_vars = {} 24 | sql_wheres = [] 25 | sql_where = '' 26 | 27 | if event in LOG_EVENTS and event != 'all': 28 | sql_vars['event'] = event 29 | sql_wheres += ["event=$event"] 30 | 31 | if iredutils.is_domain(domain): 32 | if session.get('is_global_admin') or is_domain_admin(domain=domain, admin=session['username'], conn=None): 33 | sql_vars['domain'] = domain 34 | sql_wheres += ["domain=$domain"] 35 | 36 | if not session.get('is_global_admin'): 37 | sql_vars['admin'] = session.get('username') 38 | sql_wheres += ["admin=$admin"] 39 | else: 40 | if iredutils.is_email(admin): 41 | sql_vars['admin'] = admin 42 | sql_wheres += ["admin=$admin"] 43 | 44 | # Get number of total records. 45 | if sql_wheres: 46 | sql_where = ' AND '.join(sql_wheres) 47 | 48 | qr = web.conn_iredadmin.select( 49 | 'log', 50 | vars=sql_vars, 51 | what='COUNT(id) AS total', 52 | where=sql_where, 53 | ) 54 | else: 55 | qr = web.conn_iredadmin.select('log', what='COUNT(id) AS total') 56 | 57 | total = qr[0].total or 0 58 | 59 | # Get records. 60 | if sql_wheres: 61 | qr = web.conn_iredadmin.select( 62 | 'log', 63 | vars=sql_vars, 64 | where=sql_where, 65 | offset=(cur_page - 1) * settings.PAGE_SIZE_LIMIT, 66 | limit=settings.PAGE_SIZE_LIMIT, 67 | order='timestamp DESC', 68 | ) 69 | else: 70 | # No addition filter. 71 | qr = web.conn_iredadmin.select( 72 | 'log', 73 | offset=(cur_page - 1) * settings.PAGE_SIZE_LIMIT, 74 | limit=settings.PAGE_SIZE_LIMIT, 75 | order='timestamp DESC', 76 | ) 77 | 78 | return (total, list(qr)) 79 | 80 | 81 | def delete_logs(form, delete_all=False): 82 | if delete_all: 83 | try: 84 | web.conn_iredadmin.delete('log', where="1=1") 85 | return (True, ) 86 | except Exception as e: 87 | return (False, repr(e)) 88 | else: 89 | ids = form.get('id', []) 90 | 91 | if ids: 92 | try: 93 | web.conn_iredadmin.delete('log', where="id IN %s" % web.db.sqlquote(ids)) 94 | return (True, ) 95 | except Exception as e: 96 | return (False, repr(e)) 97 | 98 | return (True, ) 99 | -------------------------------------------------------------------------------- /libs/regxes.py: -------------------------------------------------------------------------------- 1 | # Regular expressions of email address, IP address, network. 2 | 3 | import re 4 | 5 | # Email address. 6 | # 7 | # - `+`, `=` are used in SRS rewritten addresses. 8 | # - `/` is sub-folder. e.g. 'john+lists/abc/def@domain.com' will create 9 | # directory `lists` and its sub-folders `lists/abc/`, `lists/abc/def`. 10 | email = r"""[\w\-\#][\w\-\.\+\=\/\&\#]*@[\w\-][\w\-\.]*\.[a-zA-Z0-9\-]{2,15}""" 11 | cmp_email = re.compile(r"^" + email + r"$", re.IGNORECASE | re.DOTALL) 12 | 13 | # Email address allowed by locally created mail user. 14 | # 15 | # `auth_email` allows less characters than `email`. 16 | # Disallowed chars: `+`, `=`, `/`. 17 | auth_email = r"""[\w\-\#][\w\-\.\&\#]*@[\w\-][\w\-\.]*\.[a-zA-Z0-9\-]{2,15}""" 18 | cmp_auth_email = re.compile(r"^" + auth_email + r"$", re.IGNORECASE | re.DOTALL) 19 | 20 | # Wildcard sender address: 'user@*' 21 | wildcard_addr = r"""[\w\-][\w\-\.\+\=]*@\*""" 22 | cmp_wildcard_addr = re.compile(r"^" + wildcard_addr + r"$", re.IGNORECASE | re.DOTALL) 23 | 24 | # 25 | # Domain name 26 | # 27 | # Single domain name. 28 | domain = r"""[\w\-][\w\-\.]*\.[a-z0-9\-]{2,25}""" 29 | cmp_domain = re.compile(r"^" + domain + r"$", re.IGNORECASE | re.DOTALL) 30 | 31 | # Top level domain. e.g. .com, .biz, .org. 32 | top_level_domain = r"""[a-z0-9\-]{2,25}""" 33 | cmp_top_level_domain = re.compile( 34 | r"^" + top_level_domain + r"$", re.IGNORECASE | re.DOTALL 35 | ) 36 | 37 | # Valid first char of domain name, email address. 38 | valid_account_first_char = r"""^[0-9a-zA-Z]{1,1}$""" 39 | cmp_valid_account_first_char = re.compile( 40 | r"^" + valid_account_first_char + r"$", re.IGNORECASE 41 | ) 42 | 43 | # WARNING: This is used for simple URL matching, not used to verify IP address. 44 | ip = r"[0-9a-zA-Z\.\:]+" 45 | 46 | # Wildcard IPv4: 192.168.0.* 47 | wildcard_ipv4 = r"(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})$" 48 | cmp_wildcard_ipv4 = re.compile(wildcard_ipv4, re.IGNORECASE | re.DOTALL) 49 | 50 | # Mailing list id, a server-wide unique 36-char string. 51 | mailing_list_id = r"[a-zA-Z0-9\-]{36}" 52 | cmp_mailing_list_id = re.compile(r"^" + mailing_list_id + r"$") 53 | 54 | # Mailing list subscription confirm token. a 32-char string. 55 | mailing_list_confirm_token = r"[a-zA-Z0-9]{32}" 56 | cmp_mailing_list_confirm_token = re.compile(r"^" + mailing_list_confirm_token + r"$") 57 | 58 | # 59 | # Mailbox 60 | # 61 | mailbox_folder = r"""[a-zA-Z0-9]{1,20}""" 62 | cmp_mailbox_folder = re.compile(r"^" + mailbox_folder + r"$") 63 | -------------------------------------------------------------------------------- /libs/sqllib/__init__.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | import settings 5 | 6 | from libs.logger import logger 7 | 8 | 9 | class MYSQLWrap: 10 | def __del__(self): 11 | try: 12 | self.conn.ctx.db.close() 13 | except: 14 | pass 15 | 16 | def connect(self): 17 | conn = web.database( 18 | dbn='mysql', 19 | host=settings.vmail_db_host, 20 | port=int(settings.vmail_db_port), 21 | db=settings.vmail_db_name, 22 | user=settings.vmail_db_user, 23 | pw=settings.vmail_db_password, 24 | charset='utf8') 25 | 26 | conn.supports_multiple_insert = True 27 | 28 | return conn 29 | 30 | def __init__(self): 31 | try: 32 | self.conn = self.connect() 33 | except AttributeError: 34 | # Reconnect if error raised: MySQL server has gone away. 35 | self.conn = self.connect() 36 | except Exception as e: 37 | logger.error(e) 38 | 39 | 40 | class PGSQLWrap: 41 | def __del__(self): 42 | try: 43 | self.conn.ctx.db.close() 44 | except: 45 | pass 46 | 47 | def __init__(self): 48 | # Initial DB connection and cursor. 49 | try: 50 | self.conn = web.database( 51 | dbn='postgres', 52 | host=settings.vmail_db_host, 53 | port=int(settings.vmail_db_port), 54 | db=settings.vmail_db_name, 55 | user=settings.vmail_db_user, 56 | pw=settings.vmail_db_password, 57 | ) 58 | self.conn.supports_multiple_insert = True 59 | except Exception as e: 60 | logger.error(e) 61 | 62 | 63 | if settings.backend == 'mysql': 64 | SQLWrap = MYSQLWrap 65 | elif settings.backend == 'pgsql': 66 | SQLWrap = PGSQLWrap 67 | -------------------------------------------------------------------------------- /libs/sqllib/auth.py: -------------------------------------------------------------------------------- 1 | import web 2 | import settings 3 | from libs import iredutils, iredpwd 4 | from libs.l10n import TIMEZONES 5 | from libs.sqllib import sqlutils 6 | 7 | session = web.config.get('_session', {}) 8 | 9 | 10 | def auth(conn, 11 | username, 12 | password, 13 | account_type='admin', 14 | verify_password=False): 15 | if not iredutils.is_email(username): 16 | return (False, 'INVALID_USERNAME') 17 | 18 | if not password: 19 | return (False, 'EMPTY_PASSWORD') 20 | 21 | username = str(username).lower() 22 | password = str(password) 23 | domain = username.split('@', 1)[-1] 24 | 25 | # Query account from SQL database. 26 | if account_type == 'admin': 27 | # separate admin accounts 28 | result = conn.select('admin', 29 | vars={'username': username}, 30 | where="username=$username AND active=1", 31 | what='password, language, settings', 32 | limit=1) 33 | 34 | # mail user marked as domain admin 35 | if not result: 36 | result = conn.select( 37 | 'mailbox', 38 | vars={'username': username}, 39 | where="username=$username AND isglobaladmin=1 AND active=1", 40 | what='password, language, isadmin, isglobaladmin, settings', 41 | limit=1, 42 | ) 43 | if result: 44 | session['admin_is_mail_user'] = True 45 | elif account_type == 'user': 46 | result = conn.select('mailbox', 47 | vars={'username': username}, 48 | what='password, language, isadmin, isglobaladmin, settings', 49 | where="username=$username AND isglobaladmin=1 AND active=1", 50 | limit=1) 51 | else: 52 | return (False, 'INVALID_ACCOUNT_TYPE') 53 | 54 | if not result: 55 | # Account not found. 56 | # Do NOT return msg like 'Account does not ***EXIST***', crackers 57 | # can use it to verify valid accounts. 58 | return (False, 'INVALID_CREDENTIALS') 59 | 60 | record = result[0] 61 | password_sql = str(record.password) 62 | account_settings = sqlutils.account_settings_string_to_dict(str(record.settings)) 63 | 64 | # Verify password 65 | if not iredpwd.verify_password_hash(password_sql, password): 66 | return (False, 'INVALID_CREDENTIALS') 67 | 68 | if not verify_password: 69 | session['username'] = username 70 | 71 | # Set preferred language. 72 | session['lang'] = web.safestr(record.get('language', settings.default_language)) 73 | 74 | # Set timezone (GMT-XX:XX). 75 | # Priority: per-user timezone > per-domain > global setting 76 | timezone = settings.LOCAL_TIMEZONE 77 | 78 | if 'timezone' in account_settings: 79 | tz_name = account_settings['timezone'] 80 | if tz_name in TIMEZONES: 81 | timezone = TIMEZONES[tz_name] 82 | else: 83 | # Get per-domain timezone 84 | qr_domain = conn.select('domain', 85 | vars={'domain': domain}, 86 | what='settings', 87 | where='domain=$domain', 88 | limit=1) 89 | if qr_domain: 90 | domain_settings = sqlutils.account_settings_string_to_dict(str(qr_domain[0]['settings'])) 91 | if 'timezone' in domain_settings: 92 | tz_name = domain_settings['timezone'] 93 | if tz_name in TIMEZONES: 94 | timezone = TIMEZONES[tz_name] 95 | 96 | session['timezone'] = timezone 97 | 98 | # Set session['is_global_admin'] 99 | if session.get('admin_is_mail_user'): 100 | if record.get('isglobaladmin', 0) == 1: 101 | session['is_global_admin'] = True 102 | else: 103 | return (False, "INVALID_CREDENTIALS") 104 | 105 | else: 106 | try: 107 | result = conn.select('domain_admins', 108 | vars={'username': username, 'domain': 'ALL'}, 109 | what='domain', 110 | where='username=$username AND domain=$domain', 111 | limit=1) 112 | if result: 113 | session['is_global_admin'] = True 114 | else: 115 | return (False, "INVALID_CREDENTIALS") 116 | except: 117 | pass 118 | 119 | session['logged'] = True 120 | web.config.session_parameters['cookie_name'] = 'iRedAdmin' 121 | web.config.session_parameters['ignore_change_ip'] = settings.SESSION_IGNORE_CHANGE_IP 122 | web.config.session_parameters['ignore_expiry'] = False 123 | 124 | return (True, {'account_settings': account_settings}) 125 | -------------------------------------------------------------------------------- /libs/sqllib/decorators.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | from controllers import decorators as base_decorators 4 | 5 | require_login = base_decorators.require_login 6 | require_global_admin = base_decorators.require_global_admin 7 | csrf_protected = base_decorators.csrf_protected 8 | -------------------------------------------------------------------------------- /libs/sqllib/sqlutils.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | from typing import Dict 4 | 5 | 6 | def account_settings_dict_to_string(account_settings: Dict) -> str: 7 | # Convert account setting dict to string. 8 | # - dict: {'var': 'value', 'var2: value2', ...} 9 | # - string: 'var:value;var2:value2;...' 10 | if not account_settings or not isinstance(account_settings, dict): 11 | return '' 12 | 13 | for (k, v) in list(account_settings.items()): 14 | if k in ['default_groups', 15 | 'default_mailing_lists', 16 | 'enabled_services', 17 | 'disabled_mail_services', 18 | 'disabled_domain_profiles', 19 | 'disabled_user_profiles', 20 | 'disabled_user_preferences']: 21 | if isinstance(v, (list, tuple, set)): 22 | if isinstance(v, list): 23 | v.sort() 24 | elif isinstance(v, set): 25 | v = list(v) 26 | v.sort() 27 | 28 | account_settings[k] = ','.join(v) 29 | else: 30 | # Remove item if value is not a list/tuple/set 31 | account_settings.pop(k) 32 | 33 | new_settings = ';'.join(['{}:{}'.format(str(i), j) for (i, j) in list(account_settings.items()) if j]) 34 | 35 | if new_settings: 36 | new_settings += ';' 37 | 38 | return new_settings 39 | 40 | 41 | def account_settings_string_to_dict(account_settings: str) -> Dict: 42 | # Convert account setting (string, format 'var:value;var2:value2;...', used 43 | # in MySQL/PGSQL backends) to dict. 44 | # - domain.settings 45 | # - mailbox.settings 46 | # Original setting must be a string 47 | if not account_settings: 48 | return {} 49 | 50 | new_settings = {} 51 | 52 | items = [st for st in account_settings.split(';') if ':' in st] 53 | for item in items: 54 | if item: 55 | (k, v) = item.split(':') 56 | if v: 57 | new_settings[k] = v 58 | 59 | # Convert value to proper format (int, string, ...), default is string. 60 | # It will be useful to compare values with converted values. 61 | # If original value is not stored in proper format, key:value pair will 62 | # be removed. 63 | for key in new_settings: 64 | # integer 65 | if key in ['default_user_quota', 66 | 'max_user_quota', 67 | 'min_passwd_length', 68 | 'max_passwd_length', 69 | # settings used to create new domains. 70 | 'create_max_domains', 71 | 'create_max_users', 72 | 'create_max_lists', 73 | 'create_max_aliases', 74 | 'create_max_quota']: 75 | try: 76 | new_settings[key] = int(new_settings[key]) 77 | except: 78 | new_settings.pop(key) 79 | 80 | # list 81 | if key in ['enabled_services', 82 | 'disabled_mail_services', 83 | 'default_groups', 84 | 'default_mailing_lists', 85 | 'disabled_domain_profiles', 86 | 'disabled_user_profiles', 87 | 'disabled_user_preferences']: 88 | new_settings[key] = [str(i) for i in new_settings[key].split(',') if i] 89 | 90 | return new_settings 91 | -------------------------------------------------------------------------------- /libs/sqllib/utils.py: -------------------------------------------------------------------------------- 1 | # Author: Zhang Huangbin 2 | 3 | import web 4 | 5 | from libs import iredutils 6 | from libs.logger import log_activity 7 | from libs.sqllib import SQLWrap 8 | from libs.sqllib import domain as sql_lib_domain 9 | from libs.sqllib import admin as sql_lib_admin 10 | from libs.sqllib import user as sql_lib_user 11 | 12 | session = web.config.get('_session', {}) 13 | 14 | 15 | def set_account_status(conn, 16 | accounts, 17 | account_type, 18 | enable_account=False): 19 | """Set account status. 20 | 21 | accounts -- an iterable object (list/tuple) filled with accounts. 22 | account_type -- possible value: domain, admin, user, alias 23 | enable_account -- possible value: True, False 24 | """ 25 | if account_type in ['admin', 'user']: 26 | # email 27 | accounts = [str(v).lower() for v in accounts if iredutils.is_email(v)] 28 | else: 29 | # domain name 30 | accounts = [str(v).lower() for v in accounts if iredutils.is_domain(v)] 31 | 32 | if not accounts: 33 | return (True, ) 34 | 35 | # 0: disable, 1: enable 36 | account_status = 0 37 | action = 'disable' 38 | if enable_account: 39 | account_status = 1 40 | action = 'active' 41 | 42 | if account_type == 'domain': 43 | # handle with function which handles admin privilege 44 | qr = sql_lib_domain.enable_disable_domains(domains=accounts, 45 | action=action) 46 | return qr 47 | elif account_type == 'admin': 48 | # [(, ), ...] 49 | table_column_maps = [("admin", "username")] 50 | elif account_type == 'alias': 51 | table_column_maps = [ 52 | ("alias", "address"), 53 | ("forwardings", "address"), 54 | ] 55 | else: 56 | # account_type == 'user' 57 | table_column_maps = [ 58 | ("mailbox", "username"), 59 | ("forwardings", "address"), 60 | ] 61 | 62 | for (_table, _column) in table_column_maps: 63 | sql_where = '{} IN {}'.format(_column, web.sqlquote(accounts)) 64 | try: 65 | conn.update(_table, 66 | where=sql_where, 67 | active=account_status) 68 | 69 | except Exception as e: 70 | return (False, repr(e)) 71 | 72 | log_activity(event=action, 73 | msg="{} {}: {}.".format(action.title(), account_type, ', '.join(accounts))) 74 | return (True, ) 75 | 76 | 77 | def delete_accounts(accounts, 78 | account_type, 79 | keep_mailbox_days=0, 80 | conn=None): 81 | # accounts must be a list/tuple. 82 | # account_type in ['domain', 'user', 'admin', 'alias'] 83 | if not accounts: 84 | return (True, ) 85 | 86 | if not conn: 87 | _wrap = SQLWrap() 88 | conn = _wrap.conn 89 | 90 | if account_type == 'domain': 91 | qr = sql_lib_domain.delete_domains(domains=accounts, 92 | keep_mailbox_days=keep_mailbox_days, 93 | conn=conn) 94 | return qr 95 | elif account_type == 'user': 96 | sql_lib_user.delete_users(accounts=accounts, 97 | keep_mailbox_days=keep_mailbox_days, 98 | conn=conn) 99 | elif account_type == 'admin': 100 | sql_lib_admin.delete_admins(mails=accounts, conn=conn) 101 | 102 | return (True, ) 103 | -------------------------------------------------------------------------------- /rc_scripts/iredadmin.debian: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Zhang Huangbin (zhb@iredmail.org) 3 | 4 | ### BEGIN INIT INFO 5 | # Provides: api-server 6 | # Required-Start: $network $syslog 7 | # Required-Stop: $network $syslog 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: iredadmin instance 11 | # Description: iredadmin 12 | ### END INIT INFO 13 | 14 | PROG='iredadmin' 15 | PIDFILE='/var/run/iredadmin/iredadmin.pid' 16 | UWSGI_INI_FILE='/opt/www/iredadmin/rc_scripts/uwsgi/debian.ini' 17 | 18 | check_status() { 19 | # Usage: check_status pid_number 20 | PID="${1}" 21 | l=$(ps -p ${PID} | wc -l | awk '{print $1}') 22 | if [ X"$l" == X"2" ]; then 23 | echo "running" 24 | else 25 | echo "stopped" 26 | fi 27 | } 28 | 29 | start() { 30 | if [ -f ${PIDFILE} ]; then 31 | PID="$(cat ${PIDFILE})" 32 | s="$(check_status ${PID})" 33 | 34 | if [ X"$s" == X"running" ]; then 35 | echo "${PROG} is already running." 36 | exit 0 37 | else 38 | rm -f ${PIDFILE} >/dev/null 2>&1 39 | fi 40 | 41 | unset s 42 | fi 43 | 44 | mkdir /var/run/iredadmin 2>/dev/null 45 | chown iredadmin:iredadmin /var/run/iredadmin 46 | chmod 0755 /var/run/iredadmin 47 | 48 | echo "Starting ${PROG} ..." 49 | uwsgi -d \ 50 | --ini ${UWSGI_INI_FILE} \ 51 | --pidfile ${PIDFILE} \ 52 | --log-syslog 53 | } 54 | 55 | stop() { 56 | if [ -f ${PIDFILE} ]; then 57 | PID="$(cat ${PIDFILE})" 58 | s="$(check_status ${PID})" 59 | 60 | if [ X"$s" == X"running" ]; then 61 | echo "Stopping ${PROG} ..." 62 | uwsgi --stop ${PIDFILE} 63 | if [ X"$?" == X"0" ]; then 64 | rm -f ${PIDFILE} >/dev/null 2>&1 65 | rm -rf /var/run/iredadmin 66 | else 67 | echo -e "\t\t[ FAILED ]" 68 | fi 69 | else 70 | echo "${PROG} is already stopped." 71 | rm -f ${PIDFILE} >/dev/null 2>&1 72 | fi 73 | else 74 | echo "${PROG} is already stopped." 75 | fi 76 | unset s 77 | } 78 | 79 | status() { 80 | if [ -f ${PIDFILE} ]; then 81 | PID="$(cat ${PIDFILE})" 82 | s="$(check_status ${PID})" 83 | 84 | if [ X"$s" == X"running" ]; then 85 | echo "${PROG} is running." 86 | exit 0 87 | else 88 | echo "${PROG} is stopped." 89 | exit 1 90 | fi 91 | else 92 | echo "${PROG} is stopped." 93 | exit 3 94 | fi 95 | } 96 | 97 | case "$1" in 98 | start) start ;; 99 | stop) stop ;; 100 | status) status ;; 101 | restart) stop && start ;; 102 | *) 103 | echo $"Usage: $0 {start|stop|restart|status}" 104 | RETVAL=1 105 | ;; 106 | esac 107 | -------------------------------------------------------------------------------- /rc_scripts/iredadmin.freebsd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Author: Zhang Huangbin 4 | 5 | # PROVIDE: iredadmin 6 | # REQUIRE: DAEMON 7 | # KEYWORD: shutdown 8 | 9 | . /etc/rc.subr 10 | name='iredadmin' 11 | rcvar=`set_rcvar_obsolete` 12 | start_precmd="iredadmin_precmd" 13 | 14 | RUN_DIR='/var/run/iredadmin' 15 | PIDFILE="${RUN_DIR}/iredadmin.pid" 16 | UWSGI_INI_FILE='/opt/www/iredadmin/rc_scripts/uwsgi/freebsd.ini' 17 | 18 | PATH="/usr/local/bin:/usr/local/sbin:$PATH" 19 | 20 | iredadmin_precmd() { 21 | /usr/bin/install -m 0644 -o iredadmin -g iredadmin -d ${RUN_DIR} 22 | } 23 | 24 | check_status() { 25 | # Usage: check_status pid_number 26 | PID="${1}" 27 | l=$(ps -p ${PID} | wc -l | awk '{print $1}') 28 | if [ X"$l" == X"2" ]; then 29 | echo "running" 30 | else 31 | echo "stopped" 32 | fi 33 | } 34 | 35 | start() { 36 | if [ -f ${PIDFILE} ]; then 37 | PID="$(cat ${PIDFILE})" 38 | s="$(check_status ${PID})" 39 | 40 | if [ X"$s" == X"running" ]; then 41 | echo "${name} is already running." 42 | else 43 | rm -f ${PIDFILE} >/dev/null 2>&1 44 | fi 45 | 46 | unset s 47 | fi 48 | 49 | /bin/mkdir $(dirname ${PIDFILE}) 2>/dev/null 50 | /usr/sbin/chown iredadmin:iredadmin $(dirname ${PIDFILE}) 51 | 52 | echo "Starting ${name}." 53 | uwsgi --ini ${UWSGI_INI_FILE} \ 54 | --pidfile ${PIDFILE} \ 55 | --log-syslog \ 56 | --daemonize /dev/null 57 | } 58 | 59 | stop() { 60 | if [ -f ${PIDFILE} ]; then 61 | PID="$(cat ${PIDFILE})" 62 | s="$(check_status ${PID})" 63 | 64 | if [ X"$s" == X"running" ]; then 65 | echo "Stopping ${name}." 66 | uwsgi --stop ${PIDFILE} 67 | if [ X"$?" == X"0" ]; then 68 | rm -f ${PIDFILE} >/dev/null 2>&1 69 | else 70 | echo -e "\t\t[ FAILED ]" 71 | fi 72 | else 73 | echo "${name} is already stopped." 74 | rm -f ${PIDFILE} >/dev/null 2>&1 75 | fi 76 | 77 | unset s 78 | else 79 | echo "${name} is already stopped." 80 | fi 81 | } 82 | 83 | status() { 84 | if [ -f ${PIDFILE} ]; then 85 | PID="$(cat ${PIDFILE})" 86 | s="$(check_status ${PID})" 87 | 88 | if [ X"$s" == X"running" ]; then 89 | echo "${name} is running." 90 | exit 0 91 | else 92 | echo "${name} is stopped." 93 | exit 1 94 | fi 95 | 96 | unset s 97 | else 98 | echo "${name} is stopped." 99 | exit 3 100 | fi 101 | } 102 | 103 | start_cmd="start" 104 | stop_cmd="stop" 105 | status_cmd="status" 106 | restart_cmd="stop && sleep 2 && start" 107 | 108 | command="start" 109 | load_rc_config ${name} 110 | run_rc_command "$1" 111 | -------------------------------------------------------------------------------- /rc_scripts/iredadmin.openbsd: -------------------------------------------------------------------------------- 1 | #!/bin/ksh 2 | # Author: Zhang Huangbin 3 | # Purpose: Start/stop iRedAdmin uwsgi instance. 4 | 5 | RUN_DIR='/var/run/iredadmin' 6 | PID_FILE="${RUN_DIR}/iredadmin.pid" 7 | UWSGI_INI_FILE='/opt/www/iredadmin/rc_scripts/uwsgi/openbsd.ini' 8 | 9 | daemon="/usr/local/bin/uwsgi --ini ${UWSGI_INI_FILE} --log-syslog --pidfile ${PID_FILE} --daemonize /dev/null" 10 | daemon_user='iredadmin' 11 | daemon_group='iredadmin' 12 | 13 | . /etc/rc.d/rc.subr 14 | 15 | rc_pre() { 16 | install -d -o ${daemon_user} -g ${daemon_group} -m 0775 ${RUN_DIR} 17 | } 18 | 19 | rc_stop() { 20 | kill -INT `cat ${PID_FILE}` 21 | } 22 | 23 | rc_cmd $1 24 | -------------------------------------------------------------------------------- /rc_scripts/iredadmin.rhel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Author: Zhang Huangbin (zhb@iredmail.org) 4 | 5 | ### BEGIN INIT INFO 6 | # chkconfig: - 99 99 7 | # description: iredadmin instance 8 | # processname: iredadmin 9 | ### END INIT INFO 10 | 11 | PROG='iredadmin' 12 | BINPATH='/opt/www/iredadmin/iredadmin.py' 13 | PIDFILE='/var/run/iredadmin/iredadmin.pid' 14 | UWSGI_INI_FILE='/opt/www/iredadmin/rc_scripts/uwsgi/rhel.ini' 15 | 16 | check_status() { 17 | # Usage: check_status pid_number 18 | PID="${1}" 19 | l=$(ps -p ${PID} | wc -l | awk '{print $1}') 20 | if [ X"$l" == X"2" ]; then 21 | echo "running" 22 | else 23 | echo "stopped" 24 | fi 25 | } 26 | 27 | start() { 28 | if [ -f ${PIDFILE} ]; then 29 | PID="$(cat ${PIDFILE})" 30 | s="$(check_status ${PID})" 31 | 32 | if [ X"$s" == X"running" ]; then 33 | echo "${PROG} is already running." 34 | else 35 | rm -f ${PIDFILE} >/dev/null 2>&1 36 | fi 37 | fi 38 | 39 | unset s 40 | 41 | mkdir /var/run/iredadmin 2>/dev/null 42 | chown iredadmin:iredadmin /var/run/iredadmin 43 | chmod 0755 /var/run/iredadmin 44 | 45 | echo "Starting ${PROG} ..." 46 | uwsgi -d \ 47 | --ini ${UWSGI_INI_FILE} \ 48 | --pidfile ${PIDFILE} \ 49 | --log-syslog 50 | } 51 | 52 | stop() { 53 | if [ -f ${PIDFILE} ]; then 54 | PID="$(cat ${PIDFILE})" 55 | s="$(check_status ${PID})" 56 | 57 | if [ X"$s" == X"running" ]; then 58 | echo "Stopping ${PROG} ..." 59 | kill -9 ${PID} 60 | if [ X"$?" == X"0" ]; then 61 | rm -f ${PIDFILE} >/dev/null 2>&1 62 | rm -rf /var/run/iredadmin 63 | else 64 | echo -e "\t\t[ FAILED ]" 65 | fi 66 | else 67 | echo "${PROG} is already stopped." 68 | rm -f ${PIDFILE} >/dev/null 2>&1 69 | fi 70 | else 71 | echo "${PROG} is already stopped." 72 | fi 73 | 74 | unset s 75 | } 76 | 77 | status() { 78 | if [ -f ${PIDFILE} ]; then 79 | PID="$(cat ${PIDFILE})" 80 | s="$(check_status ${PID})" 81 | 82 | if [ X"$s" == X"running" ]; then 83 | echo "${PROG} is running." 84 | exit 0 85 | else 86 | echo "${PROG} is stopped." 87 | exit 1 88 | fi 89 | else 90 | echo "${PROG} is stopped." 91 | exit 3 92 | fi 93 | } 94 | 95 | case "$1" in 96 | start) start ;; 97 | stop) stop ;; 98 | status) status ;; 99 | restart) stop && sleep 1 && start ;; 100 | *) 101 | echo $"Usage: $0 {start|stop|restart|status}" 102 | RETVAL=1 103 | ;; 104 | esac 105 | -------------------------------------------------------------------------------- /rc_scripts/systemd/debian.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=iRedAdmin daemon service 3 | After=network.target local-fs.target remote-fs.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStartPre=-/bin/mkdir -p /var/run/iredadmin 8 | ExecStartPre=/bin/chown iredadmin:iredadmin /var/run/iredadmin 9 | ExecStartPre=/bin/chmod 0755 /var/run/iredadmin 10 | ExecStart=/usr/bin/uwsgi --ini /opt/www/iredadmin/rc_scripts/uwsgi/debian.ini --pidfile /var/run/iredadmin/iredadmin.pid 11 | ExecStop=/usr/bin/uwsgi --stop /var/run/iredadmin/iredadmin.pid 12 | ExecStopPost=/bin/rm -rf /var/run/iredadmin 13 | KillSignal=SIGTERM 14 | PrivateTmp=true 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /rc_scripts/systemd/rhel7.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=iRedAdmin daemon service 3 | After=network.target local-fs.target remote-fs.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStartPre=-/usr/bin/mkdir /var/run/iredadmin 8 | ExecStartPre=/usr/bin/chown iredadmin:iredadmin /var/run/iredadmin 9 | ExecStartPre=/usr/bin/chmod 0755 /var/run/iredadmin 10 | ExecStart=/usr/sbin/uwsgi --ini /opt/www/iredadmin/rc_scripts/uwsgi/rhel7.ini --pidfile /var/run/iredadmin/iredadmin.pid 11 | ExecStop=/usr/sbin/uwsgi --stop /var/run/iredadmin/iredadmin.pid 12 | ExecStopPost=/usr/bin/rm -rf /var/run/iredadmin 13 | KillSignal=SIGTERM 14 | TimeoutStopSec=5 15 | PrivateTmp=true 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /rc_scripts/systemd/rhel8.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=iRedAdmin daemon service 3 | After=network.target local-fs.target remote-fs.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStartPre=-/usr/bin/mkdir /var/run/iredadmin 8 | ExecStartPre=/usr/bin/chown iredadmin:iredadmin /var/run/iredadmin 9 | ExecStartPre=/usr/bin/chmod 0755 /var/run/iredadmin 10 | ExecStart=/usr/local/bin/uwsgi --ini /opt/www/iredadmin/rc_scripts/uwsgi/rhel8.ini --pidfile /var/run/iredadmin/iredadmin.pid 11 | ExecStop=/usr/local/bin/uwsgi --stop /var/run/iredadmin/iredadmin.pid 12 | ExecStopPost=/usr/bin/rm -rf /var/run/iredadmin 13 | KillSignal=SIGTERM 14 | TimeoutStopSec=5 15 | PrivateTmp=true 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /rc_scripts/systemd/rhel9.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=iRedAdmin daemon service 3 | After=network.target local-fs.target remote-fs.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStartPre=-/usr/bin/mkdir /var/run/iredadmin 8 | ExecStartPre=/usr/bin/chown iredadmin:iredadmin /var/run/iredadmin 9 | ExecStartPre=/usr/bin/chmod 0755 /var/run/iredadmin 10 | ExecStart=/usr/sbin/uwsgi --ini /opt/www/iredadmin/rc_scripts/uwsgi/rhel9.ini --pidfile /var/run/iredadmin/iredadmin.pid 11 | ExecStop=/usr/sbin/uwsgi --stop /var/run/iredadmin/iredadmin.pid 12 | ExecStopPost=/usr/bin/rm -rf /var/run/iredadmin 13 | KillSignal=SIGTERM 14 | TimeoutStopSec=5 15 | PrivateTmp=true 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/debian.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | plugins = python3,syslog 3 | master = true 4 | vhost = true 5 | enable-threads = true 6 | processes = 5 7 | buffer-size = 8192 8 | logger = syslog:iredadmin,local5 9 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 10 | 11 | uwsgi-socket = 127.0.0.1:7791 12 | 13 | uid = iredadmin 14 | gid = iredadmin 15 | 16 | chdir = /opt/www/iredadmin 17 | wsgi-file = iredadmin.py 18 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/freebsd.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | vhost = true 4 | enable-threads = true 5 | processes = 5 6 | buffer-size = 8192 7 | logger = syslog:iredadmin,local5 8 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 9 | 10 | # Log pid of master process 11 | safe-pid = true 12 | pidfile = /var/run/iredadmin/iredadmin.pid 13 | 14 | uwsgi-socket = 127.0.0.1:7791 15 | 16 | uid = iredadmin 17 | gid = iredadmin 18 | 19 | chdir = /usr/local/www/iredadmin 20 | wsgi-file = iredadmin.py 21 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/openbsd.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | vhost = true 4 | enable-threads = true 5 | processes = 5 6 | buffer-size = 8192 7 | logger = syslog:iredadmin,local5 8 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 9 | 10 | uwsgi-socket = 127.0.0.1:7791 11 | 12 | chdir = /var/www/iredadmin 13 | wsgi-file = iredadmin.py 14 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/rhel7.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | plugins = python36,syslog 3 | master = true 4 | vhost = true 5 | enable-threads = true 6 | processes = 5 7 | buffer-size = 8192 8 | logger = syslog:iredadmin,local5 9 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 10 | 11 | uwsgi-socket = 127.0.0.1:7791 12 | 13 | uid = iredadmin 14 | gid = iredadmin 15 | 16 | chdir = /opt/www/iredadmin 17 | wsgi-file = iredadmin.py 18 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/rhel8.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | plugins = python3,syslog 3 | master = true 4 | vhost = true 5 | enable-threads = true 6 | processes = 5 7 | buffer-size = 8192 8 | logger = syslog:iredadmin,local5 9 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 10 | 11 | uwsgi-socket = 127.0.0.1:7791 12 | 13 | uid = iredadmin 14 | gid = iredadmin 15 | 16 | chdir = /opt/www/iredadmin 17 | wsgi-file = iredadmin.py 18 | -------------------------------------------------------------------------------- /rc_scripts/uwsgi/rhel9.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | plugins = python3,syslog 3 | master = true 4 | vhost = true 5 | enable-threads = true 6 | processes = 5 7 | buffer-size = 8192 8 | logger = syslog:iredadmin,local5 9 | log-format = [%(addr)] %(method) %(uri) %(status) %(size) "%(referer)" 10 | 11 | uwsgi-socket = 127.0.0.1:7791 12 | 13 | uid = iredadmin 14 | gid = iredadmin 15 | 16 | chdir = /opt/www/iredadmin 17 | wsgi-file = iredadmin.py 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The core Python 3 micro web framework: https://webpy.org/ 2 | web.py>=0.61 3 | 4 | # HTML template engine. 5 | Jinja2>=2.2.0 6 | 7 | # LDAP driver. 8 | python-ldap>=3.3.1 9 | 10 | # MySQL/MariaDB driver. 11 | PyMySQL>=0.9.3 12 | 13 | # PostgreSQL driver. 14 | psycopg2 15 | 16 | requests>=2.10.0 17 | 18 | # DNS queries. 19 | dnspython 20 | 21 | # Get info of network interfaces. 22 | netifaces 23 | 24 | # bcrypt password hash. 25 | bcrypt 26 | 27 | # Required by Python 3.5 and LDAP backend. 28 | # 29 | # Use `simplejson` instead of the Python builtin `json`, because `json` doesn't 30 | # support serializing bytes (mostly used by LDAP backend) and raise error 31 | # `Object of type 'bytes' is not JSON serializable`. 32 | simplejson 33 | -------------------------------------------------------------------------------- /settings.py.ldap.sample: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | ############################################################ 4 | # DO NOT MODIFY THIS LINE, IT'S USED TO IMPORT DEFAULT SETTINGS. 5 | from libs.default_settings import * 6 | ############################################################ 7 | # General settings. 8 | # 9 | # Site webmaster's mail address. 10 | webmaster = 'zhb@iredmail.org' 11 | 12 | # Default language. 13 | default_language = 'en_US' 14 | 15 | # Database backend: ldap. 16 | backend = 'ldap' 17 | 18 | # Directory used to store mailboxes. Defaults to /var/vmail/vmail1. 19 | # Note: This directory must be owned by 'vmail:vmail' with permission 0700. 20 | storage_base_directory = '/var/vmail/vmail1' 21 | 22 | # Default mta transport. 23 | # There're 3 transports available in iRedMail: 24 | # 25 | # 1. dovecot: default LDA transport. Supported by all iRedMail releases. 26 | # 2. lmtp:unix:private/dovecot-lmtp: LMTP (socket listener). Supported by 27 | # iRedMail-0.8.6 and later releases. 28 | # 3. lmtp:inet:127.0.0.1:24: LMTP (TCP listener). Supported by iRedMail-0.8.6 29 | # and later releases. 30 | # 31 | # Note: You can set per-domain or per-user transport in account profile page. 32 | default_mta_transport = 'dovecot' 33 | 34 | # Min/Max admin password length. 0 means unlimited. 35 | # - min_passwd_length: at least 1 character is required. 36 | # Normal admin can not set shorter/longer password lengths than global settings 37 | # defined here. 38 | min_passwd_length = 8 39 | max_passwd_length = 0 40 | 41 | ##################################################################### 42 | # Database used to store iRedAdmin data. e.g. sessions, log. 43 | # 44 | iredadmin_db_host = '127.0.0.1' 45 | iredadmin_db_port = 3306 46 | iredadmin_db_name = 'iredadmin' 47 | iredadmin_db_user = 'iredadmin' 48 | iredadmin_db_password = 'password' 49 | 50 | ############################################################################ 51 | # Settings used for OpenLDAP backend. 52 | # 53 | # LDAP server uri. Examples: 54 | # - ldap://127.0.0.1 normal LDAP connection (port 389) 55 | # - ldaps://127.0.0.1 secure connection through STARTTLS (port 389) 56 | ldap_uri = 'ldap://127.0.0.1' 57 | 58 | # LDAP suffix. 59 | # basedn: dn which contains virtual domains. 60 | # domainadmin_dn: dn which contains virtual domain admins. 61 | ldap_basedn = 'o=domains,dc=iredmail,dc=org' 62 | ldap_domainadmin_dn = 'o=domainAdmins,dc=iredmail,dc=org' 63 | 64 | # Bind dn and password. 65 | # - bind dn should have write privilege in LDAP. 66 | # - bind pw is plain text, not encryped/hashed. 67 | ldap_bind_dn = 'cn=vmailadmin,dc=iredmail,dc=org' 68 | ldap_bind_password = 'password' 69 | 70 | ############################################################################## 71 | # Settings used for Amavisd-new integration. Provides spam/virus quaranting, 72 | # releasing, etc. 73 | # 74 | # Log basic info of in/out emails into SQL (@storage_sql_dsn): True, False. 75 | # It's @storage_sql_dsn setting in amavisd. You can find this setting 76 | # in amavisd-new config files: 77 | # - On RHEL/CentOS: /etc/amavisd.conf or /etc/amavisd/amavisd.conf 78 | # - On Debian/Ubuntu: /etc/amavis/conf.d/50-user.conf 79 | # - On FreeBSD: /usr/local/etc/amavisd.conf 80 | amavisd_enable_logging = True 81 | 82 | amavisd_db_host = '127.0.0.1' 83 | amavisd_db_port = 3306 84 | amavisd_db_name = 'amavisd' 85 | amavisd_db_user = 'amavisd' 86 | amavisd_db_password = 'password' 87 | 88 | # #### Quarantining #### 89 | # Release quarantined SPAM/Virus mails: True, False. 90 | # iRedAdmin-Pro will connect to @amavisd_db_host to release quarantined mails. 91 | # How to enable quarantining in Amavisd-new: 92 | # http://www.iredmail.org/docs/quarantining.html 93 | amavisd_enable_quarantine = True 94 | 95 | # Port of Amavisd protocol 'AM.PDP-INET'. Default is 9998. 96 | # If Amavisd is not running on database server specified in amavisd_db_host, 97 | # please set the server address in parameter `AMAVISD_QUARANTINE_HOST`. 98 | # Default is '127.0.0.1'. Sample setting: 99 | #AMAVISD_QUARANTINE_HOST = '192.168.1.1' 100 | amavisd_quarantine_port = 9998 101 | 102 | # Enable per-recipient spam policy, white/blacklist. 103 | amavisd_enable_policy_lookup = True 104 | 105 | ############################################################################## 106 | # Settings used for iRedAPD integration. Provides throttling and more. 107 | # 108 | iredapd_enabled = True 109 | iredapd_db_host = '127.0.0.1' 110 | iredapd_db_port = '3306' 111 | iredapd_db_name = 'iredapd' 112 | iredapd_db_user = 'iredapd' 113 | iredapd_db_password = 'password' 114 | 115 | ############################################################################## 116 | # Settings used for mlmmj (mailing list manager) and mlmmjadmin integration. 117 | # 118 | # The API auth token required to access mlmmjadmin API. 119 | mlmmjadmin_api_auth_token = '' 120 | 121 | ############################################################################## 122 | # Place your custom settings below, you can override all settings in this file 123 | # and libs/default_settings.py here. 124 | # 125 | -------------------------------------------------------------------------------- /settings.py.mysql.sample: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | ############################################################### 4 | # DO NOT MODIFY THIS LINE, IT'S USED TO IMPORT DEFAULT SETTINGS. 5 | from libs.default_settings import * 6 | ############################################################### 7 | # General settings. 8 | # 9 | # Site webmaster's mail address. 10 | webmaster = 'zhb@iredmail.org' 11 | 12 | # Default language. 13 | default_language = 'en_US' 14 | 15 | # Database backend: mysql. 16 | backend = 'mysql' 17 | 18 | # Directory used to store mailboxes. Defaults to /var/vmail/vmail1. 19 | # Note: This directory must be owned by 'vmail:vmail' with permission 0700. 20 | storage_base_directory = '/var/vmail/vmail1' 21 | 22 | # Default mta transport. 23 | # There're 3 transports available in iRedMail: 24 | # 25 | # 1. dovecot: default LDA transport. Supported by all iRedMail releases. 26 | # 2. lmtp:unix:private/dovecot-lmtp: LMTP (socket listener). Supported by 27 | # iRedMail-0.8.6 and later releases. 28 | # 3. lmtp:inet:127.0.0.1:24: LMTP (TCP listener). Supported by iRedMail-0.8.6 29 | # and later releases. 30 | # 31 | # Note: You can set per-domain or per-user transport in account profile page. 32 | default_mta_transport = 'dovecot' 33 | 34 | # Min/Max admin password length. 0 means unlimited. 35 | # - min_passwd_length: at least 1 character is required. 36 | # Normal admin can not set shorter/longer password lengths than global settings 37 | # defined here. 38 | min_passwd_length = 8 39 | max_passwd_length = 0 40 | 41 | ##################################################################### 42 | # Database used to store iRedAdmin data. e.g. sessions, log. 43 | # 44 | iredadmin_db_host = '127.0.0.1' 45 | iredadmin_db_port = 3306 46 | iredadmin_db_name = 'iredadmin' 47 | iredadmin_db_user = 'iredadmin' 48 | iredadmin_db_password = 'password' 49 | 50 | ############################################ 51 | # Database used to store mail accounts. 52 | # 53 | vmail_db_host = '127.0.0.1' 54 | vmail_db_port = 3306 55 | vmail_db_name = 'vmail' 56 | vmail_db_user = 'vmailadmin' 57 | vmail_db_password = 'password' 58 | 59 | ############################################################################## 60 | # Settings used for Amavisd-new integration. Provides spam/virus quaranting, 61 | # releasing, etc. 62 | # 63 | # Log basic info of in/out emails into SQL (@storage_sql_dsn): True, False. 64 | # It's @storage_sql_dsn setting in amavisd. You can find this setting 65 | # in amavisd-new config files: 66 | # - On RHEL/CentOS: /etc/amavisd.conf or /etc/amavisd/amavisd.conf 67 | # - On Debian/Ubuntu: /etc/amavis/conf.d/50-user.conf 68 | # - On FreeBSD: /usr/local/etc/amavisd.conf 69 | amavisd_enable_logging = True 70 | 71 | amavisd_db_host = '127.0.0.1' 72 | amavisd_db_port = 3306 73 | amavisd_db_name = 'amavisd' 74 | amavisd_db_user = 'amavisd' 75 | amavisd_db_password = 'password' 76 | 77 | # #### Quarantining #### 78 | # Release quarantined SPAM/Virus mails: True, False. 79 | # iRedAdmin-Pro will connect to @amavisd_db_host to release quarantined mails. 80 | # How to enable quarantining in Amavisd-new: 81 | # http://www.iredmail.org/docs/quarantining.html 82 | amavisd_enable_quarantine = True 83 | 84 | # Port of Amavisd protocol 'AM.PDP-INET'. Default is 9998. 85 | # If Amavisd is not running on database server specified in amavisd_db_host, 86 | # please set the server address in parameter `AMAVISD_QUARANTINE_HOST`. 87 | # Default is '127.0.0.1'. Sample setting: 88 | #AMAVISD_QUARANTINE_HOST = '192.168.1.1' 89 | amavisd_quarantine_port = 9998 90 | 91 | # Enable per-recipient spam policy, white/blacklist. 92 | amavisd_enable_policy_lookup = True 93 | 94 | ############################################################################## 95 | # Settings used for iRedAPD integration. Provides throttling and more. 96 | # 97 | iredapd_enabled = True 98 | iredapd_db_host = '127.0.0.1' 99 | iredapd_db_port = 3306 100 | iredapd_db_name = 'iredapd' 101 | iredapd_db_user = 'iredapd' 102 | iredapd_db_password = 'password' 103 | 104 | ############################################################################## 105 | # Settings used for mlmmj (mailing list manager) and mlmmjadmin integration. 106 | # 107 | # The API auth token required to access mlmmjadmin API. 108 | mlmmjadmin_api_auth_token = '' 109 | 110 | ############################################################################## 111 | # Place your custom settings below, you can override all settings in this file 112 | # and libs/default_settings.py here. 113 | # 114 | -------------------------------------------------------------------------------- /settings.py.pgsql.sample: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | ############################################################ 4 | # DO NOT MODIFY THIS LINE, IT'S USED TO IMPORT DEFAULT SETTINGS. 5 | from libs.default_settings import * 6 | ############################################################ 7 | # General settings. 8 | # 9 | # Site webmaster's mail address. 10 | webmaster = 'zhb@iredmail.org' 11 | 12 | # Default language. 13 | default_language = 'en_US' 14 | 15 | # Database backend: pgsql. 16 | backend = 'pgsql' 17 | 18 | # Directory used to store mailboxes. Defaults to /var/vmail/vmail1. 19 | # Note: This directory must be owned by 'vmail:vmail' with permission 0700. 20 | storage_base_directory = '/var/vmail/vmail1' 21 | 22 | # Default mta transport. 23 | # There're 3 transports available in iRedMail: 24 | # 25 | # 1. dovecot: default LDA transport. Supported by all iRedMail releases. 26 | # 2. lmtp:unix:private/dovecot-lmtp: LMTP (socket listener). Supported by 27 | # iRedMail-0.8.6 and later releases. 28 | # 3. lmtp:inet:127.0.0.1:24: LMTP (TCP listener). Supported by iRedMail-0.8.6 29 | # and later releases. 30 | # 31 | # Note: You can set per-domain or per-user transport in account profile page. 32 | default_mta_transport = 'dovecot' 33 | 34 | # Min/Max admin password length. 0 means unlimited. 35 | # - min_passwd_length: at least 1 character is required. 36 | # Normal admin can not set shorter/longer password lengths than global settings 37 | # defined here. 38 | min_passwd_length = 8 39 | max_passwd_length = 0 40 | 41 | ##################################################################### 42 | # Database used to store iRedAdmin data. e.g. sessions, log. 43 | # 44 | iredadmin_db_host = '127.0.0.1' 45 | iredadmin_db_port = 5432 46 | iredadmin_db_name = 'iredadmin' 47 | iredadmin_db_user = 'iredadmin' 48 | iredadmin_db_password = 'password' 49 | 50 | ############################################ 51 | # Database used to store mail accounts. 52 | # 53 | vmail_db_host = '127.0.0.1' 54 | vmail_db_port = 5432 55 | vmail_db_name = 'vmail' 56 | vmail_db_user = 'vmailadmin' 57 | vmail_db_password = 'password' 58 | 59 | ############################################################################## 60 | # Settings used for Amavisd-new integration. Provides spam/virus quaranting, 61 | # releasing, etc. 62 | # 63 | # Log basic info of in/out emails into SQL (@storage_sql_dsn): True, False. 64 | # It's @storage_sql_dsn setting in amavisd. You can find this setting 65 | # in amavisd-new config files: 66 | # - On RHEL/CentOS: /etc/amavisd.conf or /etc/amavisd/amavisd.conf 67 | # - On Debian/Ubuntu: /etc/amavis/conf.d/50-user.conf 68 | # - On FreeBSD: /usr/local/etc/amavisd.conf 69 | amavisd_enable_logging = True 70 | 71 | amavisd_db_host = '127.0.0.1' 72 | amavisd_db_port = 5432 73 | amavisd_db_name = 'amavisd' 74 | amavisd_db_user = 'amavisd' 75 | amavisd_db_password = 'password' 76 | 77 | # #### Quarantining #### 78 | # Release quarantined SPAM/Virus mails: True, False. 79 | # iRedAdmin-Pro will connect to @amavisd_db_host to release quarantined mails. 80 | # How to enable quarantining in Amavisd-new: 81 | # http://www.iredmail.org/docs/quarantining.html 82 | amavisd_enable_quarantine = True 83 | 84 | # Port of Amavisd protocol 'AM.PDP-INET'. Default is 9998. 85 | # If Amavisd is not running on database server specified in amavisd_db_host, 86 | # please set the server address in parameter `AMAVISD_QUARANTINE_HOST`. 87 | # Default is '127.0.0.1'. Sample setting: 88 | #AMAVISD_QUARANTINE_HOST = '192.168.1.1' 89 | amavisd_quarantine_port = 9998 90 | 91 | # Enable per-recipient spam policy, white/blacklist. 92 | amavisd_enable_policy_lookup = True 93 | 94 | ############################################################################## 95 | # Settings used for iRedAPD integration. Provides throttling and more. 96 | # 97 | iredapd_enabled = True 98 | iredapd_db_host = '127.0.0.1' 99 | iredapd_db_port = 5432 100 | iredapd_db_name = 'iredapd' 101 | iredapd_db_user = 'iredapd' 102 | iredapd_db_password = 'password' 103 | 104 | ############################################################################## 105 | # Settings used for mlmmj (mailing list manager) and mlmmjadmin integration. 106 | # 107 | # The API auth token required to access mlmmjadmin API. 108 | mlmmjadmin_api_auth_token = '' 109 | 110 | ############################################################ 111 | # Place your custom settings below, you can override all settings in this file 112 | # and libs/default_settings.py here. 113 | # 114 | -------------------------------------------------------------------------------- /static/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | ExpiresActive On 3 | ExpiresByType application/x-javascript "access plus 1 month" 4 | ExpiresByType text/css "access plus 1 month" 5 | ExpiresByType image/png "access plus 1 month" 6 | ExpiresByType image/gif "access plus 1 month" 7 | ExpiresByType image/jpg "access plus 1 month" 8 | 9 | -------------------------------------------------------------------------------- /static/default/css/reset.css: -------------------------------------------------------------------------------- 1 | /* Eric Meyer's CSS Reset v1.0 | 20080212 */ 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, font, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | outline: 0; 16 | font-size: 13px; 17 | vertical-align: baseline; 18 | background: transparent; 19 | } 20 | table, tbody, tfoot, thead, tr, th, td { 21 | vertical-align: top; 22 | } 23 | 24 | ol, ul { 25 | list-style: none; 26 | } 27 | blockquote, q { 28 | quotes: none; 29 | } 30 | blockquote:before, blockquote:after, 31 | q:before, q:after { 32 | content: ''; 33 | content: none; 34 | } 35 | 36 | /* remember to define focus styles! */ 37 | :focus { 38 | outline: 0; 39 | } 40 | 41 | /* remember to highlight inserts somehow! */ 42 | ins { 43 | text-decoration: none; 44 | } 45 | del { 46 | text-decoration: line-through; 47 | } 48 | 49 | /* tables still need 'cellspacing="0"' in the markup */ 50 | table { 51 | border-collapse: collapse; 52 | border-spacing: 0; 53 | } 54 | -------------------------------------------------------------------------------- /static/default/images/arrow_left_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_left_off.png -------------------------------------------------------------------------------- /static/default/images/arrow_left_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_left_ovr.png -------------------------------------------------------------------------------- /static/default/images/arrow_leftend_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_leftend_off.png -------------------------------------------------------------------------------- /static/default/images/arrow_leftend_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_leftend_ovr.png -------------------------------------------------------------------------------- /static/default/images/arrow_right_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_right_off.png -------------------------------------------------------------------------------- /static/default/images/arrow_right_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_right_ovr.png -------------------------------------------------------------------------------- /static/default/images/arrow_rightend_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_rightend_off.png -------------------------------------------------------------------------------- /static/default/images/arrow_rightend_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_rightend_ovr.png -------------------------------------------------------------------------------- /static/default/images/arrow_sm_black.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_sm_black.gif -------------------------------------------------------------------------------- /static/default/images/arrow_sm_grey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/arrow_sm_grey.gif -------------------------------------------------------------------------------- /static/default/images/ball_grey_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/ball_grey_16.png -------------------------------------------------------------------------------- /static/default/images/ball_yellow_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/ball_yellow_13.png -------------------------------------------------------------------------------- /static/default/images/bck_black_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_black_10.png -------------------------------------------------------------------------------- /static/default/images/bck_black_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_black_5.png -------------------------------------------------------------------------------- /static/default/images/bck_black_70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_black_70.png -------------------------------------------------------------------------------- /static/default/images/bck_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_header.png -------------------------------------------------------------------------------- /static/default/images/bck_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_main.png -------------------------------------------------------------------------------- /static/default/images/bck_white_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_white_10.png -------------------------------------------------------------------------------- /static/default/images/bck_white_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_white_50.png -------------------------------------------------------------------------------- /static/default/images/bck_white_75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_white_75.png -------------------------------------------------------------------------------- /static/default/images/bck_white_90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/bck_white_90.png -------------------------------------------------------------------------------- /static/default/images/but_slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/but_slide.png -------------------------------------------------------------------------------- /static/default/images/button_glas1_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/button_glas1_ovr.png -------------------------------------------------------------------------------- /static/default/images/button_glas2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/button_glas2.png -------------------------------------------------------------------------------- /static/default/images/fancybox/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/fancybox/blank.gif -------------------------------------------------------------------------------- /static/default/images/fancybox/fancybox-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/fancybox/fancybox-x.png -------------------------------------------------------------------------------- /static/default/images/fancybox/fancybox-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/fancybox/fancybox-y.png -------------------------------------------------------------------------------- /static/default/images/fancybox/fancybox.blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/fancybox/fancybox.blank.gif -------------------------------------------------------------------------------- /static/default/images/fancybox/fancybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/fancybox/fancybox.png -------------------------------------------------------------------------------- /static/default/images/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/gear.png -------------------------------------------------------------------------------- /static/default/images/graph_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/graph_16.png -------------------------------------------------------------------------------- /static/default/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/header.png -------------------------------------------------------------------------------- /static/default/images/ico_close_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/ico_close_off.png -------------------------------------------------------------------------------- /static/default/images/ico_close_ovr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/ico_close_ovr.png -------------------------------------------------------------------------------- /static/default/images/line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/line.gif -------------------------------------------------------------------------------- /static/default/images/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/login.jpg -------------------------------------------------------------------------------- /static/default/images/login_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/login_header.png -------------------------------------------------------------------------------- /static/default/images/members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/members.png -------------------------------------------------------------------------------- /static/default/images/page_active.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/page_active.gif -------------------------------------------------------------------------------- /static/default/images/rule.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/rule.gif -------------------------------------------------------------------------------- /static/default/images/rule2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/rule2.gif -------------------------------------------------------------------------------- /static/default/images/tablesorter/asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/tablesorter/asc.gif -------------------------------------------------------------------------------- /static/default/images/tablesorter/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/tablesorter/bg.gif -------------------------------------------------------------------------------- /static/default/images/tablesorter/desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/default/images/tablesorter/desc.gif -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/favicon.ico -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /static/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /static/js/jquery.idtabs.js: -------------------------------------------------------------------------------- 1 | /* idTabs ~ Sean Catchpole - Version 2.2 - MIT/GPL */ 2 | (function(){var dep={"jQuery":"http://code.jquery.com/jquery-latest.min.js"};var init=function(){(function($){$.fn.idTabs=function(){var s={};for(var i=0;i= 0; 14 | }}); 15 | $.extend($.fn,{ 16 | quickfilter: function(el){ 17 | return this.each(function(){ 18 | var _this = $(this); 19 | var query = _this.val().toLowerCase(); 20 | _this.keyup(function () { 21 | query = $(this).val().toLowerCase(); 22 | if(query.replace(/\s/g,"") != ""){ 23 | $(el+':exists("' + query.toString() + '")').show(); 24 | $(el+':missing("' + query.toString() + '")').hide(); 25 | } 26 | else { 27 | $(el).show(); 28 | } 29 | }); 30 | }); 31 | } 32 | }); 33 | })(jQuery); -------------------------------------------------------------------------------- /static/js/jquery.tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | jQuery Tools 1.2.5 Tooltip - UI essentials 4 | 5 | NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE. 6 | 7 | http://flowplayer.org/tools/tooltip/ 8 | 9 | Since: November 2008 10 | Date: Wed Sep 22 06:02:10 2010 +0000 11 | */ 12 | (function(f){function p(a,b,c){var h=c.relative?a.position().top:a.offset().top,d=c.relative?a.position().left:a.offset().left,i=c.position[0];h-=b.outerHeight()-c.offset[0];d+=a.outerWidth()+c.offset[1];if(/iPad/i.test(navigator.userAgent))h-=f(window).scrollTop();var j=b.outerHeight()+a.outerHeight();if(i=="center")h+=j/2;if(i=="bottom")h+=j;i=c.position[1];a=b.outerWidth()+a.outerWidth();if(i=="center")d-=a/2;if(i=="left")d-=a;return{top:h,left:d}}function u(a,b){var c=this,h=a.add(c),d,i=0,j= 13 | 0,m=a.attr("title"),q=a.attr("data-tooltip"),r=o[b.effect],l,s=a.is(":input"),v=s&&a.is(":checkbox, :radio, select, :button, :submit"),t=a.attr("type"),k=b.events[t]||b.events[s?v?"widget":"input":"def"];if(!r)throw'Nonexistent effect "'+b.effect+'"';k=k.split(/,\s*/);if(k.length!=2)throw"Tooltip: bad events configuration for "+t;a.bind(k[0],function(e){clearTimeout(i);if(b.predelay)j=setTimeout(function(){c.show(e)},b.predelay);else c.show(e)}).bind(k[1],function(e){clearTimeout(j);if(b.delay)i= 14 | setTimeout(function(){c.hide(e)},b.delay);else c.hide(e)});if(m&&b.cancelDefault){a.removeAttr("title");a.data("title",m)}f.extend(c,{show:function(e){if(!d){if(q)d=f(q);else if(b.tip)d=f(b.tip).eq(0);else if(m)d=f(b.layout).addClass(b.tipClass).appendTo(document.body).hide().append(m);else{d=a.next();d.length||(d=a.parent().next())}if(!d.length)throw"Cannot find tooltip for "+a;}if(c.isShown())return c;d.stop(true,true);var g=p(a,d,b);b.tip&&d.html(a.data("title"));e=e||f.Event();e.type="onBeforeShow"; 15 | h.trigger(e,[g]);if(e.isDefaultPrevented())return c;g=p(a,d,b);d.css({position:"absolute",top:g.top,left:g.left});l=true;r[0].call(c,function(){e.type="onShow";l="full";h.trigger(e)});g=b.events.tooltip.split(/,\s*/);if(!d.data("__set")){d.bind(g[0],function(){clearTimeout(i);clearTimeout(j)});g[1]&&!a.is("input:not(:checkbox, :radio), textarea")&&d.bind(g[1],function(n){n.relatedTarget!=a[0]&&a.trigger(k[1].split(" ")[0])});d.data("__set",true)}return c},hide:function(e){if(!d||!c.isShown())return c; 16 | e=e||f.Event();e.type="onBeforeHide";h.trigger(e);if(!e.isDefaultPrevented()){l=false;o[b.effect][1].call(c,function(){e.type="onHide";h.trigger(e)});return c}},isShown:function(e){return e?l=="full":l},getConf:function(){return b},getTip:function(){return d},getTrigger:function(){return a}});f.each("onHide,onBeforeShow,onShow,onBeforeHide".split(","),function(e,g){f.isFunction(b[g])&&f(c).bind(g,b[g]);c[g]=function(n){n&&f(c).bind(g,n);return c}})}f.tools=f.tools||{version:"1.2.5"};f.tools.tooltip= 17 | {conf:{effect:"toggle",fadeOutSpeed:"fast",predelay:0,delay:30,opacity:1,tip:0,position:["top","center"],offset:[0,0],relative:false,cancelDefault:true,events:{def:"mouseenter,mouseleave",input:"focus,blur",widget:"focus mouseenter,blur mouseleave",tooltip:"mouseenter,mouseleave"},layout:"
",tipClass:"tooltip"},addEffect:function(a,b,c){o[a]=[b,c]}};var o={toggle:[function(a){var b=this.getConf(),c=this.getTip();b=b.opacity;b<1&&c.css({opacity:b});c.show();a.call()},function(a){this.getTip().hide(); 18 | a.call()}],fade:[function(a){var b=this.getConf();this.getTip().fadeTo(b.fadeInSpeed,b.opacity,a)},function(a){this.getTip().fadeOut(this.getConf().fadeOutSpeed,a)}]};f.fn.tooltip=function(a){var b=this.data("tooltip");if(b)return b;a=f.extend(true,{},f.tools.tooltip.conf,a);if(typeof a.position=="string")a.position=a.position.split(/,?\s/);this.each(function(){b=new u(f(this),a);f(this).data("tooltip",b)});return a.api?b:this}})(jQuery); 19 | -------------------------------------------------------------------------------- /static/js/stupidtable.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.stupidtable=function(a){return this.each(function(){var b=c(this);a=a||{};a=c.extend({},c.fn.stupidtable.default_sort_fns,a);b.data("sortFns",a);b.stupidtable_build();b.on("click.stupidtable","thead th",function(){c(this).stupidsort()});b.find("th[data-sort-onload=yes]").eq(0).stupidsort()})};c.fn.stupidtable.default_settings={should_redraw:function(a){return!0},will_manually_build_table:!1};c.fn.stupidtable.dir={ASC:"asc",DESC:"desc"};c.fn.stupidtable.default_sort_fns={"int":function(a, 2 | b){return parseInt(a,10)-parseInt(b,10)},"float":function(a,b){return parseFloat(a)-parseFloat(b)},string:function(a,b){return a.toString().localeCompare(b.toString())},"string-ins":function(a,b){a=a.toString().toLocaleLowerCase();b=b.toString().toLocaleLowerCase();return a.localeCompare(b)}};c.fn.stupidtable_settings=function(a){return this.each(function(){var b=c(this),f=c.extend({},c.fn.stupidtable.default_settings,a);b.stupidtable.settings=f})};c.fn.stupidsort=function(a){var b=c(this),f=b.data("sort")|| 3 | null;if(null!==f){var d=b.closest("table"),e={$th:b,$table:d,datatype:f};d.stupidtable.settings||(d.stupidtable.settings=c.extend({},c.fn.stupidtable.default_settings));e.compare_fn=d.data("sortFns")[f];e.th_index=h(e);e.sort_dir=k(a,e);b.data("sort-dir",e.sort_dir);d.trigger("beforetablesort",{column:e.th_index,direction:e.sort_dir,$th:b});d.css("display");setTimeout(function(){d.stupidtable.settings.will_manually_build_table||d.stupidtable_build();var a=l(e),a=m(a,e);if(d.stupidtable.settings.should_redraw(e)){d.children("tbody").append(a); 4 | var a=e.$table,c=e.$th,f=c.data("sort-dir");a.find("th").data("sort-dir",null).removeClass("sorting-desc sorting-asc");c.data("sort-dir",f).addClass("sorting-"+f);d.trigger("aftertablesort",{column:e.th_index,direction:e.sort_dir,$th:b});d.css("display")}},10);return b}};c.fn.updateSortVal=function(a){var b=c(this);b.is("[data-sort-value]")&&b.attr("data-sort-value",a);b.data("sort-value",a);return b};c.fn.stupidtable_build=function(){return this.each(function(){var a=c(this),b=[];a.children("tbody").children("tr").each(function(a, 5 | d){var e={$tr:c(d),columns:[],index:a};c(d).children("td").each(function(a,b){var d=c(b).data("sort-value");"undefined"===typeof d&&(d=c(b).text(),c(b).data("sort-value",d));e.columns.push(d)});b.push(e)});a.data("stupidsort_internaltable",b)})};var l=function(a){var b=a.$table.data("stupidsort_internaltable"),f=a.th_index,d=a.$th.data("sort-multicolumn"),d=d?d.split(","):[],e=c.map(d,function(b,d){var c=a.$table.find("th"),e=parseInt(b,10),f;e||0===e?f=c.eq(e):(f=c.siblings("#"+b),e=c.index(f)); 6 | return{index:e,$e:f}});b.sort(function(b,c){for(var d=e.slice(0),g=a.compare_fn(b.columns[f],c.columns[f]);0===g&&d.length;){var g=d[0],h=g.$e.data("sort"),g=(0,a.$table.data("sortFns")[h])(b.columns[g.index],c.columns[g.index]);d.shift()}return 0===g?b.index-c.index:g});a.sort_dir!=c.fn.stupidtable.dir.ASC&&b.reverse();return b},m=function(a,b){var f=c.map(a,function(a,c){return[[a.columns[b.th_index],a.$tr,c]]});b.column=f;return c.map(a,function(a){return a.$tr})},k=function(a,b){var f,d=b.$th, 7 | e=c.fn.stupidtable.dir;a?f=a:(f=a||d.data("sort-default")||e.ASC,d.data("sort-dir")&&(f=d.data("sort-dir")===e.ASC?e.DESC:e.ASC));return f},h=function(a){var b=0,f=a.$th.index();a.$th.parents("tr").find("th").slice(0,f).each(function(){var a=c(this).attr("colspan")||1;b+=parseInt(a,10)});return b}})(jQuery); 8 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/static/logo.png -------------------------------------------------------------------------------- /templates/default/error_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "macros/msg_handlers.html" import error_info %} 3 | 4 | {% block main %} 5 |
6 | {{ error_info(_('Security token did not match. Please refresh current page and re-perform form action.')) }} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/default/error_without_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ _('Error') }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% from "macros/msg_handlers.html" import login_msg_handler with context %} 15 | 16 | 17 | {{ login_msg_handler(error) }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/default/ldap/admin/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_submit, 5 | input_csrf_token 6 | with context 7 | %} 8 | 9 | {% from "macros/general.html" import 10 | display_random_password, 11 | display_preferred_language, 12 | display_add_admin, 13 | with context 14 | %} 15 | {% from "macros/msg_handlers.html" import admin_msg_handler with context %} 16 | 17 | {% block navlinks_create %}class="active"{% endblock %} 18 | 19 | {% block title %}{{ _('Add admin') }}{% endblock title %} 20 | 21 | {% block main %} 22 | {# Show system message #} 23 | {% if msg %} 24 | {% if msg.startswith('PW_') %} 25 | {% set _pw_errors = msg.split(',') %} 26 | {% for _err in _pw_errors %} 27 | {{ admin_msg_handler(_err) }} 28 | {% endfor %} 29 | {% else %} 30 | {{ admin_msg_handler(msg) }} 31 | {% endif %} 32 | {% endif %} 33 | 34 |
35 |
36 |
37 | {# -- Tabs -- #} 38 | 42 | 43 |

{{ _('Add admin') }}

44 |
45 | 46 |
47 |
48 |
49 |
50 | {{ input_csrf_token() }} 51 | {{ display_add_admin(min_passwd_length=min_passwd_length, 52 | max_passwd_length=max_passwd_length, 53 | lang=default_language, 54 | languagemaps=languagemaps) }} 55 | 56 | {{ input_submit(label=_('Add')) }} 57 | 58 |
59 |
60 | 61 |
62 |
63 | {{ display_random_password(password_length=min_passwd_length, 64 | password_policies=password_policies) }} 65 |
66 |
67 |
68 |
69 |
70 | 71 | {% endblock main %} 72 | -------------------------------------------------------------------------------- /templates/default/ldap/admin/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_csrf_token 5 | with context 6 | %} 7 | 8 | {% from "macros/general.html" import 9 | set_account_status_img, 10 | highlight_username_in_mail, 11 | set_admin_type_img, 12 | show_pages 13 | with context 14 | %} 15 | {% from "macros/msg_handlers.html" import admin_msg_handler with context %} 16 | 17 | {% block title %}{{ _('Domain Admins') }}{% endblock %} 18 | {% block navlinks_admins %}class="active"{% endblock %} 19 | 20 | {% block main %} 21 | {# Show system message #} 22 | {{ admin_msg_handler(msg) }} 23 | 24 | {# List admins #} 25 | 26 | {#{% if admins|length > 0 %}#} 27 | {% if admins is not string %} 28 |
29 |
30 |
31 | {% if session.get('is_global_admin') %} 32 | 35 | {% endif %} 36 | 37 |

{{ _('All admins') }} 38 | {% if total is defined and admins|length > 0 %} 39 | ({{ (cur_page-1) * page_size_limit + 1 }}-{{ (cur_page-1) * page_size_limit + admins|length}}/{{ total }}) 40 | {% endif %} 41 |

42 |
43 | 44 |
45 | {{ input_csrf_token() }} 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for admin in admins %} 59 | {% set entry = admin[1] %} 60 | 61 | {% set mail = entry.get('mail')[0] |e %} 62 | {% set cn = entry.get('cn', [''])[0] |e %} 63 | {% set accountStatus = entry.get('accountStatus', ['disabled'])[0] |lower |e %} 64 | {% set domainGlobalAdmin = entry.get('domainGlobalAdmin', ['no'])[0] |e %} 65 | 66 | 67 | 75 | 80 | 81 | 82 | 83 | {% endfor %} 84 | 85 |
{{ _('Display Name') }}{{ _('Mail Address') }}{{ _('Global Admin') }}
68 | 74 | 76 | 77 | {{ set_account_status_img(accountStatus) }} 78 | {{ cn |cut_string }} 79 | {{ highlight_username_in_mail(mail) }}{{ set_admin_type_img(domainGlobalAdmin) }}
86 | 87 | 100 | 101 | 102 | {# -- box body -- #} 103 | {# -- content box -- #} 104 | {% endif %} 105 | 106 | {% endblock main %} 107 | -------------------------------------------------------------------------------- /templates/default/ldap/domain/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/general.html" import 4 | display_add_domain 5 | with context %} 6 | 7 | {% from "macros/msg_handlers.html" import domain_msg_handler with context %} 8 | 9 | {% block title %}{{ _('Add domain') }}{% endblock title %} 10 | {% block navlinks_create %}class="active"{% endblock %} 11 | 12 | {% block main %} 13 | 14 | {# Show system message #} 15 | {{ domain_msg_handler(msg) }} 16 | 17 |
18 |
19 |
20 |

{{ _('Add domain') }}

21 |
22 | 23 | {{ display_add_domain(label=false, 24 | preferred_language=preferred_language, 25 | languagemaps=languagemaps, 26 | timezones=None) }} 27 |
28 |
29 | 30 | {% endblock main %} 31 | -------------------------------------------------------------------------------- /templates/default/ldap/ldif.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /templates/default/ldap/user/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_csrf_token 5 | with context 6 | %} 7 | 8 | {% from "macros/general.html" import 9 | display_subnav, 10 | display_input_cn, 11 | display_reset_password, 12 | display_random_password, 13 | display_quota 14 | with context 15 | %} 16 | 17 | {% from "macros/msg_handlers.html" import user_msg_handler with context %} 18 | 19 | {% block title %}{{ _('Add mail user') }}{% endblock title %} 20 | {% block navlinks_create %}class="active"{% endblock %} 21 | 22 | {% block breadcrumb %} 23 | {% set crumbs = [ 24 | (ctx.homepath + '/domains', _('All domains')), 25 | (ctx.homepath + '/profile/domain/general/' + cur_domain, cur_domain), 26 | (ctx.homepath + '/users/' + cur_domain, _('Users')), 27 | ] %} 28 | 29 | {{ display_subnav(crumbs) }} 30 | {% endblock %} 31 | 32 | {% block main %} 33 | {# Show system message #} 34 | {% if msg %} 35 | {% if msg.startswith('PW_') %} 36 | {% set _pw_errors = msg.split(',') %} 37 | {% for _err in _pw_errors %} 38 | {{ user_msg_handler(_err) }} 39 | {% endfor %} 40 | {% else %} 41 | {{ user_msg_handler(msg) }} 42 | {% endif %} 43 | {% endif %} 44 | 45 | {# Number of accounts #} 46 | {% set numberOfAccounts = domainAccountSetting.get('numberOfUsers', '0') |int %} 47 | 48 | {# Default language #} 49 | {% set defaultLanguage = domainAccountSetting.get('defaultLanguage', '') %} 50 | 51 | {# Display input field for adding new user. #} 52 |
53 |
54 |
55 | 58 | 59 |

{{ _('Add mail user') }}

60 |
61 | 62 |
63 |
64 | {{ input_csrf_token() }} 65 | 66 |
67 |
68 |
69 |

{{ _('Add mail user under domain') }} *

70 | 71 | 76 | 77 |
78 |
79 |

{{ _('Mail Address') }} *

80 | 81 | @{{ cur_domain }} 87 | 88 |
89 | 90 |
 
91 | 92 | {{ display_reset_password(min_passwd_length=min_passwd_length, 93 | max_passwd_length=max_passwd_length, 94 | store_password_in_plain_text=store_password_in_plain_text) }} 95 | 96 |
 
97 | 98 | {{ display_input_cn(value=cn, account_type='user') }} 99 | {{ display_quota(value=defaultUserQuota, 100 | show_spare_quota=true, 101 | show_value_in_input=true, 102 | show_used_quota=false) }} 103 | 104 |
{# .col2-3 #} 105 |
106 | {{ display_random_password(password_length=min_passwd_length, 107 | password_policies=password_policies) }} 108 |
109 |
{# .columns #} 110 | 111 |
112 |
113 |

 

114 | 115 | 116 | 117 |
118 |
119 |
{# -- End box-wrap -- #} 120 |
{# -- End content-box -- #} 121 |
{# -- End box-body -- #} 122 | {% endblock main %} 123 | -------------------------------------------------------------------------------- /templates/default/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ _('Login to manage your mail domains & accounts') |title }} 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 24 | 25 | 26 | {# 27 | @languagemaps language maps 28 | #} 29 | 30 | {% from "macros/msg_handlers.html" import login_msg_handler with context %} 31 | 32 | 33 | 34 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /templates/default/macros/ldap.html: -------------------------------------------------------------------------------- 1 | {% from "macros/form_inputs.html" import 2 | input_text 3 | with context 4 | %} 5 | 6 | {% macro display_job_titles(values=None) -%} 7 | {% if not values %} 8 | {{ input_text(label=_('Job Title'), 9 | input_name='title', 10 | value='') }} 11 | {% else %} 12 | {% for value in values |sort %} 13 | {% if loop.first %} 14 | {% set label = _('Job Title') %} 15 | {% else %} 16 | {% set label = '' %} 17 | {% endif %} 18 | 19 | {{ input_text(label=label, 20 | input_name='title', 21 | value=value) }} 22 | {% endfor %} 23 | {% endif %} 24 | 25 |
26 |

 

27 | 28 | 29 | 30 |
31 | {%- endmacro %} 32 | -------------------------------------------------------------------------------- /templates/default/sql/admin/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_submit, 5 | input_csrf_token 6 | with context 7 | %} 8 | 9 | {% from "macros/general.html" import 10 | display_random_password, 11 | display_preferred_language, 12 | display_add_admin, 13 | with context %} 14 | 15 | {% from "macros/msg_handlers.html" import admin_msg_handler with context %} 16 | 17 | {% block navlinks_create %}class="active"{% endblock %} 18 | 19 | {% block title %}{{ _('Add admin') }}{% endblock title %} 20 | 21 | {% block main %} 22 | {# Show system message #} 23 | {% if msg %} 24 | {% if msg.startswith('PW_') %} 25 | {% set _pw_errors = msg.split(',') %} 26 | {% for _err in _pw_errors %} 27 | {{ admin_msg_handler(_err) }} 28 | {% endfor %} 29 | {% else %} 30 | {{ admin_msg_handler(msg) }} 31 | {% endif %} 32 | {% endif %} 33 | 34 |
35 |
36 |
37 | {# -- Tabs -- #} 38 | 42 | 43 |

{{ _('Add admin') }}

44 |
45 | 46 |
47 |
48 |
49 |
50 | {{ input_csrf_token() }} 51 | {{ display_add_admin(min_passwd_length=min_passwd_length, 52 | max_passwd_length=max_passwd_length, 53 | lang=default_language, 54 | languagemaps=languagemaps) }} 55 | 56 | {{ input_submit(label=_('Add')) }} 57 |
58 |
59 |
60 | 61 |
62 |
63 | {{ display_random_password(password_length=min_passwd_length, 64 | password_policies=password_policies) }} 65 |
66 |
67 |
68 |
69 |
70 | 71 | {% endblock main %} 72 | -------------------------------------------------------------------------------- /templates/default/sql/admin/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_csrf_token 5 | with context 6 | %} 7 | 8 | {% from "macros/general.html" import 9 | set_account_status_img, 10 | highlight_username_in_mail, 11 | set_admin_type_img, 12 | show_pages 13 | with context 14 | %} 15 | {% from "macros/msg_handlers.html" import admin_msg_handler with context %} 16 | 17 | {% block title %}{{ _('Domain Admins') }}{% endblock %} 18 | {% block navlinks_admins %}class="active"{% endblock %} 19 | 20 | {% block main %} 21 | {# Show system message #} 22 | {{ admin_msg_handler(msg) }} 23 | 24 | {# List admins #} 25 | 26 | {#{% if admins|length > 0 %}#} 27 | {% if admins is not string %} 28 |
29 |
30 |
31 | {% if session.get('is_global_admin') %} 32 | 35 | {% endif %} 36 | 37 |

{{ _('All admins') }} 38 | {% if total is defined and admins|length > 0 %} 39 | ({{ (cur_page-1) * page_size_limit + 1 }}-{{ (cur_page-1) * page_size_limit + admins|length}}/{{ total }}) 40 | {% endif %} 41 |

42 |
43 | 44 |
45 | {{ input_csrf_token() }} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for r in admins %} 59 | {% set mail = r.username |e %} 60 | {% set name = r.name |e %} 61 | 62 | 63 | 71 | 78 | 79 | {% if r.get('isglobaladmin') is not sameas none %} 80 | {# users marked as admin #} 81 | {% if r.get('isglobaladmin') == 1 %} 82 | 83 | {% else %} 84 | 85 | {% endif %} 86 | {% else %} 87 | {# Separate admin accounts #} 88 | {% if mail in allGlobalAdmins %} 89 | 90 | {% else %} 91 | 92 | {% endif %} 93 | {% endif %} 94 | 95 | {% endfor %} 96 | 97 |
{{ _('Display Name') }}{{ _('Mail Address') }}{{ _('Global Admin') }}
64 | 70 | 72 | 73 | {{ set_account_status_img(r.active) }} 74 | 75 | {# -- Show name -- #} 76 | {% if name == '' %}{{ mail.split('@', 1)[0] }}{% else %}{{ name |cut_string }}{% endif %} 77 | {{ mail }}{{ set_admin_type_img('yes') }}{{ set_admin_type_img('no') }}{{ set_admin_type_img('yes') }}{{ set_admin_type_img('no') }}
98 | 99 | 112 |
113 | 114 |
{# -- box body -- #} 115 |
{# -- content box -- #} 116 | {% endif %} 117 | 118 | {% endblock main %} 119 | -------------------------------------------------------------------------------- /templates/default/sql/admin/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_submit, 5 | input_csrf_token 6 | with context 7 | %} 8 | 9 | {% from "macros/general.html" import 10 | display_subnav, 11 | set_account_status_img, 12 | display_account_status, 13 | display_input_cn, 14 | display_preferred_language, 15 | display_reset_password, 16 | display_random_password, 17 | display_input_global_admin, 18 | with context 19 | %} 20 | 21 | {% from "macros/msg_handlers.html" import admin_msg_handler with context %} 22 | 23 | {% block title %}{{ _('Edit account profile') }}{% endblock %} 24 | {% block navlinks_admins %}class="active"{% endblock %} 25 | 26 | {% block breadcrumb %} 27 | {% if session.get('is_global_admin') %} 28 | {% set crumbs = [(ctx.homepath + '/admins', _('All admins')), 29 | ('active', ctx.homepath + '/profile/admin/general/' + mail, _('Profile of admin:') + ' ' + mail)] %} 30 | {% else %} 31 | {% set crumbs = [('active', ctx.homepath + '/profile/admin/general/' + mail, _('Profile of admin:') + ' ' + mail)] %} 32 | {% endif %} 33 | 34 | {{ display_subnav(crumbs) }} 35 | {% endblock %} 36 | 37 | 38 | {% block main %} 39 | 40 | 41 | {# Show system message #} 42 | {% if msg %} 43 | {% if msg.startswith('PW_') %} 44 | {% set _pw_errors = msg.split(',') %} 45 | {% for _err in _pw_errors %} 46 | {{ admin_msg_handler(_err) }} 47 | {% endfor %} 48 | {% else %} 49 | {{ admin_msg_handler(msg) }} 50 | {% endif %} 51 | {% endif %} 52 | 53 | {% set navlinks = [ 54 | ('general', _('General'), []), 55 | ('password', _('Password'), []), 56 | ] 57 | %} 58 | 59 |
60 |
61 |
62 |
    63 | {% for nav in navlinks %} 64 | {% if not false in nav[2] and not none in nav[2] %} 65 |
  • {{ nav[1] }}
  • 66 | {% endif %} 67 | {% endfor %} 68 |
69 | 70 |

{{ _('Profile of admin:') }} {{ mail }}

71 |
72 | 73 |
74 |
75 |
76 | {{ input_csrf_token() }} 77 | 78 |
79 |
80 | {% if session.get('is_global_admin') %} 81 | {{ display_account_status(profile.active) }} 82 | {% endif %} 83 | 84 | {{ display_input_cn(value=profile.name, account_type='admin') }} 85 | {{ display_preferred_language(value=profile.get('language', 'en_US') |e, 86 | languagemaps=languagemaps) }} 87 |
88 |
{# .columns #} 89 | 90 | {{ input_submit() }} 91 |
92 |
93 | 94 |
95 |
96 | {{ input_csrf_token() }} 97 |
98 |
99 | {{ display_reset_password( 100 | min_passwd_length=min_passwd_length, 101 | max_passwd_length=max_passwd_length, 102 | show_confirmpw=true, 103 | store_password_in_plain_text=store_password_in_plain_text) }} 104 |
105 | 106 |
107 | {{ display_random_password(password_length=min_passwd_length, 108 | password_policies=password_policies) }} 109 |
110 |
111 | 112 | {{ input_submit() }} 113 |
114 |
115 |
{# .box-wrap #} 116 |
{# .box-body #} 117 |
{#-- .content-box --#} 118 | {% endblock main %} 119 | 120 | {% block extra_js %} 121 | 127 | {% endblock extra_js %} 128 | -------------------------------------------------------------------------------- /templates/default/sql/domain/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "macros/general.html" import display_add_domain with context %} 3 | {% from "macros/msg_handlers.html" import domain_msg_handler with context %} 4 | 5 | {% block title %}{{ _('Add domain') }}{% endblock title %} 6 | {% block navlinks_create %}class="active"{% endblock %} 7 | 8 | {% block main %} 9 | {# Show system message #} 10 | {{ domain_msg_handler(msg) }} 11 | 12 |
13 |
14 |
15 |

{{ _('Add domain') }}

16 |
17 | 18 | {{ display_add_domain(label=false, 19 | preferred_language=preferred_language, 20 | languagemaps=languagemaps) }} 21 |
22 |
23 | 24 | {% endblock main %} 25 | -------------------------------------------------------------------------------- /templates/default/sql/domain/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import 4 | input_submit, 5 | input_csrf_token, 6 | input_text 7 | with context 8 | %} 9 | 10 | {% from "macros/general.html" import 11 | display_subnav, 12 | set_account_status_img, 13 | display_domain_cn, 14 | display_preferred_language, 15 | display_timezones, 16 | display_account_status, 17 | display_domain_backupmx, 18 | display_domain_quota, 19 | with context 20 | %} 21 | 22 | {% from "macros/msg_handlers.html" import domain_msg_handler with context %} 23 | 24 | {% block title %}{{ _('Edit account profile') }}{% endblock %} 25 | {% block navlinks_domains %}class="active"{% endblock %} 26 | 27 | {% block breadcrumb %} 28 | {% set crumbs = [ 29 | (ctx.homepath + '/domains', _('All domains')), 30 | ('active', ctx.homepath + '/profile/domain/general/' + cur_domain, _('Profile of domain:') + ' ' + cur_domain), 31 | (ctx.homepath + '/users/' + cur_domain, _('Users')), 32 | ] 33 | %} 34 | {{ display_subnav(crumbs) }} 35 | {% endblock %} 36 | 37 | 38 | {# Domain profile. #} 39 | {% block main %} 40 | 41 | {# Show system message #} 42 | {{ domain_msg_handler(msg) }} 43 | 44 | {% set navlinks = [ 45 | ('general', _('General'), [true]), 46 | ] 47 | %} 48 | 49 |
50 |
51 | 52 | {#-- Links --#} 53 |
54 |
    55 | {% for nav in navlinks %} 56 | {% if not false in nav[2] and not none in nav[2] %} 57 |
  • {{ nav[1] }}
  • 58 | {% endif %} 59 | {% endfor %} 60 |
61 |

{{ _('Profile of domain:') }} {{ cur_domain }}

62 |
{# .box-header #} 63 | 64 |
65 | {# profile_type: general #} 66 |
67 |
68 | {{ input_csrf_token() }} 69 | 70 |
71 |
72 | {{ display_account_status(profile.active, account_type='domain') }} 73 | {{ display_domain_cn(cn=profile.description) }} 74 |
{# .col2-3 #} 75 |
76 | 77 | {{ input_submit() }} 78 |
79 |
80 |
81 |
82 |
83 | {% endblock main %} 84 | 85 | {% block extra_js %} 86 | 91 | {% endblock extra_js %} 92 | -------------------------------------------------------------------------------- /templates/default/sql/user/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% from "macros/form_inputs.html" import input_csrf_token with context %} 4 | 5 | {% from "macros/general.html" import 6 | display_subnav, 7 | display_input_cn, 8 | display_preferred_language, 9 | display_reset_password, 10 | display_random_password, 11 | display_quota 12 | with context 13 | %} 14 | {% from "macros/msg_handlers.html" import user_msg_handler with context %} 15 | 16 | {% block title %}{{ _('Add mail user') }}{% endblock title %} 17 | {% block navlinks_create %}class="active"{% endblock %} 18 | 19 | {% block breadcrumb %} 20 | {% set crumbs = [ 21 | (ctx.homepath + '/domains', _('All domains')), 22 | (ctx.homepath + '/profile/domain/general/' + cur_domain, cur_domain), 23 | (ctx.homepath + '/users/' + cur_domain, _('Users')), 24 | ] 25 | %} 26 | {{ display_subnav(crumbs) }} 27 | {% endblock %} 28 | 29 | 30 | {% block main %} 31 | {# Show system message #} 32 | {% if msg %} 33 | {% if msg.startswith('PW_') %} 34 | {% set _pw_errors = msg.split(',') %} 35 | {% for _err in _pw_errors %} 36 | {{ user_msg_handler(_err) }} 37 | {% endfor %} 38 | {% else %} 39 | {{ user_msg_handler(msg) }} 40 | {% endif %} 41 | {% endif %} 42 | 43 |
44 |
45 |
46 | 49 | 50 |

{{ _('Add mail user') }}

51 |
52 | 53 |
54 |
55 | {{ input_csrf_token() }} 56 |
57 |
58 |
59 |

{{ _('Add mail user under domain') }} *

60 | 61 | 66 | 67 |
68 |
69 |

{{ _('Mail Address') }} *

70 | 71 | @{{ cur_domain }} 77 | 78 |
79 | 80 |
 
81 | 82 | {{ display_reset_password(min_passwd_length=min_passwd_length, 83 | max_passwd_length=max_passwd_length, 84 | store_password_in_plain_text=store_password_in_plain_text) }} 85 | 86 |
 
87 | 88 | {{ display_input_cn(value=cn, account_type='user') }} 89 | 90 | {{ display_preferred_language(value=domain_settings.get('default_language', 'en_US'), 91 | languagemaps=languagemaps) }} 92 | 93 | {{ display_quota(value=domain_settings.get('default_user_quota', 0), 94 | show_value_in_input=true, 95 | show_used_quota=false) }} 96 | 97 |
{# .col2-3 #} 98 | 99 |
100 | {{ display_random_password(password_length=min_passwd_length, 101 | password_policies=password_policies) }} 102 |
103 |
{# .columns #} 104 | 105 |
106 |
107 |

 

108 | 109 | 110 | 111 |
112 |
113 |
{# -- End box-wrap -- #} 114 |
{# -- End content-box -- #} 115 |
{# -- End box-body -- #} 116 | {% endblock main %} 117 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Cron Jobs 2 | 3 | * dump_disclaimer.py 4 | 5 | Dump per-domain disclaimer which stored in LDAP or SQL database. 6 | It's safe to execute it manually. 7 | 8 | * cleanup_amavisd_db.py 9 | 10 | Cleanup old records from Amavisd database. It's safe to execute it manually. 11 | 12 | * delete_mailboxes.py 13 | 14 | Delete mailboxes which are scheduled to be removed. The schedule date 15 | was set while you removed the mail account with iRedAdmin(-Pro). 16 | 17 | # Utils 18 | 19 | * upgrade_iredadmin.sh 20 | 21 | Upgrade an old iRedAdmin(-Pro) to current release. 22 | 23 | * update_mailbox_quota.py 24 | 25 | Update mailbox quota for one user (specified on command line) or bulk users 26 | (read from a plain text file). 27 | 28 | * notify_quarantined_recipients.py 29 | 30 | Notify local recipients (via email) that they have emails quarantined on 31 | server and not delivered to their mailbox. 32 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/tools/__init__.py -------------------------------------------------------------------------------- /tools/cleanup_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Author: Zhang Huangbin 4 | # Purpose: Remove old records in iRedAdmin SQL database. 5 | 6 | # USAGE: 7 | # 8 | # 1: Make sure you have proper values for below two parameters: 9 | # 10 | # IREDADMIN_LOG_KEPT_DAYS = 30 11 | # 12 | # Default values is defined in libs/default_settings.py, you can override 13 | # them in settings.py. WARNING: DO NOT MODIFY libs/default_settings.py. 14 | # 15 | # 2: Test this script in command line directly, make sure no errors in output 16 | # message. 17 | # 18 | # # python cleanup_db.py 19 | # 20 | # 3: Setup a daily cron job to execute this script. For example: execute 21 | # it daily at 1:30AM. 22 | # 23 | # 30 1 * * * python /path/to/cleanup_db.py >/dev/null 24 | # 25 | # That's all. 26 | 27 | import os 28 | import sys 29 | import time 30 | 31 | os.environ['LC_ALL'] = 'C' 32 | 33 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 34 | sys.path.insert(0, rootdir) 35 | 36 | import web 37 | import settings 38 | from tools.ira_tool_lib import debug, logger, sql_dbn, get_db_conn, sql_count_id 39 | 40 | web.config.debug = debug 41 | 42 | backend = settings.backend 43 | logger.info('Backend: %s' % backend) 44 | logger.info('SQL server: %s:%d' % (settings.iredadmin_db_host, int(settings.iredadmin_db_port))) 45 | 46 | query_size_limit = 100 47 | 48 | conn_iredadmin = get_db_conn('iredadmin') 49 | 50 | # 51 | # iredadmin.log 52 | # 53 | _days = settings.IREDADMIN_LOG_KEPT_DAYS 54 | logger.info('Delete old admin activity log (> %d days)' % _days) 55 | 56 | if sql_dbn == 'mysql': 57 | sql_where = """timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)""" % _days 58 | elif sql_dbn == 'postgres': 59 | sql_where = """timestamp < CURRENT_TIMESTAMP - INTERVAL '%d DAYS'""" % _days 60 | else: 61 | logger.error('Invalid SQL backend: %s' % sql_dbn) 62 | sys.exit() 63 | 64 | total_before = sql_count_id(conn_iredadmin, 'log') 65 | conn_iredadmin.delete('log', where=sql_where) 66 | total_after = sql_count_id(conn_iredadmin, 'log') 67 | logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after)) 68 | 69 | # 70 | # iredadmin.domain_ownership 71 | # 72 | _days = settings.DOMAIN_OWNERSHIP_EXPIRE_DAYS 73 | logger.info('Delete old domain ownership verification records (> %d days)' % _days) 74 | 75 | total_before = sql_count_id(conn_iredadmin, 'domain_ownership') 76 | conn_iredadmin.delete('domain_ownership', where="expire > %d" % (_days * 24 * 60 * 60)) 77 | total_after = sql_count_id(conn_iredadmin, 'domain_ownership') 78 | logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after)) 79 | 80 | # 81 | # iredadmin.newsletter_subunsub_confirms 82 | # 83 | now = int(time.time()) 84 | _hours = settings.NEWSLETTER_SUBSCRIPTION_REQUEST_KEEP_HOURS 85 | logger.info('Delete expired newsletter subscription confirm tokens (> %d hours)' % _hours) 86 | 87 | total_before = sql_count_id(conn_iredadmin, 'newsletter_subunsub_confirms') 88 | _expired = now - (_hours * 60 * 60) 89 | conn_iredadmin.delete('newsletter_subunsub_confirms', where="expired <= %d" % _expired) 90 | total_after = sql_count_id(conn_iredadmin, 'newsletter_subunsub_confirms') 91 | logger.info('\t- %d removed, %d left.' % (total_before - total_after, total_after)) 92 | -------------------------------------------------------------------------------- /tools/delete_sessions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Author: Zhang Huangbin 4 | # Purpose: Delete all records in SQL table "iredadmin.sessions" to force 5 | # all admins to re-login. 6 | 7 | import os 8 | import sys 9 | 10 | os.environ['LC_ALL'] = 'C' 11 | 12 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 13 | sys.path.insert(0, rootdir) 14 | 15 | import web 16 | from tools import ira_tool_lib 17 | 18 | web.config.debug = ira_tool_lib.debug 19 | logger = ira_tool_lib.logger 20 | 21 | conn = ira_tool_lib.get_db_conn('iredadmin') 22 | 23 | logger.info('Delete all existing sessions to force all admins to re-login.') 24 | conn.query('DELETE FROM sessions') 25 | -------------------------------------------------------------------------------- /tools/dump_quarantined_mails.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Zhang Huangbin 3 | # Purpose: Dump quarantined emails to given directory (specified on command line). 4 | # 5 | # Usage: 6 | # 7 | # python dump_quarantined_mail.py /path/to/dir 8 | 9 | import os 10 | import sys 11 | import time 12 | 13 | output_dir = sys.argv[1] 14 | if not os.path.isdir(output_dir): 15 | sys.exit("Output directory doesn't exist: %s" % output_dir) 16 | 17 | os.environ['LC_ALL'] = 'C' 18 | 19 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 20 | sys.path.insert(0, rootdir) 21 | 22 | import web 23 | from tools.ira_tool_lib import debug, get_db_conn 24 | 25 | web.config.debug = debug 26 | 27 | now = int(time.time()) 28 | conn_amavisd = get_db_conn('amavisd') 29 | conn_iredadmin = get_db_conn('iredadmin') 30 | 31 | # Get last time 32 | last_time = 0 33 | try: 34 | qr = conn_iredadmin.select('tracking', what='v', where="k='dump_quarantined_mail'", limit=1) 35 | if qr: 36 | last_time = int(qr[0].v) 37 | except: 38 | pass 39 | 40 | # Get value of all `quarantine.mail_id`. 41 | try: 42 | qr = conn_amavisd.select(['msgs', 'quarantine'], 43 | what='msgs.mail_id AS mail_id', 44 | where='msgs.mail_id=quarantine.mail_id AND msgs.time_num >= %d' % last_time, 45 | group='msgs.mail_id') 46 | except Exception as e: 47 | print('<<< ERROR >>> {}'.format(repr(e))) 48 | sys.exit() 49 | 50 | total = len(qr) 51 | print("* Found {} quarantined emails in SQL db.".format(total)) 52 | 53 | counter = 1 54 | for r in qr: 55 | mail_id = str(r.mail_id) 56 | try: 57 | records = conn_amavisd.select('quarantine', 58 | what='mail_text', 59 | where='mail_id = %s' % web.sqlquote(mail_id), 60 | order='chunk_ind ASC') 61 | 62 | if not records: 63 | continue 64 | 65 | # Combine mail_text as RAW mail message. 66 | message = '' 67 | for i in list(records): 68 | for j in i.mail_text: 69 | message += j 70 | 71 | # Write message to file 72 | try: 73 | eml_path = os.path.join(output_dir, 'spam-' + mail_id) 74 | print("[{}/{}] Dumping email to file: {}".format(counter, total, eml_path)) 75 | 76 | f = open(eml_path, 'w') 77 | f.write(message) 78 | f.close() 79 | except Exception as e: 80 | print('<<< ERROR >>> cannot write file {}'.format(repr(e))) 81 | except Exception as e: 82 | print("<<< ERROR >>> {}".format(repr(e))) 83 | 84 | counter += 1 85 | 86 | # Log last time. 87 | conn_iredadmin.delete('tracking', where="k='dump_quarantined_mail'") 88 | conn_iredadmin.insert('tracking', k='dump_quarantined_mail', v=now) 89 | -------------------------------------------------------------------------------- /tools/ira_tool_lib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Zhang Huangbin 3 | # Purpose: Library used by other scripts under tools/ directory. 4 | 5 | import os 6 | import sys 7 | import logging 8 | 9 | debug = False 10 | os.environ['LC_ALL'] = 'C' 11 | 12 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 13 | sys.path.insert(0, rootdir) 14 | 15 | import web 16 | web.config.debug = debug 17 | 18 | import settings 19 | from libs import iredutils 20 | 21 | backend = settings.backend 22 | if backend in ['ldap', 'mysql']: 23 | sql_dbn = 'mysql' 24 | elif backend in ['pgsql']: 25 | sql_dbn = 'postgres' 26 | else: 27 | sys.exit('Error: Unsupported backend (%s).' % backend) 28 | 29 | # logging 30 | logger = logging.getLogger('iredadmin') 31 | _ch = logging.StreamHandler(sys.stdout) 32 | _formatter = logging.Formatter('* %(message)s') 33 | _ch.setFormatter(_formatter) 34 | logger.addHandler(_ch) 35 | logger.setLevel(logging.INFO) 36 | 37 | 38 | def get_db_conn(db_name): 39 | if backend == 'ldap' and db_name in ['ldap', 'vmail']: 40 | logger.error("""Please use code below to get LDAP connection cursor:\n 41 | 42 | from libs.ldaplib.core import LDAPWrap\n 43 | _wrap = LDAPWrap()\n 44 | conn = _wrap.conn\n""") 45 | 46 | return None 47 | 48 | try: 49 | conn = web.database( 50 | dbn=sql_dbn, 51 | host=settings.__dict__[db_name + '_db_host'], 52 | port=int(settings.__dict__[db_name + '_db_port']), 53 | db=settings.__dict__[db_name + '_db_name'], 54 | user=settings.__dict__[db_name + '_db_user'], 55 | pw=settings.__dict__[db_name + '_db_password'], 56 | ) 57 | 58 | conn.supports_multiple_insert = True 59 | return conn 60 | except Exception as e: 61 | logger.error(e) 62 | return None 63 | 64 | 65 | # Log in `iredadmin.log` 66 | def log_to_iredadmin(msg, event, admin='', username='', loglevel='info'): 67 | conn = get_db_conn('iredadmin') 68 | 69 | try: 70 | conn.insert('log', 71 | admin=admin, 72 | username=username, 73 | event=event, 74 | loglevel=loglevel, 75 | msg=str(msg), 76 | ip='127.0.0.1', 77 | timestamp=iredutils.get_gmttime()) 78 | except: 79 | pass 80 | 81 | return None 82 | 83 | 84 | def sql_count_id(conn, table, column='id', where=None): 85 | if where: 86 | qr = conn.select(table, 87 | what='count(%s) as total' % column, 88 | where=where) 89 | else: 90 | qr = conn.select(table, 91 | what='count(%s) as total' % column) 92 | if qr: 93 | total = qr[0].total 94 | else: 95 | total = 0 96 | 97 | return total 98 | -------------------------------------------------------------------------------- /tools/promote_user_to_global_admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Zhang Huangbin 3 | # Purpose: Promote given user to be a global admin. 4 | # FYI https://docs.iredmail.org/promote.user.to.be.global.admin.html 5 | # Usage: 6 | # python3 promote_to_global_admin.py 7 | 8 | 9 | def usage(): 10 | print("""Usage: Run this script with user email address: 11 | 12 | # python3 promote_to_global_admin.py user@domain.com 13 | """) 14 | 15 | 16 | import os 17 | import sys 18 | 19 | os.environ['LC_ALL'] = 'C' 20 | 21 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 22 | sys.path.insert(0, rootdir) 23 | 24 | import web 25 | import settings 26 | from tools.ira_tool_lib import debug, get_db_conn 27 | from libs.iredutils import is_email 28 | 29 | backend = settings.backend 30 | web.config.debug = debug 31 | 32 | # Check arguments 33 | if len(sys.argv) == 2: 34 | email = sys.argv[1] 35 | 36 | if not is_email(email): 37 | usage() 38 | sys.exit() 39 | else: 40 | usage() 41 | sys.exit() 42 | 43 | if backend == 'ldap': 44 | from libs.ldaplib.core import LDAPWrap 45 | from libs.ldaplib import ldaputils 46 | _wrap = LDAPWrap() 47 | conn = _wrap.conn 48 | 49 | dn = ldaputils.rdn_value_to_user_dn(email) 50 | mod_attrs = ldaputils.attr_ldif(attr="enabledService", value="domainadmin", mode="add") 51 | mod_attrs += ldaputils.attr_ldif(attr="domainGlobalAdmin", value="yes", mode="add") 52 | 53 | try: 54 | conn.modify_s(dn, mod_attrs) 55 | print("User {} is now a global admin.".format(email)) 56 | except Exception as e: 57 | print("<<< ERROR >>> {}".format(repr(e))) 58 | 59 | elif backend in ['mysql', 'pgsql']: 60 | conn = get_db_conn('vmail') 61 | try: 62 | conn.update("mailbox", 63 | isadmin=1, 64 | isglobaladmin=1, 65 | where="username='{}'".format(email)) 66 | 67 | conn.insert("domain_admins", 68 | username=email, 69 | domain="ALL") 70 | 71 | print("User {} is now a global admin.".format(email)) 72 | except Exception as e: 73 | print("<<< ERROR >>> {}".format(repr(e))) 74 | -------------------------------------------------------------------------------- /tools/reset_user_password.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Zhang Huangbin 3 | # Purpose: Update user password. 4 | # Usage: 5 | # python reset_user_password.py 6 | 7 | 8 | def usage(): 9 | print("""Usage: Run this script with user email address and new plain password: 10 | 11 | # python3 reset_user_password.py user@domain.com 123456 12 | """) 13 | 14 | 15 | import os 16 | import sys 17 | 18 | os.environ['LC_ALL'] = 'C' 19 | 20 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 21 | sys.path.insert(0, rootdir) 22 | 23 | import web 24 | import settings 25 | from tools.ira_tool_lib import debug, get_db_conn 26 | from libs.iredutils import is_email 27 | from libs.iredpwd import generate_password_hash 28 | 29 | backend = settings.backend 30 | web.config.debug = debug 31 | 32 | # Check arguments 33 | if len(sys.argv) == 3: 34 | email = sys.argv[1] 35 | pw = sys.argv[2] 36 | 37 | if not is_email(email): 38 | usage() 39 | sys.exit() 40 | else: 41 | usage() 42 | sys.exit() 43 | 44 | pw_hash = generate_password_hash(pw) 45 | if backend == 'ldap': 46 | from libs.ldaplib.core import LDAPWrap 47 | from libs.ldaplib.ldaputils import rdn_value_to_user_dn, mod_replace 48 | _wrap = LDAPWrap() 49 | conn = _wrap.conn 50 | 51 | dn = rdn_value_to_user_dn(email) 52 | mod_attrs = mod_replace('userPassword', pw_hash) 53 | try: 54 | conn.modify_s(dn, mod_attrs) 55 | print("[{}] Password has been reset.".format(email)) 56 | except Exception as e: 57 | print("<<< ERROR >>> {}".format(repr(e))) 58 | elif backend in ['mysql', 'pgsql']: 59 | conn = get_db_conn('vmail') 60 | try: 61 | conn.update('mailbox', 62 | password=pw_hash, 63 | where="username='{}'".format(email)) 64 | print("[{}] Password has been reset.".format(email)) 65 | except Exception as e: 66 | print("<<< ERROR >>> {}".format(repr(e))) 67 | -------------------------------------------------------------------------------- /tools/update_mailbox_quota.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Author: Zhang Huangbin 4 | # Purpose: Update mailbox quota for one user or multiple users. 5 | # Note: Mailbox quota size unit is bytes. for example, size `104857600` is 100 MB. 6 | 7 | 8 | def usage(): 9 | print("""Usage: 10 | 11 | 1) Update mailbox quota for one user. 12 | 13 | To simply update one user's quota, run this script with user's email 14 | address and new quota size (in bytes). For example: 15 | 16 | # python3 update_mailbox_quota.py user@domain.com 2048576000 17 | 18 | 2) Update mailbox quota for multiple users. 19 | 20 | - Create text file "new_quota.txt", each line contains one email address 21 | and the new quota size (in bytes). 22 | 23 | user1@domain.com 20480000 24 | user2@domain.com 102400000 25 | user3@domain.com 409600000 26 | 27 | - Run this script with this file: 28 | 29 | # python3 update_mailbox_quota.py new_quota.txt 30 | """) 31 | 32 | 33 | import os 34 | import sys 35 | 36 | os.environ['LC_ALL'] = 'C' 37 | 38 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 39 | sys.path.insert(0, rootdir) 40 | 41 | import web 42 | import settings 43 | from tools.ira_tool_lib import debug, logger, get_db_conn 44 | from libs.iredutils import is_email 45 | 46 | backend = settings.backend 47 | logger.info('Backend: {}'.format(backend)) 48 | 49 | web.config.debug = debug 50 | 51 | # List of (email, quota) tuples. 52 | users = [] 53 | 54 | # Check arguments 55 | if len(sys.argv) == 2: 56 | # bulk update 57 | text_file = sys.argv[1] 58 | if not os.path.isfile(text_file): 59 | sys.exit('<<< ERROR>>> Not a regular file: %s' % text_file) 60 | 61 | # Get all (email, quota) tuples. 62 | f = open(text_file) 63 | for _line in f.readlines(): 64 | (_email, _quota) = _line.strip().split(' ', 1) 65 | if is_email(_email) and _quota.isdigit(): 66 | users += [(_email, _quota)] 67 | else: 68 | print("[SKIP] no valid email address or quota: {}".format(_line)) 69 | 70 | elif len(sys.argv) == 3: 71 | # update single user 72 | _email = sys.argv[1] 73 | _quota = sys.argv[2] 74 | 75 | if is_email(_email): 76 | users += [(_email, _quota)] 77 | else: 78 | sys.exit('<<< ERROR >>> Not an valid email address: %s' % _email) 79 | else: 80 | usage() 81 | 82 | total = len(users) 83 | logger.info('{} users in total.'.format(total)) 84 | 85 | count = 1 86 | if backend == 'ldap': 87 | from libs.ldaplib.core import LDAPWrap 88 | from libs.ldaplib.ldaputils import rdn_value_to_user_dn, mod_replace 89 | _wrap = LDAPWrap() 90 | conn = _wrap.conn 91 | 92 | for (_email, _quota) in users: 93 | logger.info('(%d/%d) Updating %s -> %s' % (count, total, _email, _quota)) 94 | dn = rdn_value_to_user_dn(_email) 95 | mod_attrs = mod_replace('mailQuota', _quota) 96 | try: 97 | conn.modify_s(dn, mod_attrs) 98 | except Exception as e: 99 | print("<<< ERROR >>> {}".format(e)) 100 | elif backend in ['mysql', 'pgsql']: 101 | conn = get_db_conn('vmail') 102 | for (_email, _quota) in users: 103 | logger.info('(%d/%d) Updating %s -> %s' % (count, total, _email, _quota)) 104 | conn.update('mailbox', 105 | quota=int(_quota), 106 | where="username='%s'" % _email) 107 | -------------------------------------------------------------------------------- /tools/update_password_in_csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Author: Zhang Huangbin 4 | # Purpose: Update user passwords from records in a CSV file. 5 | 6 | import os 7 | import sys 8 | 9 | 10 | def usage(): 11 | print("""Usage: 12 | 13 | - Store the email address and new password in a plain text file, e.g. 14 | 'passwords.csv'. format is: 15 | 16 | 17 | 18 | Samples: 19 | 20 | user1@domain.com pF4mTq4jaRzDLlWl 21 | user2@domain.com SPhkTUlZs1TBxvmJ 22 | user3@domain.com 8deNR8IBLycRujDN 23 | 24 | - Run this script with this file: 25 | 26 | python3 update_password_in_csv.py passwords.csv 27 | """) 28 | 29 | 30 | os.environ['LC_ALL'] = 'C' 31 | 32 | rootdir = os.path.abspath(os.path.dirname(__file__)) + '/../' 33 | sys.path.insert(0, rootdir) 34 | 35 | import web 36 | import settings 37 | from tools.ira_tool_lib import debug, logger, get_db_conn 38 | from libs.iredutils import is_email 39 | from libs.iredpwd import generate_password_hash 40 | 41 | backend = settings.backend 42 | logger.info('Backend: %s' % backend) 43 | 44 | web.config.debug = debug 45 | 46 | logger.info('Parsing command line arguments.') 47 | 48 | # File which stores email and quota. 49 | text_file = '' 50 | 51 | # The separator 52 | column_separator = ' ' 53 | 54 | # List of (email, quota) tuples. 55 | users = [] 56 | 57 | # Check arguments 58 | if len(sys.argv) == 2: 59 | text_file = sys.argv[1] 60 | if not os.path.isfile(text_file): 61 | sys.exit('<<< ERROR>>> Not a regular file: %s' % text_file) 62 | 63 | # Get all (email, password) tuples. 64 | f = open(text_file) 65 | line_num = 0 66 | for _line in f.readlines(): 67 | line_num += 1 68 | (_email, _pw) = _line.split(column_separator, 1) 69 | if is_email(_email): 70 | users += [(_email, _pw)] 71 | else: 72 | print("[SKIP] line {}: no valid email address: {}".format(line_num, _line)) 73 | f.close() 74 | else: 75 | usage() 76 | 77 | total = len(users) 78 | logger.info('%d users in total.' % total) 79 | 80 | count = 1 81 | if backend == 'ldap': 82 | from libs.ldaplib.core import LDAPWrap 83 | from libs.ldaplib.ldaputils import rdn_value_to_user_dn, mod_replace 84 | _wrap = LDAPWrap() 85 | conn = _wrap.conn 86 | 87 | for (_email, _pw) in users: 88 | logger.info('(%d/%d) Updating %s' % (count, total, _email)) 89 | 90 | dn = rdn_value_to_user_dn(_email) 91 | pw_hash = generate_password_hash(_pw) 92 | mod_attrs = mod_replace('userPassword', pw_hash) 93 | try: 94 | conn.modify_s(dn, mod_attrs) 95 | except Exception as e: 96 | print("<<< ERROR >>> {}".format(repr(e))) 97 | elif backend in ['mysql', 'pgsql']: 98 | conn = get_db_conn('vmail') 99 | for (_email, _pw) in users: 100 | logger.info('(%d/%d) Updating %s' % (count, total, _email)) 101 | pw_hash = generate_password_hash(_pw) 102 | conn.update('mailbox', 103 | password=pw_hash, 104 | where="username='%s'" % _email) 105 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """web.py: makes web apps (http://webpy.org)""" 3 | 4 | from . import ( # noqa: F401 5 | db, 6 | debugerror, 7 | form, 8 | http, 9 | httpserver, 10 | net, 11 | session, 12 | template, 13 | utils, 14 | webapi, 15 | wsgi, 16 | ) 17 | from .application import * # noqa: F401,F403 18 | from .db import * # noqa: F401,F403 19 | from .debugerror import * # noqa: F401,F403 20 | from .http import * # noqa: F401,F403 21 | from .httpserver import * # noqa: F401,F403 22 | from .net import * # noqa: F401,F403 23 | from .utils import * # noqa: F401,F403 24 | from .webapi import * # noqa: F401,F403 25 | from .wsgi import * # noqa: F401,F403 26 | 27 | __version__ = "0.62" 28 | __author__ = [ 29 | "Aaron Swartz ", 30 | "Anand Chitipothu ", 31 | ] 32 | __license__ = "public domain" 33 | __contributors__ = "see http://webpy.org/changes" 34 | -------------------------------------------------------------------------------- /web/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iredmail/iRedAdmin/b537e71ecf522d7f10180f5f0aab4a98a881893a/web/contrib/__init__.py -------------------------------------------------------------------------------- /web/contrib/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interface to various templating engines. 3 | """ 4 | import os.path 5 | 6 | __all__ = ["render_cheetah", "render_genshi", "render_mako", "cache"] 7 | 8 | 9 | class render_cheetah: 10 | """Rendering interface to Cheetah Templates. 11 | 12 | Example: 13 | 14 | render = render_cheetah('templates') 15 | render.hello(name="cheetah") 16 | """ 17 | 18 | def __init__(self, path): 19 | # give error if Chetah is not installed 20 | from Cheetah.Template import Template # noqa: F401 21 | 22 | self.path = path 23 | 24 | def __getattr__(self, name): 25 | from Cheetah.Template import Template 26 | 27 | path = os.path.join(self.path, name + ".html") 28 | 29 | def template(**kw): 30 | t = Template(file=path, searchList=[kw]) 31 | return t.respond() 32 | 33 | return template 34 | 35 | 36 | class render_genshi: 37 | """Rendering interface genshi templates. 38 | Example: 39 | 40 | for xml/html templates. 41 | 42 | render = render_genshi(['templates/']) 43 | render.hello(name='genshi') 44 | 45 | For text templates: 46 | 47 | render = render_genshi(['templates/'], type='text') 48 | render.hello(name='genshi') 49 | """ 50 | 51 | def __init__(self, *a, **kwargs): 52 | from genshi.template import TemplateLoader 53 | 54 | self._type = kwargs.pop("type", None) 55 | self._loader = TemplateLoader(*a, **kwargs) 56 | 57 | def __getattr__(self, name): 58 | # Assuming all templates are html 59 | path = name + ".html" 60 | 61 | if self._type == "text": 62 | from genshi.template import TextTemplate 63 | 64 | cls = TextTemplate 65 | type = "text" 66 | else: 67 | cls = None 68 | type = self._type 69 | 70 | t = self._loader.load(path, cls=cls) 71 | 72 | def template(**kw): 73 | stream = t.generate(**kw) 74 | if type: 75 | return stream.render(type) 76 | else: 77 | return stream.render() 78 | 79 | return template 80 | 81 | 82 | class render_jinja: 83 | """Rendering interface to Jinja2 Templates 84 | 85 | Example: 86 | 87 | render= render_jinja('templates') 88 | render.hello(name='jinja2') 89 | """ 90 | 91 | def __init__(self, *a, **kwargs): 92 | extensions = kwargs.pop("extensions", []) 93 | globals = kwargs.pop("globals", {}) 94 | 95 | from jinja2 import Environment, FileSystemLoader 96 | 97 | self._lookup = Environment( 98 | loader=FileSystemLoader(*a, **kwargs), extensions=extensions 99 | ) 100 | self._lookup.globals.update(globals) 101 | 102 | def __getattr__(self, name): 103 | # Assuming all templates end with .html 104 | path = name + ".html" 105 | t = self._lookup.get_template(path) 106 | return t.render 107 | 108 | 109 | class render_mako: 110 | """Rendering interface to Mako Templates. 111 | 112 | Example: 113 | 114 | render = render_mako(directories=['templates']) 115 | render.hello(name="mako") 116 | """ 117 | 118 | def __init__(self, *a, **kwargs): 119 | from mako.lookup import TemplateLookup 120 | 121 | self._lookup = TemplateLookup(*a, **kwargs) 122 | 123 | def __getattr__(self, name): 124 | # Assuming all templates are html 125 | path = name + ".html" 126 | t = self._lookup.get_template(path) 127 | return t.render 128 | 129 | 130 | class cache: 131 | """Cache for any rendering interface. 132 | 133 | Example: 134 | 135 | render = cache(render_cheetah("templates/")) 136 | render.hello(name='cache') 137 | """ 138 | 139 | def __init__(self, render): 140 | self._render = render 141 | self._cache = {} 142 | 143 | def __getattr__(self, name): 144 | if name not in self._cache: 145 | self._cache[name] = getattr(self._render, name) 146 | return self._cache[name] 147 | -------------------------------------------------------------------------------- /web/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP Utilities 3 | (from web.py) 4 | """ 5 | 6 | __all__ = [ 7 | "expires", 8 | "lastmodified", 9 | "prefixurl", 10 | "modified", 11 | "changequery", 12 | "url", 13 | "profiler", 14 | ] 15 | 16 | import datetime 17 | 18 | from . import net, utils 19 | from . import webapi as web 20 | from .py3helpers import iteritems 21 | 22 | try: 23 | from urllib.parse import urlencode as urllib_urlencode 24 | except ImportError: 25 | from urllib import urlencode as urllib_urlencode 26 | 27 | 28 | def prefixurl(base=""): 29 | """ 30 | Sorry, this function is really difficult to explain. 31 | Maybe some other time. 32 | """ 33 | url = web.ctx.path.lstrip("/") 34 | for i in range(url.count("/")): 35 | base += "../" 36 | if not base: 37 | base = "./" 38 | return base 39 | 40 | 41 | def expires(delta): 42 | """ 43 | Outputs an `Expires` header for `delta` from now. 44 | `delta` is a `timedelta` object or a number of seconds. 45 | """ 46 | if isinstance(delta, int): 47 | delta = datetime.timedelta(seconds=delta) 48 | date_obj = datetime.datetime.utcnow() + delta 49 | web.header("Expires", net.httpdate(date_obj)) 50 | 51 | 52 | def lastmodified(date_obj): 53 | """Outputs a `Last-Modified` header for `datetime`.""" 54 | web.header("Last-Modified", net.httpdate(date_obj)) 55 | 56 | 57 | def modified(date=None, etag=None): 58 | """ 59 | Checks to see if the page has been modified since the version in the 60 | requester's cache. 61 | 62 | When you publish pages, you can include `Last-Modified` and `ETag` 63 | with the date the page was last modified and an opaque token for 64 | the particular version, respectively. When readers reload the page, 65 | the browser sends along the modification date and etag value for 66 | the version it has in its cache. If the page hasn't changed, 67 | the server can just return `304 Not Modified` and not have to 68 | send the whole page again. 69 | 70 | This function takes the last-modified date `date` and the ETag `etag` 71 | and checks the headers to see if they match. If they do, it returns 72 | `True`, or otherwise it raises NotModified error. It also sets 73 | `Last-Modified` and `ETag` output headers. 74 | """ 75 | n = {x.strip('" ') for x in web.ctx.env.get("HTTP_IF_NONE_MATCH", "").split(",")} 76 | m = net.parsehttpdate(web.ctx.env.get("HTTP_IF_MODIFIED_SINCE", "").split(";")[0]) 77 | validate = False 78 | if etag: 79 | if "*" in n or etag in n: 80 | validate = True 81 | if date and m: 82 | # we subtract a second because 83 | # HTTP dates don't have sub-second precision 84 | if date - datetime.timedelta(seconds=1) <= m: 85 | validate = True 86 | 87 | if date: 88 | lastmodified(date) 89 | if etag: 90 | web.header("ETag", '"' + etag + '"') 91 | if validate: 92 | raise web.notmodified() 93 | else: 94 | return True 95 | 96 | 97 | def urlencode(query, doseq=0): 98 | """ 99 | Same as urllib.urlencode, but supports unicode strings. 100 | 101 | >>> urlencode({'text':'foo bar'}) 102 | 'text=foo+bar' 103 | >>> urlencode({'x': [1, 2]}, doseq=True) 104 | 'x=1&x=2' 105 | """ 106 | 107 | def convert(value, doseq=False): 108 | if doseq and isinstance(value, list): 109 | return [convert(v) for v in value] 110 | else: 111 | return utils.safestr(value) 112 | 113 | query = {k: convert(v, doseq) for k, v in query.items()} 114 | return urllib_urlencode(query, doseq=doseq) 115 | 116 | 117 | def changequery(query=None, **kw): 118 | """ 119 | Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return 120 | `/foo?a=3&b=2` -- the same URL but with the arguments you requested 121 | changed. 122 | """ 123 | if query is None: 124 | query = web.rawinput(method="get") 125 | for k, v in iteritems(kw): 126 | if v is None: 127 | query.pop(k, None) 128 | else: 129 | query[k] = v 130 | out = web.ctx.path 131 | if query: 132 | out += "?" + urlencode(query, doseq=True) 133 | return out 134 | 135 | 136 | def url(path=None, doseq=False, **kw): 137 | """ 138 | Makes url by concatenating web.ctx.homepath and path and the 139 | query string created using the arguments. 140 | """ 141 | if path is None: 142 | path = web.ctx.path 143 | if path.startswith("/"): 144 | out = web.ctx.homepath + path 145 | else: 146 | out = path 147 | 148 | if kw: 149 | out += "?" + urlencode(kw, doseq=doseq) 150 | 151 | return out 152 | 153 | 154 | def profiler(app): 155 | """Outputs basic profiling information at the bottom of each response.""" 156 | from utils import profile 157 | 158 | def profile_internal(e, o): 159 | out, result = profile(app)(e, o) 160 | return list(out) + ["
" + net.websafe(result) + "
"] 161 | 162 | return profile_internal 163 | 164 | 165 | if __name__ == "__main__": 166 | import doctest 167 | 168 | doctest.testmod() 169 | -------------------------------------------------------------------------------- /web/py3helpers.py: -------------------------------------------------------------------------------- 1 | """Utilities for make the code run both on Python2 and Python3. 2 | """ 3 | 4 | # Dictionary iteration 5 | iterkeys = lambda d: iter(d.keys()) 6 | itervalues = lambda d: iter(d.values()) 7 | iteritems = lambda d: iter(d.items()) 8 | -------------------------------------------------------------------------------- /web/test.py: -------------------------------------------------------------------------------- 1 | """test utilities 2 | (part of web.py) 3 | """ 4 | import doctest 5 | import sys 6 | import unittest 7 | 8 | TestCase = unittest.TestCase 9 | TestSuite = unittest.TestSuite 10 | 11 | 12 | def load_modules(names): 13 | return [__import__(name, None, None, "x") for name in names] 14 | 15 | 16 | def module_suite(module, classnames=None): 17 | """Makes a suite from a module.""" 18 | if classnames: 19 | return unittest.TestLoader().loadTestsFromNames(classnames, module) 20 | elif hasattr(module, "suite"): 21 | return module.suite() 22 | else: 23 | return unittest.TestLoader().loadTestsFromModule(module) 24 | 25 | 26 | def doctest_suite(module_names): 27 | """Makes a test suite from doctests.""" 28 | suite = TestSuite() 29 | for mod in load_modules(module_names): 30 | suite.addTest(doctest.DocTestSuite(mod)) 31 | return suite 32 | 33 | 34 | def suite(module_names): 35 | """Creates a suite from multiple modules.""" 36 | suite = TestSuite() 37 | for mod in load_modules(module_names): 38 | suite.addTest(module_suite(mod)) 39 | return suite 40 | 41 | 42 | def runTests(suite): 43 | runner = unittest.TextTestRunner() 44 | return runner.run(suite) 45 | 46 | 47 | def main(suite=None): 48 | if not suite: 49 | main_module = __import__("__main__") 50 | # allow command line switches 51 | args = [a for a in sys.argv[1:] if not a.startswith("-")] 52 | suite = module_suite(main_module, args or None) 53 | 54 | result = runTests(suite) 55 | sys.exit(not result.wasSuccessful()) 56 | -------------------------------------------------------------------------------- /web/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI Utilities 3 | (from web.py) 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | from . import httpserver 10 | from . import webapi as web 11 | from .net import validaddr 12 | from .utils import intget, listget 13 | 14 | 15 | def runfcgi(func, addr=("localhost", 8000)): 16 | """Runs a WSGI function as a FastCGI server.""" 17 | import flup.server.fcgi as flups 18 | 19 | return flups.WSGIServer(func, multiplexed=True, bindAddress=addr, debug=False).run() 20 | 21 | 22 | def runscgi(func, addr=("localhost", 4000)): 23 | """Runs a WSGI function as an SCGI server.""" 24 | import flup.server.scgi as flups 25 | 26 | return flups.WSGIServer(func, bindAddress=addr, debug=False).run() 27 | 28 | 29 | def runwsgi(func): 30 | """ 31 | Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server, 32 | as appropriate based on context and `sys.argv`. 33 | """ 34 | 35 | if "SERVER_SOFTWARE" in os.environ: # cgi 36 | os.environ["FCGI_FORCE_CGI"] = "Y" 37 | 38 | # PHP_FCGI_CHILDREN is used by lighttpd fastcgi 39 | if "PHP_FCGI_CHILDREN" in os.environ or "SERVER_SOFTWARE" in os.environ: 40 | return runfcgi(func, None) 41 | 42 | if "fcgi" in sys.argv or "fastcgi" in sys.argv: 43 | args = sys.argv[1:] 44 | if "fastcgi" in args: 45 | args.remove("fastcgi") 46 | elif "fcgi" in args: 47 | args.remove("fcgi") 48 | 49 | if args: 50 | return runfcgi(func, validaddr(args[0])) 51 | else: 52 | return runfcgi(func, None) 53 | 54 | if "scgi" in sys.argv: 55 | args = sys.argv[1:] 56 | args.remove("scgi") 57 | if args: 58 | return runscgi(func, validaddr(args[0])) 59 | else: 60 | return runscgi(func) 61 | 62 | server_addr = validaddr(listget(sys.argv, 1, "")) 63 | if "PORT" in os.environ: # e.g. Heroku 64 | server_addr = ("0.0.0.0", intget(os.environ["PORT"])) 65 | 66 | return httpserver.runsimple(func, server_addr) 67 | 68 | 69 | def _is_dev_mode(): 70 | # Some embedded python interpreters won't have sys.arv 71 | # For details, see https://github.com/webpy/webpy/issues/87 72 | argv = getattr(sys, "argv", []) 73 | 74 | # quick hack to check if the program is running in dev mode. 75 | if ( 76 | "SERVER_SOFTWARE" in os.environ 77 | or "PHP_FCGI_CHILDREN" in os.environ 78 | or "fcgi" in argv 79 | or "fastcgi" in argv 80 | or "mod_wsgi" in argv 81 | ): 82 | return False 83 | return True 84 | 85 | 86 | # When running the builtin-server, enable debug mode if not already set. 87 | web.config.setdefault("debug", _is_dev_mode()) 88 | --------------------------------------------------------------------------------