mirror of
https://github.com/marcus-alicia/iRedAdmin-Pro-SQL.git
synced 2026-05-26 07:08:10 +00:00
Add files via upload
This commit is contained in:
1590
ChangeLog.sql
Normal file
1590
ChangeLog.sql
Normal file
File diff suppressed because it is too large
Load Diff
58
README.md
58
README.md
@@ -1,48 +1,22 @@
|
||||
# iRedAdmin-Pro-SQL
|
||||
* Please read file 'EULA' for End User License Agreement.
|
||||
|
||||
### Free & open-source repository of iRedAdmin-Pro-SQL, for everyone to enjoy <3
|
||||
* If you already have iRedAdmin open source edition or old iRedAdmin-Pro
|
||||
release installed, please follow below tutorial to upgrade it to the latest
|
||||
iRedAdmin-Pro, it's the easiest way with minimal steps:
|
||||
https://docs.iredmail.org/migrate.or.upgrade.iredadmin.html
|
||||
|
||||
Only very few files were changed. Original check has been commented out so you can understand what it did before.
|
||||
* Release Notes:
|
||||
https://docs.iredmail.org/iredadmin-pro.releases.html
|
||||
|
||||
```console
|
||||
- controllers/panel/sys_settings.py
|
||||
# This script did the actual check
|
||||
* iRedAdmin-Pro RESTful API interface:
|
||||
https://docs.iredmail.org/iredadmin-pro.restful.api.html
|
||||
|
||||
- templates/default/panel/license.html
|
||||
# Tiny change to remove the "Renew License" button
|
||||
* iRedAdmin-Pro documentations:
|
||||
https://docs.iredmail.org/#iredadmin
|
||||
|
||||
- static/default/css/screen.css
|
||||
# ctrl+shift+I formatting & changed color from green to purple. CSS file looks disgusting, refusing to clean that
|
||||
```
|
||||
* Report bugs/issues in our online support forum:
|
||||
https://forum.iredmail.org/
|
||||
|
||||
Thats it
|
||||
<br><br>
|
||||
|
||||
-----
|
||||
### Original Details
|
||||
|
||||
|Feature | iRedAdmin (OSE) | iRedAdmin-Pro|
|
||||
|------------------------------------|-----------------|--------------|
|
||||
$\textcolor{orange}{\textsf{Localized Web Interface}}$<br>English, German, Spanish, French, Italian, Polish, Chinese, and more. | X | X |
|
||||
$\textcolor{orange}{\textsf{RESTful API Interface}}$<br>Read our [API documentation](https://docs.iredmail.org/iredadmin-pro.restful.api.html) | | X |
|
||||
$\textcolor{orange}{\textsf{Unlimited Mail Domains}}$<br>Host as many mail domains as you want | X | X |
|
||||
$\textcolor{orange}{\textsf{Unlimited Mail Users}}$<br>With per-user mailbox quota control | X | X |
|
||||
$\textcolor{orange}{\textsf{Unlimited Mailing List/Aliases}}$<br>Manage members, access policies | | X |
|
||||
$\textcolor{orange}{\textsf{Unlimited Domain-Level Admins}}$<br>Either promote a mail user to domain admin role, or create a separated domain admin account | | X |
|
||||
$\textcolor{orange}{\textsf{Advanced Domain Management}}$<br>Domain-level mailbox quota, limit numbers of user/list/alias accounts, Relay, BCC, Alias, Domain, Catch-all, Backup MX, Throttling, Greylisting, Whitelists, Blacklists, Spam Policy, user password length and complexity control | | X |
|
||||
$\textcolor{orange}{\textsf{Advanced User Management}}$<br>Per-user BCC, Relay, Mail Forwarding, Alias Addresses, Throttling, Greylisting, Whitelists, Blacklists, Spam Policy, restrict login IP/network, Changing email address | | X |
|
||||
$\textcolor{orange}{\textsf{Self-Service}}$<br>Allow end user to manage their own preferences: Password, Mail Forwarding, Whitelists, Blacklists, Quarantined Mails, Spam Policy | | X |
|
||||
$\textcolor{orange}{\textsf{Service Control}}$<br>One click to enable/disable mail services for mail user: POP3, IMAP, SMTP, Sieve filter, Mail Forwarding, BCC, and more. | | X |
|
||||
$\textcolor{orange}{\textsf{Spam/Virus Quarantining}}$<br>Quarantine detected SPAM/Virus into SQL PostgreSQL database for later management (delete, release, whitelist, blacklist) | | X |
|
||||
$\textcolor{orange}{\textsf{View basic info of all sent and received emails}}$<br>Sender, Recipient, Subject, Spam Score, Size, Date | | X |
|
||||
$\textcolor{orange}{\textsf{Throttling}}$<br>Based on: max size of single email, number of max inbound/outbound emails, cumulative size of all inbound/outbound emails | | X |
|
||||
$\textcolor{orange}{\textsf{Whitelisting, Blacklisting}}$<br>Based on: IP addresses/networks, Sender address, Sender domain name | | X |
|
||||
$\textcolor{orange}{\textsf{Searching Account}}$<br>Searching with display name or email address, domain name | | X |
|
||||
$\textcolor{orange}{\textsf{Log Maildir Path of Deleted Dail User}}$<br>You can delete the mailbox on file system later, either manually or with a cron job | | X |
|
||||
$\textcolor{orange}{\textsf{Log Admin Activities}}$<br>Account creation, activation, removal, password change, and more. | | X |
|
||||
$\textcolor{orange}{\textsf{Fail2ban Integration}}$<br>View info of banned IP address (Country/City, reverse DNS name), log lines which triggerred the ban (easy to troubleshoot why the ban happened), and unban it with one click | | X |
|
||||
$\textcolor{orange}{\textsf{Last login track}}$<br>View the time of user last login via IMAP and POP3 services, also the time of last (locally) delivered email | | X |
|
||||
$\textcolor{orange}{\textsf{Export all managed mail accounts}}$<br>Export statistics of admins| | X |
|
||||
-----
|
||||
|
||||

|
||||
If you found any bug, typo in iRedAdmin-Pro source code, or you want to request
|
||||
a new feature, please don't hesitate to contact us: support@iredmail.org.
|
||||
Your feedback is greatly appreciated.
|
||||
|
||||
147
SQL/iredadmin.mysql
Normal file
147
SQL/iredadmin.mysql
Normal file
@@ -0,0 +1,147 @@
|
||||
-- CREATE DATABASE iredadmin DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
-- GRANT INSERT,UPDATE,DELETE,SELECT on iredadmin.* to iredadmin@localhost identified by 'secret_passwd';
|
||||
-- USE iredadmin;
|
||||
|
||||
--
|
||||
-- Session table required by webpy session module.
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `sessions` (
|
||||
`session_id` CHAR(128) UNIQUE NOT NULL,
|
||||
`atime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`data` TEXT
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
--
|
||||
-- Store all admin operations.
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `log` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`admin` VARCHAR(255) NOT NULL,
|
||||
`ip` VARCHAR(40) NOT NULL,
|
||||
`domain` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`username` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`event` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`loglevel` VARCHAR(10) NOT NULL DEFAULT 'info',
|
||||
`msg` TEXT,
|
||||
PRIMARY KEY (id),
|
||||
INDEX (timestamp),
|
||||
INDEX (admin),
|
||||
INDEX (ip),
|
||||
INDEX (domain),
|
||||
INDEX (username),
|
||||
INDEX (event),
|
||||
INDEX (loglevel)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `updatelog` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
`date` DATE NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
INDEX (date)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Used to store basic info of deleted mailboxes.
|
||||
CREATE TABLE IF NOT EXISTS `deleted_mailboxes` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- Email address of deleted user
|
||||
`username` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- Domain part of user email address
|
||||
`domain` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- Absolute path of user's mailbox
|
||||
`maildir` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- Which domain admin deleted this user
|
||||
`admin` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- The time scheduled to delete this mailbox.
|
||||
-- NOTE: it requires cron job + script to actually delete the mailbox.
|
||||
delete_date DATE DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
INDEX (timestamp),
|
||||
INDEX (username),
|
||||
INDEX (domain),
|
||||
INDEX (admin),
|
||||
INDEX (delete_date)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Key-value store.
|
||||
CREATE TABLE IF NOT EXISTS `tracking` (
|
||||
id BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
`k` VARCHAR(255) NOT NULL,
|
||||
`v` TEXT,
|
||||
`time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (k)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Store admin <-> domain <-> verify_code used to verify domain ownership
|
||||
CREATE TABLE IF NOT EXISTS domain_ownership (
|
||||
id BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
-- the admin who added this domain with iRedAdmin. Required if domain was
|
||||
-- added by a normal domain admin.
|
||||
admin VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- the domain we're going to verify. If we're going to verifying an alias
|
||||
-- domain, it stores primary domain.
|
||||
domain VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- if we're verifying an alias domain:
|
||||
-- - store primary domain in `domain`
|
||||
-- - store alias domain in `alias_domain`
|
||||
alias_domain VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- a unique string which domain admin should put in TXT type DNS record
|
||||
-- or as a web file on web server
|
||||
verify_code VARCHAR(100) NOT NULL DEFAULT '',
|
||||
-- store the verify status
|
||||
verified TINYINT(1) NOT NULL DEFAULT 0,
|
||||
-- store error message if any returned while verifying, so that domain
|
||||
-- admin can fix it
|
||||
message TEXT,
|
||||
-- the last time we verify it. If it's verified, this record will be
|
||||
-- removed in 1 month.
|
||||
last_verify TIMESTAMP NULL DEFAULT NULL,
|
||||
-- expire time. cron job `tools/cleanup_db.py` will remove verified or
|
||||
-- unverified domains regularly. e.g. one month.
|
||||
-- Note: stores seconds since Unix epoch
|
||||
expire INT UNSIGNED DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX (admin, domain, alias_domain),
|
||||
INDEX (verified)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- mailing list subscription/unsubscription confirms.
|
||||
CREATE TABLE IF NOT EXISTS `newsletter_subunsub_confirms` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
-- email of mailing list
|
||||
`mail` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- unique server wide id
|
||||
`mlid` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- email of subscriber
|
||||
`subscriber` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- kinds of 'subscribe', 'unsubscribe'
|
||||
`kind` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
-- unique server-wide id as confirm token
|
||||
`token` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`expired` INT UNSIGNED DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
INDEX (mail),
|
||||
UNIQUE INDEX (mlid, subscriber, kind),
|
||||
INDEX (token),
|
||||
INDEX (expired)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Key-value store for settings.
|
||||
-- `k` is the (unique) parameter name.
|
||||
-- `v` must be a valid JSON string with only one key: "value". Its value will
|
||||
-- be converted to Python native format (string, list, integer).
|
||||
-- Samples:
|
||||
-- {"value": 20}
|
||||
-- {"value": "a-string"}
|
||||
-- {"value": [v1, v2, v3, ...]}
|
||||
-- {"value": true}
|
||||
CREATE TABLE IF NOT EXISTS `settings` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
`account` VARCHAR(255) NOT NULL DEFAULT 'global',
|
||||
`k` VARCHAR(255) NOT NULL,
|
||||
`v` TEXT,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX (account, k)
|
||||
) ENGINE=InnoDB;
|
||||
116
SQL/iredadmin.pgsql
Normal file
116
SQL/iredadmin.pgsql
Normal file
@@ -0,0 +1,116 @@
|
||||
-- CREATE DATABASE iredadmin WITH TEMPLATE template0 ENCODING 'UTF8';
|
||||
-- CREATE ROLE iredadmin WITH LOGIN ENCRYPTED PASSWORD 'plain_password' NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
||||
-- \c iredadmin;
|
||||
|
||||
-- Session table required by webpy session module.
|
||||
CREATE TABLE sessions (
|
||||
session_id CHAR(128) UNIQUE NOT NULL,
|
||||
atime TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
data TEXT
|
||||
);
|
||||
|
||||
-- Store all admin operations.
|
||||
CREATE TABLE log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
admin VARCHAR(255) NOT NULL,
|
||||
timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ip VARCHAR(40) NOT NULL,
|
||||
domain VARCHAR(255) NOT NULL DEFAULT '',
|
||||
username VARCHAR(255) NOT NULL DEFAULT '',
|
||||
event VARCHAR(20) NOT NULL DEFAULT '',
|
||||
loglevel VARCHAR(10) NOT NULL DEFAULT 'info',
|
||||
msg TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_log_timestamp ON log (timestamp);
|
||||
CREATE INDEX idx_log_ip ON log (ip);
|
||||
CREATE INDEX idx_log_domain ON log (domain);
|
||||
CREATE INDEX idx_log_username ON log (username);
|
||||
CREATE INDEX idx_log_event ON log (event);
|
||||
CREATE INDEX idx_log_loglevel ON log (loglevel);
|
||||
|
||||
CREATE TABLE updatelog (
|
||||
date DATE NOT NULL,
|
||||
PRIMARY KEY (date)
|
||||
);
|
||||
|
||||
-- GRANT INSERT,UPDATE,DELETE,SELECT on sessions,log,updatelog to iredadmin;
|
||||
-- GRANT UPDATE,USAGE,SELECT ON log_id_seq TO iredadmin;
|
||||
|
||||
-- Key-value store.
|
||||
CREATE TABLE tracking (
|
||||
id SERIAL PRIMARY KEY,
|
||||
k VARCHAR(255) NOT NULL,
|
||||
v TEXT,
|
||||
time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_tracking_k ON tracking (k);
|
||||
|
||||
-- Store <-> domain <-> verify_code used to verify domain ownership
|
||||
CREATE TABLE domain_ownership (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- the admin who added this domain with iRedAdmin. Required if domain was
|
||||
-- added by a normal domain admin.
|
||||
admin VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- the domain we're going to verify. If we're going to verifying an alias
|
||||
-- domain, it stores primary domain.
|
||||
domain VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- if we're verifying an alias domain:
|
||||
-- - store primary domain in `domain`
|
||||
-- - store alias domain in `alias_domain`
|
||||
alias_domain VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- a unique string which domain admin should put in TXT type DNS record
|
||||
-- or as a web file on web server
|
||||
verify_code VARCHAR(100) NOT NULL DEFAULT '',
|
||||
-- store the verify status
|
||||
verified INT2 NOT NULL DEFAULT 0,
|
||||
-- store error message if any returned while verifying, so that domain
|
||||
-- admin can fix it
|
||||
message TEXT,
|
||||
-- the last time we verify it. If it's verified, this record will be
|
||||
-- removed in 1 month.
|
||||
last_verify TIMESTAMP NULL DEFAULT NULL,
|
||||
-- expire time. cron job `tools/cleanup_db.py` will remove verified or
|
||||
-- unverified domains regularly. e.g. one month.
|
||||
-- Note: stores seconds since Unix epoch
|
||||
expire INT DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_ownership_1 ON domain_ownership (admin, domain, alias_domain);
|
||||
CREATE INDEX idx_ownership_2 ON domain_ownership (verified);
|
||||
|
||||
-- mailing list subscription/unsubscription confirms.
|
||||
CREATE TABLE newsletter_subunsub_confirms (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- email of mailing list
|
||||
mail VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- unique server wide id
|
||||
mlid VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- email of subscriber
|
||||
subscriber VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- kinds of 'subscribe', 'unsubscribe'
|
||||
kind VARCHAR(20) NOT NULL DEFAULT '',
|
||||
-- unique server-wide id as confirm token
|
||||
token VARCHAR(255) NOT NULL DEFAULT '',
|
||||
expired INT DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_subunsub_confirms_1 ON newsletter_subunsub_confirms (mlid, subscriber, kind);
|
||||
CREATE INDEX idx_subunsub_confirms_2 ON newsletter_subunsub_confirms (mail);
|
||||
CREATE INDEX idx_subunsub_confirms_3 ON newsletter_subunsub_confirms (token);
|
||||
CREATE INDEX idx_subunsub_confirms_4 ON newsletter_subunsub_confirms (expired);
|
||||
|
||||
-- Key-value store for settings.
|
||||
-- `k` is the (unique) parameter name.
|
||||
-- `v` must be a valid JSON string with only one key: "value". Its value will
|
||||
-- be converted to Python native format (string, list, integer).
|
||||
-- Samples:
|
||||
-- {"value": 20}
|
||||
-- {"value": "a-string"}
|
||||
-- {"value": [v1, v2, v3, ...]}
|
||||
-- {"value": true}
|
||||
CREATE TABLE settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account VARCHAR(255) NOT NULL DEFAULT 'global',
|
||||
k VARCHAR(255) NOT NULL,
|
||||
v TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_settings_account_k ON settings (account, k);
|
||||
20
SQL/snippets/newsletter_subunsub_confirms.mysql
Normal file
20
SQL/snippets/newsletter_subunsub_confirms.mysql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- mailing list subscription/unsubscription confirms.
|
||||
CREATE TABLE IF NOT EXISTS `newsletter_subunsub_confirms` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT,
|
||||
-- email of mailing list
|
||||
`mail` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- unique server wide id
|
||||
`mlid` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- email of subscriber
|
||||
`subscriber` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- kinds of 'subscribe', 'unsubscribe'
|
||||
`kind` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
-- unique server-wide id as confirm token
|
||||
`token` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`expired` INT UNSIGNED DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
INDEX (mail),
|
||||
UNIQUE INDEX (mlid, subscriber, kind),
|
||||
INDEX (token),
|
||||
INDEX (expired)
|
||||
) ENGINE=InnoDB;
|
||||
19
SQL/snippets/newsletter_subunsub_confirms.pgsql
Normal file
19
SQL/snippets/newsletter_subunsub_confirms.pgsql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- mailing list subscription/unsubscription confirms.
|
||||
CREATE TABLE newsletter_subunsub_confirms (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- email of mailing list
|
||||
mail VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- unique server wide id
|
||||
mlid VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- email of subscriber
|
||||
subscriber VARCHAR(255) NOT NULL DEFAULT '',
|
||||
-- kinds of 'subscribe', 'unsubscribe'
|
||||
kind VARCHAR(20) NOT NULL DEFAULT '',
|
||||
-- unique server-wide id as confirm token
|
||||
token VARCHAR(255) NOT NULL DEFAULT '',
|
||||
expired INT DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_subunsub_confirms_1 ON newsletter_subunsub_confirms (mlid, subscriber, kind);
|
||||
CREATE INDEX idx_subunsub_confirms_2 ON newsletter_subunsub_confirms (mail);
|
||||
CREATE INDEX idx_subunsub_confirms_3 ON newsletter_subunsub_confirms (token);
|
||||
CREATE INDEX idx_subunsub_confirms_4 ON newsletter_subunsub_confirms (expired);
|
||||
7
SQL/snippets/settings.pgsql
Normal file
7
SQL/snippets/settings.pgsql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account VARCHAR(255) NOT NULL DEFAULT 'global',
|
||||
k VARCHAR(255) NOT NULL,
|
||||
v TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_settings_account_k ON settings (account, k);
|
||||
3
i18n/babel.cfg
Normal file
3
i18n/babel.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
[jinja2: templates/**.html]
|
||||
encoding = utf-8
|
||||
line_statement_prefix = %
|
||||
BIN
i18n/bg_BG/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/bg_BG/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2906
i18n/bg_BG/LC_MESSAGES/iredadmin.po
Normal file
2906
i18n/bg_BG/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/cs_CZ/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/cs_CZ/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2813
i18n/cs_CZ/LC_MESSAGES/iredadmin.po
Normal file
2813
i18n/cs_CZ/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/da_DK/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/da_DK/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2954
i18n/da_DK/LC_MESSAGES/iredadmin.po
Normal file
2954
i18n/da_DK/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/de_DE/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/de_DE/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
3004
i18n/de_DE/LC_MESSAGES/iredadmin.po
Normal file
3004
i18n/de_DE/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/en_US/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/en_US/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2787
i18n/en_US/LC_MESSAGES/iredadmin.po
Normal file
2787
i18n/en_US/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/es_ES/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/es_ES/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
3028
i18n/es_ES/LC_MESSAGES/iredadmin.po
Normal file
3028
i18n/es_ES/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/fi_FI/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/fi_FI/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2810
i18n/fi_FI/LC_MESSAGES/iredadmin.po
Normal file
2810
i18n/fi_FI/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/fr_FR/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/fr_FR/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2828
i18n/fr_FR/LC_MESSAGES/iredadmin.po
Normal file
2828
i18n/fr_FR/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/hu_HU/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/hu_HU/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2841
i18n/hu_HU/LC_MESSAGES/iredadmin.po
Normal file
2841
i18n/hu_HU/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
2769
i18n/iredadmin.po
Normal file
2769
i18n/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/it_IT/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/it_IT/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2946
i18n/it_IT/LC_MESSAGES/iredadmin.po
Normal file
2946
i18n/it_IT/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/ja_JP/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/ja_JP/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2798
i18n/ja_JP/LC_MESSAGES/iredadmin.po
Normal file
2798
i18n/ja_JP/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/ko_KR/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/ko_KR/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2793
i18n/ko_KR/LC_MESSAGES/iredadmin.po
Normal file
2793
i18n/ko_KR/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/lv_LV/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/lv_LV/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2953
i18n/lv_LV/LC_MESSAGES/iredadmin.po
Normal file
2953
i18n/lv_LV/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/nl_NL/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/nl_NL/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2814
i18n/nl_NL/LC_MESSAGES/iredadmin.po
Normal file
2814
i18n/nl_NL/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/pl_PL/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/pl_PL/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2817
i18n/pl_PL/LC_MESSAGES/iredadmin.po
Normal file
2817
i18n/pl_PL/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/pt_BR/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/pt_BR/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2862
i18n/pt_BR/LC_MESSAGES/iredadmin.po
Normal file
2862
i18n/pt_BR/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/ru_RU/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/ru_RU/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2811
i18n/ru_RU/LC_MESSAGES/iredadmin.po
Normal file
2811
i18n/ru_RU/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
2814
i18n/sl_SI/LC_MESSAGES/iredadmin.po
Normal file
2814
i18n/sl_SI/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/sr/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/sr/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2812
i18n/sr/LC_MESSAGES/iredadmin.po
Normal file
2812
i18n/sr/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
i18n/sv_SE/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/sv_SE/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2983
i18n/sv_SE/LC_MESSAGES/iredadmin.po
Normal file
2983
i18n/sv_SE/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
105
i18n/translation.sh
Normal file
105
i18n/translation.sh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: Zhang Huangbin (zhb _at_ iredmail.org)
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# This file is part of iRedAdmin-Pro, which is official web-based admin
|
||||
# panel (Full-Featured Edition) for iRedMail.
|
||||
#
|
||||
# ---- Restrictions ----
|
||||
# * Source code is only available after you purchase it, so that you can
|
||||
# modify it to fit your need, but it is NOT allowed to redistribute
|
||||
# and sell iRedAdmin and the one you modified based on iRedAdmin.
|
||||
#
|
||||
# * We will do our best to solve all bugs found in official iRedAdmin,
|
||||
# but we are not guarantee to solve bugs occured in your modified copy.
|
||||
#
|
||||
# * It is NOT allowed to deployed on more than 1 server.
|
||||
#
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Available actions: [all, LANG].
|
||||
ACTIONORLANG="$1"
|
||||
|
||||
if [ -z "${ACTIONORLANG}" ]; then
|
||||
cat <<EOF
|
||||
|
||||
Usage: $0 [all, LANGUAGE]
|
||||
|
||||
Example:
|
||||
|
||||
$ $0 all
|
||||
$ $0 zh_CN
|
||||
$ $0 fr_Fr
|
||||
|
||||
EOF
|
||||
exit 255
|
||||
fi
|
||||
|
||||
DOMAIN="iredadmin"
|
||||
POFILE="${DOMAIN}.po"
|
||||
#AVAILABLE_LANGS="$(ls -d *_*)"
|
||||
AVAILABLE_LANGS="$(ls -ld * | awk '/^d/ {print $NF}')"
|
||||
|
||||
extract_latest()
|
||||
{
|
||||
# Extract strings from template files.
|
||||
echo "* Extract localizable messages from template files to ${POFILE}..."
|
||||
|
||||
pybabel extract \
|
||||
-F babel.cfg \
|
||||
--no-location \
|
||||
--omit-header \
|
||||
--sort-output \
|
||||
--charset=utf-8 \
|
||||
--msgid-bugs-address=support@iredmail.org \
|
||||
-o ${POFILE} \
|
||||
.. >/dev/null
|
||||
}
|
||||
|
||||
update_po()
|
||||
{
|
||||
# Update PO files.
|
||||
echo "* Updating existing translations ..."
|
||||
|
||||
for lang in ${LANGUAGES}
|
||||
do
|
||||
pofile="${lang}/LC_MESSAGES/${DOMAIN}.po"
|
||||
|
||||
[ -d ${lang}/LC_MESSAGES/ ] || mkdir -p ${lang}/LC_MESSAGES/
|
||||
pybabel update --ignore-obsolete\
|
||||
-i ${POFILE} \
|
||||
-D ${DOMAIN} \
|
||||
-d . \
|
||||
-l ${lang}
|
||||
|
||||
# Remove 'fuzzy' tag.
|
||||
perl -pi -e 's/#, fuzzy//' ${pofile}
|
||||
|
||||
# Comment ', python-format'.
|
||||
perl -pi -e 's/^(, python-format.*)/#${1}/' ${pofile}
|
||||
|
||||
# Update 'Project-Id-Version'
|
||||
perl -pi -e 's#^("Project-Id-Version:).*#${1} iRedAdmin-Pro\\n"#g' ${pofile}
|
||||
perl -pi -e 's#^("POT-Creation-Date:.*\n)##g' ${pofile}
|
||||
perl -pi -e 's#^("Report-Msgid-Bugs-To:.*\n)##g' ${pofile}
|
||||
done
|
||||
}
|
||||
|
||||
convert_po_to_mo()
|
||||
{
|
||||
for lang in ${LANGUAGES}; do
|
||||
echo " + Converting ${lang} ..."
|
||||
msgfmt --statistics --check-format ${lang}/LC_MESSAGES/${DOMAIN}.po -o ${lang}/LC_MESSAGES/${DOMAIN}.mo
|
||||
done
|
||||
}
|
||||
|
||||
if [ X"${ACTIONORLANG}" == X"all" -o X"${ACTIONORLANG}" == X"" ]; then
|
||||
export LANGUAGES="${AVAILABLE_LANGS}"
|
||||
else
|
||||
export LANGUAGES="$(basename ${ACTIONORLANG})"
|
||||
fi
|
||||
|
||||
extract_latest && \
|
||||
update_po && \
|
||||
convert_po_to_mo
|
||||
BIN
i18n/zh_TW/LC_MESSAGES/iredadmin.mo
Normal file
BIN
i18n/zh_TW/LC_MESSAGES/iredadmin.mo
Normal file
Binary file not shown.
2797
i18n/zh_TW/LC_MESSAGES/iredadmin.po
Normal file
2797
i18n/zh_TW/LC_MESSAGES/iredadmin.po
Normal file
File diff suppressed because it is too large
Load Diff
19
iredadmin.py
Normal file
19
iredadmin.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
from libs import iredbase
|
||||
|
||||
# Initialize webpy app.
|
||||
app = iredbase.app
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Starting webpy builtin http server.
|
||||
# WARNING: this should not be used for production.
|
||||
app.run()
|
||||
else:
|
||||
# Run as a WSGI application
|
||||
application = app.wsgifunc()
|
||||
5
libs/__init__.py
Normal file
5
libs/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
__author__ = "Zhang Huangbin"
|
||||
__author_mail__ = "zhb@iredmail.org"
|
||||
__version_ldap__ = "5.4"
|
||||
__version_sql__ = "5.3"
|
||||
__url_license_terms__ = "http://www.iredmail.org/pricing.html#EULA"
|
||||
56
libs/amavisd/__init__.py
Normal file
56
libs/amavisd/__init__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
from libs import iredutils
|
||||
|
||||
# mail_id and secret_id are composed of below characters:
|
||||
# - Amavisd-new-2.7+: [ A-Z, a-z, 0-9, -, _ ]
|
||||
# - Amavisd-new-2.6.x: [ A-Z, a-z, 0-9, +, - ]
|
||||
MAIL_ID_CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+-_'
|
||||
|
||||
WBLIST_FORM_INPUT_NAMES = {'wl_sender': 'whitelistSender',
|
||||
'bl_sender': 'blacklistSender',
|
||||
'wl_rcpt': 'whitelistRecipient',
|
||||
'bl_rcpt': 'blacklistRecipient'}
|
||||
|
||||
# Available quarantined types in iRedAdmin web interface, and the short code
|
||||
# in `amavisd.msgs` sql table.
|
||||
QUARANTINE_TYPES = {'spam': 'S',
|
||||
'virus': 'V',
|
||||
'banned': 'B',
|
||||
'clean': 'C',
|
||||
'badheader': 'H',
|
||||
'badmime': 'M'}
|
||||
|
||||
# Value of `msgs.content` and comment.
|
||||
CONTENT_TYPES = {'B': 'Banned',
|
||||
'C': 'Clean',
|
||||
'H': 'Bad header',
|
||||
'M': 'Bad mime',
|
||||
'O': 'Oversized',
|
||||
'S': 'Spam',
|
||||
'T': 'MTA error',
|
||||
'V': 'Virus',
|
||||
'U': 'Unchecked'}
|
||||
|
||||
|
||||
def get_wblist_from_form(form, form_input_name):
|
||||
# Available form_input_name are listed in WBLIST_FORM_INPUT_NAMES
|
||||
input_name = WBLIST_FORM_INPUT_NAMES[form_input_name]
|
||||
|
||||
addresses = []
|
||||
for _line in form.get(input_name, '').splitlines():
|
||||
if _line:
|
||||
try:
|
||||
_line = str(_line)
|
||||
addresses.append(_line)
|
||||
except:
|
||||
pass
|
||||
|
||||
valid_addresses = []
|
||||
for addr in addresses:
|
||||
if iredutils.is_valid_wblist_address(addr) and (addr not in valid_addresses):
|
||||
valid_addresses.append(addr)
|
||||
else:
|
||||
continue
|
||||
|
||||
return valid_addresses
|
||||
572
libs/amavisd/log.py
Normal file
572
libs/amavisd/log.py
Normal file
@@ -0,0 +1,572 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import time
|
||||
import web
|
||||
import settings
|
||||
from libs import iredutils
|
||||
from libs.logger import logger, log_traceback
|
||||
from libs.amavisd import MAIL_ID_CHARACTERS
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
|
||||
# Import backend related modules.
|
||||
if settings.backend == 'ldap':
|
||||
from libs.ldaplib import admin as ldap_lib_admin
|
||||
elif settings.backend in ['mysql', 'pgsql']:
|
||||
from libs.sqllib import admin as sql_lib_admin
|
||||
|
||||
|
||||
def delete_all_records(log_type=None, account=None):
|
||||
# Delete all records, or delete records older than one week.
|
||||
# :param log_type: sent, received
|
||||
# :param account: single email address, domain name, '@.'
|
||||
account_is_email = False
|
||||
account_is_domain = False
|
||||
maddr_ids = []
|
||||
managed_domains_reversed = []
|
||||
|
||||
if account:
|
||||
if iredutils.is_email(account):
|
||||
account_is_email = True
|
||||
elif iredutils.is_domain(account):
|
||||
account_is_domain = True
|
||||
else:
|
||||
if account == '@.':
|
||||
pass
|
||||
else:
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# get `maddr.id` of this account
|
||||
if account_is_email:
|
||||
# user
|
||||
qr = web.conn_amavisd.select('maddr',
|
||||
vars={'account': account},
|
||||
what='id',
|
||||
where='email=$account',
|
||||
limit=1)
|
||||
if qr:
|
||||
maddr_ids.append(qr[0].id)
|
||||
elif account_is_domain:
|
||||
# domain
|
||||
reversed_domain = iredutils.reverse_amavisd_domain_names([account])[0]
|
||||
qr = web.conn_amavisd.select('maddr',
|
||||
vars={'account': reversed_domain},
|
||||
what='id',
|
||||
where='domain=$account')
|
||||
if qr:
|
||||
for r in qr:
|
||||
maddr_ids.append(r.id)
|
||||
|
||||
# no `maddr.id`, no mail log.
|
||||
if not maddr_ids:
|
||||
return True,
|
||||
else:
|
||||
if session.get('is_global_admin'):
|
||||
web.conn_amavisd.delete('msgs', where='1=1')
|
||||
web.conn_amavisd.delete('msgrcpt', where='1=1')
|
||||
return True,
|
||||
|
||||
# Get all managed domains by normal admin.
|
||||
managed_domains = []
|
||||
if settings.backend == 'ldap':
|
||||
_qr = ldap_lib_admin.get_managed_domains(admin=session.get('username'))
|
||||
if _qr[0]:
|
||||
managed_domains = _qr[1]
|
||||
|
||||
elif settings.backend in ['mysql', 'pgsql']:
|
||||
qr = sql_lib_admin.get_managed_domains(admin=session.get('username'),
|
||||
domain_name_only=True)
|
||||
|
||||
if qr[0]:
|
||||
managed_domains = qr[1]
|
||||
else:
|
||||
return False, 'UNKNOWN_BACKEND'
|
||||
|
||||
managed_domains_reversed = iredutils.reverse_amavisd_domain_names(managed_domains)
|
||||
if not managed_domains_reversed:
|
||||
return True,
|
||||
|
||||
try:
|
||||
# Delete records in tables: msgs, msgrcpt.
|
||||
if log_type == 'sent':
|
||||
if account:
|
||||
# Delete all records sent by single user
|
||||
web.conn_amavisd.delete('msgs',
|
||||
vars={'maddr_ids': maddr_ids},
|
||||
where='sid IN $maddr_ids')
|
||||
else:
|
||||
# Delete all records sent by domain users
|
||||
web.conn_amavisd.delete('msgs',
|
||||
vars={'managed_domains_reversed': managed_domains_reversed},
|
||||
where='sid IN (SELECT id FROM maddr WHERE domain IN $managed_domains_reversed)')
|
||||
|
||||
elif log_type == 'received':
|
||||
if account:
|
||||
web.conn_amavisd.delete('msgs',
|
||||
vars={'maddr_ids': maddr_ids},
|
||||
where='mail_id IN (SELECT mail_id FROM msgrcpt WHERE rid IN $maddr_ids)')
|
||||
|
||||
web.conn_amavisd.delete('msgrcpt',
|
||||
vars={'maddr_ids': maddr_ids},
|
||||
where='rid IN $maddr_ids')
|
||||
else:
|
||||
all_rcpt_ids = [] # maddr.id
|
||||
|
||||
qr = web.conn_amavisd.select('maddr',
|
||||
vars={'domains': managed_domains_reversed},
|
||||
what='id',
|
||||
where='domain IN $domains')
|
||||
for i in qr:
|
||||
all_rcpt_ids.append(i['id'])
|
||||
|
||||
del qr
|
||||
|
||||
web.conn_amavisd.delete('msgs',
|
||||
vars={'ids': all_rcpt_ids},
|
||||
where='mail_id IN (SELECT mail_id FROM msgrcpt WHERE rid IN $ids)')
|
||||
|
||||
web.conn_amavisd.delete('msgrcpt',
|
||||
vars={'ids': all_rcpt_ids},
|
||||
where='rid IN $ids')
|
||||
|
||||
del all_rcpt_ids
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_records_by_mail_id(log_type='sent', mail_ids=None):
|
||||
# log_type -- received, sent, quarantined, quarantine
|
||||
if not isinstance(mail_ids, list):
|
||||
return False, 'INCORRECT_MAILID'
|
||||
|
||||
# Filter unexpected mail_id strings.
|
||||
mail_ids = [v for v in mail_ids if len(set(v) - set(MAIL_ID_CHARACTERS)) == 0]
|
||||
|
||||
if not mail_ids:
|
||||
return True,
|
||||
|
||||
# Converted into SQL style list.
|
||||
mail_ids = web.sqlquote(mail_ids)
|
||||
|
||||
if log_type in ['received', 'sent', 'quarantined', 'quarantine']:
|
||||
try:
|
||||
# Delete records in tables: msgs, msgrcpt.
|
||||
web.conn_amavisd.delete('msgs', where='mail_id IN %s' % mail_ids)
|
||||
web.conn_amavisd.delete('msgrcpt', where='mail_id IN %s' % mail_ids)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
if log_type in ['quarantined', 'quarantine']:
|
||||
try:
|
||||
web.conn_amavisd.delete('quarantine', where="mail_id IN %s" % mail_ids)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def count_incoming_mails(reversedDomainNames=None,
|
||||
timeLength=None,
|
||||
sqlAppendWhere=None):
|
||||
# timeLength is seconds.
|
||||
total = 0
|
||||
|
||||
if not reversedDomainNames:
|
||||
if not session.get('account_is_mail_user'):
|
||||
return total
|
||||
|
||||
if sqlAppendWhere:
|
||||
sql_append_where = sqlAppendWhere
|
||||
else:
|
||||
sql_append_where = ' AND recip.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
|
||||
if isinstance(timeLength, int):
|
||||
_now = int(time.time())
|
||||
_length_seconds = _now - timeLength
|
||||
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
|
||||
|
||||
try:
|
||||
qr = web.conn_amavisd.query('''
|
||||
-- Get number of incoming mails.
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
LEFT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
LEFT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
LEFT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.quar_type <> 'Q' %s
|
||||
''' % sql_append_where)
|
||||
total = qr[0].total or 0
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def count_outgoing_mails(reversedDomainNames=None,
|
||||
timeLength=None,
|
||||
sqlAppendWhere=None):
|
||||
# timeLength is seconds.
|
||||
total = 0
|
||||
sql_append_where = ''
|
||||
|
||||
if not reversedDomainNames:
|
||||
return total
|
||||
|
||||
if sqlAppendWhere:
|
||||
sql_append_where = sqlAppendWhere
|
||||
else:
|
||||
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
|
||||
if isinstance(timeLength, int):
|
||||
_now = int(time.time())
|
||||
_length_seconds = _now - timeLength
|
||||
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
|
||||
|
||||
try:
|
||||
qr_count = web.conn_amavisd.query("""
|
||||
-- Get number of outgoing mails.
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.quar_type <> 'Q' %s""" % sql_append_where)
|
||||
total = qr_count[0].total or 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def count_virus_mails(reversedDomainNames=None, timeLength=None):
|
||||
# timeLength is seconds.
|
||||
total = 0
|
||||
sql_append_where = ''
|
||||
|
||||
if not reversedDomainNames:
|
||||
return total
|
||||
|
||||
if session.get('is_global_admin') is not True:
|
||||
sql_append_where += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
|
||||
web.sqlquote(reversedDomainNames),
|
||||
web.sqlquote(reversedDomainNames),
|
||||
)
|
||||
|
||||
if isinstance(timeLength, int):
|
||||
_now = int(time.time())
|
||||
_length_seconds = _now - timeLength
|
||||
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
|
||||
|
||||
try:
|
||||
qr = web.conn_amavisd.query("""
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.content = 'V'
|
||||
AND msgs.quar_type='Q'
|
||||
%s
|
||||
""" % sql_append_where)
|
||||
total = qr[0].total or 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def count_quarantined(reversedDomainNames=None, timeLength=None):
|
||||
# timeLength is seconds.
|
||||
total = 0
|
||||
sql_append_where = ''
|
||||
|
||||
if not session.get('is_global_admin'):
|
||||
sql_append_where += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
|
||||
web.sqlquote(reversedDomainNames),
|
||||
web.sqlquote(reversedDomainNames),
|
||||
)
|
||||
|
||||
if isinstance(timeLength, int):
|
||||
_now = int(time.time())
|
||||
_length_seconds = _now - timeLength
|
||||
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
|
||||
|
||||
try:
|
||||
if session.get('is_global_admin'):
|
||||
qr = web.conn_amavisd.query("""
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
WHERE msgs.quar_type = 'Q' %s
|
||||
""" % sql_append_where)
|
||||
else:
|
||||
qr = web.conn_amavisd.query("""
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.quar_type = 'Q' %s
|
||||
""" % sql_append_where)
|
||||
|
||||
total = qr[0].total or 0
|
||||
except:
|
||||
log_traceback()
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def get_in_out_mails(log_type='sent',
|
||||
cur_page=1,
|
||||
account_type='',
|
||||
account='',
|
||||
page_size_limit=None):
|
||||
"""
|
||||
@account_type: 'domain', 'user', None
|
||||
@log_type: 'sent', 'received', 'all'
|
||||
|
||||
@return (True, {'count': <int>, 'records': <list>}
|
||||
"""
|
||||
log_type = str(log_type)
|
||||
cur_page = int(cur_page)
|
||||
account_type = str(account_type) or None
|
||||
account = str(account) or None
|
||||
|
||||
result = {'count': 0, 'records': []}
|
||||
count = 0 # Number of total mails.
|
||||
records = {} # Detail records.
|
||||
sql_append_where = ''
|
||||
reversed_account = ''
|
||||
|
||||
if not page_size_limit:
|
||||
page_size_limit = settings.PAGE_SIZE_LIMIT
|
||||
|
||||
if account_type == 'domain':
|
||||
reversed_account = iredutils.reverse_amavisd_domain_names([account])
|
||||
|
||||
# Get all managed domain names and reversed names.
|
||||
all_domains = []
|
||||
allReversedDomainNames = []
|
||||
quoted_all_reversed_domain_names = []
|
||||
sql_restricted_sender_domains = ''
|
||||
sql_restricted_recip_domains = ''
|
||||
if not session.get('account_is_mail_user'):
|
||||
if settings.backend == 'ldap':
|
||||
_qr = ldap_lib_admin.get_managed_domains(admin=session.get('username'))
|
||||
if _qr[0]:
|
||||
all_domains = _qr[1]
|
||||
elif settings.backend in ['mysql', 'pgsql']:
|
||||
qr_all_domains = sql_lib_admin.get_managed_domains(admin=session.get('username'),
|
||||
domain_name_only=True)
|
||||
if qr_all_domains[0]:
|
||||
all_domains += qr_all_domains[1]
|
||||
else:
|
||||
result['count'] = count
|
||||
result['records'] = list(records)
|
||||
return True, result
|
||||
|
||||
allReversedDomainNames = iredutils.reverse_amavisd_domain_names(all_domains)
|
||||
quoted_all_reversed_domain_names = web.sqlquote(allReversedDomainNames)
|
||||
sql_restricted_sender_domains = ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
|
||||
sql_restricted_recip_domains = ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
|
||||
|
||||
# restrict permission for per-account search
|
||||
# @log_type == 'sent'
|
||||
# - if domain is under control, no restriction
|
||||
# - if domain is not under control, restrict recipient domain to managed domains
|
||||
# @log_type == 'received'
|
||||
# - if domain is under control, no restriction
|
||||
# - if domain is not under control, restrict sender domain to managed domains
|
||||
verify_domain = account
|
||||
if account_type == 'user':
|
||||
verify_domain = account.split('@', 1)[-1]
|
||||
|
||||
if log_type == 'received':
|
||||
if account_type == 'domain':
|
||||
if session.get('is_global_admin') or verify_domain in all_domains:
|
||||
sql_append_where += ' AND recip.domain IN %s' % web.sqlquote(reversed_account)
|
||||
else:
|
||||
sql_append_where += ' {} AND recip.domain IN {}'.format(sql_restricted_sender_domains, web.sqlquote(reversed_account))
|
||||
elif account_type == 'user':
|
||||
if session.get('is_global_admin') or verify_domain in all_domains:
|
||||
sql_append_where += ' AND recip.email=%s' % web.sqlquote(account)
|
||||
else:
|
||||
sql_append_where += ' {} AND recip.email={}'.format(sql_restricted_sender_domains, web.sqlquote(account))
|
||||
else:
|
||||
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
|
||||
if session.get('is_global_admin'):
|
||||
pass
|
||||
else:
|
||||
if not quoted_all_reversed_domain_names:
|
||||
return True, result
|
||||
else:
|
||||
sql_append_where += ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
|
||||
else:
|
||||
if not quoted_all_reversed_domain_names:
|
||||
return True, result
|
||||
else:
|
||||
sql_append_where += ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
|
||||
|
||||
elif log_type == 'sent':
|
||||
if account_type == 'domain':
|
||||
if session.get('is_global_admin') or verify_domain in all_domains:
|
||||
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversed_account)
|
||||
else:
|
||||
sql_append_where += ' {} AND sender.domain IN {}'.format(sql_restricted_recip_domains, web.sqlquote(reversed_account))
|
||||
elif account_type == 'user':
|
||||
if session.get('is_global_admin') or verify_domain in all_domains:
|
||||
sql_append_where += ' AND sender.email = %s' % (web.sqlquote(account))
|
||||
else:
|
||||
sql_append_where += ' {} AND sender.email = {}'.format(sql_restricted_recip_domains, web.sqlquote(account))
|
||||
else:
|
||||
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
|
||||
if session.get('is_global_admin'):
|
||||
pass
|
||||
else:
|
||||
if not quoted_all_reversed_domain_names:
|
||||
return True, result
|
||||
else:
|
||||
sql_append_where += ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
|
||||
else:
|
||||
if not quoted_all_reversed_domain_names:
|
||||
return True, result
|
||||
else:
|
||||
sql_append_where += ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
|
||||
|
||||
########################
|
||||
# Get detail records.
|
||||
#
|
||||
try:
|
||||
if log_type == 'received':
|
||||
count = count_incoming_mails(allReversedDomainNames,
|
||||
sqlAppendWhere=sql_append_where)
|
||||
|
||||
qr = web.conn_amavisd.query(
|
||||
'''
|
||||
-- Get records of received mails.
|
||||
SELECT
|
||||
msgs.mail_id, msgs.subject, msgs.time_num,
|
||||
msgs.size, msgs.spam_level, msgs.client_addr, msgs.policy,
|
||||
sender.email_raw AS sender_email,
|
||||
recip.email_raw AS recipient
|
||||
FROM msgs
|
||||
LEFT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
LEFT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
LEFT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.quar_type <> 'Q' %s
|
||||
ORDER BY msgs.time_num DESC
|
||||
LIMIT %d
|
||||
OFFSET %d
|
||||
''' % (sql_append_where,
|
||||
page_size_limit,
|
||||
(cur_page - 1) * page_size_limit)
|
||||
)
|
||||
records = iredutils.bytes2str(qr)
|
||||
elif log_type == 'sent':
|
||||
count = count_outgoing_mails(allReversedDomainNames,
|
||||
sqlAppendWhere=sql_append_where)
|
||||
|
||||
qr = web.conn_amavisd.query(
|
||||
'''
|
||||
-- Get records of sent mails.
|
||||
SELECT
|
||||
msgs.mail_id, msgs.subject, msgs.time_num,
|
||||
msgs.size, msgs.client_addr, msgs.policy,
|
||||
sender.email_raw AS sender_email,
|
||||
recip.email_raw AS recipient
|
||||
FROM msgs
|
||||
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
|
||||
WHERE msgs.quar_type <> 'Q' %s
|
||||
ORDER BY msgs.time_num DESC
|
||||
LIMIT %d
|
||||
OFFSET %d
|
||||
''' % (sql_append_where,
|
||||
page_size_limit,
|
||||
(cur_page - 1) * page_size_limit)
|
||||
)
|
||||
records = iredutils.bytes2str(qr)
|
||||
else:
|
||||
records = {}
|
||||
except:
|
||||
pass
|
||||
|
||||
return True, {'count': count, 'records': list(records)}
|
||||
|
||||
|
||||
def get_top_users(reversedDomainNames=None,
|
||||
log_type='sent',
|
||||
timeLength=None,
|
||||
number=10):
|
||||
records = {}
|
||||
sql_append_where = ''
|
||||
|
||||
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
|
||||
if session.get('is_global_admin'):
|
||||
pass
|
||||
else:
|
||||
if not reversedDomainNames:
|
||||
return []
|
||||
else:
|
||||
if log_type == 'sent':
|
||||
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
elif log_type == 'received':
|
||||
sql_append_where += ' AND rcpt.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
else:
|
||||
if log_type == 'sent':
|
||||
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
elif log_type == 'received':
|
||||
sql_append_where += ' AND rcpt.domain IN %s' % web.sqlquote(reversedDomainNames)
|
||||
|
||||
if isinstance(timeLength, int):
|
||||
_now = int(time.time())
|
||||
_length_seconds = _now - timeLength
|
||||
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
|
||||
|
||||
# `msgs.policy` (Amavisd policy bank) is used to identify account type.
|
||||
# for example, 'MLMMJ' means mlmmj mailing list.
|
||||
if log_type == 'sent':
|
||||
try:
|
||||
result = web.conn_amavisd.query(
|
||||
"""
|
||||
-- Get top 10 senders.
|
||||
SELECT COUNT(msgs.mail_id) AS total,
|
||||
sender.email_raw AS mail,
|
||||
msgs.policy AS policy
|
||||
FROM msgs
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
WHERE 1=1 %s
|
||||
GROUP BY mail, policy
|
||||
ORDER BY total DESC
|
||||
LIMIT %d
|
||||
""" % (sql_append_where, number))
|
||||
records = list(result)
|
||||
except:
|
||||
log_traceback()
|
||||
|
||||
elif log_type == 'received':
|
||||
try:
|
||||
result = web.conn_amavisd.query(
|
||||
"""
|
||||
-- Get top 10 recipients
|
||||
SELECT COUNT(msgs.mail_id) AS total,
|
||||
rcpt.email_raw AS mail
|
||||
FROM msgs
|
||||
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
|
||||
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
|
||||
RIGHT JOIN maddr AS rcpt ON (msgrcpt.rid = rcpt.id)
|
||||
WHERE 1=1 %s
|
||||
GROUP BY mail
|
||||
ORDER BY total DESC
|
||||
LIMIT %d
|
||||
""" % (sql_append_where, number))
|
||||
records = list(result)
|
||||
except:
|
||||
log_traceback()
|
||||
|
||||
records = iredutils.bytes2str(records)
|
||||
return list(records)
|
||||
315
libs/amavisd/quarantine.py
Normal file
315
libs/amavisd/quarantine.py
Normal file
@@ -0,0 +1,315 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import socket
|
||||
import web
|
||||
import settings
|
||||
from libs import iredutils
|
||||
from libs.amavisd import QUARANTINE_TYPES
|
||||
|
||||
session = web.config.get("_session")
|
||||
|
||||
# Import backend related modules.
|
||||
if settings.backend == "ldap":
|
||||
from libs.ldaplib.admin import get_managed_domains
|
||||
elif settings.backend in ["mysql", "pgsql"]:
|
||||
from libs.sqllib.admin import get_managed_domains
|
||||
|
||||
|
||||
def get_raw_message(mail_id: str) -> bytes:
|
||||
"""Get raw mail message of quarantined email specified by `mail_id`."""
|
||||
# TODO Check domain access by sender/recipient of quarantined email
|
||||
if not mail_id:
|
||||
return False, "INVALID_MAILID"
|
||||
|
||||
try:
|
||||
records = web.conn_amavisd.select(
|
||||
"quarantine",
|
||||
vars={"mail_id": mail_id},
|
||||
what="mail_text",
|
||||
where="mail_id=$mail_id",
|
||||
order="chunk_ind ASC",
|
||||
)
|
||||
|
||||
if not records:
|
||||
return False, "INVALID_MAILID"
|
||||
|
||||
# Combine mail_text as RAW mail message.
|
||||
# Note: `mail_text` is bytes type.
|
||||
message = b""
|
||||
records = list(records)
|
||||
for i in records:
|
||||
message += i['mail_text']
|
||||
|
||||
return True, message
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
# If msgs.quar_type != "Q" (SQL), we can't get mail body.
|
||||
def get_quarantined_mails(page=1,
|
||||
account_type=None,
|
||||
account="",
|
||||
quarantined_type="",
|
||||
size_limit=settings.PAGE_SIZE_LIMIT,
|
||||
sort_by_score=False):
|
||||
"""Return ([True | False], (total, records))"""
|
||||
|
||||
page = int(page)
|
||||
account = str(account) or None
|
||||
|
||||
# Pre-defined values.
|
||||
count = 0
|
||||
records = []
|
||||
sql_append_selection = ''
|
||||
|
||||
# Domain names under control.
|
||||
all_domains = []
|
||||
|
||||
# Query SQL.
|
||||
if session.get('is_normal_admin'):
|
||||
# List all managed domains in query if admin is not global admin
|
||||
qr = get_managed_domains(admin=session.get('username'), domain_name_only=True)
|
||||
if qr[0]:
|
||||
all_domains = qr[1]
|
||||
|
||||
all_reversed_domains = iredutils.reverse_amavisd_domain_names(all_domains)
|
||||
|
||||
if all_domains:
|
||||
sql_append_selection += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
|
||||
web.sqlquote(all_reversed_domains),
|
||||
web.sqlquote(all_reversed_domains),
|
||||
)
|
||||
else:
|
||||
return True, (0, {})
|
||||
|
||||
if account_type == 'domain':
|
||||
if account:
|
||||
reversed_account = iredutils.reverse_amavisd_domain_names([account])[0]
|
||||
|
||||
if not session.get('is_global_admin'):
|
||||
# Make sure account is managed domain
|
||||
if account not in all_domains:
|
||||
# PERMISSION_DENIED
|
||||
return True, (0, {})
|
||||
|
||||
sql_append_selection += ' AND (sender.domain={} OR recip.domain={})'.format(
|
||||
web.sqlquote(reversed_account), web.sqlquote(reversed_account),
|
||||
)
|
||||
elif account_type == 'user':
|
||||
if session.get('is_normal_admin'):
|
||||
# Make sure account is under managed domains
|
||||
if not account.split('@', 1)[-1] in all_domains:
|
||||
# PERMISSION_DENIED
|
||||
return True, (0, {})
|
||||
elif session.get('account_is_mail_user'):
|
||||
if account != session['username']:
|
||||
return True, (0, {})
|
||||
|
||||
sql_append_selection += ' AND (sender.email={} OR recip.email={})'.format(
|
||||
web.sqlquote(account),
|
||||
web.sqlquote(account),
|
||||
)
|
||||
|
||||
if quarantined_type == 'spam':
|
||||
sql_append_selection += " AND msgs.content IN ('S', 's', 'Y')"
|
||||
elif quarantined_type == 'virus':
|
||||
sql_append_selection += " AND msgs.content = 'V'"
|
||||
elif quarantined_type == 'banned':
|
||||
sql_append_selection += " AND msgs.content = 'B'"
|
||||
elif quarantined_type == 'badheader':
|
||||
sql_append_selection += " AND msgs.content = 'H'"
|
||||
elif quarantined_type == 'badmime':
|
||||
sql_append_selection += " AND msgs.content = 'M'"
|
||||
|
||||
# Get number of total records. SQL table: amavisd.msgs
|
||||
try:
|
||||
# Refer to templates/default/macros/amavisd.html for more detail
|
||||
# about msgs.content (content type, spam status), msgs.quar_type
|
||||
# (quarantine type).
|
||||
result = web.conn_amavisd.query(
|
||||
"""
|
||||
-- Get number of quarantined emails
|
||||
SELECT COUNT(msgs.mail_id) AS total
|
||||
FROM msgs
|
||||
LEFT JOIN msgrcpt ON msgs.mail_id = msgrcpt.mail_id
|
||||
LEFT JOIN maddr AS sender ON msgs.sid = sender.id
|
||||
LEFT JOIN maddr AS recip ON msgrcpt.rid = recip.id
|
||||
WHERE
|
||||
-- msgs.content IN ('S', 's', 'Y', 'V', 'B', 'H')
|
||||
-- AND msgs.quar_type = 'Q'
|
||||
msgs.quar_type = 'Q'
|
||||
%s
|
||||
""" % sql_append_selection)
|
||||
|
||||
count = result[0].total or 0
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get records of quarantined mails.
|
||||
try:
|
||||
# msgs.content:
|
||||
# - S: spam(kill)
|
||||
# - s: prior to 2.7.0 the CC_SPAMMY was logged as 's', now 'Y' is used.
|
||||
# msgs.quar_type:
|
||||
# - Q: sql
|
||||
# - F: file
|
||||
sort_column = 'msgs.time_num'
|
||||
if sort_by_score:
|
||||
sort_column = 'msgs.spam_level'
|
||||
|
||||
result = web.conn_amavisd.query(
|
||||
'''
|
||||
-- Get records of quarantined mails.
|
||||
SELECT
|
||||
msgs.mail_id, msgs.secret_id, msgs.subject, msgs.time_num,
|
||||
msgs.content, msgs.size, msgs.spam_level,
|
||||
sender.email AS sender_email,
|
||||
recip.email AS recipient
|
||||
FROM msgs
|
||||
LEFT JOIN msgrcpt ON msgs.mail_id = msgrcpt.mail_id
|
||||
LEFT JOIN maddr AS sender ON msgs.sid = sender.id
|
||||
LEFT JOIN maddr AS recip ON msgrcpt.rid = recip.id
|
||||
WHERE
|
||||
-- msgs.content IN ('S', 's', 'Y', 'V', 'B', 'H')
|
||||
-- AND msgs.quar_type = 'Q'
|
||||
msgs.quar_type = 'Q'
|
||||
%s
|
||||
ORDER BY %s DESC
|
||||
LIMIT %d
|
||||
OFFSET %d
|
||||
''' % (sql_append_selection, sort_column, size_limit, (page - 1) * size_limit)
|
||||
)
|
||||
records = iredutils.bytes2str(result)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True, (count, records)
|
||||
|
||||
|
||||
def delete_all_quarantined(quarantined_type=None):
|
||||
if quarantined_type in QUARANTINE_TYPES:
|
||||
_content = QUARANTINE_TYPES[quarantined_type]
|
||||
|
||||
# Delete them from `msgs`.
|
||||
# Records in `quarantine` will be cleaned up by cron job
|
||||
try:
|
||||
web.conn_amavisd.delete(
|
||||
'msgs',
|
||||
vars={'quar_type': 'Q', 'content': _content},
|
||||
where='quar_type=$quar_type AND content=$content',
|
||||
)
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
else:
|
||||
try:
|
||||
web.conn_amavisd.delete('quarantine', where='1=1')
|
||||
web.conn_amavisd.delete('msgs', where="""quar_type='Q'""")
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def release_quarantined_mails(records=None):
|
||||
# Release quarantined mails.
|
||||
#
|
||||
# records = [
|
||||
# {'mail_id': 'xxx',
|
||||
# 'secret_id': 'yyy',
|
||||
# 'requested_by': session.get('username'),
|
||||
# },
|
||||
# [],
|
||||
# ]
|
||||
#
|
||||
# Refer to amavisd doc 'README.protocol' for more detail:
|
||||
# - Releasing a message from a quarantine
|
||||
|
||||
if not records:
|
||||
return True,
|
||||
|
||||
# TODO Check domain_access
|
||||
# - Get managed domains.
|
||||
# - Check whether mail_id in `records` are one of managed domains.
|
||||
# - Get allowed mail_id list.
|
||||
|
||||
# Pre-defined variables.
|
||||
released_mail_ids = []
|
||||
|
||||
# Create socket.
|
||||
try:
|
||||
quar_server = settings.amavisd_db_host
|
||||
quar_port = int(settings.amavisd_quarantine_port)
|
||||
|
||||
if settings.AMAVISD_QUARANTINE_HOST:
|
||||
quar_server = settings.AMAVISD_QUARANTINE_HOST
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect((quar_server, quar_port))
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Generate commands from dict, used for socket communication.
|
||||
# Note: We need to update Amavisd SQL database after mail was released
|
||||
# with success, so do NOT send all release requests in ONE socket
|
||||
# command although it will get better performance (a little).
|
||||
for record in records:
|
||||
# Skip record without 'mail_id'.
|
||||
if 'mail_id' not in record:
|
||||
continue
|
||||
|
||||
cmd_release = 'request=release\r\n'
|
||||
|
||||
for k in record:
|
||||
if record[k] is not None and record[k] != '':
|
||||
cmd_release += '{}={}\r\n'.format(k, record[k])
|
||||
|
||||
cmd_release += 'quar_type=Q\r\n\r\n'
|
||||
try:
|
||||
s.send(cmd_release.encode())
|
||||
|
||||
# Must wait for Amavisd's response before deleting SQL record,
|
||||
# otherwise we may delete sql record BEFORE Amavisd releases
|
||||
# quarantined email.
|
||||
s.recv(1024)
|
||||
|
||||
released_mail_ids += [record.get('mail_id', 'NOT-EXIST')]
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Close socket.
|
||||
try:
|
||||
s.close()
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Return if no record was released successfully.
|
||||
if len(released_mail_ids) == 0:
|
||||
return True,
|
||||
|
||||
# Update Amavisd SQL database.
|
||||
try:
|
||||
# - Update msgs.content to 'C' (Clean)
|
||||
# UPDATE msgs \
|
||||
# SET msgs.content = 'C' \
|
||||
# WHERE msgs.mail_id IN ('xxx', 'yyy', ..)
|
||||
#
|
||||
# - Delete records in 'quarantine':
|
||||
# DELETE FROM quarantine \
|
||||
# WHERE quarantine.partition_tag = msgs.partition_tag \
|
||||
# AND quarantine.mail_id = msgs.mail_id
|
||||
#
|
||||
web.conn_amavisd.update(
|
||||
'msgs',
|
||||
where='mail_id IN ' + web.sqlquote(released_mail_ids),
|
||||
quar_type='',
|
||||
content='C',
|
||||
)
|
||||
|
||||
web.conn_amavisd.delete(
|
||||
'quarantine',
|
||||
where='mail_id IN ' + web.sqlquote(released_mail_ids),
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
350
libs/amavisd/spampolicy.py
Normal file
350
libs/amavisd/spampolicy.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
import settings
|
||||
|
||||
from libs import form_utils
|
||||
from libs.iredutils import is_valid_amavisd_address
|
||||
from libs.amavisd import utils
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
DEFAULT_SPAM_TAG_LEVEL = 2
|
||||
DEFAULT_SPAM_TAG2_LEVEL = 6
|
||||
|
||||
# Builtin ban rule names.
|
||||
BUILTIN_BAN_RULE_NAMES = [
|
||||
"ALLOW_MS_OFFICE",
|
||||
"ALLOW_MS_WORD",
|
||||
"ALLOW_MS_EXCEL",
|
||||
"ALLOW_MS_PPT",
|
||||
]
|
||||
|
||||
|
||||
def delete_spam_policy(account):
|
||||
account = str(account).lower()
|
||||
|
||||
if not is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
try:
|
||||
web.conn_amavisd.delete('policy',
|
||||
vars={'account': account},
|
||||
where='policy_name=$account')
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_spam_policy(account='@.'):
|
||||
account = str(account).lower()
|
||||
|
||||
if not is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
try:
|
||||
sql_where = 'users.policy_id=policy.id AND users.email=$account'
|
||||
qr = web.conn_amavisd.select(
|
||||
['policy', 'users'],
|
||||
vars={'account': account},
|
||||
what='policy.*, users.id AS users_id',
|
||||
where=sql_where,
|
||||
limit=1,
|
||||
)
|
||||
if qr:
|
||||
policy = qr[0]
|
||||
return True, policy
|
||||
else:
|
||||
return True, {}
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_global_spam_score():
|
||||
score = DEFAULT_SPAM_TAG2_LEVEL
|
||||
|
||||
(success, policy) = get_spam_policy(account='@.')
|
||||
if success and policy:
|
||||
score = policy.get('spam_tag2_level', DEFAULT_SPAM_TAG2_LEVEL)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def update_spam_policy(account, form):
|
||||
account = str(account).lower()
|
||||
|
||||
if not is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
if 'delete_policy' in form:
|
||||
try:
|
||||
web.conn_amavisd.delete(
|
||||
'policy',
|
||||
vars={'account': account},
|
||||
where='policy_name=$account',
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
qr = utils.get_policy_record(account=account, create_if_missing=True)
|
||||
if qr[0]:
|
||||
policy_id = qr[1].id
|
||||
else:
|
||||
return qr
|
||||
|
||||
# Update spam policy
|
||||
updates = {
|
||||
'spam_lover': 'N',
|
||||
'virus_lover': 'N',
|
||||
'banned_files_lover': 'N',
|
||||
'bad_header_lover': 'N',
|
||||
'bypass_spam_checks': 'N',
|
||||
'bypass_virus_checks': 'N',
|
||||
'bypass_banned_checks': 'N',
|
||||
'bypass_header_checks': 'N',
|
||||
'banned_rulenames': "",
|
||||
}
|
||||
|
||||
if 'enable_spam_checks' not in form:
|
||||
updates['bypass_spam_checks'] = 'Y'
|
||||
|
||||
if 'enable_virus_checks' not in form:
|
||||
updates['bypass_virus_checks'] = 'Y'
|
||||
|
||||
if 'enable_banned_checks' not in form:
|
||||
updates['bypass_banned_checks'] = 'Y'
|
||||
|
||||
if 'enable_header_checks' not in form:
|
||||
updates['bypass_header_checks'] = 'Y'
|
||||
|
||||
updates['spam_quarantine_to'] = ''
|
||||
updates['virus_quarantine_to'] = 'virus-quarantine'
|
||||
updates['banned_quarantine_to'] = ''
|
||||
updates['bad_header_quarantine_to'] = ''
|
||||
|
||||
if 'spam_quarantine_to' in form:
|
||||
updates['spam_quarantine_to'] = 'spam-quarantine'
|
||||
# else:
|
||||
# updates['spam_lover'] = 'Y'
|
||||
|
||||
if 'virus_quarantine_to' not in form:
|
||||
# Deliver virus to mailbox.
|
||||
updates['virus_lover'] = 'Y'
|
||||
updates['virus_quarantine_to'] = ''
|
||||
|
||||
if 'banned_quarantine_to' in form:
|
||||
updates['banned_quarantine_to'] = 'banned-quarantine'
|
||||
# else:
|
||||
# updates['banned_files_lover'] = 'Y'
|
||||
|
||||
if 'bad_header_quarantine_to' in form:
|
||||
updates['bad_header_quarantine_to'] = 'bad-header-quarantine'
|
||||
else:
|
||||
updates['bad_header_lover'] = 'Y'
|
||||
|
||||
# Modify spam subject
|
||||
if 'modify_spam_subject' in form:
|
||||
updates['spam_subject_tag2'] = settings.AMAVISD_SPAM_SUBJECT_PREFIX
|
||||
else:
|
||||
updates['spam_subject_tag2'] = None
|
||||
|
||||
updates['spam_tag_level'] = None
|
||||
updates['spam_tag2_level'] = None
|
||||
updates['spam_kill_level'] = None
|
||||
|
||||
if account == '@.' and 'always_insert_x_spam_headers' in form:
|
||||
updates['spam_tag_level'] = -100
|
||||
|
||||
for p in ['spam_tag2_level', 'spam_kill_level']:
|
||||
_score = form.get(p, '')
|
||||
|
||||
if _score:
|
||||
try:
|
||||
updates[p] = float(_score)
|
||||
except:
|
||||
pass
|
||||
|
||||
if "banned_rulenames" in form:
|
||||
names = form.get("banned_rulenames", [])
|
||||
new_names = set()
|
||||
|
||||
for n in names:
|
||||
if (n in BUILTIN_BAN_RULE_NAMES) or (n in settings.AMAVISD_BAN_RULES):
|
||||
new_names.add(n)
|
||||
|
||||
# Sort the result for easier unittest.
|
||||
new_names = sorted(new_names)
|
||||
|
||||
updates["banned_rulenames"] = ",".join(new_names)
|
||||
|
||||
try:
|
||||
web.conn_amavisd.update(
|
||||
'policy',
|
||||
vars={'id': policy_id},
|
||||
where='id=$id',
|
||||
**updates)
|
||||
|
||||
qr = utils.link_policy_to_user(account=account, policy_id=policy_id)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
# Update `policy.spam_tag3_level` and `policy.spam_subject_tag3`
|
||||
# separately, these two columns don't exist in Amavisd-new-2.6.x.
|
||||
try:
|
||||
extra_updates = {'spam_tag3_level': updates['spam_tag2_level'],
|
||||
'spam_subject_tag3': updates['spam_subject_tag2']}
|
||||
|
||||
web.conn_amavisd.update(
|
||||
'policy',
|
||||
vars={'id': policy_id},
|
||||
where='id=$id',
|
||||
**extra_updates)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def api_update_spam_policy(account, form):
|
||||
"""Create new spam policy or update existing policy."""
|
||||
account = str(account).lower()
|
||||
|
||||
if not is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# Get current `amavisd.policy.id`, it will create a new one if not present.
|
||||
qr = utils.get_policy_record(account=account, create_if_missing=True)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
# Set default policy
|
||||
policy = {
|
||||
'policy_name': account,
|
||||
# Default check policy: don't bypass checks
|
||||
'bypass_spam_checks': 'N',
|
||||
'bypass_virus_checks': 'N',
|
||||
'bypass_banned_checks': 'N',
|
||||
'bypass_header_checks': 'N',
|
||||
# Default quarantining policy: quarantine virus
|
||||
'spam_quarantine_to': None,
|
||||
'virus_quarantine_to': 'virus-quarantine',
|
||||
'banned_quarantine_to': None,
|
||||
'bad_header_quarantine_to': None,
|
||||
# tags/scores
|
||||
'spam_subject_tag': None,
|
||||
'spam_subject_tag2': None,
|
||||
'spam_tag_level': None,
|
||||
'spam_kill_level': None,
|
||||
# ban rules.
|
||||
"banned_rulenames": "",
|
||||
}
|
||||
|
||||
for k in ['spam', 'virus', 'banned', 'header']:
|
||||
# Checks: bypass_<k>_checks
|
||||
_chk = 'bypass_' + k + '_checks'
|
||||
v = form_utils.get_single_value(form, input_name=_chk, to_string=True)
|
||||
if v:
|
||||
if v == 'yes':
|
||||
v = 'Y' # Exclictly enable
|
||||
elif v == 'no':
|
||||
v = 'N' # Exclictly disable
|
||||
else:
|
||||
v = None # Don't set a value, use default policy.
|
||||
|
||||
policy[_chk] = v
|
||||
|
||||
# Quarantining: quarantine_<k>
|
||||
_quar_input = 'quarantine_' + k
|
||||
_quar_key = k + '_quarantine_to'
|
||||
if k == 'header':
|
||||
_quar_input = 'quarantine_bad_header'
|
||||
_quar_key = 'bad_header_quarantine_to'
|
||||
|
||||
v = form_utils.get_single_value(form=form, input_name=_quar_input, to_string=True)
|
||||
if v:
|
||||
if v == 'yes':
|
||||
v = k + '-quarantine'
|
||||
if k == 'header':
|
||||
v = 'bad-header-quarantine'
|
||||
else:
|
||||
v = None
|
||||
|
||||
policy[_quar_key] = v
|
||||
|
||||
# Modify spam subject
|
||||
v = form_utils.get_single_value(form=form, input_name='prefix_spam_in_subject', to_string=True)
|
||||
if v:
|
||||
if v == 'yes':
|
||||
policy['spam_subject_tag'] = settings.AMAVISD_SPAM_SUBJECT_PREFIX
|
||||
policy['spam_subject_tag2'] = settings.AMAVISD_SPAM_SUBJECT_PREFIX
|
||||
else:
|
||||
policy['spam_subject_tag'] = None
|
||||
policy['spam_subject_tag2'] = None
|
||||
|
||||
v = form_utils.get_single_value(form=form, input_name='always_insert_x_spam_headers', to_string=True)
|
||||
if v:
|
||||
if v == 'yes':
|
||||
policy['spam_tag_level'] = -100
|
||||
else:
|
||||
policy['spam_tag_level'] = None
|
||||
|
||||
v = form_utils.get_single_value(form=form, input_name='spam_score', to_string=True)
|
||||
if v.isdigit():
|
||||
try:
|
||||
_score = float(v)
|
||||
policy['spam_tag2_level'] = _score
|
||||
policy['spam_kill_level'] = _score
|
||||
except:
|
||||
return False, 'INVALID_SPAM_SCORE'
|
||||
|
||||
# Get ban rules.
|
||||
names = form_utils.get_multi_values_from_api(form,
|
||||
input_name="banned_rulenames",
|
||||
to_string=True,
|
||||
to_lowercase=False)
|
||||
if names:
|
||||
new_names = set()
|
||||
for n in names:
|
||||
if (n in BUILTIN_BAN_RULE_NAMES) or (n in settings.AMAVISD_BAN_RULES):
|
||||
new_names.add(n)
|
||||
|
||||
policy["banned_rulenames"] = ",".join(new_names)
|
||||
|
||||
qr = delete_spam_policy(account=account)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
# column `users_id` is not a column name in `amavisd.policy` table,
|
||||
# it's set by SQL statement `LEFT JOIN`.
|
||||
if 'users_id' in policy:
|
||||
policy.pop('users_id')
|
||||
|
||||
try:
|
||||
policy_id = web.conn_amavisd.insert('policy', **policy)
|
||||
|
||||
qr = utils.link_policy_to_user(account=account, policy_id=policy_id)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
# Update `policy.spam_tag3_level` and `policy.spam_subject_tag3`
|
||||
# separately, these two columns don't exist in Amavisd-new-2.6.x.
|
||||
try:
|
||||
extra_updates = {'spam_tag3_level': policy['spam_tag2_level'],
|
||||
'spam_subject_tag3': policy['spam_subject_tag2']}
|
||||
|
||||
web.conn_amavisd.update(
|
||||
'policy',
|
||||
vars={'id': policy_id},
|
||||
where='id=$id',
|
||||
**extra_updates)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
207
libs/amavisd/utils.py
Normal file
207
libs/amavisd/utils.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
import settings
|
||||
from libs import iredutils
|
||||
from libs.iredutils import is_valid_amavisd_address
|
||||
|
||||
|
||||
def create_mailaddr(addresses):
|
||||
for addr in addresses:
|
||||
addr_type = iredutils.is_valid_amavisd_address(addr)
|
||||
if addr_type in iredutils.MAILADDR_PRIORITIES:
|
||||
try:
|
||||
web.conn_amavisd.insert(
|
||||
'mailaddr',
|
||||
priority=iredutils.MAILADDR_PRIORITIES[addr_type],
|
||||
email=addr,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_user(account, return_record=True):
|
||||
# Create a new record in `amavisd.users`
|
||||
addr_type = is_valid_amavisd_address(account)
|
||||
try:
|
||||
# Use policy_id=0 to make sure it's not linked to any policy.
|
||||
web.conn_amavisd.insert(
|
||||
'users',
|
||||
policy_id=0,
|
||||
email=account,
|
||||
priority=iredutils.MAILADDR_PRIORITIES[addr_type],
|
||||
)
|
||||
|
||||
if return_record:
|
||||
qr = web.conn_amavisd.select(
|
||||
'users',
|
||||
vars={'account': account},
|
||||
what='*',
|
||||
where='email=$account',
|
||||
limit=1,
|
||||
)
|
||||
return True, qr[0]
|
||||
else:
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_user_record(account, create_if_missing=True):
|
||||
try:
|
||||
qr = web.conn_amavisd.select(
|
||||
'users',
|
||||
vars={'email': account},
|
||||
what='*',
|
||||
where='email=$email',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if qr:
|
||||
return True, qr[0]
|
||||
else:
|
||||
if create_if_missing:
|
||||
qr = create_user(account=account, return_record=True)
|
||||
|
||||
if qr[0]:
|
||||
return True, qr[1]
|
||||
else:
|
||||
return qr
|
||||
else:
|
||||
return False, 'ACCOUNT_NOT_EXIST'
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def create_policy(account, return_record=True):
|
||||
# Create a new record in `amavisd.policy`
|
||||
try:
|
||||
values = {
|
||||
'policy_name': account,
|
||||
'spam_quarantine_to': 'spam-quarantine',
|
||||
'virus_quarantine_to': 'virus-quarantine',
|
||||
'spam_subject_tag2': settings.AMAVISD_SPAM_SUBJECT_PREFIX,
|
||||
}
|
||||
|
||||
web.conn_amavisd.insert('policy', **values)
|
||||
|
||||
# Update `policy.spam_tag3_level` and `policy.spam_subject_tag3`
|
||||
# separately, these two columns don't exist in Amavisd-new-2.6.x.
|
||||
try:
|
||||
extra_values = {'spam_subject_tag3': settings.AMAVISD_SPAM_SUBJECT_PREFIX}
|
||||
web.conn_amavisd.update(
|
||||
'policy',
|
||||
vars={'policy_name': account},
|
||||
where='policy_name=$policy_name',
|
||||
**extra_values)
|
||||
except:
|
||||
pass
|
||||
|
||||
if return_record:
|
||||
qr = web.conn_amavisd.select(
|
||||
'policy',
|
||||
vars={'account': account},
|
||||
what='*',
|
||||
where='policy_name=$account',
|
||||
limit=1,
|
||||
)
|
||||
return True, qr[0]
|
||||
else:
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_policy_record(account, create_if_missing=False):
|
||||
try:
|
||||
qr = web.conn_amavisd.select(
|
||||
'policy',
|
||||
vars={'account': account},
|
||||
what='id',
|
||||
where='policy_name=$account',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if qr:
|
||||
return True, qr[0]
|
||||
else:
|
||||
if create_if_missing:
|
||||
qr = create_policy(account=account, return_record=True)
|
||||
|
||||
if qr[0]:
|
||||
return True, qr[1]
|
||||
else:
|
||||
return qr
|
||||
else:
|
||||
return True, {}
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def link_policy_to_user(account, policy_id):
|
||||
qr = get_user_record(account)
|
||||
if qr[0]:
|
||||
user_id = qr[1].id
|
||||
else:
|
||||
return qr
|
||||
|
||||
try:
|
||||
web.conn_amavisd.update(
|
||||
'users',
|
||||
vars={'id': user_id},
|
||||
policy_id=policy_id,
|
||||
where='id=$id',
|
||||
)
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_policy_accounts(accounts):
|
||||
sqlvars = {'accounts': accounts}
|
||||
try:
|
||||
# Get mailaddr.id of accounts
|
||||
qr = web.conn_amavisd.select(
|
||||
'users',
|
||||
vars=sqlvars,
|
||||
what='id',
|
||||
where='email IN $accounts',
|
||||
)
|
||||
|
||||
ids = []
|
||||
for i in qr:
|
||||
ids.append(i.id)
|
||||
|
||||
# Delete wblist
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'ids': ids},
|
||||
where='rid IN $ids',
|
||||
)
|
||||
|
||||
# Delete outbound wblist
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'ids': ids},
|
||||
where='sid IN $ids',
|
||||
)
|
||||
|
||||
# Delete policy
|
||||
web.conn_amavisd.delete(
|
||||
'policy',
|
||||
vars=sqlvars,
|
||||
where='policy_name IN $accounts',
|
||||
)
|
||||
|
||||
# Delete users
|
||||
web.conn_amavisd.delete(
|
||||
'users',
|
||||
vars=sqlvars,
|
||||
where='email IN $accounts',
|
||||
)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
454
libs/amavisd/wblist.py
Normal file
454
libs/amavisd/wblist.py
Normal file
@@ -0,0 +1,454 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
from libs import iredutils
|
||||
from libs.logger import log_activity
|
||||
from libs.amavisd import utils
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
|
||||
def get_wblist(account,
|
||||
whitelist=True,
|
||||
blacklist=True,
|
||||
outbound_whitelist=True,
|
||||
outbound_blacklist=True):
|
||||
"""Get white/blacklists of specified account."""
|
||||
inbound_sql_where = 'users.email=$user AND users.id=wblist.rid AND wblist.sid = mailaddr.id'
|
||||
if whitelist and not blacklist:
|
||||
inbound_sql_where += ' AND wblist.wb=%s' % web.sqlquote('W')
|
||||
if not whitelist and blacklist:
|
||||
inbound_sql_where += ' AND wblist.wb=%s' % web.sqlquote('B')
|
||||
|
||||
outbound_sql_where = 'users.email=$user AND users.id=outbound_wblist.sid AND outbound_wblist.rid = mailaddr.id'
|
||||
if outbound_whitelist and not outbound_blacklist:
|
||||
outbound_sql_where += ' AND outbound_wblist.wb=%s' % web.sqlquote('W')
|
||||
if not whitelist and blacklist:
|
||||
outbound_sql_where += ' AND outbound_wblist.wb=%s' % web.sqlquote('B')
|
||||
|
||||
wl = []
|
||||
bl = []
|
||||
outbound_wl = []
|
||||
outbound_bl = []
|
||||
|
||||
try:
|
||||
qr = web.conn_amavisd.select(
|
||||
['mailaddr', 'users', 'wblist'],
|
||||
vars={'user': account},
|
||||
what='mailaddr.email AS address, wblist.wb AS wb',
|
||||
where=inbound_sql_where,
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
if r.wb == 'W':
|
||||
wl.append(iredutils.bytes2str(r.address))
|
||||
else:
|
||||
bl.append(iredutils.bytes2str(r.address))
|
||||
|
||||
qr = web.conn_amavisd.select(
|
||||
['mailaddr', 'users', 'outbound_wblist'],
|
||||
vars={'user': account},
|
||||
what='mailaddr.email AS address, outbound_wblist.wb AS wb',
|
||||
where=outbound_sql_where,
|
||||
)
|
||||
for r in qr:
|
||||
if r.wb == 'W':
|
||||
outbound_wl.append(iredutils.bytes2str(r.address))
|
||||
else:
|
||||
outbound_bl.append(iredutils.bytes2str(r.address))
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
wl.sort()
|
||||
bl.sort()
|
||||
outbound_wl.sort()
|
||||
outbound_bl.sort()
|
||||
|
||||
return (True, {'inbound_whitelists': wl,
|
||||
'inbound_blacklists': bl,
|
||||
'outbound_whitelists': outbound_wl,
|
||||
'outbound_blacklists': outbound_bl})
|
||||
|
||||
|
||||
def add_wblist(account,
|
||||
wl_senders=None,
|
||||
bl_senders=None,
|
||||
wl_rcpts=None,
|
||||
bl_rcpts=None,
|
||||
flush_before_import=False):
|
||||
"""Add white/blacklists for specified account.
|
||||
|
||||
wl_senders -- whitelist senders (inbound)
|
||||
bl_senders -- blacklist senders (inbound)
|
||||
wl_rcpts -- whitelist recipients (outbound)
|
||||
bl_rcpts -- blacklist recipients (outbound)
|
||||
flush_before_import -- Delete all existing wblist before importing
|
||||
new wblist
|
||||
"""
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# Remove duplicate.
|
||||
if wl_senders:
|
||||
wl_senders = {str(s).lower()
|
||||
for s in wl_senders
|
||||
if iredutils.is_valid_wblist_address(s)}
|
||||
else:
|
||||
wl_senders = []
|
||||
|
||||
# Whitelist has higher priority, don't include whitelisted sender.
|
||||
if bl_senders:
|
||||
bl_senders = {str(s).lower()
|
||||
for s in bl_senders
|
||||
if iredutils.is_valid_wblist_address(s)}
|
||||
else:
|
||||
bl_senders = []
|
||||
|
||||
if wl_rcpts:
|
||||
wl_rcpts = {str(s).lower()
|
||||
for s in wl_rcpts
|
||||
if iredutils.is_valid_wblist_address(s)}
|
||||
else:
|
||||
wl_rcpts = []
|
||||
|
||||
if bl_rcpts:
|
||||
bl_rcpts = {str(s).lower()
|
||||
for s in bl_rcpts
|
||||
if iredutils.is_valid_wblist_address(s)}
|
||||
else:
|
||||
bl_rcpts = []
|
||||
|
||||
if flush_before_import:
|
||||
if wl_senders:
|
||||
bl_senders = {s for s in bl_senders if s not in wl_senders}
|
||||
|
||||
if wl_rcpts:
|
||||
bl_rcpts = {s for s in bl_rcpts if s not in wl_rcpts}
|
||||
|
||||
sender_addresses = set(wl_senders) | set(bl_senders)
|
||||
rcpt_addresses = set(wl_rcpts) | set(bl_rcpts)
|
||||
all_addresses = list(sender_addresses | rcpt_addresses)
|
||||
|
||||
# Get current user's id from `amavisd.users`
|
||||
qr = utils.get_user_record(account=account)
|
||||
|
||||
if qr[0]:
|
||||
user_id = qr[1].id
|
||||
else:
|
||||
return qr
|
||||
|
||||
# Delete old records
|
||||
if flush_before_import:
|
||||
# user_id = wblist.rid
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'rid': user_id},
|
||||
where='rid=$rid',
|
||||
)
|
||||
|
||||
# user_id = outbound_wblist.sid
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'sid': user_id},
|
||||
where='sid=$sid',
|
||||
)
|
||||
|
||||
if not all_addresses:
|
||||
return True,
|
||||
|
||||
# Insert all senders into `amavisd.mailaddr`
|
||||
utils.create_mailaddr(addresses=all_addresses)
|
||||
|
||||
# Get `mailaddr.id` of senders
|
||||
sender_records = {}
|
||||
if sender_addresses:
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': list(sender_addresses)},
|
||||
what='id, email',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
sender_records[iredutils.bytes2str(r.email)] = r.id
|
||||
del qr
|
||||
|
||||
# Get `mailaddr.id` of recipients
|
||||
rcpt_records = {}
|
||||
if rcpt_addresses:
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': list(rcpt_addresses)},
|
||||
what='id, email',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
rcpt_records[iredutils.bytes2str(r.email)] = r.id
|
||||
|
||||
del qr
|
||||
|
||||
# Remove existing records of current submitted records before inserting new.
|
||||
try:
|
||||
if sender_records:
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'rid': user_id, 'sid': list(sender_records.values())},
|
||||
where='rid=$rid AND sid IN $sid',
|
||||
)
|
||||
|
||||
if rcpt_records:
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'sid': user_id, 'rid': list(rcpt_records.values())},
|
||||
where='sid=$sid AND rid IN $rid',
|
||||
)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Generate dict used to build SQL statements for importing wblist
|
||||
values = []
|
||||
if sender_addresses:
|
||||
for s in wl_senders:
|
||||
if sender_records.get(s):
|
||||
values.append({'rid': user_id, 'sid': sender_records[s], 'wb': 'W'})
|
||||
|
||||
for s in bl_senders:
|
||||
# Filter out same record in blacklist
|
||||
if sender_records.get(s) and s not in wl_senders:
|
||||
values.append({'rid': user_id, 'sid': sender_records[s], 'wb': 'B'})
|
||||
|
||||
rcpt_values = []
|
||||
if rcpt_addresses:
|
||||
for s in wl_rcpts:
|
||||
if rcpt_records.get(s):
|
||||
rcpt_values.append({'sid': user_id, 'rid': rcpt_records[s], 'wb': 'W'})
|
||||
|
||||
for s in bl_rcpts:
|
||||
# Filter out same record in blacklist
|
||||
if rcpt_records.get(s) and s not in wl_rcpts:
|
||||
rcpt_values.append({'sid': user_id, 'rid': rcpt_records[s], 'wb': 'B'})
|
||||
|
||||
try:
|
||||
if values:
|
||||
web.conn_amavisd.multiple_insert('wblist', values)
|
||||
|
||||
if rcpt_values:
|
||||
web.conn_amavisd.multiple_insert('outbound_wblist', rcpt_values)
|
||||
|
||||
# Log
|
||||
if values:
|
||||
if flush_before_import:
|
||||
log_activity(msg='Update whitelists and/or blacklists for %s.' % account,
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
else:
|
||||
if wl_senders:
|
||||
log_activity(msg='Add whitelists for {}: {}.'.format(account, ', '.join(wl_senders)),
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
|
||||
if bl_senders:
|
||||
log_activity(msg='Add blacklists for {}: {}.'.format(account, ', '.join(bl_senders)),
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
|
||||
if rcpt_values:
|
||||
if flush_before_import:
|
||||
log_activity(msg='Update outbound whitelists and/or blacklists for %s.' % account,
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
else:
|
||||
if wl_rcpts:
|
||||
log_activity(msg='Add outbound whitelists for {}: {}.'.format(account, ', '.join(wl_senders)),
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
|
||||
if bl_rcpts:
|
||||
log_activity(msg='Add outbound blacklists for {}: {}.'.format(account, ', '.join(bl_senders)),
|
||||
admin=session['username'],
|
||||
event='update_wblist')
|
||||
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def delete_wblist(account,
|
||||
wl_senders=None,
|
||||
bl_senders=None,
|
||||
wl_rcpts=None,
|
||||
bl_rcpts=None):
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# Remove duplicate.
|
||||
if wl_senders:
|
||||
wl_senders = list({str(s).lower()
|
||||
for s in wl_senders
|
||||
if iredutils.is_valid_wblist_address(s)})
|
||||
|
||||
# Whitelist has higher priority, don't include whitelisted sender.
|
||||
if bl_senders:
|
||||
bl_senders = list({str(s).lower()
|
||||
for s in bl_senders
|
||||
if iredutils.is_valid_wblist_address(s)})
|
||||
|
||||
if wl_rcpts:
|
||||
wl_rcpts = list({str(s).lower()
|
||||
for s in wl_rcpts
|
||||
if iredutils.is_valid_wblist_address(s)})
|
||||
|
||||
if bl_rcpts:
|
||||
bl_rcpts = list({str(s).lower()
|
||||
for s in bl_rcpts
|
||||
if iredutils.is_valid_wblist_address(s)})
|
||||
|
||||
# Get account id from `amavisd.users`
|
||||
qr = utils.get_user_record(account=account)
|
||||
|
||||
if qr[0]:
|
||||
user_id = qr[1].id
|
||||
else:
|
||||
return qr
|
||||
|
||||
# Remove wblist.
|
||||
# No need to remove unused senders in `mailaddr` table, because we
|
||||
# have daily cron job to delete them (tools/cleanup_amavisd_db.py).
|
||||
try:
|
||||
# Get `mailaddr.id` for wblist senders
|
||||
if wl_senders:
|
||||
sids = []
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': wl_senders},
|
||||
what='id',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
sids.append(r.id)
|
||||
|
||||
if sids:
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'user_id': user_id, 'sids': sids},
|
||||
where="rid=$user_id AND sid IN $sids AND wb='W'",
|
||||
)
|
||||
|
||||
if bl_senders:
|
||||
sids = []
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': bl_senders},
|
||||
what='id',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
sids.append(r.id)
|
||||
|
||||
if sids:
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'user_id': user_id, 'sids': sids},
|
||||
where="rid=$user_id AND sid IN $sids AND wb='B'",
|
||||
)
|
||||
|
||||
if wl_rcpts:
|
||||
rids = []
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': wl_rcpts},
|
||||
what='id',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
rids.append(r.id)
|
||||
|
||||
if rids:
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'user_id': user_id, 'rids': rids},
|
||||
where="sid=$user_id AND rid IN $rids AND wb='W'",
|
||||
)
|
||||
|
||||
if bl_rcpts:
|
||||
rids = []
|
||||
qr = web.conn_amavisd.select(
|
||||
'mailaddr',
|
||||
vars={'addresses': bl_rcpts},
|
||||
what='id',
|
||||
where='email IN $addresses',
|
||||
)
|
||||
|
||||
for r in qr:
|
||||
rids.append(r.id)
|
||||
|
||||
if rids:
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'user_id': user_id, 'rids': rids},
|
||||
where="sid=$user_id AND rid IN $rids AND wb='B'",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def delete_all_wblist(account,
|
||||
wl_senders=False,
|
||||
bl_senders=False,
|
||||
wl_rcpts=False,
|
||||
bl_rcpts=False):
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# Get account id from `amavisd.users`
|
||||
qr = utils.get_user_record(account=account)
|
||||
|
||||
if qr[0]:
|
||||
user_id = qr[1].id
|
||||
else:
|
||||
return qr
|
||||
|
||||
# Remove ALL wblist.
|
||||
# No need to remove unused senders in `mailaddr` table, because we
|
||||
# have daily cron job to delete them (tools/cleanup_amavisd_db.py).
|
||||
try:
|
||||
if wl_senders:
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'user_id': user_id},
|
||||
where="rid=$user_id AND wb='W'",
|
||||
)
|
||||
|
||||
if bl_senders:
|
||||
web.conn_amavisd.delete(
|
||||
'wblist',
|
||||
vars={'user_id': user_id},
|
||||
where="rid=$user_id AND wb='B'",
|
||||
)
|
||||
|
||||
if wl_rcpts:
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'user_id': user_id},
|
||||
where="sid=$user_id AND wb='W'",
|
||||
)
|
||||
|
||||
if bl_rcpts:
|
||||
web.conn_amavisd.delete(
|
||||
'outbound_wblist',
|
||||
vars={'user_id': user_id},
|
||||
where="sid=$user_id AND wb='B'",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
686
libs/default_settings.py
Normal file
686
libs/default_settings.py
Normal file
@@ -0,0 +1,686 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
# --------------------------------------
|
||||
# WARNING
|
||||
# --------------------------------------
|
||||
# Please place all your custom settings in settings.py to override settings
|
||||
# listed in this file, so that you can simply copy settings.py while upgrading
|
||||
# iRedAdmin.
|
||||
# --------------------------------------
|
||||
|
||||
# Debug iRedAdmin: True, False.
|
||||
DEBUG = False
|
||||
|
||||
# Session timeout in seconds. Default is 30 minutes (1800 seconds).
|
||||
SESSION_TIMEOUT = 1800
|
||||
|
||||
# if set to False, session will expire when client ip was changed.
|
||||
SESSION_IGNORE_CHANGE_IP = False
|
||||
|
||||
# Mail detail message of '500 internal server error' to webmaster: True, False.
|
||||
# If set to True, iredadmin will mail detail error to webmaster when
|
||||
# it catches 'internal server error' via LOCAL mail server to aid
|
||||
# in debugging production servers.
|
||||
MAIL_ERROR_TO_WEBMASTER = False
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
# Log target: syslog, stdout.
|
||||
# If set to `syslog`, parameters start with `SYSLOG_` below are required.
|
||||
LOG_TARGET = "syslog"
|
||||
|
||||
# Log level. Used by all logging handlers.
|
||||
LOG_LEVEL = "info"
|
||||
|
||||
#
|
||||
# Syslog
|
||||
#
|
||||
# Syslog server address. Log to local syslog socket by default.
|
||||
# Syslog socket path:
|
||||
# - /dev/log on Linux/OpenBSD
|
||||
# - /var/run/log on FreeBSD.
|
||||
# Some distro running systemd may have incorrect permission on /dev/log, it's
|
||||
# ok to use alternative syslog socket /run/systemd/journal/syslog instead.
|
||||
SYSLOG_SERVER = "/dev/log"
|
||||
SYSLOG_PORT = 514
|
||||
|
||||
# Syslog facility
|
||||
SYSLOG_FACILITY = "local5"
|
||||
|
||||
# Log programming error in SQL database, and viewed in `System -> Admin Log`.
|
||||
# This should be used only in testing server, not on production server, because
|
||||
# the error message may contain sensitive information.
|
||||
LOG_PROGRAMMING_ERROR_IN_SQL = False
|
||||
|
||||
# Skin/theme. iRedAdmin will use CSS files and HTML templates under
|
||||
# - statics/{skin}/
|
||||
# - templates/{skin}/
|
||||
SKIN = "default"
|
||||
|
||||
# Set http proxy server address if iRedAdmin cannot access internet
|
||||
# (iredmail.org) directly.
|
||||
# Sample:
|
||||
# - Without authentication: HTTP_PROXY = "http://192.168.1.1:3128"
|
||||
# - With authentication: HTTP_PROXY = "http://user:password@192.168.1.1:3128"
|
||||
HTTP_PROXY = ""
|
||||
|
||||
# Local timezone. It must be one of below:
|
||||
# GMT-12:00
|
||||
# GMT-11:00
|
||||
# GMT-10:00
|
||||
# GMT-09:30
|
||||
# GMT-09:00
|
||||
# GMT-08:00
|
||||
# GMT-07:00
|
||||
# GMT-06:00
|
||||
# GMT-05:00
|
||||
# GMT-04:30
|
||||
# GMT-04:00
|
||||
# GMT-03:30
|
||||
# GMT-03:00
|
||||
# GMT-02:00
|
||||
# GMT-01:00
|
||||
# GMT
|
||||
# GMT+01:00
|
||||
# GMT+02:00
|
||||
# GMT+03:00
|
||||
# GMT+03:30
|
||||
# GMT+04:00
|
||||
# GMT+04:30
|
||||
# GMT+05:00
|
||||
# GMT+05:30
|
||||
# GMT+05:45
|
||||
# GMT+06:00
|
||||
# GMT+06:30
|
||||
# GMT+07:00
|
||||
# GMT+08:00
|
||||
# GMT+08:45
|
||||
# GMT+09:00
|
||||
# GMT+09:30
|
||||
# GMT+10:00
|
||||
# GMT+10:30
|
||||
# GMT+11:00
|
||||
# GMT+11:30
|
||||
# GMT+12:00
|
||||
# GMT+12:45
|
||||
# GMT+13:00
|
||||
# GMT+14:00
|
||||
LOCAL_TIMEZONE = "GMT"
|
||||
|
||||
###################################
|
||||
# RESTful API
|
||||
#
|
||||
# Enable RESTful API
|
||||
ENABLE_RESTFUL_API = False
|
||||
|
||||
# Restrict API access to specified IP addresses or networks.
|
||||
# if not allowed, client will receive error message 'NOT_AUTHORIZED'
|
||||
RESTFUL_API_CLIENTS = []
|
||||
|
||||
# For standalone admin account.
|
||||
#
|
||||
# Hide SQL columns (for SQL editions) or LDAP attributes (for LDAP backends)
|
||||
# in admin or user profiles.
|
||||
# If you need to verify admin password, use API endpoint
|
||||
# '/api/verify_password/admin/<mail>' instead.
|
||||
API_HIDDEN_ADMIN_PROFILES = ["password", "userPassword"]
|
||||
API_HIDDEN_USER_PROFILES = ["password", "userPassword"]
|
||||
|
||||
###################################
|
||||
# Domwin ownership verification
|
||||
#
|
||||
# Require domain ownership verification if it's added by normal domain admin:
|
||||
# True, False.
|
||||
REQUIRE_DOMAIN_OWNERSHIP_VERIFICATION = True
|
||||
|
||||
# How long should we remove verified or (inactive) unverified domain ownerships.
|
||||
#
|
||||
# iRedAdmin-Pro stores verified ownership in SQL database, if (same) admin
|
||||
# removed the domain and re-adds it, no verification required.
|
||||
#
|
||||
# Usually normal domain admin won't frequently remove and re-add same domain
|
||||
# name, so it's ok to remove saved ownership after X days.
|
||||
DOMAIN_OWNERSHIP_EXPIRE_DAYS = 30
|
||||
|
||||
# The string prefixed to verify code. Must be shorter than than 60 characters.
|
||||
DOMAIN_OWNERSHIP_VERIFY_CODE_PREFIX = "iredmail-domain-verification-"
|
||||
|
||||
# Timeout (in seconds) while performing each verification.
|
||||
DOMAIN_OWNERSHIP_VERIFY_TIMEOUT = 10
|
||||
|
||||
###################################
|
||||
# General settings
|
||||
#
|
||||
# Show percentage of mailbox quota usage. Require parameter SQL_TBL_USED_QUOTA.
|
||||
SHOW_USED_QUOTA = True
|
||||
|
||||
# SQL table used to store real-time mailbox quota usage.
|
||||
# - For SQL backends, it's stored in SQL db 'vmail'.
|
||||
# - For LDAP backend, it's stored in SQL db 'iredadmin'.
|
||||
SQL_TBL_USED_QUOTA = "used_quota"
|
||||
|
||||
# Default password scheme, must be a string.
|
||||
# Passwords of new mail accounts will be encrypted by specified scheme.
|
||||
#
|
||||
# - LDAP backends: BCRYPT, SSHA512, SSHA, PLAIN.
|
||||
# Multiple passwords are supported if you separate schemes
|
||||
# with '+'. For example:
|
||||
# 'SSHA+MD5', 'CRAM-MD5+SSHA', 'CRAM-MD5+SSHA+MD5'.
|
||||
#
|
||||
# - SQL backends: BCRYPT, SSHA512, SSHA, MD5, PLAIN-MD5 (without salt), PLAIN.
|
||||
# Multiple passwords are NOT supported.
|
||||
#
|
||||
# Recommended schemes in specified order:
|
||||
#
|
||||
# BCRYPT -> SSHA512 -> SSHA.
|
||||
#
|
||||
# WARNING: MD5, PLAIN-MD5, PLAIN are not recommended.
|
||||
#
|
||||
# Important notes:
|
||||
#
|
||||
# - Password length and complexity are probably more important then a strong
|
||||
# crypt algorithm.
|
||||
#
|
||||
# - You can get available algorithms with command `doveadm pw -l`
|
||||
# ('BLF-CRYPT' is BCRYPT).
|
||||
#
|
||||
# - BCRYPT: *) must be supported by libc on your system.
|
||||
# FreeBSD and OpenBSD support it, but most latest Linux
|
||||
# distributions not yet support it.
|
||||
# Since Dovecot-2.3.0, BCRYPT is provided by dovecot.
|
||||
#
|
||||
# *) BCRYPT is slower than SSHA512, SSHA, MD5.
|
||||
# But, "Speed is exactly what you don't want in a password hash
|
||||
# function."
|
||||
#
|
||||
# *) References:
|
||||
# - A Future-Adaptable Password Scheme:
|
||||
# http://www.openbsd.org/papers/bcrypt-paper.ps
|
||||
# - How to safely store a password:
|
||||
# http://codahale.com/how-to-safely-store-a-password/
|
||||
#
|
||||
# - SSHA512: requires Dovecot-2.0 (or later) and Python-2.5 (or later).
|
||||
# If you're running Python-2.4, iRedAdmin will generate SSHA hash
|
||||
# instead of SSHA512. But if you're running Dovecot-1.x, user
|
||||
# authentication will fail.
|
||||
#
|
||||
# OpenLDAP doesn't support user authentication with SSHA512
|
||||
# directly, so you must set 'auth_bind = no' in
|
||||
# /etc/dovecot/dovecot-ldap.conf to let Dovecot do the password
|
||||
# verification instead.
|
||||
#
|
||||
# Sample password format:
|
||||
#
|
||||
# - BCRYPT: {CRYPT}$2a$05$TKnXV39M3uJ4o.AbY1HbjeAval9bunHbxd0.6Qn782yKoBjTEBXTe
|
||||
# NOTE: Use prefix '{CRYPT}' instead of '{BLF-CRYPT}'.
|
||||
# - SSHA512: {SSHA512}FxgXDhBVYmTqoboW+ibyyzPv/wGG7y4VJtuHWrx+wfqrs/lIH2Qxn2eA0jygXtBhMvRi7GNFmL++6aAZ0kXpcy1fxag=
|
||||
# - SSHA: {SSHA}bfxqKqOOKODJw/bGqMo54f9Q/iOvQoftOQrqWA==
|
||||
# - CRAM-MD5: {CRAM-MD5}465076e1c95ac134fc2ba88ad617b6660958f388d60423504ee7c46ce44be8b4
|
||||
# - MD5: $1$ozdpg0V0$0fb643pVsPtHVPX8mCZYW/
|
||||
# - PLAIN-MD5: 900150983cd24fb0d6963f7d28e17f72.
|
||||
# - PLAIN: Plain text.
|
||||
#
|
||||
# References:
|
||||
#
|
||||
# - Dovecot password schemes:
|
||||
# https://wiki.dovecot.org/Authentication/PasswordSchemes
|
||||
#
|
||||
#
|
||||
DEFAULT_PASSWORD_SCHEME = "SSHA"
|
||||
|
||||
# List of password schemes which should not prefix scheme name in generated hash.
|
||||
# Currently, only this setting impacts NTLM only.
|
||||
# Sample setting:
|
||||
#
|
||||
# HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME = ['NTLM']
|
||||
#
|
||||
# Sample password hashes:
|
||||
#
|
||||
# NTLM without prefix: {NTLM}32ED87BDB5FDC5E9CBA88547376818D4
|
||||
# NTLM without prefix: 32ED87BDB5FDC5E9CBA88547376818D4
|
||||
HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME = ["NTLM"]
|
||||
|
||||
# Allow to store password in plain text.
|
||||
# It will show a HTML checkbox to allow admin to store newly created user
|
||||
# password or reset password in plain text. If not checked, password
|
||||
# will be stored as encrypted.
|
||||
# See DEFAULT_PASSWORD_SCHEME below.
|
||||
STORE_PASSWORD_IN_PLAIN_TEXT = False
|
||||
|
||||
# Always store plain password in additional LDAP attribute of user object, or
|
||||
# SQL column (in `vmail.mailbox` table).
|
||||
# Value must be a valid LDAP attribute name of user object, or SQL column name
|
||||
# in `vmail.mailbox` table.
|
||||
STORE_PLAIN_PASSWORD_IN_ADDITIONAL_ATTR = ""
|
||||
|
||||
# Set password last change date for newly created user. Defaults to True.
|
||||
# If you want to force end user to change password when first login or send
|
||||
# first email (with iRedAPD plugin `*_force_change_password`), please set it to
|
||||
# False.
|
||||
SET_PASSWORD_CHANGE_DATE_FOR_NEW_USER = True
|
||||
|
||||
#
|
||||
# Password restrictions
|
||||
#
|
||||
# Special characters which can be used in password.
|
||||
# Notes: iOS devices may have problem with character '^'.
|
||||
PASSWORD_SPECIAL_CHARACTERS = """#$%&*+-,.:;!=<>'"?@[]/(){}_`~"""
|
||||
# Must contain at least one letter, one uppercase letter, one number, one special character
|
||||
PASSWORD_HAS_LETTER = True
|
||||
PASSWORD_HAS_UPPERCASE = True
|
||||
PASSWORD_HAS_NUMBER = True
|
||||
PASSWORD_HAS_SPECIAL_CHAR = True
|
||||
|
||||
# Log PERMISSION_DENIED operations to stdout or web server log file.
|
||||
LOG_PERMISSION_DENIED = True
|
||||
|
||||
# Redirect to "Domains and Accounts" page instead of Dashboard.
|
||||
REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN = False
|
||||
|
||||
# List of IP addresses/networks which global admins are allowed to login from.
|
||||
# Valid formats:
|
||||
# - Single IP address: 192.168.1.1
|
||||
# - IPv4/IPv6 network: 192.168.1.0/24
|
||||
GLOBAL_ADMIN_IP_LIST = []
|
||||
|
||||
# List of IP addresses/networks which (both global and normal) admins are
|
||||
# allowed to login from.
|
||||
# Valid formats:
|
||||
# - Single IP address: 192.168.1.1
|
||||
# - IPv4/IPv6 network: 192.168.1.0/24
|
||||
ADMIN_LOGIN_IP_LIST = []
|
||||
|
||||
# List all local transports.
|
||||
LOCAL_TRANSPORTS = [
|
||||
"dovecot",
|
||||
"lmtp:unix:private/dovecot-lmtp",
|
||||
"lmtp:inet:127.0.0.1:24",
|
||||
]
|
||||
|
||||
# Redirect to which page after logged in.
|
||||
# Available values are: preferences, quarantined, received, wblist, spampolicy.
|
||||
SELF_SERVICE_DEFAULT_PAGE = "preferences"
|
||||
|
||||
###################################
|
||||
# Maildir related.
|
||||
#
|
||||
|
||||
# It's RECOMMEND for better performance. Samples:
|
||||
# - hashed: domain.com/u/s/e/username-2009.09.04.12.05.33/
|
||||
# - non-hashed: domain.com/username-2009.09.04.12.05.33/
|
||||
MAILDIR_HASHED = True
|
||||
|
||||
# Prepend domain name in path. Samples:
|
||||
# - with domain name: domain.com/username/
|
||||
# - without: username/
|
||||
MAILDIR_PREPEND_DOMAIN = True
|
||||
|
||||
# Append timestamp in path. Samples:
|
||||
# - with timestamp: domain.com/username-2010.12.20.13.13.33/
|
||||
# - without timestamp: domain.com/username/
|
||||
MAILDIR_APPEND_TIMESTAMP = True
|
||||
|
||||
# Mailbox format (in lower cases)
|
||||
#
|
||||
# Any mailbox formats supported by Dovecot can be used here, e.g. maildir,
|
||||
# mdbox. For more details please visit Dovecot website:
|
||||
# https://wiki.dovecot.org/MailboxFormat
|
||||
MAILBOX_FORMAT = "maildir"
|
||||
|
||||
# Default folder used to store mailbox under per-user HOME directory.
|
||||
#
|
||||
# - Folder name is case SeNsItIvE. Defaults to 'Maildir'.
|
||||
#
|
||||
# - If not set, Dovecot will use the hard-coded setting defined in its config
|
||||
# file.
|
||||
#
|
||||
# - It will be appended to the `mail` variable returned by Dovecot SQL/LDAP
|
||||
# query. for example, sql query in `/etc/dovecot/dovecot-mysql.conf`:
|
||||
#
|
||||
# user_query = SELECT ..., CONCAT(...) AS mail, ...
|
||||
#
|
||||
# Or LDAP query in `/etc/dovecot/dovecot-ldap.conf`:
|
||||
#
|
||||
# user_attrs = ...,=mail=%{ldap:mailboxFormat:maildir}:~/%{ldap:mailboxFolder:Maildir}/,...
|
||||
MAILBOX_FOLDER = "Maildir"
|
||||
|
||||
# How many days the normal domain admin can choose to keep the mailbox after
|
||||
# account removal.
|
||||
# To make it simpler, we use 30 days for one month, 365 days for one year.
|
||||
DAYS_TO_KEEP_REMOVED_MAILBOX = [1, 7, 14, 21, 30, 60, 90, 180, 365]
|
||||
|
||||
# How many days the global domain admin can choose to keep the mailbox after
|
||||
# account removal.
|
||||
# To make it simpler, we use 30 days for one month, 365 days for one year.
|
||||
# 0 means keeping forever.
|
||||
DAYS_TO_KEEP_REMOVED_MAILBOX_FOR_GLOBAL_ADMIN = [
|
||||
0,
|
||||
1,
|
||||
7,
|
||||
14,
|
||||
21,
|
||||
30,
|
||||
60,
|
||||
90,
|
||||
180,
|
||||
365,
|
||||
730,
|
||||
1095,
|
||||
]
|
||||
|
||||
#######################################
|
||||
# LDAP backends related settings.
|
||||
#
|
||||
# Define LDAP server product name: OPENLDAP, LDAPD (OpenBSD built-in ldap daemon)
|
||||
LDAP_SERVER_PRODUCT_NAME = "OPENLDAP"
|
||||
|
||||
# LDAP connection trace level. Must be an integer.
|
||||
LDAP_CONN_TRACE_LEVEL = 0
|
||||
|
||||
# Add full dn of (internal) members to mailing list account.
|
||||
LDAP_ADD_MEMBER_DN_TO_GROUP = True
|
||||
LDAP_ATTR_MEMBER = "member"
|
||||
|
||||
# Additional LDAP attribute names of user object you want to manage.
|
||||
# Format:
|
||||
#
|
||||
# {'attribute_name': {'desc': 'A short description of this attribute',
|
||||
# 'allowed_domains': [...],
|
||||
# 'properties': [...]}}
|
||||
# 'attribute_name2': {...}}
|
||||
#
|
||||
# Arguments
|
||||
# ----------
|
||||
#
|
||||
# desc: string. [optional]
|
||||
# a short description of this attribute.
|
||||
# If not present, defaults to show attribute name.
|
||||
#
|
||||
# allowed_domains: list. [optional]
|
||||
# a list of domain names which are allowed to use this attribute.
|
||||
# if not present, defaults to allow all domains to use the attribute.
|
||||
#
|
||||
# properties: list. [optional]
|
||||
# a list of pre-defined property names (string).
|
||||
# If not present, defaults to ['string'].
|
||||
#
|
||||
# Properties
|
||||
# ----------
|
||||
#
|
||||
# - 'require_global_admin': attribute is only managed by global domain admin.
|
||||
# - 'multivalue': indicates attribute may contain multiple values.
|
||||
# If not present, defaults to single value.
|
||||
#
|
||||
# - 'string': indicates attribute value is short text. will be displayed as
|
||||
# HTML tag "<input type='text'>".
|
||||
# - 'text': indicates attribute value is long text. will be displayed as HTML
|
||||
# "<textarea>".
|
||||
#
|
||||
# Warning: 'string', 'text', 'integer' cannot be used at the same time for same
|
||||
# attribute.
|
||||
#
|
||||
# Sample settings:
|
||||
#
|
||||
# {'carLicense': {}} # The minimalist setting, just attribute name.
|
||||
#
|
||||
# {'carLicense': {'desc': 'Car License',
|
||||
# 'properties': ['string'],
|
||||
# 'allowed_domains': ['example.com', 'test.com']}}
|
||||
ADDITIONAL_MANAGED_USER_ATTRIBUTES = {}
|
||||
|
||||
# Additional LDAP objectClass for NEWLY created mail user.
|
||||
# Sample value: ['inetOrgPerson', 'pwdPolicy', 'ownCloud']
|
||||
ADDITIONAL_USER_OBJECTCLASSES = []
|
||||
|
||||
# Additional LDAP attribute names and values for NEWLY created mail user.
|
||||
#
|
||||
# Format:
|
||||
# [(attribute_name, [...]),
|
||||
# (attribute_name, [...])]
|
||||
#
|
||||
# Several placeholders are available:
|
||||
# - %(mail)s: mail address of new user
|
||||
# - %(domain)s: domain part of new user mail address
|
||||
# - %(username)s: username part of new user mail address
|
||||
# - %(cn)s: display name of new user
|
||||
# - %(plain_password)s: new user's plain password
|
||||
# - %(passwd)s: new user's encrypted password
|
||||
# - %(quota)d: mailbox quota
|
||||
# - %(sgroups)s: a list of assigned mailing lists
|
||||
# - %(storageBaseDirectory)s: path of base storage
|
||||
# - %(language)s: default language for web UI
|
||||
# - %(recipient_bcc)s: recipient bcc email address
|
||||
# - %(sender_bcc)s: sender bcc email address
|
||||
# - %(next_uid)d: a server-wide free and unique integer for attr `uidNumber`
|
||||
# - %(next_gid)d: a server-wide free and unique integer for attr `gidNumber`
|
||||
# - %(shadowLastChange)d: number of days since 1970-01-01, defaults to today.
|
||||
# - %(shadowLastChange)d+Xd: number of days since 1970-01-01, plus X days (+Xd).
|
||||
#
|
||||
# Sample:
|
||||
#
|
||||
# ADDITIONAL_USER_ATTRIBUTES = [('uidNumber', ['%(next_uid)d']),
|
||||
# ('gidNumber', ['%(next_gid)d'])]
|
||||
ADDITIONAL_USER_ATTRIBUTES = []
|
||||
|
||||
# Additional enabled/disabled services for newly created accounts.
|
||||
#
|
||||
# - both ADDITIONAL_ENABLED_[XX]_SERVICES, ADDITIONAL_DISABLED_[XX]_SERVICES
|
||||
# are manageable in account (user/domain) profile page.
|
||||
#
|
||||
# - ADDITIONAL_ENABLED_<X>_SERVICES will be added for newly created account
|
||||
# automatically.
|
||||
#
|
||||
# NOTE: This variable is not used by SQL backends, because all services
|
||||
# are enabled by default.
|
||||
#
|
||||
# - ADDITIONAL_DISABLED_<X>_SERVICES will not be added for newly created
|
||||
# account, admin must go to account profile page to enable them for certain
|
||||
# accounts.
|
||||
#
|
||||
# Notes:
|
||||
#
|
||||
# *) for LDAP backends, the service names are assigned to attribute
|
||||
# `enabledService`. You're free to use custom words for them, for example,
|
||||
# if you want to limit vpn access for certain users, feel free to use
|
||||
# `enabledService=vpn` for this purpose.
|
||||
#
|
||||
# *) For SQL backends:
|
||||
#
|
||||
# Available enabled/disabled services are:
|
||||
#
|
||||
# smtp
|
||||
# smtpsecured
|
||||
# pop3
|
||||
# pop3secured
|
||||
# imap
|
||||
# imapsecured
|
||||
# deliver
|
||||
# managesieve
|
||||
# managesievesecured
|
||||
# sogo
|
||||
# sogowebmail
|
||||
# sogocalendar
|
||||
# sogoactivesync
|
||||
#
|
||||
# They're mapped to SQL column name in `vmail.mailbox` table with prefix
|
||||
# string 'enable'. e.g. 'smtp' is mapped to 'enablesmtp' column.
|
||||
#
|
||||
ADDITIONAL_ENABLED_DOMAIN_SERVICES = []
|
||||
ADDITIONAL_DISABLED_DOMAIN_SERVICES = []
|
||||
|
||||
# Additional services for mail user.
|
||||
ADDITIONAL_ENABLED_USER_SERVICES = []
|
||||
ADDITIONAL_DISABLED_USER_SERVICES = []
|
||||
|
||||
#######################################
|
||||
# MySQL/PostgreSQL backends related settings.
|
||||
#
|
||||
# Allow to assign per-user alias address under different domains.
|
||||
USER_ALIAS_CROSS_ALL_DOMAINS = False
|
||||
|
||||
# List all global admins while listing per-domain admins.
|
||||
# URL: https://<server>/iredadmin/admins/<domain>
|
||||
SHOW_GLOBAL_ADMINS_IN_PER_DOMAIN_ADMIN_LIST = False
|
||||
|
||||
###################################
|
||||
# iRedAPD related settings.
|
||||
#
|
||||
# Query insecure outbound session in latest hours.
|
||||
IREDAPD_QUERY_INSECURE_OUTBOUND_IN_HOURS = 24
|
||||
|
||||
###################################
|
||||
# Amavisd related settings.
|
||||
#
|
||||
# If Amavisd is not running on the database server (settings.amavisd_db_host),
|
||||
# you should specify the amavisd server address here.
|
||||
AMAVISD_QUARANTINE_HOST = ""
|
||||
|
||||
# Remove old SQL records of sent/received mails in Amavisd database.
|
||||
# NOTE: require cron job with script tools/cleanup_amavisd_db.py.
|
||||
AMAVISD_REMOVE_MAILLOG_IN_DAYS = 3
|
||||
|
||||
# Remove old SQL records of quarantined mails.
|
||||
# Since quarantined mails may take much disk space, it's better to release
|
||||
# or remove them as soon as possible.
|
||||
# NOTE: require cron job with script tools/cleanup_amavisd_db.py.
|
||||
AMAVISD_REMOVE_QUARANTINED_IN_DAYS = 7
|
||||
|
||||
# Prefix text to the subject of spam
|
||||
AMAVISD_SPAM_SUBJECT_PREFIX = "[SPAM] "
|
||||
|
||||
# If set to true, non-local mail domains/users will appear in mail logs and
|
||||
# 'Top Senders', 'Top Recipients' too.
|
||||
AMAVISD_SHOW_NON_LOCAL_DOMAINS = False
|
||||
|
||||
# Query size limit. Used by tools/cleanup_amavisd_db.py.
|
||||
#
|
||||
# If server is busy and Amavisd generates many records in a short time,
|
||||
# cleanup script will cause table lock while updating sql tables, and this
|
||||
# may cause other sql connections which operating on `amavisd` database
|
||||
# hang/timeout. in this case, you'd better set this parameter to a low
|
||||
# value to release the table lock sooner. e.g. 10.
|
||||
AMAVISD_CLEANUP_QUERY_SIZE_LIMIT = 100
|
||||
|
||||
# Additional Amavisd ban rules.
|
||||
# iRedMail has 4 builtin ban rules since iRedMail-1.4.1:
|
||||
# - ALLOW_MS_OFFICE: Allow all Microsoft Office documents.
|
||||
# - ALLOW_MS_WORD: Allow Microsoft Word documents (.doc, .docx).
|
||||
# - ALLOW_MS_EXCEL: Allow Microsoft Excel documents (.xls, .xlsx).
|
||||
# - ALLOW_MS_PPT: Allow Microsoft PowerPoint documents (.ppt, .pptx).
|
||||
# You can add your custom ban rules here. Format is:
|
||||
# {"<rule_name>": "<comment>"}
|
||||
AMAVISD_BAN_RULES = {}
|
||||
|
||||
# Show how many top senders/recipients on Dashboard page.
|
||||
NUM_TOP_SENDERS = 10
|
||||
NUM_TOP_RECIPIENTS = 10
|
||||
|
||||
# Query statistics for last X hours.
|
||||
STATISTICS_HOURS = 24
|
||||
|
||||
###################################
|
||||
# iRedAdmin related settings.
|
||||
#
|
||||
# Keep iRedAdmin admin log for days.
|
||||
IREDADMIN_LOG_KEPT_DAYS = 365
|
||||
|
||||
#####################################################
|
||||
# mlmmj and mlmmjadmin RESTful API related settings.
|
||||
#
|
||||
# The base url of newsletter subscription/unsubscription/error.
|
||||
# The full url will be: https://domain.com/<NEWSLETTER_BASE_URL>
|
||||
# WARNING: it must start with '/'
|
||||
NEWSLETTER_BASE_URL = "/newsletter"
|
||||
|
||||
# How long (in hours) the subscription/unsubscription request will expire.
|
||||
NEWSLETTER_SUBSCRIPTION_REQUEST_EXPIRE_HOURS = 24
|
||||
NEWSLETTER_UNSUBSCRIPTION_REQUEST_EXPIRE_HOURS = 24
|
||||
|
||||
# How long (in hours) we should keep the subscription requests for simple statistics.
|
||||
NEWSLETTER_SUBSCRIPTION_REQUEST_KEEP_HOURS = 24
|
||||
|
||||
# Base url of mlmmjadmin API. For example: 'http://127.0.0.1:7790/api'
|
||||
MLMMJADMIN_API_BASE_URL = "http://127.0.0.1:7790/api"
|
||||
|
||||
# HTTP header used to store the API AUTH TOKEN.
|
||||
# Defaults to 'X-MLMMJADMIN-API-AUTH-TOKEN'.
|
||||
MLMMJADMIN_API_AUTH_HEADER = "X-MLMMJADMIN-API-AUTH-TOKEN"
|
||||
|
||||
# Verify SSL cert of mlmmjadmin API
|
||||
MLMMJADMIN_API_VERIFY_SSL = False
|
||||
|
||||
# The transport name defined in Postfix master.cf used to call 'mlmmj-receive'
|
||||
# program. For example:
|
||||
#
|
||||
# mlmmj unix - n n - - pipe
|
||||
# flags=ORhu ...
|
||||
MLMMJ_MTA_TRANSPORT_NAME = "mlmmj"
|
||||
|
||||
############################################################################
|
||||
# Fail2ban integration.
|
||||
#
|
||||
# - Currently only querying banned IP from fail2ban SQL database is supported.
|
||||
# - We use lower cases for parameter names to keep consistency with the ones
|
||||
# in `settings.py`.
|
||||
fail2ban_enabled = False
|
||||
fail2ban_db_host = '127.0.0.1'
|
||||
fail2ban_db_port = '3306'
|
||||
fail2ban_db_name = 'fail2ban'
|
||||
fail2ban_db_user = 'fail2ban'
|
||||
fail2ban_db_password = ''
|
||||
|
||||
###################################
|
||||
# Minor settings. You do not need to change them.
|
||||
#
|
||||
# Recipient delimiters. If you have multiple delimiters, please list them all.
|
||||
RECIPIENT_DELIMITERS = ["+"]
|
||||
|
||||
# Show how many items in one page.
|
||||
PAGE_SIZE_LIMIT = 50
|
||||
|
||||
# Smallest uid/gid number which can be assigned to new users/groups.
|
||||
MIN_UID = 3000
|
||||
MIN_GID = 3000
|
||||
|
||||
# The link to support page on iRedAdmin footer.
|
||||
URL_SUPPORT = "https://www.iredmail.org/support.html"
|
||||
|
||||
# Path to the logo image and favicon.ico.
|
||||
# Please copy your logo image to 'static/' folder, then put the image file name
|
||||
# in BRAND_LOGO. e.g.: 'logo.png' (will load file 'static/logo.png').
|
||||
BRAND_LOGO = ""
|
||||
BRAND_FAVICON = ""
|
||||
|
||||
# Product name, short description.
|
||||
BRAND_NAME = "iRedAdmin-Cracked"
|
||||
BRAND_DESC = "iRedMail Admin Panel"
|
||||
|
||||
# Path to `sendmail` command
|
||||
CMD_SENDMAIL = "/usr/sbin/sendmail"
|
||||
|
||||
# SMTP server address, port, username, password used to send notification mail.
|
||||
NOTIFICATION_SMTP_SERVER = "localhost"
|
||||
NOTIFICATION_SMTP_PORT = 587
|
||||
NOTIFICATION_SMTP_STARTTLS = True
|
||||
NOTIFICATION_SMTP_USER = "no-reply@localhost.local"
|
||||
NOTIFICATION_SMTP_PASSWORD = ""
|
||||
NOTIFICATION_SMTP_DEBUG_LEVEL = 0
|
||||
|
||||
# The short description or full name of this smtp user. e.g. 'No Reply'
|
||||
NOTIFICATION_SENDER_NAME = "No Reply"
|
||||
|
||||
#
|
||||
# Used in notification emails sent to recipients of quarantined emails.
|
||||
#
|
||||
# URL of your iRedAdmin-Pro login page which will be shown in notification
|
||||
# email, so that user can login to manage quarantined emails.
|
||||
# Sample: 'https://your_server.com/iredadmin/'
|
||||
#
|
||||
# Note: mail domain must have self-service enabled, otherwise normal
|
||||
# mail user cannot login to iRedAdmin-Pro for self-service.
|
||||
NOTIFICATION_URL_SELF_SERVICE = ""
|
||||
|
||||
# Subject of notification email. Available placeholders:
|
||||
# - %(total)d -- number of quarantined mails in total
|
||||
NOTIFICATION_QUARANTINE_MAIL_SUBJECT = "[Attention] You have %(total)d emails quarantined and not delivered to mailbox"
|
||||
0
libs/f2b/__init__.py
Normal file
0
libs/f2b/__init__.py
Normal file
17
libs/f2b/log.py
Normal file
17
libs/f2b/log.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import web
|
||||
|
||||
from controllers import decorators
|
||||
from libs.logger import logger
|
||||
|
||||
|
||||
@decorators.require_global_admin
|
||||
def num_banned() -> int:
|
||||
total = 0
|
||||
|
||||
try:
|
||||
_qr = web.conn_f2b.select("banned", what="COUNT(id) AS total")
|
||||
total = _qr[0]['total']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return total
|
||||
723
libs/form_utils.py
Normal file
723
libs/form_utils.py
Normal file
@@ -0,0 +1,723 @@
|
||||
"""Functions used to extract required data from web form."""
|
||||
|
||||
import settings
|
||||
from libs import iredutils, iredpwd
|
||||
from libs.l10n import TIMEZONES
|
||||
|
||||
|
||||
# Return single value of specified form name.
|
||||
def get_single_value(form,
|
||||
input_name,
|
||||
default_value='',
|
||||
is_domain=False,
|
||||
is_email=False,
|
||||
is_integer=False,
|
||||
is_strict_ip=False,
|
||||
is_ip_or_network=False,
|
||||
to_lowercase=False,
|
||||
to_uppercase=False,
|
||||
to_string=False,
|
||||
split_value=False,
|
||||
split_separator=None,
|
||||
strip_str_before_split=False,
|
||||
strip_str=None):
|
||||
v = form.get(input_name, '')
|
||||
if not v:
|
||||
v = default_value
|
||||
|
||||
if not isinstance(v, (int, float)):
|
||||
try:
|
||||
v = v.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_domain:
|
||||
if not iredutils.is_domain(v):
|
||||
return ''
|
||||
|
||||
if is_email:
|
||||
if not iredutils.is_email(v):
|
||||
v = default_value
|
||||
|
||||
if is_integer:
|
||||
try:
|
||||
v = int(v)
|
||||
except:
|
||||
v = default_value
|
||||
|
||||
if is_strict_ip:
|
||||
if not iredutils.is_strict_ip(v):
|
||||
return ''
|
||||
|
||||
if is_ip_or_network:
|
||||
if not iredutils.is_ip_or_network(v):
|
||||
return ''
|
||||
|
||||
if to_string:
|
||||
try:
|
||||
if isinstance(v, (list, tuple)):
|
||||
v = [str(i) for i in v]
|
||||
else:
|
||||
v = str(v)
|
||||
except:
|
||||
pass
|
||||
|
||||
if to_lowercase:
|
||||
if isinstance(v, (list, tuple)):
|
||||
v = [i.lower() for i in v]
|
||||
else:
|
||||
v = v.lower()
|
||||
|
||||
if to_uppercase:
|
||||
if isinstance(v, (list, tuple)):
|
||||
v = [i.upper() for i in v]
|
||||
else:
|
||||
v = v.upper()
|
||||
|
||||
if split_value:
|
||||
# return a list
|
||||
if isinstance(v, str):
|
||||
if strip_str_before_split:
|
||||
if not strip_str:
|
||||
strip_str = ' '
|
||||
|
||||
v.strip(strip_str)
|
||||
|
||||
if split_separator:
|
||||
v = v.split(split_separator)
|
||||
else:
|
||||
v = v.split()
|
||||
|
||||
# Remove empty values
|
||||
v = [i for i in v if i]
|
||||
|
||||
return v
|
||||
|
||||
|
||||
# Return single value of specified form name.
|
||||
def get_multi_values(form,
|
||||
input_name,
|
||||
default_value=None,
|
||||
input_is_textarea=False,
|
||||
is_domain=False,
|
||||
is_email=False,
|
||||
is_ip_or_network=False,
|
||||
to_lowercase=False,
|
||||
to_uppercase=False,
|
||||
to_string=False):
|
||||
v = form.get(input_name)
|
||||
if v:
|
||||
if input_is_textarea:
|
||||
v = v.splitlines()
|
||||
v = [i.strip() for i in v]
|
||||
else:
|
||||
if default_value is None:
|
||||
v = []
|
||||
else:
|
||||
v = default_value
|
||||
|
||||
# Remove duplicate items.
|
||||
try:
|
||||
v = list(set(v))
|
||||
except:
|
||||
v = []
|
||||
|
||||
if is_domain:
|
||||
v = [str(i).lower() for i in v if iredutils.is_domain(i)]
|
||||
|
||||
if is_email:
|
||||
v = [str(i).lower() for i in v if iredutils.is_email(i)]
|
||||
|
||||
if is_ip_or_network:
|
||||
v = [str(i) for i in v if iredutils.is_ip_or_network(i)]
|
||||
|
||||
if to_lowercase:
|
||||
if not (is_domain or is_email):
|
||||
v = [i.lower() for i in v]
|
||||
|
||||
if to_uppercase:
|
||||
if not (is_domain or is_email):
|
||||
v = [i.upper() for i in v]
|
||||
|
||||
if to_string:
|
||||
v = [str(i) for i in v]
|
||||
|
||||
v.sort()
|
||||
return v
|
||||
|
||||
|
||||
def get_multi_values_from_api(form,
|
||||
input_name,
|
||||
to_string=True,
|
||||
to_lowercase=True,
|
||||
is_domain=False,
|
||||
is_email=False):
|
||||
"""Param/value posted from API will be: key=value1,value2,value3,...
|
||||
This function extract values and return them as a list.
|
||||
"""
|
||||
values = get_single_value(form=form,
|
||||
input_name=input_name,
|
||||
to_string=to_string,
|
||||
to_lowercase=to_lowercase,
|
||||
split_value=True,
|
||||
split_separator=',',
|
||||
strip_str_before_split=True)
|
||||
|
||||
if is_domain:
|
||||
values = [i for i in values if iredutils.is_domain(i)]
|
||||
|
||||
if is_email:
|
||||
values = [i for i in values if iredutils.is_email(i)]
|
||||
|
||||
return list(set(values))
|
||||
|
||||
|
||||
def get_multi_values_from_textarea(form,
|
||||
input_name,
|
||||
is_domain=False,
|
||||
is_email=False,
|
||||
to_lowercase=False):
|
||||
"""Param/value posted from API will be: key=value1,value2,value3,...
|
||||
This function extract values and return them as a list.
|
||||
"""
|
||||
v = get_single_value(form=form,
|
||||
input_name=input_name,
|
||||
to_string=True,
|
||||
to_lowercase=to_lowercase,
|
||||
split_value=True,
|
||||
split_separator=None,
|
||||
strip_str_before_split=True)
|
||||
|
||||
if is_domain:
|
||||
v = [i for i in v if iredutils.is_domain(i)]
|
||||
|
||||
if is_email:
|
||||
v = [i for i in v if iredutils.is_email(i)]
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def get_form_dict(form,
|
||||
input_name,
|
||||
key_name=None,
|
||||
multi_values=False,
|
||||
default_value='',
|
||||
input_is_textarea=False,
|
||||
is_domain=False,
|
||||
is_email=False,
|
||||
is_integer=False,
|
||||
to_lowercase=False,
|
||||
to_uppercase=False,
|
||||
to_string=False):
|
||||
d = {}
|
||||
if input_name in form:
|
||||
if multi_values:
|
||||
# Value is a list
|
||||
v = get_multi_values(form,
|
||||
input_name,
|
||||
default_value=default_value,
|
||||
input_is_textarea=input_is_textarea,
|
||||
is_domain=is_domain,
|
||||
is_email=is_email,
|
||||
to_lowercase=to_lowercase,
|
||||
to_uppercase=to_uppercase)
|
||||
else:
|
||||
v = get_single_value(form,
|
||||
input_name=input_name,
|
||||
default_value=default_value,
|
||||
is_domain=is_domain,
|
||||
is_email=is_email,
|
||||
is_integer=is_integer,
|
||||
to_lowercase=to_lowercase,
|
||||
to_uppercase=to_uppercase,
|
||||
to_string=to_string)
|
||||
|
||||
# Convert values of some parameters
|
||||
if settings.backend == 'ldap':
|
||||
if input_name == 'accountStatus':
|
||||
# When 'accountStatus' is used by a checkbox, its value will
|
||||
# be 'on' which means the checkbox is checked.
|
||||
if v in ['enable', 'active', 'yes', 'on', 1]:
|
||||
v = 'active'
|
||||
else:
|
||||
v = 'disabled'
|
||||
elif input_name == 'isGlobalAdmin':
|
||||
if v != 'yes':
|
||||
v = None
|
||||
elif input_name in ['quota', 'defaultQuota', 'maxUserQuota',
|
||||
'minPasswordLength', 'maxPasswordLength',
|
||||
'numberOfUsers', 'numberOfAliases',
|
||||
'numberOfLists']:
|
||||
# Require integer number
|
||||
try:
|
||||
v = int(v)
|
||||
except:
|
||||
# Don't return any value.
|
||||
return {}
|
||||
|
||||
else:
|
||||
if input_name in ['accountStatus', 'backupmx']:
|
||||
if v in ['enable', 'active', 'yes', 1]:
|
||||
v = 1
|
||||
else:
|
||||
v = 0
|
||||
|
||||
if key_name:
|
||||
d[key_name] = v
|
||||
else:
|
||||
if settings.backend == 'ldap':
|
||||
# Map some input names to LDAP attribute names
|
||||
# Warning: do not map the key names used in accountSetting.
|
||||
if input_name == 'name':
|
||||
key_name = 'cn'
|
||||
elif input_name == 'accountStatus':
|
||||
key_name = input_name
|
||||
elif input_name == 'language':
|
||||
key_name = 'preferredLanguage'
|
||||
elif input_name == 'transport':
|
||||
key_name = 'mtaTransport'
|
||||
else:
|
||||
key_name = input_name
|
||||
else:
|
||||
key_name = input_name
|
||||
|
||||
d[key_name] = v
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def get_name(form, input_name='cn'):
|
||||
return get_single_value(form,
|
||||
input_name=input_name,
|
||||
default_value='')
|
||||
|
||||
|
||||
def get_domain_name(form, input_name='domainName'):
|
||||
return get_single_value(form,
|
||||
input_name=input_name,
|
||||
default_value='',
|
||||
is_domain=True,
|
||||
to_lowercase=True,
|
||||
to_string=True)
|
||||
|
||||
|
||||
def get_domain_names(form, input_name='domainName'):
|
||||
return get_multi_values(form,
|
||||
input_name=input_name,
|
||||
default_value=[],
|
||||
is_domain=True,
|
||||
to_lowercase=True)
|
||||
|
||||
|
||||
# Get default language for new mail user from web form.
|
||||
def get_language(form, input_name='preferredLanguage'):
|
||||
lang = get_single_value(form, input_name=input_name, to_string=True)
|
||||
if lang not in iredutils.get_language_maps():
|
||||
lang = ''
|
||||
|
||||
return lang
|
||||
|
||||
|
||||
def get_domain_quota_and_unit(form,
|
||||
input_quota='domainQuota',
|
||||
input_quota_unit='domainQuotaUnit',
|
||||
convert_to_mb=True):
|
||||
"""Get domain quota and quota unit from web form, return a dict contains
|
||||
quota (in MB) and ORIGINAL quota unit: {'quota': <integer>, 'unit': <string>}.
|
||||
"""
|
||||
# multiply is used for SQL backends.
|
||||
quota = str(form.get(input_quota))
|
||||
if quota.isdigit():
|
||||
quota = abs(int(quota))
|
||||
else:
|
||||
quota = 0
|
||||
|
||||
quota_unit = str(form.get(input_quota_unit, 'MB'))
|
||||
if quota > 0:
|
||||
# Convert to MB
|
||||
if convert_to_mb:
|
||||
if quota_unit == 'GB':
|
||||
quota = quota * 1024
|
||||
elif quota_unit == 'TB':
|
||||
quota = quota * 1024 * 1024
|
||||
|
||||
return {'quota': quota, 'unit': quota_unit}
|
||||
|
||||
|
||||
# Get mailbox quota (in MB).
|
||||
def get_quota(form, input_name='defaultQuota', default=0):
|
||||
quota = str(form.get(input_name))
|
||||
if quota.isdigit():
|
||||
quota = abs(int(quota))
|
||||
|
||||
if input_name == 'maxUserQuota':
|
||||
quota_unit = str(form.get('maxUserQuotaUnit', 'MB'))
|
||||
if quota_unit == 'TB':
|
||||
quota = quota * 1024 * 1024
|
||||
elif quota_unit == 'GB':
|
||||
quota = quota * 1024
|
||||
else:
|
||||
# MB
|
||||
pass
|
||||
else:
|
||||
quota = default
|
||||
|
||||
return quota
|
||||
|
||||
|
||||
def get_account_status(form,
|
||||
input_name='accountStatus',
|
||||
default_value='active',
|
||||
to_integer=False):
|
||||
status = get_single_value(form, input_name=input_name, to_string=True)
|
||||
|
||||
if not (status in ['active', 'disabled']):
|
||||
status = default_value
|
||||
|
||||
# SQL backends store the account status as `active=[1|0]`
|
||||
# LDAP backends store the account status as `accountStatus=[active|disabled]`
|
||||
if to_integer:
|
||||
if status == 'active':
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
else:
|
||||
return status
|
||||
|
||||
|
||||
def get_password(form,
|
||||
input_name='newpw',
|
||||
confirm_pw_input_name='confirmpw',
|
||||
min_passwd_length=None,
|
||||
max_passwd_length=None):
|
||||
pw = get_single_value(form,
|
||||
input_name=input_name,
|
||||
to_string=True)
|
||||
|
||||
confirm_pw = get_single_value(form,
|
||||
input_name=confirm_pw_input_name,
|
||||
to_string=True)
|
||||
|
||||
qr = iredpwd.verify_new_password(newpw=pw,
|
||||
confirmpw=confirm_pw,
|
||||
min_passwd_length=min_passwd_length,
|
||||
max_passwd_length=max_passwd_length)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
if 'store_password_in_plain_text' in form and settings.STORE_PASSWORD_IN_PLAIN:
|
||||
pw_hash = iredpwd.generate_password_hash(pw, pwscheme='PLAIN')
|
||||
else:
|
||||
pw_hash = iredpwd.generate_password_hash(pw)
|
||||
|
||||
return True, {'pw_plain': pw, 'pw_hash': pw_hash}
|
||||
|
||||
|
||||
def get_timezone(form, input_name='timezone'):
|
||||
tz = get_single_value(form,
|
||||
input_name=input_name,
|
||||
to_string=True)
|
||||
|
||||
if tz in TIMEZONES:
|
||||
return tz
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_list_access_policy(form,
|
||||
input_name='accessPolicy',
|
||||
default_value='public'):
|
||||
policy = get_single_value(form=form,
|
||||
input_name=input_name,
|
||||
default_value=default_value,
|
||||
to_string=True)
|
||||
|
||||
if policy not in iredutils.MAILLIST_ACCESS_POLICIES:
|
||||
policy = 'public'
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
# iRedAPD: Get throttle setting for
|
||||
def get_throttle_setting(form, account, inout_type='inbound'):
|
||||
# inout_type -- inbound, outbound.
|
||||
var_enable_throttle = 'enable_%s_throttling' % inout_type
|
||||
|
||||
# not enabled.
|
||||
if var_enable_throttle not in form:
|
||||
return {}
|
||||
|
||||
# name of form <input> tag:
|
||||
# [inout_type]_[name]
|
||||
# custom_[inout_type]_[name]
|
||||
|
||||
# Pre-defined values
|
||||
setting = {'account': account,
|
||||
'priority': iredutils.get_account_priority(account),
|
||||
'period': 0,
|
||||
'max_msgs': 0,
|
||||
'max_quota': 0,
|
||||
'msg_size': 0,
|
||||
'kind': inout_type}
|
||||
|
||||
input_keys = ['period', 'max_msgs', 'max_quota', 'msg_size']
|
||||
|
||||
if inout_type == "outbound":
|
||||
setting["max_rcpts"] = 0
|
||||
input_keys.append("max_rcpts")
|
||||
|
||||
for k in input_keys:
|
||||
var = inout_type + '_' + k
|
||||
|
||||
# Get pre-defined value first
|
||||
v = form.get(var, '')
|
||||
|
||||
if v == 'on':
|
||||
# Get custom value if it's not pre-defined
|
||||
v = form.get('custom_' + var)
|
||||
|
||||
try:
|
||||
v = int(v)
|
||||
setting[k] = v
|
||||
except:
|
||||
continue
|
||||
|
||||
# Return empty dict if all values are 0.
|
||||
return setting
|
||||
|
||||
|
||||
# NOTE: used by LDAP backends.
|
||||
def update_domain_creation_settings(form,
|
||||
account_settings,
|
||||
check_creation_permission=True):
|
||||
"""Update `account_settings` with data from form.
|
||||
|
||||
:param form: web form data
|
||||
:param account_settings: dict of per-admin account settings
|
||||
:param check_creation_permission: check whether html tag
|
||||
"<input name='allowed_to_create_domain' ... />" exists, used in user
|
||||
profile page.
|
||||
"""
|
||||
_allowed = True
|
||||
if check_creation_permission:
|
||||
if 'allowed_to_create_domain' not in form:
|
||||
_allowed = False
|
||||
|
||||
if _allowed:
|
||||
for i in ['create_max_domains',
|
||||
'create_max_quota',
|
||||
'create_max_users',
|
||||
'create_max_aliases',
|
||||
'create_max_lists']:
|
||||
if i in form:
|
||||
try:
|
||||
v = int(form.get(i, '0'))
|
||||
except:
|
||||
v = 0
|
||||
|
||||
if v > 0:
|
||||
account_settings[i] = v
|
||||
else:
|
||||
if i in account_settings:
|
||||
account_settings.pop(i)
|
||||
|
||||
for i in ['disable_domain_ownership_verification']:
|
||||
if i in form:
|
||||
account_settings[i] = 'yes'
|
||||
else:
|
||||
if i in account_settings:
|
||||
account_settings.pop(i)
|
||||
|
||||
if 'create_max_quota' in account_settings:
|
||||
if 'create_quota_unit' in form:
|
||||
v = form.get('create_quota_unit', 'TB')
|
||||
if v in ['TB', 'GB']:
|
||||
account_settings['create_quota_unit'] = v
|
||||
else:
|
||||
if 'create_quota_unit' in account_settings:
|
||||
account_settings.pop('create_quota_unit')
|
||||
|
||||
for i in ['create_max_domains',
|
||||
'create_max_quota',
|
||||
'create_max_users',
|
||||
'create_max_aliases',
|
||||
'create_max_lists']:
|
||||
if i in account_settings:
|
||||
account_settings['create_new_domains'] = 'yes'
|
||||
break
|
||||
else:
|
||||
# Remove account_settings['create_new_domains']
|
||||
try:
|
||||
account_settings.pop('create_new_domains')
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
for i in ['create_new_domains',
|
||||
'create_max_domains',
|
||||
'create_max_quota',
|
||||
'create_max_users',
|
||||
'create_max_aliases',
|
||||
'create_max_lists',
|
||||
'disable_domain_ownership_verification']:
|
||||
if i in account_settings:
|
||||
account_settings.pop(i)
|
||||
|
||||
return account_settings
|
||||
|
||||
|
||||
# NOTE: used by LDAP backends.
|
||||
def get_domain_creation_settings(form):
|
||||
"""Get per-admin domain creation limits from web form."""
|
||||
d = {}
|
||||
|
||||
kv = get_form_dict(form=form,
|
||||
input_name='create_max_domains',
|
||||
default_value=0,
|
||||
is_integer=True)
|
||||
if kv:
|
||||
d.update(kv)
|
||||
d['create_new_domains'] = 'yes'
|
||||
|
||||
kv = get_form_dict(form=form,
|
||||
input_name='create_max_users',
|
||||
default_value=0,
|
||||
is_integer=True)
|
||||
d.update(kv)
|
||||
|
||||
kv = get_form_dict(form=form,
|
||||
input_name='create_max_aliases',
|
||||
default_value=0,
|
||||
is_integer=True)
|
||||
d.update(kv)
|
||||
|
||||
kv = get_form_dict(form=form,
|
||||
input_name='create_max_lists',
|
||||
default_value=0,
|
||||
is_integer=True)
|
||||
d.update(kv)
|
||||
|
||||
# format: 10TB, 10GB, 10MB.
|
||||
kv = get_form_dict(form=form,
|
||||
input_name='create_max_quota',
|
||||
default_value=0,
|
||||
is_integer=True)
|
||||
|
||||
if kv:
|
||||
_kv = get_form_dict(form=form,
|
||||
input_name='create_quota_unit',
|
||||
default_value='MB',
|
||||
to_uppercase=True,
|
||||
to_string=True)
|
||||
d.update(_kv)
|
||||
d.update(kv)
|
||||
|
||||
# Discard item which has value == '0'
|
||||
if d:
|
||||
for (k, v) in list(d.items()):
|
||||
if v == 0:
|
||||
d.pop(k)
|
||||
|
||||
if k == 'create_max_quota':
|
||||
if 'create_quota_unit' in d:
|
||||
d.pop('create_quota_unit')
|
||||
|
||||
return d
|
||||
|
||||
|
||||
#
|
||||
# mlmmj
|
||||
#
|
||||
def get_mlmmj_params_from_web_form(form):
|
||||
"""Convert parameter names/values in web form to mlmmj parameters."""
|
||||
mlmmj_params = form.copy()
|
||||
|
||||
# Remove parameters used by web form but not mlmmjadmin API
|
||||
for k in ['csrf_token', 'modified', 'active', 'accountStatus']:
|
||||
if k in mlmmj_params:
|
||||
mlmmj_params.pop(k)
|
||||
|
||||
#
|
||||
# Get access policy
|
||||
#
|
||||
if 'accessPolicy' in form:
|
||||
access_policy = form.get('accessPolicy', '').lower()
|
||||
mlmmj_params.pop('accessPolicy')
|
||||
else:
|
||||
access_policy = 'public'
|
||||
|
||||
if access_policy not in iredutils.ML_ACCESS_POLICIES:
|
||||
access_policy = 'public'
|
||||
|
||||
mlmmj_params['access_policy'] = access_policy
|
||||
mlmmj_params['only_subscriber_can_post'] = 'no'
|
||||
mlmmj_params['only_moderator_can_post'] = 'no'
|
||||
if access_policy == 'membersonly':
|
||||
mlmmj_params['only_subscriber_can_post'] = 'yes'
|
||||
elif access_policy == 'moderatorsonly':
|
||||
mlmmj_params['only_moderator_can_post'] = 'yes'
|
||||
|
||||
#
|
||||
# Get max message size (in bytes)
|
||||
#
|
||||
mlmmj_params['max_message_size'] = 0
|
||||
|
||||
# `max_mail_size` and `max_mail_size_unit` are used by web form.
|
||||
_size = form.get('max_mail_size', 0)
|
||||
_unit = form.get('max_mail_size_unit', 'KB')
|
||||
try:
|
||||
_size = int(_size)
|
||||
except:
|
||||
pass
|
||||
|
||||
if _size:
|
||||
if _unit == 'KB':
|
||||
mlmmj_params['max_message_size'] = _size * 1024
|
||||
elif _unit == 'MB':
|
||||
mlmmj_params['max_message_size'] = _size * 1024 * 1024
|
||||
|
||||
if 'max_mail_size' in mlmmj_params:
|
||||
mlmmj_params.pop('max_mail_size')
|
||||
|
||||
if 'max_mail_size_unit' in mlmmj_params:
|
||||
mlmmj_params.pop('max_mail_size_unit')
|
||||
|
||||
# Other radio/checkbox options.
|
||||
for (k, v) in list(mlmmj_params.items()):
|
||||
# mlmmjadmin API expects values in 'yes', 'no'.
|
||||
if v == 'on':
|
||||
mlmmj_params[k] = 'yes'
|
||||
|
||||
# Rename 'hidden_<key>' to '<key>'
|
||||
if k.startswith('hidden_'):
|
||||
nk = k.replace('hidden_', '') # don't use `string.lstrip()`
|
||||
mlmmj_params.pop(k)
|
||||
|
||||
if nk not in mlmmj_params:
|
||||
mlmmj_params[nk] = 'no'
|
||||
|
||||
return mlmmj_params
|
||||
|
||||
|
||||
def get_mlmmj_params_from_api(form):
|
||||
"""Convert parameter names/values in API form to mlmmjadmin parameters.
|
||||
|
||||
:param form: dict of web form.
|
||||
|
||||
It also supports all parameters supported by mlmmjadmin.
|
||||
"""
|
||||
# `kvs` stores mlmmj parameters
|
||||
kvs = form.copy()
|
||||
|
||||
#
|
||||
# Get max message size (in bytes)
|
||||
#
|
||||
if 'max_message_size' in form:
|
||||
kvs['max_message_size'] = 0
|
||||
|
||||
try:
|
||||
_size = abs(int(form.get('max_message_size', 0)))
|
||||
kvs['max_message_size'] = _size
|
||||
except:
|
||||
kvs.pop('max_message_size')
|
||||
|
||||
return kvs
|
||||
16
libs/hooks.py
Normal file
16
libs/hooks.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import web
|
||||
|
||||
|
||||
def hook_set_language():
|
||||
# parameter `lang` in URI. e.g. https://xxx/?lang=en_US
|
||||
_lang = web.input(lang=None, _method="GET").get("lang")
|
||||
|
||||
# parameter `lang` in session.
|
||||
if not _lang:
|
||||
_lang = web.config.get("_session", {}).get("lang")
|
||||
|
||||
web.ctx.lang = _lang or "en_US"
|
||||
|
||||
|
||||
def hook_session():
|
||||
pass
|
||||
0
libs/iredapd/__init__.py
Normal file
0
libs/iredapd/__init__.py
Normal file
535
libs/iredapd/greylist.py
Normal file
535
libs/iredapd/greylist.py
Normal file
@@ -0,0 +1,535 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
|
||||
from libs import iredutils
|
||||
from libs.logger import logger
|
||||
|
||||
|
||||
def get_all_greylist_settings():
|
||||
"""Return all existing greylisting settings."""
|
||||
gl_settings = {}
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting',
|
||||
what='id, account, sender, active',
|
||||
)
|
||||
if qr:
|
||||
gl_settings = list(qr)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return gl_settings
|
||||
|
||||
|
||||
def get_greylist_setting(account=None):
|
||||
"""Return greylisting setting of specified account."""
|
||||
gl_setting = {}
|
||||
|
||||
if not account:
|
||||
account = '@.'
|
||||
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return gl_setting
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting',
|
||||
vars={'account': account},
|
||||
what='id, account, sender, active',
|
||||
where="""account = $account AND sender='@.'""",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if qr:
|
||||
gl_setting = qr[0]
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return gl_setting
|
||||
|
||||
|
||||
def get_greylist_whitelists(account, address_only=False):
|
||||
"""Return greylisting whitelists of specified account."""
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return []
|
||||
|
||||
whitelists = []
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting_whitelists',
|
||||
vars={'account': account},
|
||||
what='id, sender, comment',
|
||||
where='account = $account',
|
||||
order='sender',
|
||||
)
|
||||
if qr:
|
||||
whitelists = list(qr)
|
||||
|
||||
# Don't explore SQL structure, just export the sender addresses
|
||||
if address_only and whitelists:
|
||||
wl = []
|
||||
for i in whitelists:
|
||||
wl.append(i.sender.lower())
|
||||
|
||||
whitelists = wl
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return whitelists
|
||||
|
||||
|
||||
def get_greylist_whitelist_domains():
|
||||
"""Return greylisting whitelist domains of specified account."""
|
||||
domains = []
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting_whitelist_domains',
|
||||
what='domain',
|
||||
order='domain',
|
||||
)
|
||||
if qr:
|
||||
for i in qr:
|
||||
domains.append(str(i.domain).lower())
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def delete_greylist_setting(account, senders=None):
|
||||
"""Delete greylisting setting of specified account."""
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return True
|
||||
|
||||
try:
|
||||
if senders:
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'account': account, 'senders': senders},
|
||||
where="""account = $account AND sender IN $sender""",
|
||||
)
|
||||
else:
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'account': account},
|
||||
where="""account = $account""",
|
||||
)
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def enable_disable_greylist_setting(account, enable=False):
|
||||
"""Update (or create) greylisting setting of specified account."""
|
||||
account_type = iredutils.is_valid_amavisd_address(account)
|
||||
if not account_type:
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
active = 0
|
||||
if enable:
|
||||
active = 1
|
||||
|
||||
gl_setting = {'account': account,
|
||||
'priority': iredutils.IREDAPD_ACCOUNT_PRIORITIES.get(account_type, 0),
|
||||
'sender': '@.',
|
||||
'sender_priority': 0,
|
||||
'active': active}
|
||||
|
||||
try:
|
||||
# Delete existing record first.
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'account': account, 'sender': gl_setting['sender']},
|
||||
where='account = $account AND sender = $sender',
|
||||
)
|
||||
|
||||
# Create new record
|
||||
web.conn_iredapd.insert('greylisting', **gl_setting)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def reset_greylist_whitelist_domains(domains=None):
|
||||
"""Update greylisting whitelist domains for specified account.
|
||||
|
||||
@domains -- must be a list/tuple/set
|
||||
@conn -- sql connection cursor
|
||||
"""
|
||||
# Delete existing records first
|
||||
try:
|
||||
web.conn_iredapd.delete('greylisting_whitelist_domains', where='1=1')
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Insert new records
|
||||
if domains:
|
||||
values = []
|
||||
for d in domains:
|
||||
values += [{'domain': d}]
|
||||
|
||||
try:
|
||||
web.conn_iredapd.multiple_insert('greylisting_whitelist_domains', values=values)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True, 'GL_WLD_UPDATED'
|
||||
|
||||
|
||||
def update_greylist_whitelist_domains(new=None, removed=None):
|
||||
"""Add new or remove existing whitelist SPF domains for greylisting service.
|
||||
|
||||
@new - must be a list/tuple/set of sender domains
|
||||
@removed - must be a list/tuple/set of sender domains
|
||||
@conn - sql connection cursor
|
||||
"""
|
||||
_new = []
|
||||
if new:
|
||||
_new = [str(i).lower()
|
||||
for i in new
|
||||
if iredutils.is_domain(i)]
|
||||
_new = list(set(_new))
|
||||
|
||||
_removed = []
|
||||
if removed:
|
||||
_removed = [str(i).lower()
|
||||
for i in removed
|
||||
if iredutils.is_domain(i)]
|
||||
_removed = list(set(_removed))
|
||||
|
||||
# Remove duplicates
|
||||
_removed = [i for i in _removed if i not in _new]
|
||||
|
||||
if not (_new or _removed):
|
||||
return True,
|
||||
|
||||
# Insert new whitelists
|
||||
if _new:
|
||||
for i in _new:
|
||||
try:
|
||||
web.conn_iredapd.insert('greylisting_whitelist_domains', domain=i)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
# Remove existing ones
|
||||
if _removed:
|
||||
try:
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelist_domains',
|
||||
vars={'removed': _removed},
|
||||
where='domain IN $removed',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def reset_greylist_whitelists(account, whitelists=None):
|
||||
"""Reset greylisting whitelists for specified account.
|
||||
|
||||
If `whitelists` is empty, all existing whitelists will be removed.
|
||||
|
||||
@whitelists - must be a list/tuple/set of whitelist senders, or a list of
|
||||
dict which maps to sql column/value pairs. e.g.
|
||||
[{'account': '@.',
|
||||
'sender': '192.168.1.1',
|
||||
'comment': ''},
|
||||
...]
|
||||
"""
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
# Delete existing whitelists first
|
||||
try:
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelists',
|
||||
vars={'account': account},
|
||||
where='account = $account',
|
||||
)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Insert new whitelists
|
||||
if whitelists:
|
||||
for w in whitelists:
|
||||
if isinstance(w, dict):
|
||||
try:
|
||||
web.conn_iredapd.insert('greylisting_whitelists', **w)
|
||||
except:
|
||||
pass
|
||||
elif isinstance(w, str):
|
||||
try:
|
||||
web.conn_iredapd.insert(
|
||||
'greylisting_whitelists',
|
||||
account=account,
|
||||
sender=w,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def update_greylist_whitelists(account, new=None, removed=None):
|
||||
"""Add new or remove existing greylisting whitelists for specified account.
|
||||
|
||||
:param account: must be an valid iRedAPD account
|
||||
:param new: must be a list/tuple/set of whitelist senders
|
||||
:param removed: must be a list/tuple/set of whitelist senders
|
||||
"""
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
_new = []
|
||||
if new:
|
||||
_new = [str(i).lower()
|
||||
for i in new
|
||||
if iredutils.is_valid_wblist_address(i)]
|
||||
_new = list(set(_new))
|
||||
|
||||
_removed = []
|
||||
if removed:
|
||||
_removed = [str(i).lower()
|
||||
for i in removed
|
||||
if iredutils.is_valid_wblist_address(i)]
|
||||
_removed = list(set(_removed))
|
||||
|
||||
# Remove duplicates
|
||||
_removed = [i for i in _removed if i not in _new]
|
||||
|
||||
if not (_new or _removed):
|
||||
return True,
|
||||
|
||||
# Insert new whitelists
|
||||
if _new:
|
||||
for w in _new:
|
||||
try:
|
||||
web.conn_iredapd.insert(
|
||||
'greylisting_whitelists',
|
||||
account=account,
|
||||
sender=w,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Remove existing ones
|
||||
if _removed:
|
||||
try:
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelists',
|
||||
vars={'removed': removed},
|
||||
where='sender IN $removed',
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def update_greylist_settings_from_form(account, form):
|
||||
# Enable/disable greylisting
|
||||
# @inherit - inherit from global setting
|
||||
# @enable - explicitly enable
|
||||
# @disable - explicitly disable
|
||||
_gl_value = form.get('greylisting', 'inherit')
|
||||
if _gl_value == 'inherit':
|
||||
# Delete greylisting setting
|
||||
qr = delete_greylist_setting(account=account)
|
||||
elif _gl_value == 'enable':
|
||||
qr = enable_disable_greylist_setting(account=account, enable=True)
|
||||
elif _gl_value == 'disable':
|
||||
qr = enable_disable_greylist_setting(account=account, enable=False)
|
||||
else:
|
||||
return True, 'GL_UPDATED'
|
||||
|
||||
if qr[0] is not True:
|
||||
return qr
|
||||
|
||||
# Update greylisting whitelist domains.
|
||||
if account == '@.':
|
||||
wl_domains = set()
|
||||
lines = form.get('whitelist_domains', '').splitlines()
|
||||
for line in lines:
|
||||
if iredutils.is_domain(line):
|
||||
wl_domains.add(str(line).lower())
|
||||
|
||||
qr = reset_greylist_whitelist_domains(domains=wl_domains)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
# Update greylisting whitelists.
|
||||
whitelists = []
|
||||
|
||||
# Store senders to avoid duplicate
|
||||
_senders = set()
|
||||
lines = form.get('whitelists', '').splitlines()
|
||||
for line in lines:
|
||||
# Split sender and comment with '#'
|
||||
wl = line.split('#', 1)
|
||||
|
||||
sender = ''
|
||||
comment = ''
|
||||
|
||||
if len(wl) == 1:
|
||||
sender = str(wl[0]).strip()
|
||||
comment = ''
|
||||
elif len(wl) == 2:
|
||||
sender = str(wl[0]).strip()
|
||||
comment = wl[1].strip()
|
||||
|
||||
# Validate sender.
|
||||
if not iredutils.is_valid_wblist_address(sender):
|
||||
continue
|
||||
|
||||
if sender not in _senders:
|
||||
whitelists += [{'account': account, 'sender': sender, 'comment': comment}]
|
||||
_senders.add(sender)
|
||||
|
||||
qr = reset_greylist_whitelists(account=account, whitelists=whitelists)
|
||||
if qr[0]:
|
||||
return True, 'GL_UPDATED'
|
||||
else:
|
||||
return qr
|
||||
|
||||
|
||||
def delete_settings_for_removed_users(mails):
|
||||
mails = [str(v).lower() for v in mails if iredutils.is_email(v)]
|
||||
if not mails:
|
||||
return True,
|
||||
|
||||
try:
|
||||
# Delete settings for user
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'mails': mails},
|
||||
where="""account IN $mails""",
|
||||
)
|
||||
|
||||
# Delete whitelists
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelists',
|
||||
vars={'mails': mails},
|
||||
where='account IN $mails',
|
||||
)
|
||||
|
||||
# Delete greylisting tracking
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_tracking',
|
||||
vars={'mails': mails},
|
||||
where="""recipient IN $mails""",
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_settings_for_removed_domain(domain):
|
||||
if not iredutils.is_domain(domain):
|
||||
return True,
|
||||
|
||||
try:
|
||||
# Delete settings for domain ('@domain.com')
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'domain': '@' + domain},
|
||||
where='account=$domain',
|
||||
)
|
||||
|
||||
# Delete settings for all users under this domain
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting',
|
||||
vars={'domain': '%@' + domain},
|
||||
where="""account LIKE $domain""",
|
||||
)
|
||||
|
||||
# Delete whitelists
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelists',
|
||||
vars={'domain': '@' + domain},
|
||||
where='account=$domain',
|
||||
)
|
||||
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_whitelists',
|
||||
vars={'domain': '%@' + domain},
|
||||
where='account LIKE $domain',
|
||||
)
|
||||
|
||||
# Delete greylisting tracking
|
||||
web.conn_iredapd.delete(
|
||||
'greylisting_tracking',
|
||||
vars={'domain': domain},
|
||||
where='rcpt_domain=$domain',
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_tracking_data(account):
|
||||
"""Get tracking data of given local account."""
|
||||
_account_type = iredutils.is_valid_amavisd_address(account)
|
||||
if not _account_type:
|
||||
return True, []
|
||||
|
||||
try:
|
||||
if _account_type == 'catchall':
|
||||
# account = '@.'
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting_tracking',
|
||||
what='COUNT(blocked_count) AS total, sender_domain',
|
||||
where='passed=0',
|
||||
group='sender_domain',
|
||||
order='total DESC',
|
||||
)
|
||||
|
||||
elif _account_type == 'domain':
|
||||
domain = account.lstrip('@')
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting_tracking',
|
||||
vars={'domain': domain},
|
||||
where='sender_domain=$domain AND passed=0',
|
||||
order='init_time DESC',
|
||||
)
|
||||
else:
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
return True, list(qr)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_domain_tracking_data(domain):
|
||||
"""Get tracking data of given domain."""
|
||||
domain = str(domain).lower()
|
||||
return get_tracking_data(account='@' + domain)
|
||||
|
||||
|
||||
def filter_whitelisted_ips(ips):
|
||||
"""Return list of (globally) whitelisted IPs."""
|
||||
ips = [i for i in ips if iredutils.is_strict_ip(i)]
|
||||
if not ips:
|
||||
return True, []
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'greylisting_whitelists',
|
||||
vars={'account': '@.', 'ips': ips},
|
||||
what='sender',
|
||||
where='account=$account AND sender IN $ips',
|
||||
order='sender',
|
||||
)
|
||||
whitelisted_ips = [i.sender for i in qr]
|
||||
|
||||
return True, whitelisted_ips
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False, repr(e)
|
||||
286
libs/iredapd/log.py
Normal file
286
libs/iredapd/log.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import time
|
||||
import web
|
||||
|
||||
import settings
|
||||
|
||||
from libs import iredutils
|
||||
from libs.logger import logger
|
||||
|
||||
|
||||
if settings.backend == 'ldap':
|
||||
from libs.ldaplib.admin import get_managed_domains
|
||||
else:
|
||||
from libs.sqllib.admin import get_managed_domains
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
|
||||
def __get_managed_domains():
|
||||
domains = []
|
||||
|
||||
kw = {'admin': session.get('username'),
|
||||
'domain_name_only': True,
|
||||
'conn': None}
|
||||
|
||||
if settings.backend != 'ldap':
|
||||
kw['listed_only'] = True
|
||||
|
||||
qr = get_managed_domains(**kw)
|
||||
|
||||
if qr[0]:
|
||||
domains = qr[1]
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def get_num_rejected(hours=None):
|
||||
"""Return amount of rejected mails in last given `hours`."""
|
||||
num = 0
|
||||
|
||||
if not hours:
|
||||
hours = 24
|
||||
|
||||
sql_vars = {
|
||||
"action": "REJECT",
|
||||
"time_num": (int(time.time()) - (hours * 3600)),
|
||||
}
|
||||
|
||||
sql_wheres = ["action = $action AND time_num >= $time_num"]
|
||||
|
||||
if not session.get('is_global_admin'):
|
||||
domains = __get_managed_domains()
|
||||
|
||||
if domains:
|
||||
sql_vars['domains'] = domains
|
||||
sql_wheres += ['(sender_domain IN $domains OR sasl_username IN $domains OR recipient_domain IN $domains)']
|
||||
else:
|
||||
return num
|
||||
|
||||
sql_where = ' AND '.join(sql_wheres)
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'smtp_sessions',
|
||||
vars=sql_vars,
|
||||
what="COUNT(id) AS total",
|
||||
where=sql_where,
|
||||
)
|
||||
if qr:
|
||||
num = qr[0]['total']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return num
|
||||
|
||||
|
||||
def get_num_smtp_outbound_sessions(hours=None):
|
||||
"""Return amount of smtp authentications in last given `hours`."""
|
||||
num = 0
|
||||
|
||||
if not hours:
|
||||
hours = 24
|
||||
|
||||
sql_vars = {
|
||||
"time_num": (int(time.time()) - (hours * 3600)),
|
||||
}
|
||||
|
||||
sql_wheres = ["sasl_username <> '' AND time_num >= $time_num"]
|
||||
|
||||
if not session.get('is_global_admin'):
|
||||
domains = __get_managed_domains()
|
||||
|
||||
if domains:
|
||||
sql_vars['domains'] = domains
|
||||
sql_wheres += ['sasl_domain IN $domains']
|
||||
else:
|
||||
return num
|
||||
|
||||
sql_where = ' AND '.join(sql_wheres)
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'smtp_sessions',
|
||||
vars=sql_vars,
|
||||
what="COUNT(id) AS total",
|
||||
where=sql_where,
|
||||
)
|
||||
|
||||
if qr:
|
||||
num = qr[0]['total']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return num
|
||||
|
||||
|
||||
def get_log_smtp_sessions(domains=None,
|
||||
sasl_usernames=None,
|
||||
senders=None,
|
||||
recipients=None,
|
||||
client_addresses=None,
|
||||
encryption_protocols=None,
|
||||
outbound_only=False,
|
||||
rejected_only=False,
|
||||
offset=None,
|
||||
limit=None):
|
||||
"""Return a dict with amount of smtp rejections and list of (SQL) rows."""
|
||||
result = {'total': 0, 'rows': []}
|
||||
|
||||
if not offset or not isinstance(offset, int):
|
||||
offset = 0
|
||||
|
||||
if not limit or not isinstance(limit, int):
|
||||
limit = settings.PAGE_SIZE_LIMIT
|
||||
|
||||
query_domains = []
|
||||
sql_vars = {}
|
||||
sql_wheres = []
|
||||
sql_where = None
|
||||
|
||||
if domains:
|
||||
query_domains = [str(i).lower() for i in domains if iredutils.is_domain(i)]
|
||||
|
||||
if session.get('is_global_admin'):
|
||||
if query_domains:
|
||||
sql_vars['domains'] = query_domains
|
||||
|
||||
if outbound_only:
|
||||
sql_wheres += ['sasl_domain IN $domains']
|
||||
else:
|
||||
sql_wheres += ['(sender_domain IN $domains OR sasl_domain IN $domains OR recipient_domain IN $domains)']
|
||||
else:
|
||||
if outbound_only:
|
||||
sql_wheres += ["sasl_username <> ''"]
|
||||
else:
|
||||
managed_domains = __get_managed_domains()
|
||||
if not managed_domains:
|
||||
return result
|
||||
|
||||
if domains:
|
||||
query_domains = [str(i).lower() for i in domains if i in managed_domains]
|
||||
|
||||
if not query_domains:
|
||||
return result
|
||||
else:
|
||||
query_domains = managed_domains
|
||||
|
||||
sql_vars['domains'] = query_domains
|
||||
if outbound_only:
|
||||
sql_wheres += ['sasl_domain in $domains']
|
||||
else:
|
||||
sql_wheres += ['(sender_domain IN $domains OR sasl_domain IN $domains OR recipient_domain IN $domains)']
|
||||
|
||||
if sasl_usernames:
|
||||
sql_vars['sasl_usernames'] = [str(i).lower() for i in sasl_usernames if iredutils.is_email(i)]
|
||||
sql_wheres += ['sasl_username IN $sasl_usernames']
|
||||
|
||||
if senders:
|
||||
sql_vars['senders'] = [str(i).lower() for i in senders if iredutils.is_email(i)]
|
||||
sql_wheres += ['sender IN $senders']
|
||||
|
||||
if recipients:
|
||||
sql_vars['recipients'] = [str(i).lower() for i in recipients if iredutils.is_email(i)]
|
||||
sql_wheres += ['recipient IN $recipients']
|
||||
|
||||
if client_addresses:
|
||||
sql_vars['client_addresses'] = [i for i in client_addresses if iredutils.is_strict_ip(i)]
|
||||
sql_wheres += ['client_address IN $client_addresses']
|
||||
|
||||
if encryption_protocols:
|
||||
sql_vars['encryption_protocols'] = encryption_protocols
|
||||
sql_wheres += ['encryption_protocol IN $encryption_protocols']
|
||||
|
||||
if rejected_only:
|
||||
sql_wheres += ["action='REJECT'"]
|
||||
|
||||
if sql_wheres:
|
||||
sql_where = ' AND '.join(sql_wheres)
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'smtp_sessions',
|
||||
vars=sql_vars,
|
||||
what='COUNT(id) AS total',
|
||||
where=sql_where,
|
||||
)
|
||||
if qr:
|
||||
result['total'] = qr[0].total
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
columns = [
|
||||
'id', 'time', 'time_num',
|
||||
'action', 'reason', 'instance',
|
||||
'sasl_username', 'sender', 'recipient',
|
||||
'client_address', 'encryption_protocol',
|
||||
]
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'smtp_sessions',
|
||||
vars=sql_vars,
|
||||
what=','.join(columns),
|
||||
where=sql_where,
|
||||
order='time_num DESC',
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
if qr:
|
||||
result['rows'] = list(qr)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_smtp_insecure_outbound(hours=None):
|
||||
"""
|
||||
Return info of insecure smtp outbound sessions in last given `hours`.
|
||||
|
||||
(True, {'total': '<int>', 'usernames': [<mail>, <mail>, ...]})
|
||||
(False, '<error>')
|
||||
"""
|
||||
result = {'total': 0, 'usernames': []}
|
||||
|
||||
if not isinstance(hours, int):
|
||||
hours = 24
|
||||
|
||||
sql_vars = {
|
||||
"time_num": (int(time.time()) - (hours * 3600)),
|
||||
}
|
||||
|
||||
sql_wheres = ["sasl_username <> '' AND encryption_protocol = '' AND time_num >= $time_num"]
|
||||
|
||||
if not session.get('is_global_admin'):
|
||||
domains = __get_managed_domains()
|
||||
|
||||
if domains:
|
||||
sql_vars['domains'] = domains
|
||||
sql_wheres += ['sasl_domain IN $domains']
|
||||
else:
|
||||
return True, result
|
||||
|
||||
sql_where = ' AND '.join(sql_wheres)
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'smtp_sessions',
|
||||
vars=sql_vars,
|
||||
what='sasl_username',
|
||||
where=sql_where,
|
||||
group='sasl_username',
|
||||
)
|
||||
|
||||
for row in qr:
|
||||
result['total'] += 1
|
||||
_email = str(row['sasl_username']).lower().strip()
|
||||
result['usernames'].append(_email)
|
||||
|
||||
result['usernames'].sort()
|
||||
return True, result
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False, repr(e)
|
||||
180
libs/iredapd/throttle.py
Normal file
180
libs/iredapd/throttle.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
from libs import iredutils
|
||||
|
||||
|
||||
def get_throttle_setting(account, inout_type='outbound'):
|
||||
"""Get throttle setting.
|
||||
|
||||
@account -- a valid throttling account
|
||||
@inout_type -- inbound, outbound
|
||||
"""
|
||||
setting = {}
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return setting
|
||||
|
||||
qr = web.conn_iredapd.select(
|
||||
'throttle',
|
||||
vars={'account': account, 'inout_type': inout_type},
|
||||
where='kind=$inout_type AND account=$account',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if qr:
|
||||
setting = qr[0]
|
||||
|
||||
return setting
|
||||
|
||||
|
||||
def delete_throttle_setting(account, inout_type):
|
||||
if not iredutils.is_valid_amavisd_address(account):
|
||||
return False, 'INVALID_ACCOUNT'
|
||||
|
||||
if not (inout_type in ['inbound', 'outbound']):
|
||||
return False, 'INVALID_INOUT_TYPE'
|
||||
|
||||
if account and inout_type:
|
||||
web.conn_iredapd.delete(
|
||||
'throttle',
|
||||
vars={'account': account, 'inout_type': inout_type},
|
||||
where='account=$account AND kind=$inout_type',
|
||||
)
|
||||
|
||||
return True,
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def delete_throttle_tracking(account, inout_type):
|
||||
tid = get_throttle_id(account, inout_type)
|
||||
|
||||
if tid:
|
||||
try:
|
||||
web.conn_iredapd.delete(
|
||||
'throttle_tracking',
|
||||
vars={'tid': tid},
|
||||
where='tid=$tid',
|
||||
)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def delete_settings_for_removed_users(mails):
|
||||
mails = [str(v).lower() for v in mails if iredutils.is_email(v)]
|
||||
if not mails:
|
||||
return True,
|
||||
|
||||
try:
|
||||
web.conn_iredapd.delete(
|
||||
'throttle',
|
||||
vars={'mails': mails},
|
||||
where="""account IN $mails""",
|
||||
)
|
||||
|
||||
web.conn_iredapd.delete(
|
||||
'throttle_tracking',
|
||||
vars={'mails': mails},
|
||||
where="""account IN $mails""",
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_settings_for_removed_domain(domain):
|
||||
if not iredutils.is_domain(domain):
|
||||
return True,
|
||||
|
||||
try:
|
||||
# Delete settings for domain ('@domain.com')
|
||||
web.conn_iredapd.delete(
|
||||
'throttle',
|
||||
vars={'domain': '@' + domain},
|
||||
where='account=$domain',
|
||||
)
|
||||
|
||||
# Delete settings for all users under this domain
|
||||
web.conn_iredapd.delete(
|
||||
'throttle',
|
||||
vars={'domain': '%@' + domain},
|
||||
where="""account LIKE $domain""")
|
||||
|
||||
web.conn_iredapd.delete(
|
||||
'throttle_tracking',
|
||||
vars={'domain': '%@' + domain},
|
||||
where="""account LIKE $domain""",
|
||||
)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_throttle_id(account, inout_type):
|
||||
tid = None
|
||||
|
||||
# get `throttle.id`
|
||||
qr = web.conn_iredapd.select(
|
||||
'throttle',
|
||||
vars={'account': account, 'inout_type': inout_type},
|
||||
where='account=$account AND kind=$inout_type',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if qr:
|
||||
tid = qr[0].id
|
||||
|
||||
return tid
|
||||
|
||||
|
||||
def add_throttle(account,
|
||||
setting,
|
||||
inout_type='inbound'):
|
||||
if not setting:
|
||||
# Delete tracking and setting
|
||||
delete_throttle_tracking(account=account, inout_type=inout_type)
|
||||
delete_throttle_setting(account=account, inout_type=inout_type)
|
||||
return True,
|
||||
|
||||
# Delete record if
|
||||
# - no period. (period == 0) means disabled
|
||||
# - account mismatch
|
||||
# - account is '@.' (global setting) and no valid setting (all are 0)
|
||||
# - account is not '@.' (not global setting) and no valid setting (all are -1)
|
||||
if (not setting.get('period', 0)) \
|
||||
or (account != setting.get('account')) \
|
||||
or (account == '@.'
|
||||
and (not setting.get('max_msgs'))
|
||||
and (not setting.get('msg_size'))
|
||||
and (not setting.get('max_quota'))
|
||||
and (not setting.get("max_rcpts"))) \
|
||||
or (account != '@.'
|
||||
and setting.get("max_msgs") == -1
|
||||
and setting.get("msg_size") == -1
|
||||
and setting.get("max_quota") == -1
|
||||
and setting.get("max_rcpts") in (None, -1)):
|
||||
delete_throttle_tracking(account=account, inout_type=inout_type)
|
||||
delete_throttle_setting(account=account, inout_type=inout_type)
|
||||
else:
|
||||
try:
|
||||
# Get `throttle.id` if there's a setting.
|
||||
tid = get_throttle_id(account=account, inout_type=inout_type)
|
||||
|
||||
if tid:
|
||||
# Update existing setting
|
||||
web.conn_iredapd.update(
|
||||
'throttle',
|
||||
vars={'tid': tid},
|
||||
where='id=$tid',
|
||||
**setting)
|
||||
else:
|
||||
# Add new throttle setting.
|
||||
web.conn_iredapd.insert('throttle', **setting)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
28
libs/iredapd/utils.py
Normal file
28
libs/iredapd/utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
from libs import iredutils
|
||||
from libs.iredapd import throttle as iredapd_throttle
|
||||
from libs.iredapd import greylist as iredapd_greylist
|
||||
|
||||
|
||||
def delete_settings_for_removed_users(mails):
|
||||
try:
|
||||
iredapd_greylist.delete_settings_for_removed_users(mails=mails)
|
||||
iredapd_throttle.delete_settings_for_removed_users(mails=mails)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_settings_for_removed_domains(domains):
|
||||
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
|
||||
|
||||
if not domains:
|
||||
return True,
|
||||
|
||||
for d in domains:
|
||||
iredapd_throttle.delete_settings_for_removed_domain(domain=d)
|
||||
iredapd_greylist.delete_settings_for_removed_domain(domain=d)
|
||||
|
||||
return True,
|
||||
66
libs/iredapd/wblist_rdns.py
Normal file
66
libs/iredapd/wblist_rdns.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
import web
|
||||
|
||||
|
||||
def get_wblist_rdns():
|
||||
"""Get wblist of rDNS."""
|
||||
whitelists = []
|
||||
blacklists = []
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
'wblist_rdns',
|
||||
what='rdns,wb',
|
||||
order='rdns',
|
||||
)
|
||||
|
||||
for i in qr:
|
||||
_rdns = str(i.rdns).lower()
|
||||
if i.wb == 'W':
|
||||
whitelists.append(_rdns)
|
||||
elif i.wb == 'B':
|
||||
blacklists.append(_rdns)
|
||||
|
||||
return True, {'whitelists': whitelists, 'blacklists': blacklists}
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def reset_wblist_rdns(whitelists=None, blacklists=None):
|
||||
"""Reset wblist rdns.
|
||||
|
||||
@whitelists -- a list/tuple/set of whitelist rdns domain names. Notes:
|
||||
- if it's None, no reset.
|
||||
- if it's empty list/tuple/set, all existing records will be
|
||||
removed.
|
||||
@blacklists -- a list/tuple/set of blacklist rdns domain names.
|
||||
@conn -- sql connection cursor
|
||||
"""
|
||||
if whitelists and blacklists:
|
||||
# Remove duplicate records
|
||||
blacklists = [i for i in blacklists if i not in whitelists]
|
||||
|
||||
# Delete first to avoid possible duplicate records while inserting new
|
||||
# records later.
|
||||
for (_lists, _wb) in [(whitelists, 'W'), (blacklists, 'B')]:
|
||||
if _lists is not None:
|
||||
try:
|
||||
# Delete all existing records first
|
||||
web.conn_iredapd.delete(
|
||||
'wblist_rdns',
|
||||
vars={'wb': _wb},
|
||||
where='WB=$wb',
|
||||
)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Insert new records
|
||||
for (_lists, _wb) in [(whitelists, 'W'), (blacklists, 'B')]:
|
||||
if _lists:
|
||||
for i in _lists:
|
||||
try:
|
||||
web.conn_iredapd.insert('wblist_rdns', rdns=i, wb=_wb)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True, 'UPDATED'
|
||||
83
libs/iredapd/wblist_senderscore.py
Normal file
83
libs/iredapd/wblist_senderscore.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import web
|
||||
|
||||
from libs import iredutils
|
||||
|
||||
# `4102444799` seconds since 1970-01-01 is '2099-12-31 23:59:59'.
|
||||
# It's a trick to use this time as whitelist and not cleaned by
|
||||
# script `tools/cleanup_db.py`.
|
||||
# It's ok to use any long epoch seconds to avoid cleanup, but we use this
|
||||
# hard-coded value for easier management.
|
||||
expire_epoch_seconds = 4102444799
|
||||
|
||||
|
||||
def get_whitelists():
|
||||
total = 0
|
||||
ips = []
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
"senderscore_cache",
|
||||
vars={'seconds': expire_epoch_seconds},
|
||||
what='COUNT(client_address) AS total',
|
||||
where="time=$seconds",
|
||||
)
|
||||
total = qr[0].total
|
||||
|
||||
if total:
|
||||
qr = web.conn_iredapd.select(
|
||||
"senderscore_cache",
|
||||
vars={'seconds': expire_epoch_seconds},
|
||||
what='client_address',
|
||||
where="time=$seconds",
|
||||
)
|
||||
|
||||
ips = [i.client_address for i in qr]
|
||||
|
||||
return True, {'total': total, 'ips': ips}
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def filter_whitelisted_ips(ips):
|
||||
# Return a list of whitelisted IP addresses of given ones.
|
||||
ips = [i for i in ips if iredutils.is_strict_ip(i)]
|
||||
|
||||
try:
|
||||
qr = web.conn_iredapd.select(
|
||||
"senderscore_cache",
|
||||
vars={'ips': ips, 'seconds': expire_epoch_seconds},
|
||||
what='client_address',
|
||||
where="client_address IN $ips AND time=$seconds",
|
||||
)
|
||||
|
||||
ips = [i.client_address for i in qr]
|
||||
return True, ips
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def whitelist_ips(ips):
|
||||
# Whitelist given IP addresses.
|
||||
ips = [i for i in ips if iredutils.is_strict_ip(i)]
|
||||
|
||||
if not ips:
|
||||
return True,
|
||||
|
||||
# Remove existing records first.
|
||||
try:
|
||||
web.conn_iredapd.delete("senderscore_cache",
|
||||
vars={'ips': ips},
|
||||
where="client_address IN $ips")
|
||||
|
||||
rows = []
|
||||
for ip in ips:
|
||||
rows += [{'client_address': ip,
|
||||
'score': 100,
|
||||
'time': expire_epoch_seconds}]
|
||||
|
||||
# Insert whitelists.
|
||||
web.conn_iredapd.multiple_insert("senderscore_cache", rows)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
206
libs/iredbase.py
Normal file
206
libs/iredbase.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import os
|
||||
|
||||
import web
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
# Directory to be used as the Python egg cache directory.
|
||||
# Note that the directory specified must exist and be writable by the
|
||||
# user that the daemon process run as.
|
||||
os.environ["PYTHON_EGG_CACHE"] = "/tmp/.iredadmin-eggs"
|
||||
os.environ["LC_ALL"] = "C"
|
||||
|
||||
# Absolute path to this file.
|
||||
rootdir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
import settings
|
||||
from . import iredutils
|
||||
from . import iredpwd
|
||||
from . import jinja_filters
|
||||
from . import ireddate
|
||||
from . import hooks
|
||||
|
||||
# Set debug mode.
|
||||
web.config.debug = settings.DEBUG
|
||||
|
||||
# Set session parameters.
|
||||
web.config.session_parameters["cookie_name"] = "iRedAdmin-Pro-%s" % settings.backend.upper()
|
||||
web.config.session_parameters["cookie_domain"] = None
|
||||
web.config.session_parameters["ignore_expiry"] = True
|
||||
web.config.session_parameters["ignore_change_ip"] = settings.SESSION_IGNORE_CHANGE_IP
|
||||
web.config.session_parameters["timeout"] = settings.SESSION_TIMEOUT
|
||||
web.config.session_parameters["httponly"] = True
|
||||
web.config.session_parameters["samesite"] = "Strict"
|
||||
# web.config.session_parameters['secure'] = True
|
||||
|
||||
# Initialize session object.
|
||||
__sql_dbn = "mysql"
|
||||
if settings.backend == "pgsql":
|
||||
__sql_dbn = "postgres"
|
||||
|
||||
conn_iredadmin = iredutils.get_db_conn(db_name="iredadmin", sql_dbn=__sql_dbn)
|
||||
web.conn_iredadmin = conn_iredadmin
|
||||
|
||||
# URL handlers.
|
||||
# Import backend related urls.
|
||||
urls_backend = []
|
||||
if settings.backend == "ldap":
|
||||
from controllers.ldap.urls import urls as urls_backend
|
||||
elif settings.backend in ["mysql", "pgsql"]:
|
||||
from controllers.sql.urls import urls as urls_backend
|
||||
|
||||
urls = urls_backend
|
||||
|
||||
# Amavisd.
|
||||
if (
|
||||
settings.amavisd_enable_quarantine
|
||||
or settings.amavisd_enable_logging
|
||||
or settings.amavisd_enable_policy_lookup
|
||||
):
|
||||
from controllers.amavisd.urls import urls as urls_amavisd
|
||||
urls += urls_amavisd
|
||||
|
||||
web.conn_amavisd = iredutils.get_db_conn(db_name="amavisd", sql_dbn=__sql_dbn)
|
||||
else:
|
||||
web.conn_amavisd = None
|
||||
|
||||
# iRedAPD
|
||||
from controllers.iredapd.urls import urls as urls_iredapd
|
||||
urls += urls_iredapd
|
||||
if settings.iredapd_enabled:
|
||||
web.conn_iredapd = iredutils.get_db_conn(db_name="iredapd", sql_dbn=__sql_dbn)
|
||||
else:
|
||||
web.conn_iredapd = None
|
||||
|
||||
# iRedAdmin.
|
||||
from controllers.panel.urls import urls as urls_panel
|
||||
urls += urls_panel
|
||||
|
||||
# mlmmj.
|
||||
from controllers.mlmmj.urls import urls as urls_mlmmj
|
||||
urls += urls_mlmmj
|
||||
|
||||
# Fail2ban.
|
||||
if settings.fail2ban_enabled:
|
||||
from controllers.f2b.urls import urls as urls_f2b
|
||||
urls += urls_f2b
|
||||
web.conn_f2b = iredutils.get_db_conn(db_name="fail2ban", sql_dbn=__sql_dbn)
|
||||
else:
|
||||
web.conn_f2b = None
|
||||
|
||||
# Initialize application object.
|
||||
app = web.application(urls)
|
||||
|
||||
session_initializer = {
|
||||
"webmaster": settings.webmaster,
|
||||
"username": None,
|
||||
"logged": False,
|
||||
# Admin
|
||||
"is_global_admin": False,
|
||||
"is_normal_admin": False,
|
||||
# normal mail user
|
||||
"account_is_mail_user": False,
|
||||
"failed_times": 0, # Integer.
|
||||
"lang": settings.default_language,
|
||||
# Show used quota.
|
||||
"show_used_quota": settings.SHOW_USED_QUOTA,
|
||||
# Amavisd related features.
|
||||
"amavisd_enable_quarantine": settings.amavisd_enable_quarantine,
|
||||
"amavisd_enable_logging": settings.amavisd_enable_logging,
|
||||
"amavisd_enable_policy_lookup": settings.amavisd_enable_policy_lookup,
|
||||
# iRedAPD related features.
|
||||
"iredapd_enabled": settings.iredapd_enabled,
|
||||
"fail2ban_enabled": settings.fail2ban_enabled,
|
||||
}
|
||||
|
||||
session = web.session.Session(
|
||||
app=app,
|
||||
store=web.session.DBStore(conn_iredadmin, "sessions"),
|
||||
initializer=session_initializer,
|
||||
)
|
||||
|
||||
web.config._session = session
|
||||
|
||||
|
||||
# Generate CSRF token and store it in session.
|
||||
def csrf_token():
|
||||
if "csrf_token" not in session:
|
||||
session["csrf_token"] = iredutils.generate_random_strings(32)
|
||||
|
||||
return session["csrf_token"]
|
||||
|
||||
|
||||
jinja_env_vars = {
|
||||
# Set global variables for Jinja2 template
|
||||
"_": iredutils.ired_gettext, # Override _() which provided by Jinja2.
|
||||
"ctx": web.ctx, # Used to get 'homepath'.
|
||||
"skin": settings.SKIN,
|
||||
"session": web.config._session,
|
||||
"backend": settings.backend,
|
||||
"csrf_token": csrf_token,
|
||||
"page_size_limit": settings.PAGE_SIZE_LIMIT,
|
||||
"url_support": settings.URL_SUPPORT,
|
||||
# newsletter (mlmmj mailing list)
|
||||
"newsletter_base_url": settings.NEWSLETTER_BASE_URL,
|
||||
# Brand logo, name, description
|
||||
"brand_logo": settings.BRAND_LOGO,
|
||||
"brand_name": settings.BRAND_NAME,
|
||||
"brand_desc": settings.BRAND_DESC,
|
||||
"brand_favicon": settings.BRAND_FAVICON,
|
||||
}
|
||||
|
||||
jinja_env_filters = {
|
||||
"file_size_format": jinja_filters.file_size_format,
|
||||
"cut_string": jinja_filters.cut_string,
|
||||
"convert_to_percentage": jinja_filters.convert_to_percentage,
|
||||
"epoch_seconds_to_gmt": iredutils.epoch_seconds_to_gmt,
|
||||
"epoch_days_to_date": iredutils.epoch_days_to_date,
|
||||
"set_datetime_format": iredutils.set_datetime_format,
|
||||
"generate_random_password": iredpwd.generate_random_password,
|
||||
"utc_to_timezone": ireddate.utc_to_timezone,
|
||||
}
|
||||
|
||||
_default_template_dir = rootdir + "/../templates/" + settings.SKIN
|
||||
|
||||
|
||||
# Define template renders.
|
||||
def render_template(template_name, **kwargs):
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader(_default_template_dir),
|
||||
extensions=["jinja2.ext.do"],
|
||||
)
|
||||
|
||||
jinja_env.globals.update(jinja_env_vars)
|
||||
jinja_env.filters.update(jinja_env_filters)
|
||||
|
||||
web.header("Content-Type", "text/html")
|
||||
return jinja_env.get_template(template_name).render(kwargs)
|
||||
|
||||
|
||||
class SessionExpired(web.HTTPError):
|
||||
def __init__(self, message):
|
||||
try:
|
||||
# Expire the cookie. Fixed in webpy master branch on Sep 21, 2020.
|
||||
cookie_name = web.config.session_parameters['cookie_name']
|
||||
web.setcookie(cookie_name, session.session_id, expires=-1)
|
||||
session.kill()
|
||||
except:
|
||||
pass
|
||||
|
||||
message = web.seeother("/login?msg=SESSION_EXPIRED")
|
||||
web.HTTPError.__init__(self, "303 See Other", {}, data=message)
|
||||
|
||||
|
||||
# Load hooks
|
||||
app.add_processor(web.loadhook(hooks.hook_set_language))
|
||||
|
||||
if settings.DEBUG:
|
||||
app.internalerror = web.debugerror
|
||||
elif settings.MAIL_ERROR_TO_WEBMASTER:
|
||||
# Mail 500 error to webmaster.
|
||||
app.internalerror = web.emailerrors(settings.webmaster, web.webapi._InternalError)
|
||||
|
||||
# Store objects in 'web' module.
|
||||
web.render = render_template
|
||||
web.session.SessionExpired = SessionExpired
|
||||
200
libs/ireddate.py
Normal file
200
libs/ireddate.py
Normal file
@@ -0,0 +1,200 @@
|
||||
import time
|
||||
import re
|
||||
from datetime import tzinfo, timedelta, datetime
|
||||
|
||||
from libs.l10n import TIMEZONE_OFFSETS
|
||||
import settings
|
||||
|
||||
__timezone__ = None
|
||||
__local_timezone__ = None
|
||||
__timezones__ = {}
|
||||
|
||||
DEFAULT_DATETIME_INPUT_FORMATS = (
|
||||
"%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
|
||||
"%Y-%m-%d %H:%M", # '2006-10-25 14:30'
|
||||
"%Y-%m-%d", # '2006-10-25'
|
||||
"%Y/%m/%d %H:%M:%S", # '2006/10/25 14:30:59'
|
||||
"%Y/%m/%d %H:%M", # '2006/10/25 14:30'
|
||||
"%Y/%m/%d ", # '2006/10/25 '
|
||||
"%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
|
||||
"%m/%d/%Y %H:%M", # '10/25/2006 14:30'
|
||||
"%m/%d/%Y", # '10/25/2006'
|
||||
"%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
|
||||
"%m/%d/%y %H:%M", # '10/25/06 14:30'
|
||||
"%m/%d/%y", # '10/25/06'
|
||||
"%H:%M:%S", # '14:30:59'
|
||||
"%H:%M", # '14:30'
|
||||
)
|
||||
|
||||
ZERO = timedelta(0)
|
||||
|
||||
|
||||
class UTCTimeZone(tzinfo):
|
||||
"""UTC"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
def __repr__(self):
|
||||
return "<tzinfo UTC>"
|
||||
|
||||
|
||||
UTC = UTCTimeZone()
|
||||
|
||||
|
||||
class FixedOffset(tzinfo):
|
||||
"""Fixed offset in minutes east from UTC."""
|
||||
|
||||
def __init__(self, offset, name):
|
||||
self.__offset = timedelta(minutes=offset)
|
||||
self.__name = name
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.__offset
|
||||
|
||||
def tzname(self, dt):
|
||||
return self.__name
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
|
||||
for (tzname, offset) in list(TIMEZONE_OFFSETS.items()):
|
||||
__timezones__[tzname] = FixedOffset(offset, tzname)
|
||||
|
||||
re_timezone = re.compile(r"GMT\s?([+-]?)(\d+):(\d\d)", re.IGNORECASE)
|
||||
|
||||
|
||||
def fix_gmt_timezone(tz):
|
||||
if isinstance(tz, str):
|
||||
b = re_timezone.match(tz)
|
||||
if b:
|
||||
sign = b.group(1)
|
||||
if not sign:
|
||||
sign = "+"
|
||||
|
||||
hour = b.group(2)
|
||||
if hour in ["0", "00"]:
|
||||
return "UTC"
|
||||
|
||||
minute = b.group(3)
|
||||
return "GMT" + sign + hour + ":" + minute
|
||||
return tz
|
||||
|
||||
|
||||
def set_local_timezone(tz):
|
||||
global __local_timezone__
|
||||
__local_timezone__ = timezone(tz)
|
||||
|
||||
|
||||
def get_local_timezone():
|
||||
return __local_timezone__
|
||||
|
||||
|
||||
def timezone(tzname):
|
||||
# Validate tzname and return it
|
||||
if not tzname:
|
||||
return None
|
||||
|
||||
if isinstance(tzname, str):
|
||||
# not pytz module imported, so just return None
|
||||
tzname = fix_gmt_timezone(tzname)
|
||||
tz = __timezones__.get(tzname, None)
|
||||
if not tz:
|
||||
tz = UTC
|
||||
return tz
|
||||
elif isinstance(tzname, tzinfo):
|
||||
return tzname
|
||||
else:
|
||||
return UTC
|
||||
|
||||
|
||||
def pick_timezone(*args):
|
||||
for x in args:
|
||||
tz = timezone(x)
|
||||
if tz:
|
||||
return tz
|
||||
|
||||
|
||||
def to_timezone(dt, tzinfo=None):
|
||||
"""
|
||||
Convert a datetime to timezone
|
||||
"""
|
||||
if not dt:
|
||||
return dt
|
||||
tz = pick_timezone(tzinfo, __timezone__)
|
||||
if not tz:
|
||||
return dt
|
||||
dttz = getattr(dt, "tzinfo", None)
|
||||
if not dttz:
|
||||
return dt.replace(tzinfo=tz)
|
||||
else:
|
||||
return dt.astimezone(tz)
|
||||
|
||||
|
||||
def to_datetime_with_tzinfo(dt, tzinfo=None, formatstr=None):
|
||||
"""
|
||||
Convert a date or time to datetime with tzinfo
|
||||
"""
|
||||
if not dt:
|
||||
return dt
|
||||
|
||||
tz = pick_timezone(tzinfo, __timezone__)
|
||||
|
||||
if isinstance(dt, str):
|
||||
if not formatstr:
|
||||
formats = DEFAULT_DATETIME_INPUT_FORMATS
|
||||
else:
|
||||
formats = list(formatstr)
|
||||
d = None
|
||||
for fmt in formats:
|
||||
try:
|
||||
d = datetime(*time.strptime(dt, fmt)[:6])
|
||||
except ValueError:
|
||||
continue
|
||||
if not d:
|
||||
return None
|
||||
d = d.replace(tzinfo=tz)
|
||||
else:
|
||||
d = datetime(
|
||||
getattr(dt, "year", 1970),
|
||||
getattr(dt, "month", 1),
|
||||
getattr(dt, "day", 1),
|
||||
getattr(dt, "hour", 0),
|
||||
getattr(dt, "minute", 0),
|
||||
getattr(dt, "second", 0),
|
||||
getattr(dt, "microsecond", 0),
|
||||
)
|
||||
|
||||
if not getattr(dt, "tzinfo", None):
|
||||
d = d.replace(tzinfo=tz)
|
||||
else:
|
||||
d = d.replace(tzinfo=dt.tzinfo)
|
||||
return to_timezone(d, tzinfo)
|
||||
|
||||
|
||||
def utc_to_timezone(dt, timezone=None, format="%Y-%m-%d %H:%M:%S"):
|
||||
if not timezone:
|
||||
timezone = settings.LOCAL_TIMEZONE
|
||||
|
||||
# Convert original timestamp to new timestamp with UTC timezone.
|
||||
t = to_datetime_with_tzinfo(dt, tzinfo=UTC)
|
||||
|
||||
# Convert original timestamp (with UTC timezone) to timestamp with
|
||||
# local timezone.
|
||||
ft = to_datetime_with_tzinfo(t, tzinfo=timezone)
|
||||
|
||||
if ft:
|
||||
# Check 'daylight saving time'
|
||||
if time.localtime().tm_isdst:
|
||||
ft += timedelta(seconds=3600)
|
||||
|
||||
return ft.strftime(format)
|
||||
else:
|
||||
return "--"
|
||||
567
libs/iredpwd.py
Normal file
567
libs/iredpwd.py
Normal file
@@ -0,0 +1,567 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import crypt
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
from base64 import b64encode, b64decode
|
||||
from hmac import compare_digest
|
||||
from os import urandom
|
||||
from typing import Union, List
|
||||
|
||||
import settings
|
||||
|
||||
from libs import iredutils
|
||||
|
||||
|
||||
def __has_non_ascii_character(s):
|
||||
"""
|
||||
Detect whether a string contains non-ascii character or not.
|
||||
integer ordinal of a one-character string between 32 and 126 are digits,
|
||||
letters, punctuation.
|
||||
Reference: http://docs.python.org/2/library/string.html#string.printable
|
||||
"""
|
||||
for i in s:
|
||||
try:
|
||||
if not (32 <= ord(i) <= 126):
|
||||
return True
|
||||
except TypeError:
|
||||
# ord() will raise TypeError for non-ascii character
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def verify_new_password(
|
||||
newpw, confirmpw, min_passwd_length=None, max_passwd_length=None, db_settings=None
|
||||
):
|
||||
# Confirm password
|
||||
if newpw == confirmpw:
|
||||
passwd = newpw
|
||||
else:
|
||||
return False, "PW_MISMATCH"
|
||||
|
||||
# Empty password is not allowed.
|
||||
if not passwd:
|
||||
return False, "PW_EMPTY"
|
||||
|
||||
errors = []
|
||||
# Non-ascii character is not allowed
|
||||
if __has_non_ascii_character(passwd):
|
||||
errors.append("PW_NON_ASCII")
|
||||
|
||||
# Get settings from db
|
||||
if not db_settings:
|
||||
params = [
|
||||
"min_passwd_length",
|
||||
"max_passwd_length",
|
||||
"password_has_letter",
|
||||
"password_has_uppercase",
|
||||
"password_has_number",
|
||||
"password_has_special_char",
|
||||
]
|
||||
|
||||
db_settings = iredutils.get_settings_from_db(params=params)
|
||||
|
||||
# Get and verify password length
|
||||
if not min_passwd_length or not isinstance(min_passwd_length, int):
|
||||
min_passwd_length = db_settings["min_passwd_length"]
|
||||
|
||||
if not max_passwd_length or not isinstance(max_passwd_length, int):
|
||||
max_passwd_length = db_settings["max_passwd_length"]
|
||||
|
||||
if len(passwd) < min_passwd_length:
|
||||
errors.append("PW_SHORTER_THAN_MIN_LENGTH")
|
||||
|
||||
if max_passwd_length > 0:
|
||||
if len(passwd) > max_passwd_length:
|
||||
errors.append("PW_GREATER_THAN_MAX_LENGTH")
|
||||
|
||||
# Password restriction rules
|
||||
if db_settings["password_has_letter"]:
|
||||
if not (set(passwd) & set(string.ascii_letters)):
|
||||
errors.append("PW_NO_LETTER")
|
||||
|
||||
if db_settings["password_has_uppercase"]:
|
||||
if not (set(passwd) & set(string.ascii_uppercase)):
|
||||
errors.append("PW_NO_UPPERCASE")
|
||||
|
||||
if db_settings["password_has_number"]:
|
||||
if not (set(passwd) & set(string.digits)):
|
||||
errors.append("PW_NO_DIGIT_NUMBER")
|
||||
|
||||
if db_settings["password_has_special_char"]:
|
||||
if not (set(passwd) & set(settings.PASSWORD_SPECIAL_CHARACTERS)):
|
||||
errors.append("PW_NO_SPECIAL_CHAR")
|
||||
|
||||
if errors:
|
||||
return False, ",".join(errors)
|
||||
else:
|
||||
return True, passwd
|
||||
|
||||
|
||||
def generate_random_password(length=10, db_settings=None):
|
||||
try:
|
||||
length = int(length)
|
||||
|
||||
if length <= 0:
|
||||
length = 10
|
||||
elif length <= 10:
|
||||
# We should always suggest a strong password
|
||||
length = 10
|
||||
except:
|
||||
length = 10
|
||||
|
||||
if not db_settings:
|
||||
params = [
|
||||
"min_passwd_length",
|
||||
"max_passwd_length",
|
||||
"password_has_letter",
|
||||
"password_has_uppercase",
|
||||
"password_has_number",
|
||||
"password_has_special_char",
|
||||
]
|
||||
|
||||
db_settings = iredutils.get_settings_from_db(account="global", params=params)
|
||||
|
||||
if length < db_settings["min_passwd_length"]:
|
||||
length = db_settings["min_passwd_length"]
|
||||
|
||||
numbers = "23456789" # No 0, 1
|
||||
letters = "abcdefghjkmnpqrstuvwxyz" # no i, l
|
||||
uppercases = "ABCDEFGHJKLMNPQRSTUVWXYZ" # no I
|
||||
|
||||
opts = []
|
||||
if db_settings["password_has_letter"]:
|
||||
opts += random.choice(letters)
|
||||
length -= 1
|
||||
|
||||
if db_settings["password_has_uppercase"]:
|
||||
opts += random.choice(uppercases)
|
||||
length -= 1
|
||||
|
||||
if db_settings["password_has_number"]:
|
||||
opts += random.choice(numbers)
|
||||
length -= 1
|
||||
|
||||
if (
|
||||
db_settings["password_has_special_char"]
|
||||
and settings.PASSWORD_SPECIAL_CHARACTERS
|
||||
):
|
||||
# Don't use few characters which may mess HTML render.
|
||||
while True:
|
||||
char = random.choice(settings.PASSWORD_SPECIAL_CHARACTERS)
|
||||
if char in ["'", '"', ">", "<"]:
|
||||
continue
|
||||
else:
|
||||
opts += char
|
||||
length -= 1
|
||||
break
|
||||
|
||||
opts += list(iredutils.generate_random_strings(length))
|
||||
|
||||
password = ""
|
||||
for _ in range(len(opts)):
|
||||
one = random.choice(opts)
|
||||
password += one
|
||||
opts.remove(one)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def generate_bcrypt_password(p) -> str:
|
||||
if isinstance(p, str):
|
||||
p = p.encode()
|
||||
|
||||
try:
|
||||
import bcrypt
|
||||
except:
|
||||
return generate_ssha_password(p)
|
||||
|
||||
return "{CRYPT}" + bcrypt.hashpw(p, bcrypt.gensalt()).decode()
|
||||
|
||||
|
||||
def verify_bcrypt_password(challenge_password: str, plain_password: str) -> bool:
|
||||
try:
|
||||
import bcrypt
|
||||
except:
|
||||
return False
|
||||
|
||||
crypt_suffixes = ("{CRYPT}$2a$", "{CRYPT}$2b$",
|
||||
"{crypt}$2a$", "{crypt}$2b$")
|
||||
blf_crypt_suffixes = ("{BLF-CRYPT}", "{blf-crypt}")
|
||||
|
||||
if challenge_password.startswith(crypt_suffixes):
|
||||
challenge_password = challenge_password[7:]
|
||||
elif challenge_password.startswith(blf_crypt_suffixes):
|
||||
challenge_password = challenge_password[11:]
|
||||
|
||||
return bcrypt.checkpw(plain_password.encode(), challenge_password.encode())
|
||||
|
||||
|
||||
def generate_md5_password(p: str) -> str:
|
||||
return crypt.crypt(p, salt=crypt.METHOD_MD5)
|
||||
|
||||
|
||||
def verify_md5_password(challenge_password: Union[str, bytes],
|
||||
plain_password: str) -> bool:
|
||||
"""Verify salted MD5 password"""
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
if challenge_password.startswith(("{MD5}", "{md5}")):
|
||||
challenge_password = challenge_password[5:]
|
||||
elif challenge_password.startswith(("{CRYPT}", "{crypt}")):
|
||||
challenge_password = challenge_password[7:]
|
||||
|
||||
if not (challenge_password.startswith("$")
|
||||
and len(challenge_password) == 34
|
||||
and challenge_password.count("$") == 3):
|
||||
return False
|
||||
|
||||
return compare_digest(challenge_password,
|
||||
crypt.crypt(plain_password, challenge_password))
|
||||
|
||||
|
||||
def generate_plain_md5_password(p: Union[str, bytes]) -> str:
|
||||
if isinstance(p, str):
|
||||
p = p.encode()
|
||||
|
||||
p = p.strip()
|
||||
return hashlib.md5(p).hexdigest()
|
||||
|
||||
|
||||
def verify_plain_md5_password(challenge_password, plain_password):
|
||||
if challenge_password.startswith(("{PLAIN-MD5}", "{plain-md5}")):
|
||||
challenge_password = challenge_password[11:]
|
||||
|
||||
if challenge_password == generate_plain_md5_password(plain_password):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def generate_ssha_password(p: Union[str, bytes]) -> str:
|
||||
if isinstance(p, str):
|
||||
p = p.encode()
|
||||
|
||||
p = p.strip()
|
||||
salt = urandom(8)
|
||||
pw = hashlib.sha1(p)
|
||||
pw.update(salt)
|
||||
|
||||
return "{SSHA}" + b64encode(pw.digest() + salt).decode()
|
||||
|
||||
|
||||
def verify_ssha_password(challenge_password: Union[str, bytes],
|
||||
plain_password: Union[str, bytes]) -> bool:
|
||||
"""Verify SHA or SSHA (salted SHA) hash with or without prefix {SHA}, {SSHA}"""
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
if isinstance(plain_password, str):
|
||||
plain_password = plain_password.encode()
|
||||
|
||||
if challenge_password.startswith(("{SSHA}", "{ssha}")):
|
||||
challenge_password = challenge_password[6:]
|
||||
elif challenge_password.startswith(("{SHA}", "{sha}")):
|
||||
challenge_password = challenge_password[5:]
|
||||
|
||||
if len(challenge_password) < 20:
|
||||
# Not a valid SSHA hash
|
||||
return False
|
||||
|
||||
try:
|
||||
challenge_bytes = b64decode(challenge_password)
|
||||
digest = challenge_bytes[:20]
|
||||
salt = challenge_bytes[20:]
|
||||
hr = hashlib.sha1(plain_password)
|
||||
hr.update(salt)
|
||||
return digest == hr.digest()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def generate_sha512_password(p: Union[str, bytes]) -> str:
|
||||
"""Generate SHA512 password with prefix '{SHA512}'."""
|
||||
if isinstance(p, str):
|
||||
p = p.encode()
|
||||
|
||||
p = p.strip()
|
||||
pw = hashlib.sha512(p)
|
||||
return "{SHA512}" + b64encode(pw.digest()).decode()
|
||||
|
||||
|
||||
def verify_sha512_password(challenge_password: Union[str, bytes],
|
||||
plain_password: Union[str, bytes]) -> bool:
|
||||
"""Verify SHA512 password with or without prefix '{SHA512}'."""
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
if isinstance(plain_password, str):
|
||||
plain_password = plain_password.encode()
|
||||
|
||||
if challenge_password.startswith(("{SHA512}", "{sha512}")):
|
||||
challenge_password = challenge_password[8:]
|
||||
|
||||
if len(challenge_password) != 88:
|
||||
return False
|
||||
|
||||
try:
|
||||
challenge_bytes = b64decode(challenge_password)
|
||||
digest = challenge_bytes[:64]
|
||||
hr = hashlib.sha512(plain_password)
|
||||
return digest == hr.digest() # bytes == bytes
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def verify_sha512_crypt_password(challenge_password: Union[str, bytes],
|
||||
plain_password: str) -> bool:
|
||||
"""Verify SHA512 password with prefix '{SHA512-CRYPT}'."""
|
||||
if not challenge_password.startswith(("{SHA512-CRYPT}", "{sha512-crypt}")):
|
||||
return False
|
||||
|
||||
challenge_password = challenge_password[14:]
|
||||
return compare_digest(challenge_password,
|
||||
crypt.crypt(plain_password, challenge_password))
|
||||
|
||||
|
||||
def generate_ssha512_password(p: Union[str, bytes]) -> str:
|
||||
"""Generate salted SHA512 password with prefix '{SSHA512}'."""
|
||||
if isinstance(p, str):
|
||||
p = p.encode()
|
||||
|
||||
p = p.strip()
|
||||
salt = urandom(8)
|
||||
pw = hashlib.sha512(p)
|
||||
pw.update(salt)
|
||||
return "{SSHA512}" + b64encode(pw.digest() + salt).decode()
|
||||
|
||||
|
||||
def verify_ssha512_password(challenge_password: Union[str, bytes],
|
||||
plain_password: Union[str, bytes]) -> bool:
|
||||
"""Verify SSHA512 password with or without prefix '{SSHA512}'."""
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
if isinstance(plain_password, str):
|
||||
plain_password = plain_password.encode()
|
||||
|
||||
if challenge_password.startswith(("{SSHA512}", "{ssha512}")):
|
||||
challenge_password = challenge_password[9:]
|
||||
|
||||
# With SSHA512, hash itself is 64 bytes (512 bits/8 bits per byte),
|
||||
# everything after that 64 bytes is the salt.
|
||||
if len(challenge_password) < 64:
|
||||
return False
|
||||
|
||||
try:
|
||||
challenge_bytes = b64decode(challenge_password)
|
||||
digest = challenge_bytes[:64]
|
||||
salt = challenge_bytes[64:]
|
||||
hr = hashlib.sha512(plain_password)
|
||||
hr.update(salt)
|
||||
|
||||
return digest == hr.digest()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def generate_password_with_doveadmpw(scheme: str, plain_password: str) -> str:
|
||||
"""Generate password hash with `doveadm pw` command.
|
||||
Return SSHA instead if no 'doveadm' command found or other error raised."""
|
||||
# scheme: CRAM-MD5, NTLM
|
||||
scheme = scheme.upper()
|
||||
|
||||
p = str(plain_password).strip()
|
||||
|
||||
try:
|
||||
pp = subprocess.Popen(
|
||||
args=["doveadm", "pw", "-s", scheme, "-p", p],
|
||||
stdout=subprocess.PIPE
|
||||
)
|
||||
pw = pp.communicate()[0].decode()
|
||||
|
||||
if scheme in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
|
||||
pw = pw.lstrip("{" + scheme + "}")
|
||||
|
||||
# remove '\n'
|
||||
pw = pw.strip()
|
||||
|
||||
return pw
|
||||
except:
|
||||
return generate_ssha_password(p)
|
||||
|
||||
|
||||
def verify_password_with_doveadmpw(challenge_password: Union[str, bytes],
|
||||
plain_password: str) -> bool:
|
||||
"""Verify password hash with `doveadm pw` command."""
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"doveadm",
|
||||
"pw",
|
||||
"-t",
|
||||
challenge_password.strip(),
|
||||
"-p",
|
||||
plain_password.strip(),
|
||||
]
|
||||
|
||||
_return_code = subprocess.call(cmd, stdin=None, stdout=None, stderr=None, shell=False)
|
||||
if _return_code == 0:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def generate_cram_md5_password(p):
|
||||
return generate_password_with_doveadmpw("CRAM-MD5", p)
|
||||
|
||||
|
||||
def verify_cram_md5_password(challenge_password, plain_password):
|
||||
"""Verify CRAM-MD5 hash with 'doveadm pw' command."""
|
||||
if not challenge_password.lower().strip().startswith("{cram-md5}"):
|
||||
return False
|
||||
|
||||
return verify_password_with_doveadmpw(challenge_password, plain_password)
|
||||
|
||||
|
||||
def generate_ntlm_password(p):
|
||||
return generate_password_with_doveadmpw("NTLM", p)
|
||||
|
||||
|
||||
def verify_ntlm_password(challenge_password, plain_password):
|
||||
"""Verify NTLM hash with 'doveadm pw' command."""
|
||||
if "NTLM" not in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
|
||||
if not challenge_password.startswith(("{NTLM}", "{ntlm}")):
|
||||
# Prefix '{NTLM}' so that doveadm can verify it.
|
||||
challenge_password = "{NTLM}" + challenge_password
|
||||
else:
|
||||
if not challenge_password.startswith(("{NTLM}", "{ntlm}")):
|
||||
return False
|
||||
|
||||
return verify_password_with_doveadmpw(challenge_password, plain_password)
|
||||
|
||||
|
||||
def generate_password_hash(p: Union[str, bytes],
|
||||
pwscheme: str = None) -> Union[str, List[str]]:
|
||||
"""Generate password for LDAP mail user and admin."""
|
||||
if isinstance(p, bytes):
|
||||
p = p.decode()
|
||||
|
||||
p = p.strip()
|
||||
pwscheme = pwscheme or settings.DEFAULT_PASSWORD_SCHEME
|
||||
|
||||
# Supports returning multiple passwords.
|
||||
pw_schemes = pwscheme.split("+")
|
||||
pws = []
|
||||
|
||||
for scheme in pw_schemes:
|
||||
if scheme == "BCRYPT":
|
||||
pw_hash = generate_bcrypt_password(p)
|
||||
elif scheme == "SSHA512":
|
||||
pw_hash = generate_ssha512_password(p)
|
||||
elif scheme == "SHA512":
|
||||
pw_hash = generate_sha512_password(p)
|
||||
elif scheme == "SSHA":
|
||||
pw_hash = generate_ssha_password(p)
|
||||
elif scheme == "MD5":
|
||||
pw_hash = "{CRYPT}" + generate_md5_password(p)
|
||||
elif scheme == "CRAM-MD5":
|
||||
pw_hash = generate_cram_md5_password(p)
|
||||
elif scheme == "PLAIN-MD5":
|
||||
pw_hash = generate_plain_md5_password(p)
|
||||
elif scheme == "NTLM":
|
||||
pw_hash = generate_ntlm_password(p)
|
||||
elif scheme == "PLAIN":
|
||||
if "PLAIN" in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
|
||||
pw_hash = p
|
||||
else:
|
||||
pw_hash = "{PLAIN}" + p
|
||||
else:
|
||||
pw_hash = p
|
||||
|
||||
pws.append(pw_hash)
|
||||
|
||||
if len(pws) == 1:
|
||||
return pws[0]
|
||||
else:
|
||||
return pws
|
||||
|
||||
|
||||
def verify_password_hash(challenge_password: Union[str, bytes],
|
||||
plain_password: Union[str, bytes]) -> bool:
|
||||
if isinstance(challenge_password, bytes):
|
||||
challenge_password = challenge_password.decode()
|
||||
|
||||
if isinstance(plain_password, bytes):
|
||||
plain_password = plain_password.decode()
|
||||
|
||||
# Check plain password and MD5 first.
|
||||
if challenge_password in [
|
||||
plain_password,
|
||||
"{PLAIN}" + plain_password,
|
||||
"{plain}" + plain_password,
|
||||
]:
|
||||
return True
|
||||
elif verify_md5_password(challenge_password, plain_password):
|
||||
return True
|
||||
|
||||
upwd = challenge_password.upper()
|
||||
if upwd.startswith(("{SSHA}", "{SHA}")):
|
||||
return verify_ssha_password(challenge_password, plain_password)
|
||||
elif upwd.startswith("{SSHA512}"):
|
||||
return verify_ssha512_password(challenge_password, plain_password)
|
||||
elif upwd.startswith(("{CRYPT}$2A$", "{CRYPT}$2B$", "{BLF-CRYPT}$2A$", "{BLF-CRYPT}$2B$", "{BLF-CRYPT}$2Y$")):
|
||||
return verify_bcrypt_password(challenge_password, plain_password)
|
||||
elif upwd.startswith(("{CRYPT}$6$", "{CRYPT}$2B$")):
|
||||
# CRYPT-SHA-512
|
||||
return verify_password_with_doveadmpw(challenge_password, plain_password)
|
||||
elif upwd.startswith("{SHA512}"):
|
||||
return verify_sha512_password(challenge_password, plain_password)
|
||||
elif upwd.startswith("{PLAIN-MD5}"):
|
||||
return verify_plain_md5_password(challenge_password, plain_password)
|
||||
elif upwd.startswith("{SHA512-CRYPT}"):
|
||||
return verify_sha512_crypt_password(challenge_password, plain_password)
|
||||
elif upwd.startswith("{CRAM-MD5}"):
|
||||
return verify_cram_md5_password(challenge_password, plain_password)
|
||||
elif upwd.startswith("{NTLM}"):
|
||||
return verify_ntlm_password(challenge_password, plain_password)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_supported_password_scheme(pw_hash):
|
||||
if not (pw_hash.startswith("{") and "}" in pw_hash):
|
||||
return False
|
||||
|
||||
# Extract scheme name from password hash: "{SSHA}xxxx" -> "SSHA"
|
||||
try:
|
||||
scheme = pw_hash.split("}", 1)[0].split("{", 1)[-1]
|
||||
scheme = scheme.upper()
|
||||
|
||||
if scheme in [
|
||||
"PLAIN",
|
||||
"CRYPT",
|
||||
"MD5",
|
||||
"PLAIN-MD5",
|
||||
"SHA",
|
||||
"SSHA",
|
||||
"SHA512",
|
||||
"SSHA512",
|
||||
"SHA512-CRYPT",
|
||||
"BCRYPT",
|
||||
"CRAM-MD5",
|
||||
"NTLM",
|
||||
]:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
1540
libs/iredutils.py
Normal file
1540
libs/iredutils.py
Normal file
File diff suppressed because it is too large
Load Diff
78
libs/jinja_filters.py
Normal file
78
libs/jinja_filters.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Custom Jinja2 filters."""
|
||||
|
||||
|
||||
def file_size_format(value, base_mb=False):
|
||||
"""Convert file size to a human-readable format, e.g. 20 MB, 1 GB, 2 TB.
|
||||
|
||||
@value -- file size in KB
|
||||
@base_mb -- if True, @value is in MB.
|
||||
"""
|
||||
ret = "0"
|
||||
|
||||
try:
|
||||
_bytes = float(value)
|
||||
except:
|
||||
return ret
|
||||
|
||||
if base_mb:
|
||||
_bytes = _bytes * 1024 * 1024
|
||||
|
||||
# byte
|
||||
base = 1024
|
||||
|
||||
if _bytes == 0:
|
||||
return ret
|
||||
|
||||
if _bytes < base:
|
||||
ret = "%d Bytes" % _bytes
|
||||
elif _bytes < base * base:
|
||||
ret = "%d KB" % (_bytes / base)
|
||||
elif _bytes < base * base * base:
|
||||
ret = "%d MB" % (_bytes / (base * base))
|
||||
elif _bytes < base * base * base * base:
|
||||
if _bytes % (base * base * base) == 0:
|
||||
ret = "%d GB" % (_bytes / (base * base * base))
|
||||
else:
|
||||
ret = "%.2f GB" % (_bytes / (base * base * base))
|
||||
else:
|
||||
if _bytes % (base * base * base * base) == 0:
|
||||
ret = "%d TB" % (_bytes / (base * base * base * base))
|
||||
else:
|
||||
ret = "%d GB" % (_bytes / (base * base * base))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def cut_string(s, length=40):
|
||||
try:
|
||||
if len(s) != len(s.encode("utf-8", "replace")):
|
||||
length = length / 2
|
||||
|
||||
if len(s) >= length:
|
||||
return s[:length] + "..."
|
||||
else:
|
||||
return s
|
||||
except UnicodeDecodeError:
|
||||
return str(s, encoding="utf-8", errors="replace")
|
||||
except:
|
||||
return s
|
||||
|
||||
|
||||
# Return value of percentage.
|
||||
def convert_to_percentage(current, total):
|
||||
try:
|
||||
current = int(current)
|
||||
total = int(total)
|
||||
except:
|
||||
return 0
|
||||
|
||||
if current == 0 or total == 0:
|
||||
return 0
|
||||
else:
|
||||
percent = (current * 100) // total
|
||||
if percent < 0:
|
||||
return 0
|
||||
elif percent > 100:
|
||||
return 100
|
||||
else:
|
||||
return percent
|
||||
531
libs/l10n.py
Normal file
531
libs/l10n.py
Normal file
@@ -0,0 +1,531 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
langmaps = {
|
||||
"en_US": "English (US)",
|
||||
"sq_AL": "Albanian",
|
||||
"ar_SA": "Arabic",
|
||||
"hy_AM": "Armenian",
|
||||
"az_AZ": "Azerbaijani",
|
||||
"bs_BA": "Bosnian (Serbian Latin)",
|
||||
"bg_BG": "Bulgarian",
|
||||
"ca_ES": "Català",
|
||||
"cy_GB": "Cymraeg",
|
||||
"hr_HR": "Croatian (Hrvatski)",
|
||||
"cs_CZ": "Čeština",
|
||||
"da_DK": "Dansk",
|
||||
"de_DE": "Deutsch (Deutsch)",
|
||||
"de_CH": "Deutsch (Schweiz)",
|
||||
"en_GB": "English (GB)",
|
||||
"es_ES": "Español",
|
||||
"eo": "Esperanto",
|
||||
"et_EE": "Estonian",
|
||||
"eu_ES": "Euskara (Basque)",
|
||||
"fi_FI": "Finnish (Suomi)",
|
||||
"nl_BE": "Flemish",
|
||||
"fr_FR": "Français",
|
||||
"gl_ES": "Galego (Galician)",
|
||||
"ka_GE": "Georgian (Kartuli)",
|
||||
"el_GR": "Greek",
|
||||
"he_IL": "Hebrew",
|
||||
"hi_IN": "Hindi",
|
||||
"hu_HU": "Hungarian",
|
||||
"is_IS": "Icelandic",
|
||||
"id_ID": "Indonesian",
|
||||
"ga_IE": "Irish",
|
||||
"it_IT": "Italiano",
|
||||
"ja_JP": "Japanese (日本語)",
|
||||
"ko_KR": "Korean",
|
||||
"ku": "Kurdish (Kurmancî)",
|
||||
"lv_LV": "Latvian",
|
||||
"lt_LT": "Lithuanian",
|
||||
"mk_MK": "Macedonian",
|
||||
"ms_MY": "Malay",
|
||||
"nl_NL": "Netherlands",
|
||||
"ne_NP": "Nepali",
|
||||
"nb_NO": "Norsk (Bokmål)",
|
||||
"nn_NO": "Norsk (Nynorsk)",
|
||||
"fa": "Persian (Farsi)",
|
||||
"pl_PL": "Polski",
|
||||
"pt_BR": "Portuguese (Brazilian)",
|
||||
"pt_PT": "Portuguese (Standard)",
|
||||
"ro_RO": "Romanian",
|
||||
"ru_RU": "Русский",
|
||||
"sr_CS": "Serbian (Cyrillic)",
|
||||
"sr_LT": "Serbian (Latin)",
|
||||
"si_LK": "Sinhala",
|
||||
"sk_SK": "Slovak",
|
||||
"sl_SI": "Slovenian",
|
||||
"sv_SE": "Swedish (Svenska)",
|
||||
"th_TH": "Thai",
|
||||
"tr_TR": "Türkçe",
|
||||
"uk_UA": "Ukrainian",
|
||||
"vi_VN": "Vietnamese",
|
||||
"zh_CN": "简体中文",
|
||||
"zh_TW": "繁體中文",
|
||||
}
|
||||
|
||||
|
||||
# All available timezone names and time offsets (in minutes).
|
||||
TIMEZONE_OFFSETS = {
|
||||
"GMT-12:00": -720,
|
||||
"GMT-11:00": -660,
|
||||
"GMT-10:00": -600,
|
||||
"GMT-09:30": -570,
|
||||
"GMT-09:00": -540,
|
||||
"GMT-08:00": -480,
|
||||
"GMT-07:00": -420,
|
||||
"GMT-06:00": -360,
|
||||
"GMT-05:00": -300,
|
||||
"GMT-04:30": -270,
|
||||
"GMT-04:00": -240,
|
||||
"GMT-03:30": -210,
|
||||
"GMT-03:00": -180,
|
||||
"GMT-02:00": -120,
|
||||
"GMT-01:00": -60,
|
||||
"GMT": 0,
|
||||
"GMT+01:00": 60,
|
||||
"GMT+02:00": 120,
|
||||
"GMT+03:00": 180,
|
||||
"GMT+03:30": 210,
|
||||
"GMT+04:00": 240,
|
||||
"GMT+04:30": 270,
|
||||
"GMT+05:00": 300,
|
||||
"GMT+05:30": 330,
|
||||
"GMT+05:45": 345,
|
||||
"GMT+06:00": 360,
|
||||
"GMT+06:30": 390,
|
||||
"GMT+07:00": 420,
|
||||
"GMT+08:00": 480,
|
||||
"GMT+08:45": 525,
|
||||
"GMT+09:00": 540,
|
||||
"GMT+09:30": 570,
|
||||
"GMT+10:00": 600,
|
||||
"GMT+10:30": 630,
|
||||
"GMT+11:00": 660,
|
||||
"GMT+11:30": 690,
|
||||
"GMT+12:00": 720,
|
||||
"GMT+12:45": 765,
|
||||
"GMT+13:00": 780,
|
||||
"GMT+14:00": 840,
|
||||
}
|
||||
|
||||
TIMEZONES = {
|
||||
"Pacific/Midway": "GMT-11:00",
|
||||
"Pacific/Niue": "GMT-11:00",
|
||||
"Pacific/Pago_Pago": "GMT-11:00",
|
||||
"America/Adak": "GMT-10:00",
|
||||
"Pacific/Honolulu": "GMT-10:00",
|
||||
"Pacific/Johnston": "GMT-10:00",
|
||||
"Pacific/Rarotonga": "GMT-10:00",
|
||||
"Pacific/Tahiti": "GMT-10:00",
|
||||
"Pacific/Marquesas": "GMT-09:30",
|
||||
"America/Anchorage": "GMT-09:00",
|
||||
"America/Juneau": "GMT-09:00",
|
||||
"America/Nome": "GMT-09:00",
|
||||
"America/Sitka": "GMT-09:00",
|
||||
"America/Yakutat": "GMT-09:00",
|
||||
"Pacific/Gambier": "GMT-09:00",
|
||||
"America/Dawson": "GMT-08:00",
|
||||
"America/Los_Angeles": "GMT-08:00",
|
||||
"America/Metlakatla": "GMT-08:00",
|
||||
"America/Santa_Isabel": "GMT-08:00",
|
||||
"America/Tijuana": "GMT-08:00",
|
||||
"America/Vancouver": "GMT-08:00",
|
||||
"America/Whitehorse": "GMT-08:00",
|
||||
"Pacific/Pitcairn": "GMT-08:00",
|
||||
"America/Boise": "GMT-07:00",
|
||||
"America/Cambridge_Bay": "GMT-07:00",
|
||||
"America/Chihuahua": "GMT-07:00",
|
||||
"America/Creston": "GMT-07:00",
|
||||
"America/Dawson_Creek": "GMT-07:00",
|
||||
"America/Denver": "GMT-07:00",
|
||||
"America/Edmonton": "GMT-07:00",
|
||||
"America/Hermosillo": "GMT-07:00",
|
||||
"America/Inuvik": "GMT-07:00",
|
||||
"America/Mazatlan": "GMT-07:00",
|
||||
"America/Ojinaga": "GMT-07:00",
|
||||
"America/Phoenix": "GMT-07:00",
|
||||
"America/Yellowknife": "GMT-07:00",
|
||||
"America/Bahia_Banderas": "GMT-06:00",
|
||||
"America/Belize": "GMT-06:00",
|
||||
"America/Chicago": "GMT-06:00",
|
||||
"America/Costa_Rica": "GMT-06:00",
|
||||
"America/El_Salvador": "GMT-06:00",
|
||||
"America/Guatemala": "GMT-06:00",
|
||||
"America/Indiana/Knox": "GMT-06:00",
|
||||
"America/Indiana/Tell_City": "GMT-06:00",
|
||||
"America/Managua": "GMT-06:00",
|
||||
"America/Matamoros": "GMT-06:00",
|
||||
"America/Menominee": "GMT-06:00",
|
||||
"America/Merida": "GMT-06:00",
|
||||
"America/Mexico_City": "GMT-06:00",
|
||||
"America/Monterrey": "GMT-06:00",
|
||||
"America/North_Dakota/Beulah": "GMT-06:00",
|
||||
"America/North_Dakota/Center": "GMT-06:00",
|
||||
"America/North_Dakota/New_Salem": "GMT-06:00",
|
||||
"America/Rainy_River": "GMT-06:00",
|
||||
"America/Rankin_Inlet": "GMT-06:00",
|
||||
"America/Regina": "GMT-06:00",
|
||||
"America/Resolute": "GMT-06:00",
|
||||
"America/Swift_Current": "GMT-06:00",
|
||||
"America/Tegucigalpa": "GMT-06:00",
|
||||
"America/Winnipeg": "GMT-06:00",
|
||||
"Pacific/Galapagos": "GMT-06:00",
|
||||
"America/Atikokan": "GMT-05:00",
|
||||
"America/Bogota": "GMT-05:00",
|
||||
"America/Cancun": "GMT-05:00",
|
||||
"America/Cayman": "GMT-05:00",
|
||||
"America/Detroit": "GMT-05:00",
|
||||
"America/Eirunepe": "GMT-05:00",
|
||||
"America/Guayaquil": "GMT-05:00",
|
||||
"America/Havana": "GMT-05:00",
|
||||
"America/Indiana/Indianapolis": "GMT-05:00",
|
||||
"America/Indiana/Marengo": "GMT-05:00",
|
||||
"America/Indiana/Petersburg": "GMT-05:00",
|
||||
"America/Indiana/Vevay": "GMT-05:00",
|
||||
"America/Indiana/Vincennes": "GMT-05:00",
|
||||
"America/Indiana/Winamac": "GMT-05:00",
|
||||
"America/Iqaluit": "GMT-05:00",
|
||||
"America/Jamaica": "GMT-05:00",
|
||||
"America/Kentucky/Louisville": "GMT-05:00",
|
||||
"America/Kentucky/Monticello": "GMT-05:00",
|
||||
"America/Lima": "GMT-05:00",
|
||||
"America/Nassau": "GMT-05:00",
|
||||
"America/New_York": "GMT-05:00",
|
||||
"America/Nipigon": "GMT-05:00",
|
||||
"America/Panama": "GMT-05:00",
|
||||
"America/Pangnirtung": "GMT-05:00",
|
||||
"America/Port-au-Prince": "GMT-05:00",
|
||||
"America/Rio_Branco": "GMT-05:00",
|
||||
"America/Thunder_Bay": "GMT-05:00",
|
||||
"America/Toronto": "GMT-05:00",
|
||||
"Pacific/Easter": "GMT-05:00",
|
||||
"America/Caracas": "GMT-04:30",
|
||||
"America/Anguilla": "GMT-04:00",
|
||||
"America/Antigua": "GMT-04:00",
|
||||
"America/Aruba": "GMT-04:00",
|
||||
"America/Barbados": "GMT-04:00",
|
||||
"America/Blanc-Sablon": "GMT-04:00",
|
||||
"America/Boa_Vista": "GMT-04:00",
|
||||
"America/Curacao": "GMT-04:00",
|
||||
"America/Dominica": "GMT-04:00",
|
||||
"America/Glace_Bay": "GMT-04:00",
|
||||
"America/Goose_Bay": "GMT-04:00",
|
||||
"America/Grand_Turk": "GMT-04:00",
|
||||
"America/Grenada": "GMT-04:00",
|
||||
"America/Guadeloupe": "GMT-04:00",
|
||||
"America/Guyana": "GMT-04:00",
|
||||
"America/Halifax": "GMT-04:00",
|
||||
"America/Kralendijk": "GMT-04:00",
|
||||
"America/La_Paz": "GMT-04:00",
|
||||
"America/Lower_Princes": "GMT-04:00",
|
||||
"America/Manaus": "GMT-04:00",
|
||||
"America/Marigot": "GMT-04:00",
|
||||
"America/Martinique": "GMT-04:00",
|
||||
"America/Moncton": "GMT-04:00",
|
||||
"America/Montserrat": "GMT-04:00",
|
||||
"America/Port_of_Spain": "GMT-04:00",
|
||||
"America/Porto_Velho": "GMT-04:00",
|
||||
"America/Puerto_Rico": "GMT-04:00",
|
||||
"America/Santo_Domingo": "GMT-04:00",
|
||||
"America/St_Barthelemy": "GMT-04:00",
|
||||
"America/St_Kitts": "GMT-04:00",
|
||||
"America/St_Lucia": "GMT-04:00",
|
||||
"America/St_Thomas": "GMT-04:00",
|
||||
"America/St_Vincent": "GMT-04:00",
|
||||
"America/Thule": "GMT-04:00",
|
||||
"America/Tortola": "GMT-04:00",
|
||||
"Atlantic/Bermuda": "GMT-04:00",
|
||||
"America/St_Johns": "GMT-03:30",
|
||||
"America/Araguaina": "GMT-03:00",
|
||||
"America/Argentina/Buenos_Aires": "GMT-03:00",
|
||||
"America/Argentina/Catamarca": "GMT-03:00",
|
||||
"America/Argentina/Cordoba": "GMT-03:00",
|
||||
"America/Argentina/Jujuy": "GMT-03:00",
|
||||
"America/Argentina/La_Rioja": "GMT-03:00",
|
||||
"America/Argentina/Mendoza": "GMT-03:00",
|
||||
"America/Argentina/Rio_Gallegos": "GMT-03:00",
|
||||
"America/Argentina/Salta": "GMT-03:00",
|
||||
"America/Argentina/San_Juan": "GMT-03:00",
|
||||
"America/Argentina/San_Luis": "GMT-03:00",
|
||||
"America/Argentina/Tucuman": "GMT-03:00",
|
||||
"America/Argentina/Ushuaia": "GMT-03:00",
|
||||
"America/Asuncion": "GMT-03:00",
|
||||
"America/Bahia": "GMT-03:00",
|
||||
"America/Belem": "GMT-03:00",
|
||||
"America/Campo_Grande": "GMT-03:00",
|
||||
"America/Cayenne": "GMT-03:00",
|
||||
"America/Cuiaba": "GMT-03:00",
|
||||
"America/Fortaleza": "GMT-03:00",
|
||||
"America/Godthab": "GMT-03:00",
|
||||
"America/Maceio": "GMT-03:00",
|
||||
"America/Miquelon": "GMT-03:00",
|
||||
"America/Paramaribo": "GMT-03:00",
|
||||
"America/Recife": "GMT-03:00",
|
||||
"America/Santarem": "GMT-03:00",
|
||||
"America/Santiago": "GMT-03:00",
|
||||
"Antarctica/Palmer": "GMT-03:00",
|
||||
"Antarctica/Rothera": "GMT-03:00",
|
||||
"Atlantic/Stanley": "GMT-03:00",
|
||||
"America/Montevideo": "GMT-02:00",
|
||||
"America/Noronha": "GMT-02:00",
|
||||
"America/Sao_Paulo": "GMT-02:00",
|
||||
"Atlantic/South_Georgia": "GMT-02:00",
|
||||
"America/Scoresbysund": "GMT-01:00",
|
||||
"Atlantic/Azores": "GMT-01:00",
|
||||
"Atlantic/Cape_Verde": "GMT-01:00",
|
||||
"Africa/Abidjan": "GMT+00:00",
|
||||
"Africa/Accra": "GMT+00:00",
|
||||
"Africa/Bamako": "GMT+00:00",
|
||||
"Africa/Banjul": "GMT+00:00",
|
||||
"Africa/Bissau": "GMT+00:00",
|
||||
"Africa/Casablanca": "GMT+00:00",
|
||||
"Africa/Conakry": "GMT+00:00",
|
||||
"Africa/Dakar": "GMT+00:00",
|
||||
"Africa/El_Aaiun": "GMT+00:00",
|
||||
"Africa/Freetown": "GMT+00:00",
|
||||
"Africa/Lome": "GMT+00:00",
|
||||
"Africa/Monrovia": "GMT+00:00",
|
||||
"Africa/Nouakchott": "GMT+00:00",
|
||||
"Africa/Ouagadougou": "GMT+00:00",
|
||||
"Africa/Sao_Tome": "GMT+00:00",
|
||||
"America/Danmarkshavn": "GMT+00:00",
|
||||
"Antarctica/Troll": "GMT+00:00",
|
||||
"Atlantic/Canary": "GMT+00:00",
|
||||
"Atlantic/Faroe": "GMT+00:00",
|
||||
"Atlantic/Madeira": "GMT+00:00",
|
||||
"Atlantic/Reykjavik": "GMT+00:00",
|
||||
"Atlantic/St_Helena": "GMT+00:00",
|
||||
"Europe/Dublin": "GMT+00:00",
|
||||
"Europe/Guernsey": "GMT+00:00",
|
||||
"Europe/Isle_of_Man": "GMT+00:00",
|
||||
"Europe/Jersey": "GMT+00:00",
|
||||
"Europe/Lisbon": "GMT+00:00",
|
||||
"Europe/London": "GMT+00:00",
|
||||
"UTC": "GMT+00:00",
|
||||
"Africa/Algiers": "GMT+01:00",
|
||||
"Africa/Bangui": "GMT+01:00",
|
||||
"Africa/Brazzaville": "GMT+01:00",
|
||||
"Africa/Ceuta": "GMT+01:00",
|
||||
"Africa/Douala": "GMT+01:00",
|
||||
"Africa/Kinshasa": "GMT+01:00",
|
||||
"Africa/Lagos": "GMT+01:00",
|
||||
"Africa/Libreville": "GMT+01:00",
|
||||
"Africa/Luanda": "GMT+01:00",
|
||||
"Africa/Malabo": "GMT+01:00",
|
||||
"Africa/Ndjamena": "GMT+01:00",
|
||||
"Africa/Niamey": "GMT+01:00",
|
||||
"Africa/Porto-Novo": "GMT+01:00",
|
||||
"Africa/Tunis": "GMT+01:00",
|
||||
"Arctic/Longyearbyen": "GMT+01:00",
|
||||
"Europe/Amsterdam": "GMT+01:00",
|
||||
"Europe/Andorra": "GMT+01:00",
|
||||
"Europe/Belgrade": "GMT+01:00",
|
||||
"Europe/Berlin": "GMT+01:00",
|
||||
"Europe/Bratislava": "GMT+01:00",
|
||||
"Europe/Brussels": "GMT+01:00",
|
||||
"Europe/Budapest": "GMT+01:00",
|
||||
"Europe/Busingen": "GMT+01:00",
|
||||
"Europe/Copenhagen": "GMT+01:00",
|
||||
"Europe/Gibraltar": "GMT+01:00",
|
||||
"Europe/Ljubljana": "GMT+01:00",
|
||||
"Europe/Luxembourg": "GMT+01:00",
|
||||
"Europe/Madrid": "GMT+01:00",
|
||||
"Europe/Malta": "GMT+01:00",
|
||||
"Europe/Monaco": "GMT+01:00",
|
||||
"Europe/Oslo": "GMT+01:00",
|
||||
"Europe/Paris": "GMT+01:00",
|
||||
"Europe/Podgorica": "GMT+01:00",
|
||||
"Europe/Prague": "GMT+01:00",
|
||||
"Europe/Rome": "GMT+01:00",
|
||||
"Europe/San_Marino": "GMT+01:00",
|
||||
"Europe/Sarajevo": "GMT+01:00",
|
||||
"Europe/Skopje": "GMT+01:00",
|
||||
"Europe/Stockholm": "GMT+01:00",
|
||||
"Europe/Tirane": "GMT+01:00",
|
||||
"Europe/Vaduz": "GMT+01:00",
|
||||
"Europe/Vatican": "GMT+01:00",
|
||||
"Europe/Vienna": "GMT+01:00",
|
||||
"Europe/Warsaw": "GMT+01:00",
|
||||
"Europe/Zagreb": "GMT+01:00",
|
||||
"Europe/Zurich": "GMT+01:00",
|
||||
"Africa/Blantyre": "GMT+02:00",
|
||||
"Africa/Bujumbura": "GMT+02:00",
|
||||
"Africa/Cairo": "GMT+02:00",
|
||||
"Africa/Gaborone": "GMT+02:00",
|
||||
"Africa/Harare": "GMT+02:00",
|
||||
"Africa/Johannesburg": "GMT+02:00",
|
||||
"Africa/Kigali": "GMT+02:00",
|
||||
"Africa/Lubumbashi": "GMT+02:00",
|
||||
"Africa/Lusaka": "GMT+02:00",
|
||||
"Africa/Maputo": "GMT+02:00",
|
||||
"Africa/Maseru": "GMT+02:00",
|
||||
"Africa/Mbabane": "GMT+02:00",
|
||||
"Africa/Tripoli": "GMT+02:00",
|
||||
"Africa/Windhoek": "GMT+02:00",
|
||||
"Asia/Amman": "GMT+02:00",
|
||||
"Asia/Beirut": "GMT+02:00",
|
||||
"Asia/Damascus": "GMT+02:00",
|
||||
"Asia/Gaza": "GMT+02:00",
|
||||
"Asia/Hebron": "GMT+02:00",
|
||||
"Asia/Jerusalem": "GMT+02:00",
|
||||
"Asia/Nicosia": "GMT+02:00",
|
||||
"Europe/Athens": "GMT+02:00",
|
||||
"Europe/Bucharest": "GMT+02:00",
|
||||
"Europe/Chisinau": "GMT+02:00",
|
||||
"Europe/Helsinki": "GMT+02:00",
|
||||
"Europe/Istanbul": "GMT+03:00",
|
||||
"Europe/Kaliningrad": "GMT+02:00",
|
||||
"Europe/Kiev": "GMT+02:00",
|
||||
"Europe/Mariehamn": "GMT+02:00",
|
||||
"Europe/Riga": "GMT+02:00",
|
||||
"Europe/Sofia": "GMT+02:00",
|
||||
"Europe/Tallinn": "GMT+02:00",
|
||||
"Europe/Uzhgorod": "GMT+02:00",
|
||||
"Europe/Vilnius": "GMT+02:00",
|
||||
"Europe/Zaporozhye": "GMT+02:00",
|
||||
"Africa/Addis_Ababa": "GMT+03:00",
|
||||
"Africa/Asmara": "GMT+03:00",
|
||||
"Africa/Dar_es_Salaam": "GMT+03:00",
|
||||
"Africa/Djibouti": "GMT+03:00",
|
||||
"Africa/Juba": "GMT+03:00",
|
||||
"Africa/Kampala": "GMT+03:00",
|
||||
"Africa/Khartoum": "GMT+03:00",
|
||||
"Africa/Mogadishu": "GMT+03:00",
|
||||
"Africa/Nairobi": "GMT+03:00",
|
||||
"Antarctica/Syowa": "GMT+03:00",
|
||||
"Asia/Aden": "GMT+03:00",
|
||||
"Asia/Baghdad": "GMT+03:00",
|
||||
"Asia/Bahrain": "GMT+03:00",
|
||||
"Asia/Kuwait": "GMT+03:00",
|
||||
"Asia/Qatar": "GMT+03:00",
|
||||
"Asia/Riyadh": "GMT+03:00",
|
||||
"Europe/Minsk": "GMT+03:00",
|
||||
"Europe/Moscow": "GMT+03:00",
|
||||
"Europe/Simferopol": "GMT+03:00",
|
||||
"Europe/Volgograd": "GMT+03:00",
|
||||
"Indian/Antananarivo": "GMT+03:00",
|
||||
"Indian/Comoro": "GMT+03:00",
|
||||
"Indian/Mayotte": "GMT+03:00",
|
||||
"Asia/Tehran": "GMT+03:30",
|
||||
"Asia/Baku": "GMT+04:00",
|
||||
"Asia/Dubai": "GMT+04:00",
|
||||
"Asia/Muscat": "GMT+04:00",
|
||||
"Asia/Tbilisi": "GMT+04:00",
|
||||
"Asia/Yerevan": "GMT+04:00",
|
||||
"Europe/Samara": "GMT+04:00",
|
||||
"Indian/Mahe": "GMT+04:00",
|
||||
"Indian/Mauritius": "GMT+04:00",
|
||||
"Indian/Reunion": "GMT+04:00",
|
||||
"Asia/Kabul": "GMT+04:30",
|
||||
"Antarctica/Mawson": "GMT+05:00",
|
||||
"Asia/Aqtau": "GMT+05:00",
|
||||
"Asia/Aqtobe": "GMT+05:00",
|
||||
"Asia/Ashgabat": "GMT+05:00",
|
||||
"Asia/Dushanbe": "GMT+05:00",
|
||||
"Asia/Karachi": "GMT+05:00",
|
||||
"Asia/Oral": "GMT+05:00",
|
||||
"Asia/Samarkand": "GMT+05:00",
|
||||
"Asia/Tashkent": "GMT+05:00",
|
||||
"Asia/Yekaterinburg": "GMT+05:00",
|
||||
"Indian/Kerguelen": "GMT+05:00",
|
||||
"Indian/Maldives": "GMT+05:00",
|
||||
"Asia/Colombo": "GMT+05:30",
|
||||
"Asia/Kolkata": "GMT+05:30",
|
||||
"Asia/Kathmandu": "GMT+05:45",
|
||||
"Antarctica/Vostok": "GMT+06:00",
|
||||
"Asia/Almaty": "GMT+06:00",
|
||||
"Asia/Bishkek": "GMT+06:00",
|
||||
"Asia/Dhaka": "GMT+06:00",
|
||||
"Asia/Novosibirsk": "GMT+06:00",
|
||||
"Asia/Omsk": "GMT+06:00",
|
||||
"Asia/Qyzylorda": "GMT+06:00",
|
||||
"Asia/Thimphu": "GMT+06:00",
|
||||
"Asia/Urumqi": "GMT+06:00",
|
||||
"Indian/Chagos": "GMT+06:00",
|
||||
"Asia/Rangoon": "GMT+06:30",
|
||||
"Indian/Cocos": "GMT+06:30",
|
||||
"Antarctica/Davis": "GMT+07:00",
|
||||
"Asia/Bangkok": "GMT+07:00",
|
||||
"Asia/Ho_Chi_Minh": "GMT+07:00",
|
||||
"Asia/Hovd": "GMT+07:00",
|
||||
"Asia/Jakarta": "GMT+07:00",
|
||||
"Asia/Krasnoyarsk": "GMT+07:00",
|
||||
"Asia/Novokuznetsk": "GMT+07:00",
|
||||
"Asia/Phnom_Penh": "GMT+07:00",
|
||||
"Asia/Pontianak": "GMT+07:00",
|
||||
"Asia/Vientiane": "GMT+07:00",
|
||||
"Indian/Christmas": "GMT+07:00",
|
||||
"Antarctica/Casey": "GMT+08:00",
|
||||
"Asia/Beijing": "GMT+08:00",
|
||||
"Asia/Brunei": "GMT+08:00",
|
||||
"Asia/Chita": "GMT+08:00",
|
||||
"Asia/Choibalsan": "GMT+08:00",
|
||||
"Asia/Hong_Kong": "GMT+08:00",
|
||||
"Asia/Irkutsk": "GMT+08:00",
|
||||
"Asia/Kuala_Lumpur": "GMT+08:00",
|
||||
"Asia/Kuching": "GMT+08:00",
|
||||
"Asia/Macau": "GMT+08:00",
|
||||
"Asia/Makassar": "GMT+08:00",
|
||||
"Asia/Manila": "GMT+08:00",
|
||||
"Asia/Shanghai": "GMT+08:00",
|
||||
"Asia/Singapore": "GMT+08:00",
|
||||
"Asia/Taipei": "GMT+08:00",
|
||||
"Asia/Ulaanbaatar": "GMT+08:00",
|
||||
"Australia/Perth": "GMT+08:00",
|
||||
"Australia/Eucla": "GMT+08:45",
|
||||
"Asia/Dili": "GMT+09:00",
|
||||
"Asia/Jayapura": "GMT+09:00",
|
||||
"Asia/Khandyga": "GMT+09:00",
|
||||
"Asia/Pyongyang": "GMT+09:00",
|
||||
"Asia/Seoul": "GMT+09:00",
|
||||
"Asia/Tokyo": "GMT+09:00",
|
||||
"Asia/Yakutsk": "GMT+09:00",
|
||||
"Pacific/Palau": "GMT+09:00",
|
||||
"Australia/Darwin": "GMT+09:30",
|
||||
"Antarctica/DumontDUrville": "GMT+10:00",
|
||||
"Asia/Magadan": "GMT+10:00",
|
||||
"Asia/Sakhalin": "GMT+10:00",
|
||||
"Asia/Ust-Nera": "GMT+10:00",
|
||||
"Asia/Vladivostok": "GMT+10:00",
|
||||
"Australia/Brisbane": "GMT+10:00",
|
||||
"Australia/Lindeman": "GMT+10:00",
|
||||
"Pacific/Chuuk": "GMT+10:00",
|
||||
"Pacific/Guam": "GMT+10:00",
|
||||
"Pacific/Port_Moresby": "GMT+10:00",
|
||||
"Pacific/Saipan": "GMT+10:00",
|
||||
"Australia/Sydney": "GMT+10:00",
|
||||
"Australia/Adelaide": "GMT+10:30",
|
||||
"Australia/Broken_Hill": "GMT+10:30",
|
||||
"Antarctica/Macquarie": "GMT+11:00",
|
||||
"Asia/Srednekolymsk": "GMT+11:00",
|
||||
"Australia/Currie": "GMT+11:00",
|
||||
"Australia/Hobart": "GMT+11:00",
|
||||
"Australia/Lord_Howe": "GMT+11:00",
|
||||
"Australia/Melbourne": "GMT+11:00",
|
||||
"Pacific/Bougainville": "GMT+11:00",
|
||||
"Pacific/Efate": "GMT+11:00",
|
||||
"Pacific/Guadalcanal": "GMT+11:00",
|
||||
"Pacific/Kosrae": "GMT+11:00",
|
||||
"Pacific/Noumea": "GMT+11:00",
|
||||
"Pacific/Pohnpei": "GMT+11:00",
|
||||
"Pacific/Norfolk": "GMT+11:30",
|
||||
"Asia/Anadyr": "GMT+12:00",
|
||||
"Asia/Kamchatka": "GMT+12:00",
|
||||
"Pacific/Funafuti": "GMT+12:00",
|
||||
"Pacific/Kwajalein": "GMT+12:00",
|
||||
"Pacific/Majuro": "GMT+12:00",
|
||||
"Pacific/Nauru": "GMT+12:00",
|
||||
"Pacific/Tarawa": "GMT+12:00",
|
||||
"Pacific/Wake": "GMT+12:00",
|
||||
"Pacific/Wallis": "GMT+12:00",
|
||||
"Antarctica/McMurdo": "GMT+13:00",
|
||||
"Pacific/Auckland": "GMT+13:00",
|
||||
"Pacific/Enderbury": "GMT+13:00",
|
||||
"Pacific/Fakaofo": "GMT+13:00",
|
||||
"Pacific/Fiji": "GMT+13:00",
|
||||
"Pacific/Tongatapu": "GMT+13:00",
|
||||
"Pacific/Chatham": "GMT+13:45",
|
||||
"Pacific/Apia": "GMT+14:00",
|
||||
"Pacific/Kiritimati": "GMT+14:00",
|
||||
}
|
||||
83
libs/logger.py
Normal file
83
libs/logger.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import sys
|
||||
import logging
|
||||
import traceback
|
||||
from logging.handlers import SysLogHandler
|
||||
|
||||
import web
|
||||
from libs import iredutils
|
||||
import settings
|
||||
|
||||
session = web.config.get("_session")
|
||||
|
||||
# Set application name.
|
||||
logger = logging.getLogger("iredadmin")
|
||||
|
||||
# Set log level.
|
||||
_log_level = getattr(logging, str(settings.LOG_LEVEL).upper())
|
||||
logger.setLevel(_log_level)
|
||||
|
||||
if settings.LOG_TARGET == "stdout":
|
||||
_handler = logging.StreamHandler()
|
||||
_formatter = logging.Formatter("%(message)s (%(pathname)s, L%(lineno)d)")
|
||||
else:
|
||||
# Defaults to "syslog":
|
||||
_facility = getattr(SysLogHandler, "LOG_" + settings.SYSLOG_FACILITY.upper())
|
||||
_formatter = logging.Formatter("%(name)s %(message)s (%(pathname)s, L%(lineno)d)")
|
||||
|
||||
if settings.SYSLOG_SERVER.startswith("/"):
|
||||
# Log to a local socket
|
||||
_handler = SysLogHandler(address=settings.SYSLOG_SERVER, facility=_facility)
|
||||
else:
|
||||
# Log to a network address
|
||||
_server = (settings.SYSLOG_SERVER, settings.SYSLOG_PORT)
|
||||
_handler = SysLogHandler(address=_server, facility=_facility)
|
||||
|
||||
_handler.setFormatter(_formatter)
|
||||
logger.addHandler(_handler)
|
||||
|
||||
|
||||
def log_traceback():
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
msg = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
def log_activity(msg, admin="", domain="", username="", event="", loglevel="info"):
|
||||
try:
|
||||
if not admin:
|
||||
admin = session.get("username")
|
||||
|
||||
if username and not domain:
|
||||
domain = username.split("@", 1)[-1]
|
||||
|
||||
msg = str(msg)
|
||||
|
||||
# Prepend '[API]' in log message
|
||||
try:
|
||||
if web.ctx.fullpath.startswith("/api/"):
|
||||
msg = "[API] " + msg
|
||||
except:
|
||||
pass
|
||||
|
||||
web.conn_iredadmin.insert(
|
||||
"log",
|
||||
admin=str(admin),
|
||||
domain=str(domain),
|
||||
username=str(username),
|
||||
loglevel=str(loglevel),
|
||||
event=str(event),
|
||||
msg=msg,
|
||||
ip=web.ctx.ip,
|
||||
timestamp=iredutils.get_gmttime(),
|
||||
)
|
||||
|
||||
if loglevel == "info":
|
||||
logger.info("{0} admin={1}, domain={2}, username={3}, event={4}, "
|
||||
"ip={5}".format(msg, admin, domain, username, event, web.ctx.ip))
|
||||
elif loglevel == "error":
|
||||
logger.error("{0} admin={1}, domain={2}, username={3}, event={4}, "
|
||||
"ip={5}".format(msg, admin, domain, username, event, web.ctx.ip))
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
72
libs/mailparser.py
Normal file
72
libs/mailparser.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import email
|
||||
from email.header import decode_header
|
||||
from libs.logger import log_traceback
|
||||
from libs import iredutils
|
||||
|
||||
|
||||
def parse_raw_message(msg: bytes):
|
||||
"""Read RAW message from string. Return a tuple with 3 elements:
|
||||
|
||||
- `headers`: a list of tuple with mail header. [(hdr, value), (hdr, value), ...]
|
||||
- `bodies`: a list of body parts: [part1, part2, ...]
|
||||
- `attachments`: a list of attachment file names: [name1, name2, ...]
|
||||
"""
|
||||
|
||||
# Get all mail headers. Sample:
|
||||
# [('From', 'sender@xx.com'), ('To', 'recipient@xx.net')]
|
||||
headers = []
|
||||
|
||||
# Get decoded content parts of mail body.
|
||||
bodies = []
|
||||
|
||||
# Get list of attachment names.
|
||||
attachments = []
|
||||
|
||||
msg = email.message_from_bytes(msg)
|
||||
|
||||
# Extract all headers.
|
||||
for (header, value) in msg.items():
|
||||
for (text, encoding) in decode_header(value):
|
||||
if encoding:
|
||||
if isinstance(text, bytes):
|
||||
try:
|
||||
value = iredutils.bytes2str(text)
|
||||
except:
|
||||
pass
|
||||
|
||||
headers.append((header, value))
|
||||
|
||||
for part in msg.walk():
|
||||
_content_type = part.get_content_maintype()
|
||||
|
||||
# multipart/* is just a container
|
||||
if _content_type == 'multipart':
|
||||
continue
|
||||
|
||||
# either a string or None.
|
||||
_filename = part.get_filename()
|
||||
if _filename:
|
||||
attachments += [_filename]
|
||||
|
||||
if _content_type == 'text':
|
||||
# Plain text, not an attachment.
|
||||
try:
|
||||
if part.get_content_charset():
|
||||
encoding = part.get_content_charset()
|
||||
elif part.get_charset():
|
||||
encoding = part.get_charset()
|
||||
else:
|
||||
encoding = 'utf-8'
|
||||
|
||||
text = str(part.get_payload(decode=True),
|
||||
encoding=encoding,
|
||||
errors='replace')
|
||||
|
||||
text = text.strip()
|
||||
bodies.append(text)
|
||||
except:
|
||||
log_traceback()
|
||||
|
||||
return headers, bodies, attachments
|
||||
363
libs/mlmmj/__init__.py
Normal file
363
libs/mlmmj/__init__.py
Normal file
@@ -0,0 +1,363 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
# Functions used to interactive with mlmmjadmin RESTful API server:
|
||||
# https://bitbucket.org/iredmail/mlmmjadmin/src
|
||||
|
||||
import uuid
|
||||
import requests
|
||||
from libs import iredutils
|
||||
from urllib.parse import urlencode
|
||||
import settings
|
||||
|
||||
api_headers = {settings.MLMMJADMIN_API_AUTH_HEADER: settings.mlmmjadmin_api_auth_token}
|
||||
base_url = settings.MLMMJADMIN_API_BASE_URL
|
||||
_verify_ssl = settings.MLMMJADMIN_API_VERIFY_SSL
|
||||
|
||||
|
||||
def __get(mail, params=None):
|
||||
"""
|
||||
Send a http GET to mlmmjadmin RESTful API server.
|
||||
|
||||
:param mail: mail address of mailing list account.
|
||||
"""
|
||||
url = base_url + "/" + mail
|
||||
try:
|
||||
r = requests.get(url, headers=api_headers, params=params, verify=_verify_ssl)
|
||||
|
||||
return r.json()
|
||||
except requests.ConnectionError:
|
||||
return {"_success": False, "_msg": "API_SERVER_NOT_REACHABLE"}
|
||||
except Exception as e:
|
||||
return {"_success": False, "_msg": repr(e)}
|
||||
|
||||
|
||||
def __post(mail, data=None):
|
||||
"""
|
||||
Send a http POST to mlmmjadmin RESTful API server.
|
||||
|
||||
:param mail: mail address of mailing list account.
|
||||
:param data: a dict used to be sent to mlmmjadmin API server.
|
||||
"""
|
||||
url = base_url + "/" + mail
|
||||
try:
|
||||
r = requests.post(url, data=data, headers=api_headers, verify=_verify_ssl)
|
||||
|
||||
return r.json()
|
||||
except requests.ConnectionError:
|
||||
return {"_success": False, "_msg": "API_SERVER_NOT_REACHABLE"}
|
||||
except Exception as e:
|
||||
return {"_success": False, "_msg": repr(e)}
|
||||
|
||||
|
||||
def __put(mail, data=None):
|
||||
"""
|
||||
Send a http PUT to mlmmjadmin RESTful API server.
|
||||
|
||||
:param mail: mail address of mailing list account.
|
||||
:param data: a dict used to be sent to mlmmjadmin API server.
|
||||
"""
|
||||
url = base_url + "/" + mail
|
||||
try:
|
||||
r = requests.put(url, data=data, headers=api_headers, verify=_verify_ssl)
|
||||
|
||||
return r.json()
|
||||
except requests.ConnectionError:
|
||||
return {"_success": False, "_msg": "API_SERVER_NOT_REACHABLE"}
|
||||
except Exception as e:
|
||||
return {"_success": False, "_msg": repr(e)}
|
||||
|
||||
|
||||
def __delete(mail, data=None):
|
||||
"""
|
||||
Send a http DELETE to mlmmjadmin RESTful API server.
|
||||
|
||||
:param mail: mail address of mailing list account.
|
||||
:param data: a dict used to be encoded as URL parameters and sent to
|
||||
mlmmjadmin API server.
|
||||
"""
|
||||
url = base_url + "/" + mail
|
||||
|
||||
if data:
|
||||
url = url + "?" + urlencode(data)
|
||||
|
||||
try:
|
||||
r = requests.delete(url, headers=api_headers, verify=_verify_ssl)
|
||||
|
||||
return r.json()
|
||||
except requests.ConnectionError:
|
||||
return {"_success": False, "_msg": "API_SERVER_NOT_REACHABLE"}
|
||||
except Exception as e:
|
||||
return {"_success": False, "_msg": repr(e)}
|
||||
|
||||
|
||||
def __get_subscribers(mail, email_only=False):
|
||||
url = base_url + "/%s/subscribers" % mail
|
||||
|
||||
params = {}
|
||||
if email_only:
|
||||
params["email_only"] = "yes"
|
||||
|
||||
try:
|
||||
r = requests.get(url, params=params, headers=api_headers, verify=_verify_ssl)
|
||||
|
||||
return r.json()
|
||||
except requests.ConnectionError:
|
||||
return {"_success": False, "_msg": "API_SERVER_NOT_REACHABLE"}
|
||||
except Exception as e:
|
||||
return {"_success": False, "_msg": repr(e)}
|
||||
|
||||
|
||||
def generate_transport(mail):
|
||||
(listname, domain) = str(mail).lower().split("@", 1)
|
||||
transport = "{}:{}/{}".format(settings.MLMMJ_MTA_TRANSPORT_NAME, domain, listname)
|
||||
return transport
|
||||
|
||||
|
||||
def generate_mlid():
|
||||
"""Generate an server-wide unique uuid as mailing list id."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def create_account(mail, form):
|
||||
"""
|
||||
Create a mlmmj account by sending a HTTP POST to mlmmjadmin RESTful API.
|
||||
|
||||
Arguments:
|
||||
|
||||
:param mail: full email address of mailing list account
|
||||
:param form: form submitted by a web page
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
qr = __post(mail=mail, data=form)
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def get_account_profile(mail, with_subscribers=False):
|
||||
"""
|
||||
Send a HTTP GET to get mailing list profile.
|
||||
|
||||
Arguments:
|
||||
|
||||
@mail - full email address of mailing list account
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
qr = __get(mail=mail)
|
||||
if qr["_success"]:
|
||||
profile = qr["_data"]
|
||||
|
||||
if with_subscribers:
|
||||
_qr = __get_subscribers(mail=mail, email_only=True)
|
||||
if _qr["_success"]:
|
||||
profile["subscribers"] = _qr["_data"]
|
||||
|
||||
return True, profile
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def update_account_profile(mail, data):
|
||||
"""
|
||||
Send a HTTP PUT to mlmmjadmin RESTful API.
|
||||
|
||||
Arguments:
|
||||
|
||||
@mail - full email address of mailing list account
|
||||
@data - a dict of parameter/value pairs.
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
qr = __put(mail=mail, data=data)
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def delete_account(mail, keep_archive=True):
|
||||
"""
|
||||
Send a HTTP DELETE to mlmmjadmin RESTful API.
|
||||
|
||||
Arguments:
|
||||
|
||||
@mail - full email address of mailing list account
|
||||
@keep_archive - archive the account or not
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
params = {"archive": "yes"}
|
||||
if not keep_archive:
|
||||
params["archive"] = "no"
|
||||
|
||||
qr = __delete(mail=mail, data=params)
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def delete_accounts(mails, keep_archive=True):
|
||||
mails = [str(i).lower() for i in mails if iredutils.is_email(i)]
|
||||
if not mails:
|
||||
return True,
|
||||
|
||||
for i in mails:
|
||||
qr = delete_account(mail=i, keep_archive=keep_archive)
|
||||
if not qr[0]:
|
||||
return qr[0], i + "-" + qr[1]
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
def get_subscribers(mail, email_only=False):
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
qr = __get_subscribers(mail=mail, email_only=email_only)
|
||||
|
||||
if qr["_success"]:
|
||||
subscribers = qr.get("_data", [])
|
||||
return True, subscribers
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def add_subscribers(mail, subscribers, subscription="normal", require_confirm=False):
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
if subscription not in ["normal", "digest", "nomail"]:
|
||||
subscription = "normal"
|
||||
|
||||
url = base_url + "/%s/subscribers" % mail
|
||||
|
||||
params = {"add_subscribers": ",".join(subscribers), "subscription": subscription}
|
||||
|
||||
if require_confirm in [True, "yes"]:
|
||||
params["require_confirm"] = "yes"
|
||||
|
||||
r = requests.post(url, data=params, headers=api_headers, verify=_verify_ssl)
|
||||
|
||||
qr = r.json()
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def remove_subscribers(mail, subscribers):
|
||||
"""Remove subscribers from mailing list.
|
||||
|
||||
:param mail: mail address of mailing list account
|
||||
:param subscribers: a list/tuple/set of subscribers' mail addresses
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
if subscribers:
|
||||
subscribers = [i.lower() for i in subscribers]
|
||||
else:
|
||||
subscribers = []
|
||||
|
||||
url = base_url + "/%s/subscribers" % mail
|
||||
params = {"remove_subscribers": ",".join(subscribers)}
|
||||
r = requests.post(url, data=params, headers=api_headers, verify=_verify_ssl)
|
||||
qr = r.json()
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def remove_all_subscribers(mail):
|
||||
"""Remove all subscribers from mailing list.
|
||||
|
||||
:param mail: mail address of mailing list account
|
||||
"""
|
||||
mail = str(mail).lower()
|
||||
if not iredutils.is_email(mail):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
url = base_url + "/%s/subscribers" % mail
|
||||
params = {"remove_subscribers": "ALL"}
|
||||
r = requests.post(url, data=params, headers=api_headers, verify=_verify_ssl)
|
||||
qr = r.json()
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def subscribe_to_lists(subscriber, lists):
|
||||
"""Subscribe one mail address to multiple lists."""
|
||||
subscriber = str(subscriber).lower()
|
||||
lists = [str(i).lower() for i in lists if iredutils.is_email(i)]
|
||||
if not lists:
|
||||
return True,
|
||||
|
||||
url = base_url + "/subscriber/%s/subscribe" % subscriber
|
||||
params = {"lists": ",".join(lists), "require_confirm": "no"}
|
||||
r = requests.post(url, data=params, headers=api_headers, verify=_verify_ssl)
|
||||
qr = r.json()
|
||||
if qr["_success"]:
|
||||
return True,
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def get_subscribed_lists(mail, query_all_lists=False, email_only=False):
|
||||
mail = str(mail).lower()
|
||||
|
||||
url = base_url + "/subscriber/%s/subscribed" % mail
|
||||
|
||||
params = {"query_all_lists": "no", "email_only": "no"}
|
||||
if query_all_lists:
|
||||
params["query_all_lists"] = "yes"
|
||||
|
||||
if email_only:
|
||||
params["email_only"] = "yes"
|
||||
|
||||
r = requests.get(url, params=params, headers=api_headers, verify=_verify_ssl)
|
||||
qr = r.json()
|
||||
if qr["_success"]:
|
||||
return True, qr["_data"]
|
||||
else:
|
||||
return False, qr.get("_msg", "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
def remove_subscriber_from_all_subscribed_lists(subscriber):
|
||||
"""Remove one subscriber from all subscribed lists under same domain."""
|
||||
if not iredutils.is_email(subscriber):
|
||||
return False, "INVALID_EMAIL"
|
||||
|
||||
qr = get_subscribed_lists(mail=subscriber, email_only=True)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
_lists = qr[1]
|
||||
|
||||
_errors = []
|
||||
for ml in _lists:
|
||||
_qr = remove_subscribers(mail=ml, subscribers=[subscriber])
|
||||
if not _qr[0]:
|
||||
_errors += ["{}: {}".format(subscriber, repr(_qr[1]))]
|
||||
|
||||
if _errors:
|
||||
return False, " ".join(_errors)
|
||||
else:
|
||||
return True,
|
||||
19
libs/panel/__init__.py
Normal file
19
libs/panel/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Events in admin log. Detailed comments of event names are defined in
|
||||
# templates/default/macros/general.html
|
||||
LOG_EVENTS = [
|
||||
'all',
|
||||
'login',
|
||||
'user_login',
|
||||
'active',
|
||||
'disable',
|
||||
'create',
|
||||
'delete',
|
||||
'update',
|
||||
'grant', # Grant user as domain admin
|
||||
'revoke', # Revoke admin privilege
|
||||
'backup',
|
||||
'delete_mailboxes',
|
||||
'update_wblist',
|
||||
'iredapd', # iRedAPD rejection.
|
||||
'unban', # Unban IP address
|
||||
]
|
||||
429
libs/panel/domain_ownership.py
Normal file
429
libs/panel/domain_ownership.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import time
|
||||
|
||||
from dns import resolver
|
||||
import requests
|
||||
import web
|
||||
|
||||
import settings
|
||||
from libs import iredutils
|
||||
|
||||
if settings.backend == 'ldap':
|
||||
from libs.ldaplib.admin import get_managed_domains
|
||||
else:
|
||||
from libs.sqllib.admin import get_managed_domains
|
||||
|
||||
session = web.config.get('_session', {})
|
||||
|
||||
|
||||
def is_pending_domain(domain, conn=None):
|
||||
if not iredutils.is_domain(domain):
|
||||
return True
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
try:
|
||||
qr = conn.select('domain_ownership',
|
||||
vars={'domain': domain},
|
||||
where='(domain=$domain OR alias_domain=$domain) AND verified=0',
|
||||
limit=1)
|
||||
if qr:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return True
|
||||
|
||||
|
||||
def get_pending_domains(domains=None,
|
||||
domain_name_only=False,
|
||||
conn=None):
|
||||
"""Query `iredadmin.domain_ownership` to get list of pending domains.
|
||||
|
||||
Return list of domain names."""
|
||||
admin = session.get('username')
|
||||
|
||||
if domains:
|
||||
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
|
||||
else:
|
||||
if not session.get('is_global_admin'):
|
||||
# Get managed domains
|
||||
if settings.backend == 'ldap':
|
||||
qr = get_managed_domains(admin=admin, conn=None)
|
||||
else:
|
||||
# settings.backend in ['mysql', 'pgsql']
|
||||
qr = get_managed_domains(admin=admin,
|
||||
domain_name_only=True,
|
||||
listed_only=False)
|
||||
|
||||
if qr[0]:
|
||||
domains = qr[1]
|
||||
|
||||
if not domains:
|
||||
return True, []
|
||||
else:
|
||||
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
try:
|
||||
if session.get('is_global_admin'):
|
||||
qr = conn.select('domain_ownership',
|
||||
where='verified=0')
|
||||
else:
|
||||
qr = conn.select('domain_ownership',
|
||||
vars={'domains': domains, 'admin': admin},
|
||||
where='admin=$admin AND (domain IN $domains OR alias_domain IN $domains) AND verified=0')
|
||||
|
||||
if domain_name_only:
|
||||
pending_domains = set()
|
||||
for r in qr:
|
||||
if r.alias_domain:
|
||||
pending_domains.add(r.alias_domain)
|
||||
else:
|
||||
pending_domains.add(r.domain)
|
||||
|
||||
pending_domains = [str(i).lower() for i in pending_domains if iredutils.is_domain(i)]
|
||||
pending_domains.sort()
|
||||
return True, pending_domains
|
||||
else:
|
||||
return True, list(qr)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_verified_domains(domains=None, conn=None):
|
||||
"""Query `iredadmin.domain_ownership` to get list of verified domains.
|
||||
|
||||
Return list of domain names."""
|
||||
admin = session.get('username')
|
||||
|
||||
if domains:
|
||||
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
|
||||
else:
|
||||
if not session.get('is_global_admin'):
|
||||
# Get managed domains
|
||||
if settings.backend == 'ldap':
|
||||
qr = get_managed_domains(admin=admin, conn=None)
|
||||
else:
|
||||
# settings.backend in ['mysql', 'pgsql']
|
||||
qr = get_managed_domains(admin=admin,
|
||||
domain_name_only=True,
|
||||
listed_only=False)
|
||||
|
||||
if qr[0]:
|
||||
domains = qr[1]
|
||||
else:
|
||||
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
|
||||
|
||||
if not domains:
|
||||
return True, []
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
try:
|
||||
if session.get('is_global_admin'):
|
||||
qr = conn.select('domain_ownership',
|
||||
what='domain,alias_domain',
|
||||
where='verified=1')
|
||||
else:
|
||||
qr = conn.select('domain_ownership',
|
||||
vars={'domains': domains, 'admin': admin},
|
||||
what='domain,alias_domain',
|
||||
where='admin=$admin AND (domain IN $domains OR alias_domain IN $domains) AND verified=1')
|
||||
|
||||
verified_domains = []
|
||||
for r in qr:
|
||||
if r.alias_domain:
|
||||
verified_domains += [str(r.alias_domain).lower()]
|
||||
else:
|
||||
verified_domains += [str(r.domain).lower()]
|
||||
|
||||
verified_domains.sort()
|
||||
return True, verified_domains
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def remove_pending_domains(domains=None):
|
||||
"""Remove pending domains.
|
||||
|
||||
:param domains: a list/tuple/set of domain names
|
||||
"""
|
||||
if domains:
|
||||
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
|
||||
else:
|
||||
return True,
|
||||
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
try:
|
||||
if session.get('is_global_admin'):
|
||||
conn.delete('domain_ownership',
|
||||
vars={'domains': domains},
|
||||
where='(domain IN $domains OR alias_domain IN $domains) AND verified=0')
|
||||
else:
|
||||
conn.delete('domain_ownership',
|
||||
vars={'domains': domains, 'admin': session.get('username')},
|
||||
where='(domain IN $domains OR alias_domain IN $domains) AND admin=$admin AND verified=0')
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def _generate_verify_code():
|
||||
"""Generate a random and unique string as verify code."""
|
||||
s = iredutils.generate_random_strings(20)
|
||||
return settings.DOMAIN_OWNERSHIP_VERIFY_CODE_PREFIX + s
|
||||
|
||||
|
||||
def set_verify_code_for_new_domains(primary_domain, alias_domains=None, conn=None):
|
||||
"""Generate new unique verify codes for mail domains.
|
||||
|
||||
primary_domain -- the primary mail domain name
|
||||
alias_domains -- alias domains of primary domain
|
||||
conn -- sql connection cursor (for `iredadmin` database)
|
||||
"""
|
||||
if not settings.REQUIRE_DOMAIN_OWNERSHIP_VERIFICATION:
|
||||
# Bypass domain verification.
|
||||
return True,
|
||||
|
||||
if not iredutils.is_domain(primary_domain):
|
||||
return False, 'INVALID_DOMAIN_NAME'
|
||||
|
||||
if alias_domains:
|
||||
alias_domains = [str(d).lower() for d in alias_domains if iredutils.is_domain(d)]
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
if session.get('is_global_admin'):
|
||||
admin = ''
|
||||
else:
|
||||
admin = session.get('username')
|
||||
|
||||
try:
|
||||
expire = int(time.time()) + settings.DOMAIN_OWNERSHIP_EXPIRE_DAYS * 24 * 60 * 60
|
||||
|
||||
if alias_domains:
|
||||
for d in alias_domains:
|
||||
try:
|
||||
conn.insert('domain_ownership',
|
||||
admin=admin,
|
||||
domain=primary_domain,
|
||||
alias_domain=d,
|
||||
verify_code=_generate_verify_code(),
|
||||
expire=expire)
|
||||
except Exception as e:
|
||||
if e.__class__.__name__ != 'IntegrityError':
|
||||
return False, repr(e)
|
||||
else:
|
||||
try:
|
||||
conn.insert('domain_ownership',
|
||||
admin=admin,
|
||||
domain=primary_domain,
|
||||
verify_code=_generate_verify_code(),
|
||||
expire=expire)
|
||||
except Exception as e:
|
||||
if e.__class__.__name__ != 'IntegrityError':
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def mark_ownership_as_verified(rid=None, domain=None, message=None, conn=None):
|
||||
"""Update `iredadmin.domain_ownership` with `verified=1` and
|
||||
`message=<reason>` (optional).
|
||||
|
||||
@rid -- the value of column `domain_ownership.id`
|
||||
@domain -- domain name of `domain_ownership.domain` or `domain_ownership.alias_domain`
|
||||
@message -- the verify message
|
||||
@conn -- sql connection cursor
|
||||
"""
|
||||
if not (rid or domain):
|
||||
return True,
|
||||
|
||||
if domain:
|
||||
if not iredutils.is_domain:
|
||||
return False, 'INVALID_DOMAIN_NAME'
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
if not message:
|
||||
message = ''
|
||||
|
||||
# Get value of sql column `domain_ownership.id`
|
||||
if domain:
|
||||
try:
|
||||
qr = conn.select('domain_ownership',
|
||||
vars={'domain': domain},
|
||||
what='id',
|
||||
where="(alias_domain=$domain) OR (domain=$domain AND alias_domain='')",
|
||||
limit=1)
|
||||
if qr:
|
||||
rid = qr[0].id
|
||||
else:
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
try:
|
||||
conn.update('domain_ownership',
|
||||
vars={'id': rid},
|
||||
verified=1,
|
||||
message=message,
|
||||
last_verify=web.sqlliteral('NOW()'),
|
||||
where='id=$id')
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def verify_domain_ownership(domains, conn=None):
|
||||
"""Verify domain ownership for given domain names.
|
||||
|
||||
Returned values:
|
||||
|
||||
(True, [(primary_domain, alias_domain), ...]): if some domains were
|
||||
successfully verified.
|
||||
(False, <reason>): if some error happened while verifying.
|
||||
|
||||
Parameters:
|
||||
|
||||
@domains -- a list/tuple/set of domain names
|
||||
@conn -- sql connection cursor (of 'iredadmin' database)
|
||||
"""
|
||||
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
|
||||
if not domains:
|
||||
return True, []
|
||||
|
||||
if not conn:
|
||||
conn = web.conn_iredadmin
|
||||
|
||||
# Get verify code of given domains.
|
||||
if session.get('is_global_admin'):
|
||||
qr = conn.select(
|
||||
'domain_ownership',
|
||||
vars={'domains': domains},
|
||||
where="verified=0 AND ((domain IN $domains AND alias_domain='') OR (alias_domain IN $domains))",
|
||||
)
|
||||
else:
|
||||
qr = conn.select(
|
||||
'domain_ownership',
|
||||
vars={'domains': domains, 'admin': session.get('username')},
|
||||
where="verified=0 AND admin=$admin AND ((domain IN $domains AND alias_domain='') OR (alias_domain IN $domains))",
|
||||
)
|
||||
|
||||
if not qr:
|
||||
return True, []
|
||||
|
||||
verified_domains = []
|
||||
expire = int(time.time()) + settings.DOMAIN_OWNERSHIP_EXPIRE_DAYS * 24 * 60 * 60
|
||||
for r in qr:
|
||||
rid = int(r.id)
|
||||
domain = str(r.domain).lower()
|
||||
alias_domain = str(r.alias_domain).lower()
|
||||
verify_code = str(r.verify_code)
|
||||
|
||||
if iredutils.is_domain(alias_domain):
|
||||
verify_domain = alias_domain
|
||||
else:
|
||||
verify_domain = domain
|
||||
|
||||
# web files
|
||||
_web_file = str(verify_domain + '/' + verify_code)
|
||||
|
||||
_verified = False
|
||||
_verified_reason = ''
|
||||
_verify_result = ''
|
||||
|
||||
# Verify web files
|
||||
for _scheme in ['http', 'https']:
|
||||
url = _scheme + '://' + _web_file
|
||||
|
||||
# settings.HTTP_PROXY
|
||||
_proxies = {}
|
||||
if settings.HTTP_PROXY:
|
||||
_proxies = {
|
||||
'http': settings.HTTP_PROXY,
|
||||
'https': settings.HTTP_PROXY,
|
||||
}
|
||||
|
||||
# MAXFILESIZE, 1024) # maximum file size allowed to download, read, fetch
|
||||
# setopt(c.BUFFERSIZE, 1024) # buffer read size: 1024 bytes
|
||||
# _resp_code == 200:
|
||||
try:
|
||||
with requests.get(url,
|
||||
proxies=_proxies,
|
||||
verify=False, # no SSL certificate verifying
|
||||
timeout=settings.DOMAIN_OWNERSHIP_VERIFY_TIMEOUT,
|
||||
stream=True, # defer downloading the response body
|
||||
) as resp:
|
||||
if resp.status_code == 200:
|
||||
pass
|
||||
elif resp.status_code == 404:
|
||||
_verify_result += '%s:// file not found. ' % _scheme
|
||||
else:
|
||||
_verify_result += '%s://, response code must be 200, but got %d. ' % (_scheme, resp.status_code)
|
||||
continue
|
||||
|
||||
try:
|
||||
if int(r.headers['content-length']) < 1024:
|
||||
_body = r.content.strip()
|
||||
|
||||
if _body == verify_code:
|
||||
_verified = True
|
||||
_verified_reason = '%s matches' % _scheme
|
||||
break
|
||||
else:
|
||||
_verify_result += '{}:// file content too long. '.format(_scheme)
|
||||
continue
|
||||
except Exception as e:
|
||||
_verify_result += '{}:// error while reading file content: {}. '.format(_scheme, repr(e))
|
||||
continue
|
||||
except Exception as e:
|
||||
_verify_result += 'Error while verifying {}://: {}. '.format(_scheme, repr(e))
|
||||
|
||||
# Verify TXT type DNS record
|
||||
if not _verified:
|
||||
try:
|
||||
_res = resolver.Resolver()
|
||||
_res.timeout = settings.DOMAIN_OWNERSHIP_VERIFY_TIMEOUT
|
||||
qr_dns = _res.query(domain, 'TXT')
|
||||
for i in qr_dns:
|
||||
_txt = i.to_text().strip('"')
|
||||
if verify_code == _txt:
|
||||
_verified = True
|
||||
_verified_reason = 'DNS record matches'
|
||||
break
|
||||
|
||||
_verify_result += "Verify code is not found as one of TXT type DNS records."
|
||||
except Exception as e:
|
||||
_verify_result += 'Error while querying DNS: %s.' % repr(e)
|
||||
|
||||
if _verified:
|
||||
verified_domains += [(domain, alias_domain)]
|
||||
|
||||
qr = mark_ownership_as_verified(rid=rid, message=_verified_reason, conn=conn)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
else:
|
||||
# Update last verify time, verify result, and expire time
|
||||
try:
|
||||
conn.update('domain_ownership',
|
||||
message=_verify_result,
|
||||
last_verify=web.sqlliteral('NOW()'),
|
||||
expire=expire,
|
||||
where='id=%d' % rid)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True, verified_domains
|
||||
126
libs/panel/log.py
Normal file
126
libs/panel/log.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
|
||||
import settings
|
||||
from libs import iredutils
|
||||
from libs.panel import LOG_EVENTS
|
||||
|
||||
if settings.backend == 'ldap':
|
||||
from libs.ldaplib.general import is_domain_admin
|
||||
from libs.ldaplib.admin import get_managed_domains
|
||||
else:
|
||||
from libs.sqllib.general import is_domain_admin
|
||||
from libs.sqllib.admin import get_managed_domains
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
|
||||
def list_logs(event='all', domain='all', admin='all', cur_page=1):
|
||||
event = web.safestr(event)
|
||||
domain = web.safestr(domain)
|
||||
admin = web.safestr(admin)
|
||||
cur_page = int(cur_page)
|
||||
|
||||
sql_vars = {}
|
||||
sql_wheres = []
|
||||
sql_where = ''
|
||||
|
||||
if event not in LOG_EVENTS:
|
||||
event = "all"
|
||||
|
||||
if event != 'all':
|
||||
sql_vars['event'] = event
|
||||
sql_wheres += ["event=$event"]
|
||||
|
||||
if iredutils.is_domain(domain):
|
||||
if session.get('is_global_admin') or is_domain_admin(domain=domain, admin=session['username'], conn=None):
|
||||
sql_vars['domain'] = domain
|
||||
sql_wheres += ["domain=$domain"]
|
||||
else:
|
||||
# Get managed domains.
|
||||
if not session.get("is_global_admin"):
|
||||
if settings.backend == 'ldap':
|
||||
qr = get_managed_domains(admin=session["username"],
|
||||
attributes=None,
|
||||
domain_name_only=True,
|
||||
conn=None)
|
||||
|
||||
else:
|
||||
qr = get_managed_domains(admin=session["username"],
|
||||
domain_name_only=True,
|
||||
listed_only=True,
|
||||
conn=None)
|
||||
if qr[0]:
|
||||
sql_vars["managed_domains"] = qr[1]
|
||||
sql_wheres += ["domain IN $managed_domains"]
|
||||
else:
|
||||
return qr
|
||||
|
||||
if iredutils.is_email(admin):
|
||||
if session.get('is_global_admin'):
|
||||
sql_vars['admin'] = admin
|
||||
sql_wheres += ["admin=$admin"]
|
||||
else:
|
||||
sql_vars['admin'] = session.get('username')
|
||||
sql_wheres += ["admin=$admin"]
|
||||
else:
|
||||
if not session.get('is_global_admin'):
|
||||
sql_vars['admin'] = session.get('username')
|
||||
sql_wheres += ["admin=$admin"]
|
||||
|
||||
# Get number of total records.
|
||||
if sql_wheres:
|
||||
sql_where = ' AND '.join(sql_wheres)
|
||||
|
||||
qr = web.conn_iredadmin.select(
|
||||
'log',
|
||||
vars=sql_vars,
|
||||
what='COUNT(id) AS total',
|
||||
where=sql_where,
|
||||
)
|
||||
else:
|
||||
qr = web.conn_iredadmin.select('log', what='COUNT(id) AS total')
|
||||
|
||||
total = qr[0].total or 0
|
||||
|
||||
# Get records.
|
||||
if sql_wheres:
|
||||
qr = web.conn_iredadmin.select(
|
||||
'log',
|
||||
vars=sql_vars,
|
||||
where=sql_where,
|
||||
offset=(cur_page - 1) * settings.PAGE_SIZE_LIMIT,
|
||||
limit=settings.PAGE_SIZE_LIMIT,
|
||||
order='timestamp DESC',
|
||||
)
|
||||
else:
|
||||
# No addition filter.
|
||||
qr = web.conn_iredadmin.select(
|
||||
'log',
|
||||
offset=(cur_page - 1) * settings.PAGE_SIZE_LIMIT,
|
||||
limit=settings.PAGE_SIZE_LIMIT,
|
||||
order='timestamp DESC',
|
||||
)
|
||||
|
||||
return total, list(qr)
|
||||
|
||||
|
||||
def delete_logs(form, delete_all=False):
|
||||
if delete_all:
|
||||
try:
|
||||
web.conn_iredadmin.delete('log', where="1=1")
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
else:
|
||||
ids = form.get('id', [])
|
||||
|
||||
if ids:
|
||||
try:
|
||||
web.conn_iredadmin.delete('log', where="id IN %s" % web.db.sqlquote(ids))
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return True,
|
||||
59
libs/regxes.py
Normal file
59
libs/regxes.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Regular expressions of email address, IP address, network.
|
||||
|
||||
import re
|
||||
|
||||
# Email address.
|
||||
#
|
||||
# - `+`, `=` are used in SRS rewritten addresses.
|
||||
# - `/` is sub-folder. e.g. 'john+lists/abc/def@domain.com' will create
|
||||
# directory `lists` and its sub-folders `lists/abc/`, `lists/abc/def`.
|
||||
email = r"""[\w\-\#][\w\-\.\+\=\/\&\#]*@[\w\-][\w\-\.]*\.[a-zA-Z0-9\-]{2,25}"""
|
||||
cmp_email = re.compile(r"^" + email + r"$", re.IGNORECASE | re.DOTALL | re.ASCII)
|
||||
|
||||
# Email address allowed by locally created mail user.
|
||||
#
|
||||
# `auth_email` allows less characters than `email`.
|
||||
# Disallowed chars: `+`, `=`, `/`.
|
||||
auth_email = r"""[\w\-\#][\w\-\=\.\&\#]*@[\w\-][\w\-\.]*\.[a-zA-Z0-9\-]{2,25}"""
|
||||
cmp_auth_email = re.compile(r"^" + auth_email + r"$", re.IGNORECASE | re.DOTALL | re.ASCII)
|
||||
|
||||
# Wildcard sender address: 'user@*'
|
||||
wildcard_addr = r"""[\w\-][\w\-\.\+\=]*@\*"""
|
||||
cmp_wildcard_addr = re.compile(r"^" + wildcard_addr + r"$", re.IGNORECASE | re.DOTALL | re.ASCII)
|
||||
|
||||
#
|
||||
# Domain name
|
||||
#
|
||||
# Single domain name.
|
||||
domain = r"""[\w\-][\w\-\.]*\.[a-z0-9\-]{2,25}"""
|
||||
cmp_domain = re.compile(r"^" + domain + r"$", re.IGNORECASE | re.DOTALL | re.ASCII)
|
||||
|
||||
# Top level domain. e.g. .com, .biz, .org.
|
||||
top_level_domain = r"""[a-z0-9\-]{2,25}"""
|
||||
cmp_top_level_domain = re.compile(r"^" + top_level_domain + r"$", re.IGNORECASE | re.DOTALL | re.ASCII)
|
||||
|
||||
# Valid first char of domain name, email address.
|
||||
valid_account_first_char = r"""^[0-9a-zA-Z]{1,1}$"""
|
||||
cmp_valid_account_first_char = re.compile(r"^" + valid_account_first_char + r"$", re.IGNORECASE)
|
||||
|
||||
# WARNING: This is used for simple URL matching, not used to verify IP address.
|
||||
ip = r"[0-9a-zA-Z\.\:]+"
|
||||
|
||||
# Wildcard IPv4: 192.168.0.*
|
||||
wildcard_ipv4 = r"(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})\.(?:[\d\*]{1,3})$"
|
||||
cmp_wildcard_ipv4 = re.compile(wildcard_ipv4, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
# Mailing list id, a server-wide unique 36-char string.
|
||||
mailing_list_id = r"[a-zA-Z0-9\-]{36}"
|
||||
cmp_mailing_list_id = re.compile(r"^" + mailing_list_id + r"$")
|
||||
|
||||
# Mailing list subscription confirm token. a 32-char string.
|
||||
mailing_list_confirm_token = r"[a-zA-Z0-9]{32}"
|
||||
cmp_mailing_list_confirm_token = re.compile(r"^" + mailing_list_confirm_token + r"$")
|
||||
|
||||
#
|
||||
# Mailbox
|
||||
#
|
||||
# Set mailbox folder name. Could be either empty or up to 20 characters.
|
||||
mailbox_folder = r"""[a-zA-Z0-9]{0,20}"""
|
||||
cmp_mailbox_folder = re.compile(r"^" + mailbox_folder + r"$")
|
||||
66
libs/sqllib/__init__.py
Normal file
66
libs/sqllib/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
import settings
|
||||
|
||||
from libs.logger import logger
|
||||
|
||||
|
||||
class MYSQLWrap:
|
||||
def __del__(self):
|
||||
try:
|
||||
self.conn.ctx.db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
conn = web.database(
|
||||
dbn='mysql',
|
||||
host=settings.vmail_db_host,
|
||||
port=int(settings.vmail_db_port),
|
||||
db=settings.vmail_db_name,
|
||||
user=settings.vmail_db_user,
|
||||
pw=settings.vmail_db_password,
|
||||
charset='utf8')
|
||||
|
||||
conn.supports_multiple_insert = True
|
||||
|
||||
return conn
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.conn = self.connect()
|
||||
except AttributeError:
|
||||
# Reconnect if error raised: MySQL server has gone away.
|
||||
self.conn = self.connect()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
class PGSQLWrap:
|
||||
def __del__(self):
|
||||
try:
|
||||
self.conn.ctx.db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
# Initial DB connection and cursor.
|
||||
try:
|
||||
self.conn = web.database(
|
||||
dbn='postgres',
|
||||
host=settings.vmail_db_host,
|
||||
port=int(settings.vmail_db_port),
|
||||
db=settings.vmail_db_name,
|
||||
user=settings.vmail_db_user,
|
||||
pw=settings.vmail_db_password,
|
||||
)
|
||||
self.conn.supports_multiple_insert = True
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
if settings.backend == 'mysql':
|
||||
SQLWrap = MYSQLWrap
|
||||
elif settings.backend == 'pgsql':
|
||||
SQLWrap = PGSQLWrap
|
||||
1279
libs/sqllib/admin.py
Normal file
1279
libs/sqllib/admin.py
Normal file
File diff suppressed because it is too large
Load Diff
656
libs/sqllib/alias.py
Normal file
656
libs/sqllib/alias.py
Normal file
@@ -0,0 +1,656 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
import settings
|
||||
from libs import iredutils, form_utils
|
||||
|
||||
from libs.logger import logger, log_activity
|
||||
from libs.sqllib import SQLWrap, decorators
|
||||
from libs.sqllib import general as sql_lib_general
|
||||
from libs.sqllib import domain as sql_lib_domain
|
||||
|
||||
session = web.config.get('_session')
|
||||
|
||||
|
||||
@decorators.require_domain_access
|
||||
def change_email(mail, new_mail, conn=None):
|
||||
if not iredutils.is_email(mail):
|
||||
return False, 'INVALID_OLD_EMAIL'
|
||||
|
||||
if not iredutils.is_email(new_mail):
|
||||
return False, 'INVALID_NEW_EMAIL'
|
||||
|
||||
old_domain = mail.split('@', 1)[-1]
|
||||
new_domain = new_mail.split('@', 1)[-1]
|
||||
|
||||
if old_domain != new_domain:
|
||||
return False, 'PERMISSION_DENIED'
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
if not sql_lib_general.is_email_exists(mail=mail, conn=conn):
|
||||
return False, 'OLD_EMAIL_NOT_EXIST'
|
||||
|
||||
if sql_lib_general.is_email_exists(mail=new_mail, conn=conn):
|
||||
return False, 'NEW_EMAIL_ALREADY_EXISTS'
|
||||
|
||||
# Change email address
|
||||
try:
|
||||
sql_vars = {'mail': mail, 'new_mail': new_mail}
|
||||
|
||||
conn.update('alias',
|
||||
vars=sql_vars,
|
||||
address=new_mail,
|
||||
where='address=$mail')
|
||||
|
||||
# Update per-user mail forwardings, alias memberships
|
||||
conn.update('forwardings',
|
||||
vars=sql_vars,
|
||||
address=new_mail,
|
||||
where='address=$mail')
|
||||
|
||||
conn.update('forwardings',
|
||||
vars=sql_vars,
|
||||
forwarding=new_mail,
|
||||
where='forwarding=$mail')
|
||||
|
||||
# Update moderators
|
||||
conn.update('moderators',
|
||||
vars=sql_vars,
|
||||
address=new_mail,
|
||||
where='address=$mail')
|
||||
|
||||
conn.update('moderators',
|
||||
vars=sql_vars,
|
||||
moderator=new_mail,
|
||||
where='moderator=$mail')
|
||||
|
||||
log_activity(event='update',
|
||||
domain=old_domain,
|
||||
msg="Change alias account email address: {} -> {}.".format(mail, new_mail))
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def add_alias_from_form(domain, form, conn=None):
|
||||
# Get domain name, username, cn.
|
||||
form_domain = form_utils.get_domain_name(form)
|
||||
username = web.safestr(form.get('listname')).strip().lower()
|
||||
mail = username + '@' + form_domain
|
||||
|
||||
if domain != form_domain:
|
||||
return False, 'PERMISSION_DENIED'
|
||||
|
||||
if not iredutils.is_domain(domain):
|
||||
return False, 'INVALID_DOMAIN_NAME'
|
||||
|
||||
if not iredutils.is_auth_email(mail):
|
||||
return False, 'INVALID_MAIL'
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
# Check account existing.
|
||||
if sql_lib_general.is_email_exists(mail=mail, conn=conn):
|
||||
return False, 'ALREADY_EXISTS'
|
||||
|
||||
# Get domain profile.
|
||||
qr_profile = sql_lib_domain.profile(conn=conn, domain=domain)
|
||||
|
||||
if qr_profile[0]:
|
||||
domain_profile = qr_profile[1]
|
||||
else:
|
||||
return qr_profile
|
||||
|
||||
# Check account limit.
|
||||
num_exist = num_aliases_under_domain(conn=conn, domain=domain)
|
||||
|
||||
if domain_profile.aliases == -1:
|
||||
return False, 'NOT_ALLOWED'
|
||||
elif domain_profile.aliases > 0:
|
||||
if domain_profile.aliases <= num_exist:
|
||||
return False, 'EXCEEDED_DOMAIN_ACCOUNT_LIMIT'
|
||||
|
||||
# Define columns and values used to insert.
|
||||
columns = {
|
||||
'address': mail, 'domain': domain,
|
||||
'name': form_utils.get_name(form=form),
|
||||
'created': iredutils.get_gmttime(), 'active': 1,
|
||||
'accesspolicy': form_utils.get_list_access_policy(form=form,
|
||||
input_name='accessPolicy',
|
||||
default_value='public'),
|
||||
}
|
||||
|
||||
# Get access policy
|
||||
|
||||
try:
|
||||
conn.insert('alias', **columns)
|
||||
|
||||
log_activity(msg="Create mail alias: %s." % mail,
|
||||
domain=domain,
|
||||
event='create')
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def delete_aliases(accounts, conn=None):
|
||||
"""Delete alias accounts under same domain."""
|
||||
accounts = [str(i).lower() for i in accounts if iredutils.is_email(i)]
|
||||
if not accounts:
|
||||
return True,
|
||||
|
||||
# Get domain from first account
|
||||
domain = accounts[0].split('@', 1)[-1]
|
||||
if not iredutils.is_domain(domain):
|
||||
return True,
|
||||
|
||||
sql_vars = {'domain': domain, 'accounts': accounts}
|
||||
|
||||
try:
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
conn.delete('alias',
|
||||
vars=sql_vars,
|
||||
where='address IN $accounts')
|
||||
|
||||
conn.delete('forwardings',
|
||||
vars=sql_vars,
|
||||
where='address IN $accounts OR forwarding IN $accounts')
|
||||
|
||||
log_activity(event='delete',
|
||||
domain=accounts[0].split('@', 1)[-1],
|
||||
msg="Delete alias: %s." % ', '.join(accounts))
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
# Remove alias from domain.settings: default_groups
|
||||
qr = sql_lib_domain.remove_default_maillists_in_domain_setting(domain=domain,
|
||||
maillists=accounts,
|
||||
conn=conn)
|
||||
if not qr[0]:
|
||||
return qr
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
@decorators.require_domain_access
|
||||
def num_aliases_under_domain(conn, domain, disabled_only=False, first_char=None):
|
||||
if not iredutils.is_domain(domain):
|
||||
return False, 'INVALID_DOMAIN_NAME'
|
||||
|
||||
num = 0
|
||||
sql_vars = {'domain': domain}
|
||||
|
||||
sql_where = ''
|
||||
if disabled_only:
|
||||
sql_where = ' AND active=0'
|
||||
|
||||
if first_char:
|
||||
sql_where += ' AND address LIKE %s' % web.sqlquote(first_char.lower() + '%')
|
||||
|
||||
try:
|
||||
qr = conn.select('alias',
|
||||
vars=sql_vars,
|
||||
what='COUNT(address) AS total',
|
||||
where='domain=$domain %s' % sql_where)
|
||||
num = qr[0].total or 0
|
||||
except:
|
||||
pass
|
||||
|
||||
return num
|
||||
|
||||
|
||||
@decorators.require_domain_access
|
||||
def get_basic_alias_profiles(domain,
|
||||
columns=None,
|
||||
first_char=None,
|
||||
page=0,
|
||||
email_only=False,
|
||||
disabled_only=False,
|
||||
conn=None):
|
||||
"""Get all aliases under domain.
|
||||
|
||||
Return data:
|
||||
(True, [{'mail': 'alias@domain.com',
|
||||
'name': '...',
|
||||
...other profiles in `vmail.alias` table...
|
||||
'members', [...],
|
||||
'moderators', [...]]
|
||||
"""
|
||||
domain = web.safestr(domain).lower()
|
||||
if not iredutils.is_domain(domain):
|
||||
raise web.seeother('/domains?msg=INVALID_DOMAIN_NAME')
|
||||
|
||||
sql_vars = {'domain': domain}
|
||||
|
||||
if columns:
|
||||
sql_what = ','.join(columns)
|
||||
else:
|
||||
if email_only:
|
||||
sql_what = 'address'
|
||||
else:
|
||||
sql_what = '*'
|
||||
|
||||
# Get alias members
|
||||
additional_sql_where = ''
|
||||
if first_char:
|
||||
additional_sql_where = ' AND address LIKE %s' % web.sqlquote(first_char.lower() + '%')
|
||||
|
||||
if disabled_only:
|
||||
additional_sql_where = ' AND active=0'
|
||||
|
||||
# Get basic alias profiles first
|
||||
try:
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
if page:
|
||||
qr = conn.select('alias',
|
||||
vars=sql_vars,
|
||||
what=sql_what,
|
||||
where='domain=$domain %s' % additional_sql_where,
|
||||
order='address ASC',
|
||||
limit=settings.PAGE_SIZE_LIMIT,
|
||||
offset=(page - 1) * settings.PAGE_SIZE_LIMIT)
|
||||
else:
|
||||
qr = conn.select('alias',
|
||||
vars=sql_vars,
|
||||
what=sql_what,
|
||||
where='domain=$domain %s' % additional_sql_where,
|
||||
order='address ASC')
|
||||
|
||||
if email_only:
|
||||
emails = []
|
||||
for r in qr:
|
||||
email = str(r.address).lower()
|
||||
emails.append(email)
|
||||
|
||||
emails.sort()
|
||||
return True, emails
|
||||
else:
|
||||
return True, list(qr)
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
@decorators.require_domain_access
|
||||
def get_profile(mail,
|
||||
with_members=True,
|
||||
with_moderators=True,
|
||||
conn=None):
|
||||
if not iredutils.is_email(mail):
|
||||
return False, 'INVALID_MAIL'
|
||||
|
||||
try:
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
qr = conn.select('alias',
|
||||
vars={'address': mail},
|
||||
where='address=$address',
|
||||
limit=1)
|
||||
|
||||
if qr:
|
||||
profile = list(qr)[0]
|
||||
|
||||
if with_members:
|
||||
_qr = get_member_emails(mail=mail, conn=conn)
|
||||
if _qr[0]:
|
||||
profile['members'] = _qr[1]
|
||||
profile['members'].sort()
|
||||
else:
|
||||
return _qr
|
||||
|
||||
if with_moderators:
|
||||
_qr = get_moderators(mail=mail, conn=conn)
|
||||
if _qr[0]:
|
||||
profile['moderators'] = _qr[1]
|
||||
profile['moderators'].sort()
|
||||
else:
|
||||
return _qr
|
||||
|
||||
return True, profile
|
||||
else:
|
||||
return False, 'NO_SUCH_ACCOUNT'
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
@decorators.require_domain_access
|
||||
def update(mail, profile_type, form, conn=None):
|
||||
mail = web.safestr(mail).lower()
|
||||
domain = mail.split('@', 1)[-1]
|
||||
|
||||
if not iredutils.is_email(mail):
|
||||
return False, 'INVALID_MAIL'
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
# change email address
|
||||
if profile_type == 'rename':
|
||||
# new email address
|
||||
new_mail = web.safestr(form.get('new_mail_username')).strip().lower() + '@' + domain
|
||||
qr = change_email(mail=mail, new_mail=new_mail, conn=conn)
|
||||
if qr[0]:
|
||||
raise web.seeother('/profile/alias/general/%s?msg=EMAIL_CHANGED' % new_mail)
|
||||
else:
|
||||
raise web.seeother('/profile/alias/general/{}?msg={}'.format(new_mail, web.urlquote(qr[1])))
|
||||
|
||||
# Pre-defined.
|
||||
values = {'modified': iredutils.get_gmttime()}
|
||||
|
||||
# Get cn.
|
||||
cn = form.get('cn', '')
|
||||
values['name'] = cn
|
||||
|
||||
# check account status.
|
||||
values['active'] = 0
|
||||
if 'accountStatus' in form:
|
||||
# Enabled.
|
||||
values['active'] = 1
|
||||
|
||||
# Get access policy.
|
||||
access_policy = str(form.get('accessPolicy'))
|
||||
if access_policy in iredutils.MAILLIST_ACCESS_POLICIES:
|
||||
values['accesspolicy'] = access_policy
|
||||
|
||||
# Get members & moderators from web form.
|
||||
_members = form_utils.get_multi_values_from_textarea(form=form,
|
||||
input_name='members',
|
||||
is_email=True)
|
||||
|
||||
_members = list({iredutils.lower_email_with_upper_ext_address(v) for v in _members})
|
||||
|
||||
_moderators = [str(v).strip().lower() for v in form.get('moderators', '').splitlines()]
|
||||
_moderators = list({iredutils.lower_email_with_upper_ext_address(v)
|
||||
for v in _moderators
|
||||
if iredutils.is_email(v) or v.startswith('*@')})
|
||||
_moderators_wildcard = [v for v in _moderators if iredutils.is_domain(v.split('@', 1)[-1])]
|
||||
|
||||
# Remove non-exist accounts in same domain.
|
||||
# Get members & moderators which in same domain.
|
||||
_members_in_domain = [i for i in _members if i.endswith('@' + domain)]
|
||||
_members_not_in_domain = [i for i in _members if not i.endswith('@' + domain)]
|
||||
_moderators_in_domain = [i for i in _moderators if i.endswith('@' + domain) and i not in _moderators_wildcard]
|
||||
_moderators_not_in_domain = [i for i in _moderators if not (i.endswith('@' + domain) or i in _moderators_wildcard)]
|
||||
|
||||
# Verify internal users
|
||||
addresses_in_domain = []
|
||||
_addresses_in_domain = list(set(_members_in_domain + _moderators_in_domain))
|
||||
if _addresses_in_domain:
|
||||
try:
|
||||
# Remove non-existing addresses
|
||||
_qr = sql_lib_general.filter_existing_emails(mails=_addresses_in_domain, conn=conn)
|
||||
addresses_in_domain = _qr['exist']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
members_in_domain = [v for v in _members_in_domain if v in addresses_in_domain]
|
||||
moderators_in_domain = [v for v in _moderators_in_domain if v in addresses_in_domain]
|
||||
|
||||
try:
|
||||
# Update profile
|
||||
conn.update('alias',
|
||||
vars={'address': mail},
|
||||
where='address=$address',
|
||||
**values)
|
||||
|
||||
# Delete all members and moderators first
|
||||
conn.delete('forwardings',
|
||||
vars={'address': mail},
|
||||
where='address=$address')
|
||||
|
||||
conn.delete('moderators',
|
||||
vars={'address': mail},
|
||||
where='address=$address')
|
||||
|
||||
# Add members by inserting new records
|
||||
_all_members = members_in_domain + _members_not_in_domain
|
||||
if _all_members:
|
||||
v = []
|
||||
for _member in _all_members:
|
||||
v += [{'address': mail,
|
||||
'forwarding': _member,
|
||||
'domain': domain,
|
||||
'dest_domain': _member.split('@', 1)[-1],
|
||||
'active': values['active'],
|
||||
'is_list': 1}]
|
||||
|
||||
conn.multiple_insert('forwardings', values=v)
|
||||
|
||||
# Add moderators by inserting new records
|
||||
_all_moderators = moderators_in_domain + _moderators_not_in_domain + _moderators_wildcard
|
||||
if _all_moderators:
|
||||
v = []
|
||||
for _moderator in _all_moderators:
|
||||
v += [{'address': mail,
|
||||
'moderator': _moderator,
|
||||
'domain': domain,
|
||||
'dest_domain': _moderator.split('@', 1)[-1]}]
|
||||
|
||||
conn.multiple_insert('moderators', values=v)
|
||||
|
||||
# Log changes.
|
||||
msg = "Update alias profile (%s)." % mail
|
||||
|
||||
if access_policy:
|
||||
msg += " Access policy: %s." % access_policy
|
||||
|
||||
if _all_members:
|
||||
msg += " Members: %s." % (', '.join(_all_members))
|
||||
else:
|
||||
msg += " No members."
|
||||
|
||||
if _all_moderators:
|
||||
msg += " Moderators: %s." % (', '.join(_all_moderators))
|
||||
else:
|
||||
msg += " No moderators."
|
||||
|
||||
log_activity(msg=msg, username=mail, domain=domain, event='update')
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_member_emails(mail, conn=None):
|
||||
"""Get members of mail alias account. Return a list of mail addresses.
|
||||
|
||||
Return a list with all members' email addresses."""
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
try:
|
||||
qr = conn.select(
|
||||
'forwardings',
|
||||
vars={'mail': mail},
|
||||
what='forwarding',
|
||||
where='address=$mail AND is_list=1',
|
||||
)
|
||||
|
||||
_addresses = [iredutils.lower_email_with_upper_ext_address(i.forwarding)
|
||||
for i in qr if iredutils.is_email(i.forwarding)]
|
||||
_addresses.sort()
|
||||
|
||||
return True, _addresses
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def get_moderators(mail, conn=None):
|
||||
"""Get moderators of given mail alias account.
|
||||
|
||||
Return a list with all moderators' email addresses."""
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
try:
|
||||
qr = conn.select('moderators',
|
||||
vars={'mail': mail},
|
||||
what='moderator',
|
||||
where='address=$mail')
|
||||
|
||||
_addresses = [iredutils.lower_email_with_upper_ext_address(i.moderator)
|
||||
for i in qr
|
||||
if iredutils.is_email(i.moderator) or i.moderator.startswith('*@')]
|
||||
_addresses.sort()
|
||||
|
||||
return True, _addresses
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def reset_members(mail, members, conn=None):
|
||||
"""Assign all given addresses specified in `@members` as members."""
|
||||
_addresses = {iredutils.lower_email_with_upper_ext_address(i)
|
||||
for i in members
|
||||
if iredutils.is_email(i)}
|
||||
|
||||
domain = mail.split('@', 1)[-1]
|
||||
|
||||
_addresses_in_domain = [v for v in _addresses if v.endswith('@' + domain) and v != mail]
|
||||
_addresses_not_in_domain = [v for v in _addresses if not v.endswith('@' + domain)]
|
||||
del _addresses
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
# Verify existence of addresses in same domain
|
||||
if _addresses_in_domain:
|
||||
try:
|
||||
# Remove non-existing addresses
|
||||
qr = sql_lib_general.filter_existing_emails(mails=_addresses_in_domain, conn=conn)
|
||||
_addresses_in_domain = qr['exist']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
try:
|
||||
# Delete all existing members first
|
||||
conn.delete('forwardings',
|
||||
vars={'mail': mail},
|
||||
where='address=$mail AND is_list=1')
|
||||
|
||||
# Add member by inserting new record
|
||||
_all_addresses = _addresses_in_domain + _addresses_not_in_domain
|
||||
if _all_addresses:
|
||||
v = []
|
||||
for i in _all_addresses:
|
||||
v += [{'address': mail,
|
||||
'forwarding': i,
|
||||
'domain': domain,
|
||||
'dest_domain': i.split('@', 1)[-1],
|
||||
'is_list': 1}]
|
||||
|
||||
conn.multiple_insert('forwardings', values=v)
|
||||
|
||||
log_activity(msg='Reset alias ({}) members to: {}'.format(mail, ', '.join(_all_addresses)),
|
||||
admin=session.get('username'),
|
||||
username=mail,
|
||||
domain=domain,
|
||||
event='update')
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
|
||||
def update_members(mail,
|
||||
new_members=None,
|
||||
removed_members=None,
|
||||
conn=None):
|
||||
"""Add new members to mail alias account, and remove removed_members."""
|
||||
_new = []
|
||||
if new_members:
|
||||
_new = [iredutils.lower_email_with_upper_ext_address(i)
|
||||
for i in new_members if iredutils.is_email(i)]
|
||||
|
||||
_removed = []
|
||||
if removed_members:
|
||||
_removed = [iredutils.lower_email_with_upper_ext_address(i)
|
||||
for i in removed_members if iredutils.is_email(i)]
|
||||
|
||||
if not (_new or _removed):
|
||||
return True, 'NO_VALID_MEMBERS'
|
||||
|
||||
domain = mail.split('@', 1)[-1]
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
# Verify existence of addresses in same domain
|
||||
_new_in_domain = set()
|
||||
_new_not_in_domain = set()
|
||||
if _new:
|
||||
for i in _new:
|
||||
if i.endswith('@' + domain):
|
||||
_new_in_domain.add(i)
|
||||
else:
|
||||
_new_not_in_domain.add(i)
|
||||
|
||||
# remove self
|
||||
_new_in_domain.discard(mail)
|
||||
|
||||
if _new_in_domain:
|
||||
try:
|
||||
# Remove non-existing addresses
|
||||
qr = sql_lib_general.filter_existing_emails(mails=_new_in_domain, conn=conn)
|
||||
_new_in_domain = qr['exist']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
# Get existing members
|
||||
qr = get_member_emails(mail=mail, conn=conn)
|
||||
if qr[0]:
|
||||
_old_members = qr[1]
|
||||
else:
|
||||
return qr
|
||||
|
||||
# Add new, remove removed
|
||||
_members = set(_old_members)
|
||||
_members.update(_new_in_domain)
|
||||
_members.update(_new_not_in_domain)
|
||||
_members -= set(_removed)
|
||||
|
||||
try:
|
||||
# Delete all existing members first
|
||||
conn.delete('forwardings',
|
||||
vars={'mail': mail},
|
||||
where='address=$mail AND is_list=1')
|
||||
|
||||
# Add member by inserting new record
|
||||
if _members:
|
||||
v = []
|
||||
for i in _members:
|
||||
v += [{'address': mail,
|
||||
'forwarding': i,
|
||||
'domain': domain,
|
||||
'dest_domain': i.split('@', 1)[-1],
|
||||
'is_list': 1}]
|
||||
|
||||
conn.multiple_insert('forwardings', values=v)
|
||||
|
||||
log_activity(msg='Update alias ({}) members to: {}'.format(mail, ', '.join(_members)),
|
||||
admin=session.get('username'),
|
||||
username=mail,
|
||||
domain=domain,
|
||||
event='update')
|
||||
|
||||
return True,
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
73
libs/sqllib/api_utils.py
Normal file
73
libs/sqllib/api_utils.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import datetime
|
||||
from libs import form_utils
|
||||
|
||||
from libs.sqllib import general as sql_lib_general
|
||||
from libs.sqllib import sqlutils
|
||||
|
||||
import settings
|
||||
|
||||
|
||||
def get_form_password_dict(form,
|
||||
domain,
|
||||
input_name='password',
|
||||
min_passwd_length=None,
|
||||
max_passwd_length=None):
|
||||
"""Extract password from form, verify it, return both plain and hashed password.
|
||||
|
||||
>>> get_form_password_dict(form=form,
|
||||
domain='domain.tld',
|
||||
input_name='password',
|
||||
min_passwd_length=None,
|
||||
max_passwd_length=None)
|
||||
(True, {'pw_plain', '123456',
|
||||
'pw_hash', '{SSHA512}....'})
|
||||
"""
|
||||
if input_name not in form:
|
||||
return False, 'NO_PASSWORD'
|
||||
|
||||
# Get min/max password length from domain profile
|
||||
if not (min_passwd_length or max_passwd_length):
|
||||
qr = sql_lib_general.get_domain_settings(domain=domain)
|
||||
|
||||
if qr[0]:
|
||||
ds = qr[1]
|
||||
min_passwd_length = ds.get('min_passwd_length', settings.min_passwd_length)
|
||||
max_passwd_length = ds.get('max_passwd_length', settings.max_passwd_length)
|
||||
|
||||
qr = form_utils.get_password(form=form,
|
||||
input_name=input_name,
|
||||
confirm_pw_input_name=input_name,
|
||||
min_passwd_length=min_passwd_length,
|
||||
max_passwd_length=max_passwd_length)
|
||||
|
||||
return qr
|
||||
|
||||
|
||||
def export_sql_record(record, remove_columns=None):
|
||||
"""Convert some values in SQL format to general string.
|
||||
|
||||
- datetime
|
||||
- settings
|
||||
"""
|
||||
for (k, v) in list(record.items()):
|
||||
if remove_columns:
|
||||
if k in remove_columns:
|
||||
record.pop(k)
|
||||
continue
|
||||
|
||||
if isinstance(v, datetime.datetime):
|
||||
record[k] = v.isoformat()
|
||||
elif k == 'settings':
|
||||
record[k] = sqlutils.account_settings_string_to_dict(v)
|
||||
|
||||
return record
|
||||
|
||||
|
||||
def export_sql_records(records, remove_columns=None):
|
||||
new_records = []
|
||||
|
||||
for rcd in records:
|
||||
new_rcd = export_sql_record(record=rcd, remove_columns=remove_columns)
|
||||
new_records.append(new_rcd)
|
||||
|
||||
return new_records
|
||||
138
libs/sqllib/auth.py
Normal file
138
libs/sqllib/auth.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import web
|
||||
import settings
|
||||
from libs import iredutils, iredpwd
|
||||
from libs.l10n import TIMEZONES
|
||||
from libs.sqllib import sqlutils
|
||||
|
||||
session = web.config.get('_session', {})
|
||||
|
||||
|
||||
def auth(conn,
|
||||
username,
|
||||
password,
|
||||
account_type='admin',
|
||||
verify_password=False):
|
||||
if not iredutils.is_email(username):
|
||||
return False, 'INVALID_USERNAME'
|
||||
|
||||
if not password:
|
||||
return False, 'EMPTY_PASSWORD'
|
||||
|
||||
username = str(username).lower()
|
||||
password = str(password)
|
||||
domain = username.split('@', 1)[-1]
|
||||
|
||||
# Query account from SQL database.
|
||||
if account_type == 'admin':
|
||||
# separate admin accounts
|
||||
result = conn.select('admin',
|
||||
vars={'username': username},
|
||||
where="username=$username AND active=1",
|
||||
what='password, language, settings',
|
||||
limit=1)
|
||||
|
||||
# mail user marked as domain admin
|
||||
if not result:
|
||||
result = conn.select(
|
||||
["mailbox", "domain"],
|
||||
vars={'username': username},
|
||||
where="mailbox.username=$username AND mailbox.active=1 AND (mailbox.isadmin=1 OR mailbox.isglobaladmin=1) AND mailbox.domain=domain.domain and domain.active=1",
|
||||
what='mailbox.password, mailbox.language, mailbox.isadmin, mailbox.isglobaladmin, mailbox.settings',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if result:
|
||||
session['admin_is_mail_user'] = True
|
||||
elif account_type == 'user':
|
||||
result = conn.select('mailbox',
|
||||
vars={'username': username},
|
||||
what='password, language, isadmin, isglobaladmin, settings',
|
||||
where="username=$username AND active=1",
|
||||
limit=1)
|
||||
else:
|
||||
return False, 'INVALID_ACCOUNT_TYPE'
|
||||
|
||||
if not result:
|
||||
# Account not found.
|
||||
# Do NOT return msg like 'Account does not ***EXIST***', crackers
|
||||
# can use it to verify valid accounts.
|
||||
return False, 'INVALID_CREDENTIALS'
|
||||
|
||||
record = result[0]
|
||||
password_sql = str(record.password)
|
||||
account_settings = sqlutils.account_settings_string_to_dict(str(record.settings))
|
||||
|
||||
# Verify password
|
||||
if not iredpwd.verify_password_hash(password_sql, password):
|
||||
return False, 'INVALID_CREDENTIALS'
|
||||
|
||||
if not verify_password:
|
||||
session['username'] = username
|
||||
|
||||
if account_type == 'user':
|
||||
session['account_is_mail_user'] = True
|
||||
|
||||
# Set preferred language.
|
||||
session['lang'] = web.safestr(record.get('language', settings.default_language))
|
||||
|
||||
# Set timezone (GMT-XX:XX).
|
||||
# Priority: per-user timezone > per-domain > global setting
|
||||
timezone = settings.LOCAL_TIMEZONE
|
||||
|
||||
if 'timezone' in account_settings:
|
||||
tz_name = account_settings['timezone']
|
||||
if tz_name in TIMEZONES:
|
||||
timezone = TIMEZONES[tz_name]
|
||||
else:
|
||||
# Get per-domain timezone
|
||||
qr_domain = conn.select('domain',
|
||||
vars={'domain': domain},
|
||||
what='settings',
|
||||
where='domain=$domain',
|
||||
limit=1)
|
||||
if qr_domain:
|
||||
domain_settings = sqlutils.account_settings_string_to_dict(str(qr_domain[0]['settings']))
|
||||
if 'timezone' in domain_settings:
|
||||
tz_name = domain_settings['timezone']
|
||||
if tz_name in TIMEZONES:
|
||||
timezone = TIMEZONES[tz_name]
|
||||
|
||||
session['timezone'] = timezone
|
||||
|
||||
# Set session['is_global_admin']
|
||||
if session.get('admin_is_mail_user'):
|
||||
if record.get('isglobaladmin', 0) == 1:
|
||||
session['is_global_admin'] = True
|
||||
else:
|
||||
session['is_normal_admin'] = True
|
||||
|
||||
# Set session['allowed_to_grant_admin']
|
||||
if 'grant_admin' in account_settings:
|
||||
session['allowed_to_grant_admin'] = True
|
||||
else:
|
||||
try:
|
||||
result = conn.select('domain_admins',
|
||||
vars={'username': username, 'domain': 'ALL'},
|
||||
what='domain',
|
||||
where='username=$username AND domain=$domain',
|
||||
limit=1)
|
||||
if result:
|
||||
session['is_global_admin'] = True
|
||||
else:
|
||||
if account_type == 'admin':
|
||||
session['is_normal_admin'] = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if session['is_global_admin']:
|
||||
if not iredutils.is_allowed_global_admin_login_ip(client_ip=web.ctx.ip):
|
||||
session.kill()
|
||||
raise web.seeother('/login?msg=NOT_ALLOWED_IP')
|
||||
|
||||
session['logged'] = True
|
||||
|
||||
web.config.session_parameters['cookie_name'] = 'iRedAdmin-Pro'
|
||||
web.config.session_parameters['ignore_change_ip'] = settings.SESSION_IGNORE_CHANGE_IP
|
||||
web.config.session_parameters['ignore_expiry'] = False
|
||||
|
||||
return True, {'account_settings': account_settings}
|
||||
201
libs/sqllib/decorators.py
Normal file
201
libs/sqllib/decorators.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
|
||||
import settings
|
||||
from controllers import decorators as base_decorators
|
||||
from controllers.utils import api_render
|
||||
from libs import iredutils
|
||||
from libs.logger import logger
|
||||
from libs.sqllib import general as sql_lib_general
|
||||
|
||||
session = web.config.get('_session', {})
|
||||
|
||||
# require_api_auth_token = base_decorators.require_api_auth_token
|
||||
require_login = base_decorators.require_login
|
||||
require_admin_login = base_decorators.require_admin_login
|
||||
require_global_admin = base_decorators.require_global_admin
|
||||
csrf_protected = base_decorators.csrf_protected
|
||||
require_permission_create_domain = base_decorators.require_permission_create_domain
|
||||
require_preference_access = base_decorators.require_preference_access
|
||||
|
||||
api_require_admin_login = base_decorators.api_require_admin_login
|
||||
api_require_global_admin = base_decorators.api_require_global_admin
|
||||
|
||||
|
||||
def require_domain_access(func):
|
||||
def proxyfunc(*args, **kw):
|
||||
if not session.get('username'):
|
||||
raise web.seeother('/login?msg=LOGIN_REQUIRED')
|
||||
|
||||
# Check domain global admin.
|
||||
if session.get('is_global_admin'):
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
username = session.get('username')
|
||||
# admin/user is viewing its own data
|
||||
if username == kw.get('mail') \
|
||||
or username.endswith('@' + kw.get('domain', 'NONE')):
|
||||
return func(*args, **kw)
|
||||
|
||||
if 'domain' in kw and iredutils.is_domain(kw.get('domain')):
|
||||
domain = web.safestr(kw['domain'])
|
||||
elif 'mail' in kw and iredutils.is_email(kw.get('mail')):
|
||||
domain = web.safestr(kw['mail']).split('@')[-1]
|
||||
elif 'admin' in kw and iredutils.is_email(kw.get('admin')):
|
||||
domain = web.safestr(kw['admin']).split('@')[-1]
|
||||
else:
|
||||
domain = None
|
||||
# Try to use the first valid domain name or email address as
|
||||
# key, it's passed from controllers/*.
|
||||
for arg in args:
|
||||
if iredutils.is_domain(arg):
|
||||
domain = arg
|
||||
break
|
||||
elif iredutils.is_email(arg):
|
||||
domain = arg.split('@', 1)[-1]
|
||||
break
|
||||
|
||||
if not domain:
|
||||
if settings.LOG_PERMISSION_DENIED:
|
||||
logger.error("PERMISSION_DENIED (1) raised in "
|
||||
"@require_domain_access, triggered in module: "
|
||||
"%s.py, function: %s(). No target domain for "
|
||||
"accessing." % (func.__module__, func.__name__))
|
||||
|
||||
raise web.seeother('/domains?msg=PERMISSION_DENIED')
|
||||
|
||||
# Check whether is domain admin.
|
||||
is_admin = sql_lib_general.is_domain_admin(domain=domain,
|
||||
admin=username)
|
||||
if is_admin:
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
if settings.LOG_PERMISSION_DENIED:
|
||||
logger.error("PERMISSION_DENIED (2) raised in "
|
||||
"@require_domain_access, triggered in module: %s.py, "
|
||||
"function: %s(), accessing data: admin=%s, "
|
||||
"domain=%s" % (func.__module__, func.__name__, username, domain))
|
||||
|
||||
raise web.seeother('/domains?msg=PERMISSION_DENIED')
|
||||
return proxyfunc
|
||||
|
||||
|
||||
def require_user_login(func):
|
||||
def proxyfunc(self, *args, **kw):
|
||||
if session.get('account_is_mail_user'):
|
||||
return func(self, *args, **kw)
|
||||
|
||||
"""
|
||||
elif session.get('is_normal_admin') and session.get('admin_is_mail_user'):
|
||||
# Admin manages other domains but not self domain.
|
||||
# <admin>@<domain.com> doesn't manage <domain.com>
|
||||
admin = session.get('username')
|
||||
domain = admin.split('@', 1)[-1]
|
||||
if not sql_lib_general.is_domain_admin(domain=domain, admin=admin):
|
||||
return func(self, *args, **kw)
|
||||
"""
|
||||
|
||||
session.kill()
|
||||
raise web.seeother('/login?msg=LOGIN_REQUIRED')
|
||||
return proxyfunc
|
||||
|
||||
|
||||
# self-service.
|
||||
def require_ml_owner_or_moderator(func):
|
||||
def proxyfunc(*args, **kw):
|
||||
username = session.get('username')
|
||||
if not username:
|
||||
raise web.seeother('/login?msg=LOGIN_REQUIRED')
|
||||
|
||||
mail = None
|
||||
if 'mail' in kw:
|
||||
# the mailing list
|
||||
mail = kw['mail']
|
||||
if not iredutils.is_email(mail):
|
||||
raise web.seeother("/self-service/mls?msg=INVALID_MAILLIST")
|
||||
else:
|
||||
for i in args:
|
||||
if iredutils.is_email(i):
|
||||
mail = i
|
||||
break
|
||||
|
||||
if not mail:
|
||||
raise web.seeother("/self-service/mls?msg=INVALID_MAILLIST")
|
||||
|
||||
# Check whether user is an owner or moderator.
|
||||
_is_owner_or_moderator = sql_lib_general.is_ml_owner_or_moderator(ml=mail, user=username, conn=None)
|
||||
if _is_owner_or_moderator:
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
if settings.LOG_PERMISSION_DENIED:
|
||||
logger.error("PERMISSION_DENIED (2) raised in "
|
||||
"@require_ml_owner_or_moderator, triggered in module: %s.py, "
|
||||
"function: %s(), accessing data: user=%s, "
|
||||
"maillist=%s" % (func.__module__, func.__name__, username, mail))
|
||||
|
||||
raise web.seeother('/self-service/mls?msg=PERMISSION_DENIED')
|
||||
|
||||
return proxyfunc
|
||||
|
||||
|
||||
def api_require_domain_access(func):
|
||||
def proxyfunc(*args, **kw):
|
||||
if not iredutils.is_allowed_api_client(web.ctx.ip):
|
||||
return api_render((False, 'NOT_AUTHORIZED'))
|
||||
|
||||
if not session.get('username'):
|
||||
return api_render((False, 'LOGIN_REQUIRED'))
|
||||
|
||||
# Check domain global admin.
|
||||
if session.get('is_global_admin'):
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
username = session.get('username')
|
||||
# admin/user is viewing its own data
|
||||
if username == kw.get('mail') \
|
||||
or username.endswith('@' + kw.get('domain', 'NONE')):
|
||||
return func(*args, **kw)
|
||||
|
||||
if 'domain' in kw and iredutils.is_domain(kw.get('domain')):
|
||||
domain = web.safestr(kw['domain'])
|
||||
elif 'mail' in kw and iredutils.is_email(kw.get('mail')):
|
||||
domain = web.safestr(kw['mail']).split('@')[-1]
|
||||
elif 'admin' in kw and iredutils.is_email(kw.get('admin')):
|
||||
domain = web.safestr(kw['admin']).split('@')[-1]
|
||||
else:
|
||||
domain = None
|
||||
# Try to use the first valid domain name or email address as
|
||||
# key, it's passed from controllers/*.
|
||||
for arg in args:
|
||||
if iredutils.is_domain(arg):
|
||||
domain = arg
|
||||
break
|
||||
elif iredutils.is_email(arg):
|
||||
domain = arg.split('@', 1)[-1]
|
||||
break
|
||||
|
||||
if not domain:
|
||||
if settings.LOG_PERMISSION_DENIED:
|
||||
logger.error("PERMISSION_DENIED (1) raised in "
|
||||
"@require_domain_access: module=%s.py, "
|
||||
"function=%s(), admin=%s. "
|
||||
"No target domain for accessing." % (func.__module__, func.__name__, username))
|
||||
|
||||
return api_render((False, 'PERMISSION_DENIED'))
|
||||
|
||||
# Check whether is domain admin.
|
||||
is_admin = sql_lib_general.is_domain_admin(domain=domain,
|
||||
admin=username)
|
||||
if is_admin:
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
if settings.LOG_PERMISSION_DENIED:
|
||||
logger.error("PERMISSION_DENIED (2) raised in "
|
||||
"@require_domain_access: module=%s.py, "
|
||||
"function=%s(), "
|
||||
"admin=%s, "
|
||||
"domain=%s" % (func.__module__, func.__name__, username, domain))
|
||||
|
||||
return api_render((False, 'PERMISSION_DENIED'))
|
||||
return proxyfunc
|
||||
2705
libs/sqllib/domain.py
Normal file
2705
libs/sqllib/domain.py
Normal file
File diff suppressed because it is too large
Load Diff
1084
libs/sqllib/general.py
Normal file
1084
libs/sqllib/general.py
Normal file
File diff suppressed because it is too large
Load Diff
1108
libs/sqllib/ml.py
Normal file
1108
libs/sqllib/ml.py
Normal file
File diff suppressed because it is too large
Load Diff
90
libs/sqllib/sqlutils.py
Normal file
90
libs/sqllib/sqlutils.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
def account_settings_dict_to_string(account_settings: Dict) -> str:
|
||||
# Convert account setting dict to string.
|
||||
# - dict: {'var': 'value', 'var2: value2', ...}
|
||||
# - string: 'var:value;var2:value2;...'
|
||||
if not account_settings or not isinstance(account_settings, dict):
|
||||
return ''
|
||||
|
||||
for (k, v) in list(account_settings.items()):
|
||||
if k in ['default_groups',
|
||||
'default_mailing_lists',
|
||||
'enabled_services',
|
||||
'disabled_mail_services',
|
||||
'disabled_domain_profiles',
|
||||
'disabled_user_profiles',
|
||||
'disabled_user_preferences']:
|
||||
if isinstance(v, (list, tuple, set)):
|
||||
if isinstance(v, list):
|
||||
v.sort()
|
||||
elif isinstance(v, set):
|
||||
v = list(v)
|
||||
v.sort()
|
||||
|
||||
account_settings[k] = ','.join(v)
|
||||
else:
|
||||
# Remove item if value is not a list/tuple/set
|
||||
account_settings.pop(k)
|
||||
|
||||
new_settings = ';'.join(['{}:{}'.format(str(i), j) for (i, j) in list(account_settings.items()) if j])
|
||||
|
||||
if new_settings:
|
||||
new_settings += ';'
|
||||
|
||||
return new_settings
|
||||
|
||||
|
||||
def account_settings_string_to_dict(account_settings: str) -> Dict:
|
||||
# Convert account setting (string, format 'var:value;var2:value2;...', used
|
||||
# in MySQL/PGSQL backends) to dict.
|
||||
# - domain.settings
|
||||
# - mailbox.settings
|
||||
# Original setting must be a string
|
||||
if not account_settings:
|
||||
return {}
|
||||
|
||||
new_settings = {}
|
||||
|
||||
items = [st for st in account_settings.split(';') if ':' in st]
|
||||
for item in items:
|
||||
if item:
|
||||
(k, v) = item.split(':')
|
||||
if v:
|
||||
new_settings[k] = v
|
||||
|
||||
# Convert value to proper format (int, string, ...), default is string.
|
||||
# It will be useful to compare values with converted values.
|
||||
# If original value is not stored in proper format, key:value pair will
|
||||
# be removed.
|
||||
for key in new_settings:
|
||||
# integer
|
||||
if key in ['default_user_quota',
|
||||
'max_user_quota',
|
||||
'min_passwd_length',
|
||||
'max_passwd_length',
|
||||
# settings used to create new domains.
|
||||
'create_max_domains',
|
||||
'create_max_users',
|
||||
'create_max_lists',
|
||||
'create_max_aliases',
|
||||
'create_max_quota']:
|
||||
try:
|
||||
new_settings[key] = int(new_settings[key])
|
||||
except:
|
||||
new_settings.pop(key)
|
||||
|
||||
# list
|
||||
if key in ['enabled_services',
|
||||
'disabled_mail_services',
|
||||
'default_groups',
|
||||
'default_mailing_lists',
|
||||
'disabled_domain_profiles',
|
||||
'disabled_user_profiles',
|
||||
'disabled_user_preferences']:
|
||||
new_settings[key] = [str(i) for i in new_settings[key].split(',') if i]
|
||||
|
||||
return new_settings
|
||||
2626
libs/sqllib/user.py
Normal file
2626
libs/sqllib/user.py
Normal file
File diff suppressed because it is too large
Load Diff
344
libs/sqllib/utils.py
Normal file
344
libs/sqllib/utils.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import web
|
||||
|
||||
from libs import iredutils
|
||||
from libs.logger import log_activity
|
||||
from libs.sqllib import SQLWrap
|
||||
from libs.sqllib import domain as sql_lib_domain
|
||||
from libs.sqllib import admin as sql_lib_admin
|
||||
from libs.sqllib import user as sql_lib_user
|
||||
from libs.sqllib import alias as sql_lib_alias
|
||||
from libs.sqllib import ml as sql_lib_ml
|
||||
from libs.sqllib import general as sql_lib_general
|
||||
|
||||
session = web.config.get('_session', {})
|
||||
|
||||
|
||||
def set_account_status(conn,
|
||||
accounts,
|
||||
account_type,
|
||||
enable_account=False):
|
||||
"""Set account status.
|
||||
|
||||
accounts -- an iterable object (list/tuple) filled with accounts.
|
||||
account_type -- possible value: domain, admin, user, alias, ml
|
||||
enable_account -- possible value: True, False
|
||||
"""
|
||||
if account_type in ['admin', 'user', 'alias', 'maillist', 'ml']:
|
||||
# email
|
||||
accounts = [str(v).lower() for v in accounts if iredutils.is_email(v)]
|
||||
else:
|
||||
# domain name
|
||||
accounts = [str(v).lower() for v in accounts if iredutils.is_domain(v)]
|
||||
|
||||
if not accounts:
|
||||
return True,
|
||||
|
||||
# 0: disable, 1: enable
|
||||
account_status = 0
|
||||
action = 'disable'
|
||||
if enable_account:
|
||||
account_status = 1
|
||||
action = 'active'
|
||||
|
||||
if account_type == 'domain':
|
||||
# handle with function which handles admin privilege
|
||||
qr = sql_lib_domain.enable_disable_domains(domains=accounts,
|
||||
action=action)
|
||||
return qr
|
||||
elif account_type == 'admin':
|
||||
# [(<table>, <column-used-for-query>), ...]
|
||||
table_column_maps = [("admin", "username")]
|
||||
elif account_type == 'alias':
|
||||
table_column_maps = [
|
||||
("alias", "address"),
|
||||
("forwardings", "address"),
|
||||
]
|
||||
elif account_type in ['maillist', 'ml']:
|
||||
table_column_maps = [("maillists", "address")]
|
||||
else:
|
||||
# account_type == 'user'
|
||||
table_column_maps = [
|
||||
("mailbox", "username"),
|
||||
("forwardings", "address"),
|
||||
]
|
||||
|
||||
for (_table, _column) in table_column_maps:
|
||||
sql_where = '{} IN {}'.format(_column, web.sqlquote(accounts))
|
||||
try:
|
||||
conn.update(_table,
|
||||
where=sql_where,
|
||||
active=account_status)
|
||||
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
log_activity(event=action,
|
||||
msg="{} {}: {}.".format(action.title(), account_type, ', '.join(accounts)))
|
||||
return True,
|
||||
|
||||
|
||||
def delete_accounts(accounts,
|
||||
account_type,
|
||||
keep_mailbox_days=0,
|
||||
conn=None):
|
||||
# accounts must be a list/tuple.
|
||||
# account_type in ['domain', 'user', 'admin', 'alias', 'ml']
|
||||
if not accounts:
|
||||
return True,
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
if account_type == 'domain':
|
||||
qr = sql_lib_domain.delete_domains(domains=accounts,
|
||||
keep_mailbox_days=keep_mailbox_days,
|
||||
conn=conn)
|
||||
return qr
|
||||
elif account_type == 'user':
|
||||
sql_lib_user.delete_users(accounts=accounts,
|
||||
keep_mailbox_days=keep_mailbox_days,
|
||||
conn=conn)
|
||||
elif account_type == 'admin':
|
||||
sql_lib_admin.delete_admins(mails=accounts, conn=conn)
|
||||
elif account_type == 'alias':
|
||||
sql_lib_alias.delete_aliases(conn=conn, accounts=accounts)
|
||||
elif account_type == 'ml':
|
||||
sql_lib_ml.delete_maillists(conn=conn, accounts=accounts)
|
||||
|
||||
return True,
|
||||
|
||||
|
||||
# Search accounts with display name, email.
|
||||
def search(search_string,
|
||||
account_type=None,
|
||||
account_status=None,
|
||||
conn=None):
|
||||
"""Return search result in dict.
|
||||
|
||||
(True, {
|
||||
'domain': sql_query_result,
|
||||
'user': sql_query_result,
|
||||
...
|
||||
}
|
||||
)
|
||||
"""
|
||||
sql_vars = {
|
||||
'search_str': '%%' + search_string + '%%',
|
||||
'search_str_exclude_domain': '%%' + search_string + '%%@%%',
|
||||
}
|
||||
|
||||
if not account_type:
|
||||
account_type = ['domain', 'user', 'alias', 'ml', 'admin']
|
||||
|
||||
if not account_status:
|
||||
account_status = ['active', 'disabled']
|
||||
|
||||
sql_where_domain_status = ''
|
||||
sql_where_admin_status = ''
|
||||
sql_where_user_status = ''
|
||||
sql_where_ml_status = ''
|
||||
sql_where_alias_status = ''
|
||||
sql_where_user_domain = ''
|
||||
sql_where_alias_domain = ''
|
||||
sql_where_ml_domain = ''
|
||||
|
||||
if 'active' in account_status and 'disabled' in account_status:
|
||||
pass
|
||||
elif 'active' in account_status:
|
||||
sql_where_domain_status = ' AND domain.active=1'
|
||||
sql_where_admin_status = ' AND domain.active=1'
|
||||
sql_where_user_status = ' AND mailbox.active=1'
|
||||
sql_where_alias_status = ' AND alias.active=1'
|
||||
sql_where_ml_status = ' AND maillists.active=1'
|
||||
elif 'disabled' in account_status:
|
||||
sql_where_domain_status = ' AND domain.active=0'
|
||||
sql_where_admin_status = ' AND domain.active=0'
|
||||
sql_where_user_status = ' AND mailbox.active=0'
|
||||
sql_where_alias_status = ' AND alias.active=0'
|
||||
sql_where_ml_status = ' AND maillists.active=0'
|
||||
|
||||
if not conn:
|
||||
_wrap = SQLWrap()
|
||||
conn = _wrap.conn
|
||||
|
||||
# Get managed domains.
|
||||
if not session.get('is_global_admin'):
|
||||
qr = sql_lib_admin.get_managed_domains(admin=session.get('username'),
|
||||
domain_name_only=True,
|
||||
listed_only=True,
|
||||
conn=conn)
|
||||
|
||||
if qr[0]:
|
||||
managed_domains = qr[1]
|
||||
sql_where_user_domain = ' AND mailbox.domain IN %s' % web.sqlquote(managed_domains)
|
||||
sql_where_alias_domain = ' AND alias.domain IN %s' % web.sqlquote(managed_domains)
|
||||
sql_where_ml_domain = ' AND maillists.domain IN %s' % web.sqlquote(managed_domains)
|
||||
else:
|
||||
raise web.seeother('/search?msg=%s' % web.urlquote(qr[1]))
|
||||
|
||||
result = {
|
||||
'domain': [],
|
||||
'admin': [],
|
||||
'user': [],
|
||||
'ml': [],
|
||||
'last_logins': {},
|
||||
'user_alias_addresses': {},
|
||||
'user_forwarding_addresses': {},
|
||||
'user_assigned_groups': {},
|
||||
'alias': [],
|
||||
# List of email addresses of global admins.
|
||||
'allGlobalAdmins': [],
|
||||
}
|
||||
|
||||
if session.get('is_global_admin'):
|
||||
if 'domain' in account_type:
|
||||
qr_domain = conn.select(
|
||||
'domain',
|
||||
vars=sql_vars,
|
||||
what='domain,description,aliases,mailboxes,maxquota,active',
|
||||
where='(domain LIKE $search_str OR description LIKE $search_str) %s' % sql_where_domain_status,
|
||||
order='domain',
|
||||
)
|
||||
|
||||
if qr_domain:
|
||||
result['domain'] = iredutils.bytes2str(qr_domain)
|
||||
|
||||
if 'admin' in account_type:
|
||||
qr_admin = conn.select(
|
||||
'admin',
|
||||
vars=sql_vars,
|
||||
what='username,name,active',
|
||||
where='(username LIKE $search_str OR name LIKE $search_str) %s' % sql_where_admin_status,
|
||||
order='username',
|
||||
)
|
||||
|
||||
if qr_admin:
|
||||
result['admin'] = iredutils.bytes2str(qr_admin) or []
|
||||
|
||||
# Get all global admin accounts.
|
||||
qr = sql_lib_admin.get_all_global_admins(conn=conn)
|
||||
if qr[0]:
|
||||
result['allGlobalAdmins'] = qr[1]
|
||||
|
||||
# Search user accounts.
|
||||
if 'user' in account_type:
|
||||
search_str_user = sql_vars['search_str_exclude_domain']
|
||||
if '@' in sql_vars['search_str']:
|
||||
search_str_user = sql_vars['search_str']
|
||||
sql_vars['search_str_user'] = search_str_user
|
||||
|
||||
# Query users by email address or display name
|
||||
qr_user = conn.select(
|
||||
'mailbox',
|
||||
vars=sql_vars,
|
||||
what='username,name,quota,employeeid,active',
|
||||
where='(username LIKE $search_str_user OR name LIKE $search_str) {} {}'.format(sql_where_user_status, sql_where_user_domain),
|
||||
order='username')
|
||||
|
||||
# Query users by per-user alias address
|
||||
qr_user_alias = conn.select(
|
||||
['forwardings', 'mailbox'],
|
||||
vars=sql_vars,
|
||||
what='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
|
||||
where='(forwardings.address LIKE $search_str_user) AND forwardings.forwarding=mailbox.username AND forwardings.is_alias=1 {} {}'.format(sql_where_user_status, sql_where_user_domain),
|
||||
group='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
|
||||
order='mailbox.username')
|
||||
|
||||
# Query users by mail forwarding address
|
||||
qr_user_forwarding = conn.select(
|
||||
['forwardings', 'mailbox'],
|
||||
vars=sql_vars,
|
||||
what='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
|
||||
where='(forwardings.forwarding LIKE $search_str_user) AND forwardings.address=mailbox.username AND forwardings.is_forwarding=1 {} {}'.format(sql_where_user_status, sql_where_user_domain),
|
||||
group='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
|
||||
order='mailbox.username')
|
||||
|
||||
if qr_user:
|
||||
result['user'] += iredutils.bytes2str(qr_user)
|
||||
|
||||
if qr_user_alias:
|
||||
_records = iredutils.bytes2str(qr_user_alias)
|
||||
|
||||
# Add new, remove duplicate records.
|
||||
for i in _records:
|
||||
if not (i in result['user']):
|
||||
result['user'] += [i]
|
||||
|
||||
if qr_user_forwarding:
|
||||
_records = iredutils.bytes2str(qr_user_forwarding)
|
||||
|
||||
# Add new, remove duplicate records.
|
||||
for i in _records:
|
||||
if not (i in result['user']):
|
||||
result['user'] += [i]
|
||||
|
||||
# Get email addresses of returned user accounts
|
||||
_user_emails = []
|
||||
for i in result['user']:
|
||||
_user_emails.append(str(i['username']).lower())
|
||||
_user_emails.sort()
|
||||
|
||||
# Get per-user alias and mail forwarding addresses
|
||||
if _user_emails:
|
||||
(_status, _result) = sql_lib_user.get_bulk_user_alias_addresses(mails=_user_emails, conn=conn)
|
||||
if _status:
|
||||
result['user_alias_addresses'] = _result
|
||||
|
||||
(_status, _result) = sql_lib_user.get_bulk_user_forwardings(mails=_user_emails, conn=conn)
|
||||
if _status:
|
||||
result['user_forwarding_addresses'] = _result
|
||||
|
||||
(_status, _result) = sql_lib_user.get_bulk_user_assigned_groups(mails=_user_emails, conn=conn)
|
||||
if _status:
|
||||
result['user_assigned_groups'] = _result
|
||||
|
||||
# Get user last login
|
||||
result['last_logins'] = sql_lib_general.get_account_last_login(accounts=_user_emails, conn=conn)
|
||||
|
||||
# Search alias accounts.
|
||||
if 'alias' in account_type:
|
||||
search_str_alias = sql_vars['search_str_exclude_domain']
|
||||
if '@' in sql_vars['search_str']:
|
||||
search_str_alias = sql_vars['search_str']
|
||||
sql_vars['search_str_alias'] = search_str_alias
|
||||
|
||||
qr_alias = conn.select(
|
||||
'alias',
|
||||
vars=sql_vars,
|
||||
what='address,name,accesspolicy,domain,active',
|
||||
where='(address LIKE $search_str_alias OR name LIKE $search_str) {} {}'.format(
|
||||
sql_where_alias_status, sql_where_alias_domain,
|
||||
),
|
||||
order='address',
|
||||
)
|
||||
|
||||
if qr_alias:
|
||||
result['alias'] = iredutils.bytes2str(qr_alias) or []
|
||||
|
||||
# Search mailing list accounts.
|
||||
if 'ml' in account_type:
|
||||
search_str_ml = sql_vars['search_str_exclude_domain']
|
||||
if '@' in sql_vars['search_str']:
|
||||
search_str_ml = sql_vars['search_str']
|
||||
sql_vars['search_str_ml'] = search_str_ml
|
||||
|
||||
qr_ml = conn.select(
|
||||
'maillists',
|
||||
vars=sql_vars,
|
||||
what='address,name,accesspolicy,domain,active',
|
||||
where='(address LIKE $search_str_alias OR name LIKE $search_str) {} {}'.format(
|
||||
sql_where_ml_status, sql_where_ml_domain,
|
||||
),
|
||||
order='address',
|
||||
)
|
||||
|
||||
if qr_ml:
|
||||
result['ml'] = iredutils.bytes2str(qr_ml) or []
|
||||
|
||||
if result:
|
||||
return True, result
|
||||
else:
|
||||
return False, []
|
||||
230
libs/sysinfo.py
Normal file
230
libs/sysinfo.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# Author: Zhang Huangbin <zhb@iredmail.org>
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import socket
|
||||
import platform
|
||||
import web
|
||||
from os import getloadavg
|
||||
import time
|
||||
|
||||
import simplejson as json
|
||||
|
||||
from libs.logger import log_traceback
|
||||
import settings
|
||||
|
||||
if settings.backend == "ldap":
|
||||
from libs import __version_ldap__ as __version__
|
||||
else:
|
||||
from libs import __version_sql__ as __version__
|
||||
|
||||
__id__ = "meow"
|
||||
session = web.config.get("_session")
|
||||
|
||||
|
||||
def get_iredmail_version():
|
||||
v = "Unknown, check /etc/iredmail-release please."
|
||||
|
||||
# Read first word splited by space in first line.
|
||||
try:
|
||||
f = open("/etc/iredmail-release")
|
||||
vline = f.readline().split()
|
||||
f.close()
|
||||
|
||||
if vline:
|
||||
v = vline[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def __get_proxied_urlopen():
|
||||
socket.setdefaulttimeout(5)
|
||||
|
||||
if settings.HTTP_PROXY:
|
||||
# urllib2 adds proxy handlers with environment variables automatically
|
||||
os.environ["http_proxy"] = settings.HTTP_PROXY
|
||||
os.environ["https_proxy"] = settings.HTTP_PROXY
|
||||
|
||||
return urllib.request.urlopen
|
||||
|
||||
|
||||
def get_license_info():
|
||||
return True, {
|
||||
"status": "active",
|
||||
"product": "iRedAdmin-Pro-SQL",
|
||||
"licensekey": "open-source",
|
||||
"upgradetutorials": "https://docs.iredmail.org/iredadmin-pro.releases.html",
|
||||
"purchased": "Never",
|
||||
"contacts": "Your sys-admin",
|
||||
"latestversion": "5.3",
|
||||
"expired": "Never",
|
||||
"releasenotes": "https://docs.iredmail.org/iredadmin-pro.releases.html",
|
||||
"id": __id__
|
||||
}
|
||||
|
||||
# if len(__id__) != 32:
|
||||
# web.conn_iredadmin.delete("updatelog")
|
||||
# session.kill()
|
||||
# raise web.seeother("/login?msg=INVALID_PRODUCT_ID")
|
||||
|
||||
# params = {
|
||||
# "v": __version__,
|
||||
# "f": __id__,
|
||||
# "lang": settings.default_language,
|
||||
# "host": get_hostname(),
|
||||
# "backend": settings.backend,
|
||||
# "webmaster": settings.webmaster,
|
||||
# "mac": ",".join(get_all_mac_addresses()),
|
||||
# }
|
||||
|
||||
# url = "https://lic.iredmail.org/check_version/licenseinfo/" + __id__ + ".json"
|
||||
# url += "?" + urllib.parse.urlencode(params)
|
||||
|
||||
# try:
|
||||
# urlopen = __get_proxied_urlopen()
|
||||
# _json = urlopen(url).read()
|
||||
# lic_info = json.loads(_json)
|
||||
# lic_info["id"] = __id__
|
||||
# return True, lic_info
|
||||
# except Exception as e:
|
||||
# return False, web.urlquote(e)
|
||||
|
||||
|
||||
def check_new_version():
|
||||
"""Check new version.
|
||||
|
||||
Return (None, None) if no new version available.
|
||||
Return (False, <error>) if any error while checking.
|
||||
Return (True, <new_version_number>) if new version available.
|
||||
"""
|
||||
try:
|
||||
today = time.strftime("%Y-%m-%d")
|
||||
sql_vars = {"today": today}
|
||||
|
||||
# Check whether we already checked new version today
|
||||
r = web.conn_iredadmin.select("updatelog", vars=sql_vars, where="date=$today", limit=1)
|
||||
|
||||
if not r:
|
||||
qr = get_license_info()
|
||||
|
||||
# Always remove all old records, just keep the last one.
|
||||
web.conn_iredadmin.delete("updatelog", vars=sql_vars, where="date < $today")
|
||||
|
||||
if qr[0]:
|
||||
if __version__ >= qr[1]["latestversion"]:
|
||||
# Insert updating date if no new version available.
|
||||
web.conn_iredadmin.insert("updatelog", date=today)
|
||||
else:
|
||||
return True, qr[1]["latestversion"]
|
||||
except Exception as e:
|
||||
return False, repr(e)
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def get_hostname():
|
||||
_hostname = ""
|
||||
|
||||
try:
|
||||
_hostname = socket.getfqdn()
|
||||
except:
|
||||
try:
|
||||
_hostname = platform.node()
|
||||
except:
|
||||
pass
|
||||
|
||||
return _hostname
|
||||
|
||||
|
||||
def get_server_uptime():
|
||||
try:
|
||||
# Works on Linux.
|
||||
f = open("/proc/uptime")
|
||||
contents = f.read().split()
|
||||
f.close()
|
||||
except:
|
||||
return None
|
||||
|
||||
total_seconds = float(contents[0])
|
||||
|
||||
# convert to seconds
|
||||
_minute_secs = 60
|
||||
_hour_secs = _minute_secs * 60
|
||||
_day_secs = _hour_secs * 24
|
||||
|
||||
# Get the days, hours, minutes.
|
||||
days = int(total_seconds / _day_secs)
|
||||
hours = int((total_seconds % _day_secs) / _hour_secs)
|
||||
minutes = int((total_seconds % _hour_secs) / _minute_secs)
|
||||
|
||||
return days, hours, minutes
|
||||
|
||||
|
||||
def get_system_load_average():
|
||||
try:
|
||||
(a1, a2, a3) = getloadavg()
|
||||
a1 = "%.3f" % a1
|
||||
a2 = "%.3f" % a2
|
||||
a3 = "%.3f" % a3
|
||||
return a1, a2, a3
|
||||
except:
|
||||
log_traceback()
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
def get_nic_info():
|
||||
# Return list of basic info of available network interfaces.
|
||||
# Format: [(name, ip_address, netmask), ...]
|
||||
# Sample: [('eth0', '192.168.1.1', '255.255.255.0'), ...]
|
||||
netif_data = []
|
||||
|
||||
try:
|
||||
import netifaces
|
||||
except:
|
||||
return netif_data
|
||||
|
||||
try:
|
||||
ifaces = netifaces.interfaces()
|
||||
|
||||
for iface in ifaces:
|
||||
if iface in ["lo", "lo0"]:
|
||||
# `lo` -> Linux
|
||||
# `lo0` -> OpenBSD
|
||||
continue
|
||||
|
||||
try:
|
||||
addr = netifaces.ifaddresses(iface)
|
||||
|
||||
for af in addr:
|
||||
if af in (netifaces.AF_INET, netifaces.AF_INET6):
|
||||
for item in addr[af]:
|
||||
netif_data.append(
|
||||
(iface, item.get("addr", ""), item.get("netmask", ""))
|
||||
)
|
||||
except:
|
||||
log_traceback()
|
||||
except:
|
||||
log_traceback()
|
||||
|
||||
return netif_data
|
||||
|
||||
|
||||
def get_all_mac_addresses():
|
||||
"""
|
||||
Get list of hardware MAC addresses of all network interfaces.
|
||||
Return a list of addresses.
|
||||
"""
|
||||
mac_addresses = []
|
||||
|
||||
try:
|
||||
for (_iface, _addr, _netmask) in get_nic_info():
|
||||
if _iface != "lo":
|
||||
mac_addresses.append(_addr)
|
||||
except:
|
||||
pass
|
||||
|
||||
return mac_addresses
|
||||
32
requirements.txt
Normal file
32
requirements.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
# The core Python 3 micro web framework: https://webpy.org/
|
||||
web.py>=0.61
|
||||
|
||||
# HTML template engine.
|
||||
Jinja2>=2.2.0
|
||||
|
||||
# LDAP driver.
|
||||
python-ldap>=3.3.1
|
||||
|
||||
# MySQL/MariaDB driver.
|
||||
PyMySQL>=0.9.3
|
||||
|
||||
# PostgreSQL driver.
|
||||
psycopg2
|
||||
|
||||
requests>=2.10.0
|
||||
|
||||
# DNS queries.
|
||||
dnspython
|
||||
|
||||
# Get info of network interfaces.
|
||||
netifaces
|
||||
|
||||
# bcrypt password hash.
|
||||
bcrypt
|
||||
|
||||
# Required by Python 3.5 and LDAP backend.
|
||||
#
|
||||
# Use `simplejson` instead of the Python builtin `json`, because `json` doesn't
|
||||
# support serializing bytes (mostly used by LDAP backend) and raise error
|
||||
# `Object of type 'bytes' is not JSON serializable`.
|
||||
simplejson
|
||||
113
settings.py.mysql.sample
Normal file
113
settings.py.mysql.sample
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
###############################################################
|
||||
# DO NOT MODIFY THIS LINE, IT'S USED TO IMPORT DEFAULT SETTINGS.
|
||||
from libs.default_settings import *
|
||||
###############################################################
|
||||
# General settings.
|
||||
#
|
||||
# Site webmaster's mail address.
|
||||
webmaster = 'zhb@iredmail.org'
|
||||
|
||||
# Default language.
|
||||
default_language = 'en_US'
|
||||
|
||||
# Database backend: mysql.
|
||||
backend = 'mysql'
|
||||
|
||||
# Directory used to store mailboxes. Defaults to /var/vmail/vmail1.
|
||||
# Note: This directory must be owned by 'vmail:vmail' with permission 0700.
|
||||
storage_base_directory = '/var/vmail/vmail1'
|
||||
|
||||
# Default mta transport.
|
||||
# There're 3 transports available in iRedMail:
|
||||
#
|
||||
# 1. dovecot: default LDA transport. Supported by all iRedMail releases.
|
||||
# 2. lmtp:unix:private/dovecot-lmtp: LMTP (socket listener). Supported by
|
||||
# iRedMail-0.8.6 and later releases.
|
||||
# 3. lmtp:inet:127.0.0.1:24: LMTP (TCP listener). Supported by iRedMail-0.8.6
|
||||
# and later releases.
|
||||
#
|
||||
# Note: You can set per-domain or per-user transport in account profile page.
|
||||
default_mta_transport = 'dovecot'
|
||||
|
||||
# Min/Max admin password length. 0 means unlimited.
|
||||
# - min_passwd_length: at least 1 character is required.
|
||||
# Normal admin can not set shorter/longer password lengths than global settings
|
||||
# defined here.
|
||||
min_passwd_length = 8
|
||||
max_passwd_length = 0
|
||||
|
||||
#####################################################################
|
||||
# Database used to store iRedAdmin data. e.g. sessions, log.
|
||||
#
|
||||
iredadmin_db_host = '127.0.0.1'
|
||||
iredadmin_db_port = 3306
|
||||
iredadmin_db_name = 'iredadmin'
|
||||
iredadmin_db_user = 'iredadmin'
|
||||
iredadmin_db_password = 'password'
|
||||
|
||||
############################################
|
||||
# Database used to store mail accounts.
|
||||
#
|
||||
vmail_db_host = '127.0.0.1'
|
||||
vmail_db_port = 3306
|
||||
vmail_db_name = 'vmail'
|
||||
vmail_db_user = 'vmailadmin'
|
||||
vmail_db_password = 'password'
|
||||
|
||||
##############################################################################
|
||||
# Settings used for Amavisd-new integration. Provides spam/virus quaranting,
|
||||
# releasing, etc.
|
||||
#
|
||||
# Log basic info of in/out emails into SQL (@storage_sql_dsn): True, False.
|
||||
# It's @storage_sql_dsn setting in amavisd. You can find this setting
|
||||
# in amavisd-new config files:
|
||||
# - On RHEL/CentOS: /etc/amavisd.conf or /etc/amavisd/amavisd.conf
|
||||
# - On Debian/Ubuntu: /etc/amavis/conf.d/50-user.conf
|
||||
# - On FreeBSD: /usr/local/etc/amavisd.conf
|
||||
amavisd_enable_logging = True
|
||||
|
||||
amavisd_db_host = '127.0.0.1'
|
||||
amavisd_db_port = 3306
|
||||
amavisd_db_name = 'amavisd'
|
||||
amavisd_db_user = 'amavisd'
|
||||
amavisd_db_password = 'password'
|
||||
|
||||
# #### Quarantining ####
|
||||
# Release quarantined SPAM/Virus mails: True, False.
|
||||
# iRedAdmin-Pro will connect to @amavisd_db_host to release quarantined mails.
|
||||
# How to enable quarantining in Amavisd-new:
|
||||
# http://www.iredmail.org/docs/quarantining.html
|
||||
amavisd_enable_quarantine = True
|
||||
|
||||
# Port of Amavisd protocol 'AM.PDP-INET'. Default is 9998.
|
||||
# If Amavisd is not running on database server specified in amavisd_db_host,
|
||||
# please set the server address in parameter `AMAVISD_QUARANTINE_HOST`.
|
||||
# Default is '127.0.0.1'. Sample setting:
|
||||
#AMAVISD_QUARANTINE_HOST = '192.168.1.1'
|
||||
amavisd_quarantine_port = 9998
|
||||
|
||||
# Enable per-recipient spam policy, white/blacklist.
|
||||
amavisd_enable_policy_lookup = True
|
||||
|
||||
##############################################################################
|
||||
# Settings used for iRedAPD integration. Provides throttling and more.
|
||||
#
|
||||
iredapd_enabled = True
|
||||
iredapd_db_host = '127.0.0.1'
|
||||
iredapd_db_port = 3306
|
||||
iredapd_db_name = 'iredapd'
|
||||
iredapd_db_user = 'iredapd'
|
||||
iredapd_db_password = 'password'
|
||||
|
||||
##############################################################################
|
||||
# Settings used for mlmmj (mailing list manager) and mlmmjadmin integration.
|
||||
#
|
||||
# The API auth token required to access mlmmjadmin API.
|
||||
mlmmjadmin_api_auth_token = ''
|
||||
|
||||
##############################################################################
|
||||
# Place your custom settings below, you can override all settings in this file
|
||||
# and libs/default_settings.py here.
|
||||
#
|
||||
113
settings.py.pgsql.sample
Normal file
113
settings.py.pgsql.sample
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
############################################################
|
||||
# DO NOT MODIFY THIS LINE, IT'S USED TO IMPORT DEFAULT SETTINGS.
|
||||
from libs.default_settings import *
|
||||
############################################################
|
||||
# General settings.
|
||||
#
|
||||
# Site webmaster's mail address.
|
||||
webmaster = 'zhb@iredmail.org'
|
||||
|
||||
# Default language.
|
||||
default_language = 'en_US'
|
||||
|
||||
# Database backend: pgsql.
|
||||
backend = 'pgsql'
|
||||
|
||||
# Directory used to store mailboxes. Defaults to /var/vmail/vmail1.
|
||||
# Note: This directory must be owned by 'vmail:vmail' with permission 0700.
|
||||
storage_base_directory = '/var/vmail/vmail1'
|
||||
|
||||
# Default mta transport.
|
||||
# There're 3 transports available in iRedMail:
|
||||
#
|
||||
# 1. dovecot: default LDA transport. Supported by all iRedMail releases.
|
||||
# 2. lmtp:unix:private/dovecot-lmtp: LMTP (socket listener). Supported by
|
||||
# iRedMail-0.8.6 and later releases.
|
||||
# 3. lmtp:inet:127.0.0.1:24: LMTP (TCP listener). Supported by iRedMail-0.8.6
|
||||
# and later releases.
|
||||
#
|
||||
# Note: You can set per-domain or per-user transport in account profile page.
|
||||
default_mta_transport = 'dovecot'
|
||||
|
||||
# Min/Max admin password length. 0 means unlimited.
|
||||
# - min_passwd_length: at least 1 character is required.
|
||||
# Normal admin can not set shorter/longer password lengths than global settings
|
||||
# defined here.
|
||||
min_passwd_length = 8
|
||||
max_passwd_length = 0
|
||||
|
||||
#####################################################################
|
||||
# Database used to store iRedAdmin data. e.g. sessions, log.
|
||||
#
|
||||
iredadmin_db_host = '127.0.0.1'
|
||||
iredadmin_db_port = 5432
|
||||
iredadmin_db_name = 'iredadmin'
|
||||
iredadmin_db_user = 'iredadmin'
|
||||
iredadmin_db_password = 'password'
|
||||
|
||||
############################################
|
||||
# Database used to store mail accounts.
|
||||
#
|
||||
vmail_db_host = '127.0.0.1'
|
||||
vmail_db_port = 5432
|
||||
vmail_db_name = 'vmail'
|
||||
vmail_db_user = 'vmailadmin'
|
||||
vmail_db_password = 'password'
|
||||
|
||||
##############################################################################
|
||||
# Settings used for Amavisd-new integration. Provides spam/virus quaranting,
|
||||
# releasing, etc.
|
||||
#
|
||||
# Log basic info of in/out emails into SQL (@storage_sql_dsn): True, False.
|
||||
# It's @storage_sql_dsn setting in amavisd. You can find this setting
|
||||
# in amavisd-new config files:
|
||||
# - On RHEL/CentOS: /etc/amavisd.conf or /etc/amavisd/amavisd.conf
|
||||
# - On Debian/Ubuntu: /etc/amavis/conf.d/50-user.conf
|
||||
# - On FreeBSD: /usr/local/etc/amavisd.conf
|
||||
amavisd_enable_logging = True
|
||||
|
||||
amavisd_db_host = '127.0.0.1'
|
||||
amavisd_db_port = 5432
|
||||
amavisd_db_name = 'amavisd'
|
||||
amavisd_db_user = 'amavisd'
|
||||
amavisd_db_password = 'password'
|
||||
|
||||
# #### Quarantining ####
|
||||
# Release quarantined SPAM/Virus mails: True, False.
|
||||
# iRedAdmin-Pro will connect to @amavisd_db_host to release quarantined mails.
|
||||
# How to enable quarantining in Amavisd-new:
|
||||
# http://www.iredmail.org/docs/quarantining.html
|
||||
amavisd_enable_quarantine = True
|
||||
|
||||
# Port of Amavisd protocol 'AM.PDP-INET'. Default is 9998.
|
||||
# If Amavisd is not running on database server specified in amavisd_db_host,
|
||||
# please set the server address in parameter `AMAVISD_QUARANTINE_HOST`.
|
||||
# Default is '127.0.0.1'. Sample setting:
|
||||
#AMAVISD_QUARANTINE_HOST = '192.168.1.1'
|
||||
amavisd_quarantine_port = 9998
|
||||
|
||||
# Enable per-recipient spam policy, white/blacklist.
|
||||
amavisd_enable_policy_lookup = True
|
||||
|
||||
##############################################################################
|
||||
# Settings used for iRedAPD integration. Provides throttling and more.
|
||||
#
|
||||
iredapd_enabled = True
|
||||
iredapd_db_host = '127.0.0.1'
|
||||
iredapd_db_port = 5432
|
||||
iredapd_db_name = 'iredapd'
|
||||
iredapd_db_user = 'iredapd'
|
||||
iredapd_db_password = 'password'
|
||||
|
||||
##############################################################################
|
||||
# Settings used for mlmmj (mailing list manager) and mlmmjadmin integration.
|
||||
#
|
||||
# The API auth token required to access mlmmjadmin API.
|
||||
mlmmjadmin_api_auth_token = ''
|
||||
|
||||
############################################################
|
||||
# Place your custom settings below, you can override all settings in this file
|
||||
# and libs/default_settings.py here.
|
||||
#
|
||||
Reference in New Issue
Block a user