From 117aaa82d3c23370222dcc02484a5eb644de9a45 Mon Sep 17 00:00:00 2001 From: Daniel Winzen Date: Wed, 23 Sep 2026 20:24:28 +0200 Subject: [PATCH] Add several administration tools --- .gitignore | 3 + common_config.php | 8 +- setup.php | 49 ++++-- tools/create_export.php | 57 +++++++ tools/crypt_maildir.sh | 4 +- tools/monitor_login.php | 23 +++ tools/process_postmaster_reports.php | 230 +++++++++++++++++++++++++++ 7 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 tools/create_export.php create mode 100755 tools/monitor_login.php create mode 100755 tools/process_postmaster_reports.php diff --git a/.gitignore b/.gitignore index 61cde56..4d50b21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ composer.lock vendor/ *~ +www/snappymail/ +www/squirrelmail/ +.vscode/ diff --git a/common_config.php b/common_config.php index a55706a..8b35843 100644 --- a/common_config.php +++ b/common_config.php @@ -9,16 +9,16 @@ const DBHOST = 'localhost'; // Database host const DBUSER = 'postfix'; // Database user const DBPASS = 'YOUR_PASSWORD'; // Database password const DBNAME = 'postfix'; // Database -const DBVERSION = 1; // Database schema version +const DBVERSION = 2; // Database schema version const PERSISTENT = true; // persistent database connection const CAPTCHA_DIFFICULTY = 1; // captcha difficulty from 0 to 4 const RESERVED_USERNAMES = ['about', 'abuse', 'admin', 'administrator', 'billing', 'contact', 'daemon', 'ftp', 'help', 'hostmaster', 'info', 'legal', 'list', 'list-request', 'lists', 'maildaemon', 'mailerdaemon', 'mailer-daemon', 'marketing', 'media', 'news', 'newsletter', 'nobody', 'noc', 'noreply', 'no-reply', 'notification', 'notifications', 'notify', 'offer', 'offers', 'office', 'official', 'order', 'orders', 'phish', 'phishing', 'postmaster', 'root', 'sale', 'sales', 'security', 'service', 'services', 'shop', 'shopping', 'spam', 'staff', 'support', 'survey', 'system', 'team', 'teams', 'unsbubscribe', 'uucp', 'usenet', 'user', 'username', 'users', 'web', 'webmail', 'webmaster', 'webmasters', 'welcome', 'www']; // list of reserved usernames that can mot be used on public registration -const CANONICAL_URL = 'https://danwin1210.de/mail/'; // our preferred URL prefix for search engines +const CANONICAL_URL = 'https://mail.danwin1210.de/'; // our preferred URL prefix for search engines const PRIVACY_POLICY_URL = '/privacy.php'; // URL to privacy policy const WEB_XMPP_URL = 'https://danwin1210.de:5281/conversejs'; // URL to Web-XMPP const XMPP_BOSH_URL = 'https://danwin1210.de:5281/http-bind'; // XMPP BOSH URL const XMPP_FILE_PROXY = 'proxy.danwin1210.de'; // File proxy domain -const ROOT_URL = '/mail/'; // Relative root URL under which the mail hosting is installed +const ROOT_URL = '/'; // Relative root URL under which the mail hosting is installed const CONTACT_URL = '/contact.php'; // URL to get in contact with you const CLEARNET_SERVER = 'danwin1210.de'; // Clearnet domain of the mail server const ONION_SERVER = 'danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion'; // Onion domain of the mail server @@ -28,6 +28,8 @@ const DBPASS_PROSODY = 'YOUR_PASSWORD'; // Database password const DBNAME_PROSODY = 'prosody'; // Database const REGISTRATION_ENABLED = true; // Whether registration is enabled const DEFAULT_QUOTA = 50 * 1024 * 1024; // Default mailbox quota in bytes +const POSTMASTER_EMAIL = 'postmaster@danwin1210.de'; // Email address of the postmaster account receiving TLS and DMARC reports +const POSTMASTER_PASSWORD = 'YOUR_PASSWORD'; // Password of the postmaster account receiving TLS and DMARC reports const LANGUAGES = [ 'cs' => ['name' => 'čeština', 'locale' => 'cs_CZ', 'flag' => '🇨🇿', 'show_in_menu' => true, 'dir' => 'ltr'], diff --git a/setup.php b/setup.php index ea0fa2e..c7ea5b4 100644 --- a/setup.php +++ b/setup.php @@ -23,12 +23,38 @@ try{ die( _('No Connection to MySQL database!') . PHP_EOL); } } +$createTableStatements = [ + 'admin' => "CREATE TABLE IF NOT EXISTS `admin` (`username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `superadmin` tinyint(1) NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `password_hash_type` varchar(20) NOT NULL DEFAULT '{MD5-CRYPT}', PRIMARY KEY (`username`), KEY `active` (`active`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'domain' => "CREATE TABLE IF NOT EXISTS `domain` (`domain` varchar(255) NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`domain`), KEY `active` (`active`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'alias' => "CREATE TABLE IF NOT EXISTS `alias` (`address` varchar(255) NOT NULL, `goto` text NOT NULL, `domain` varchar(255) NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `enforce_tls_in` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`address`), KEY `domain` (`domain`), KEY `active` (`active`), CONSTRAINT `alias_ibfk_1` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'alias_domain' => "CREATE TABLE IF NOT EXISTS `alias_domain` (`alias_domain` varchar(255) NOT NULL DEFAULT '', `target_domain` varchar(255) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`alias_domain`), KEY `active` (`active`), KEY `target_domain` (`target_domain`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'captcha' => "CREATE TABLE IF NOT EXISTS `captcha` (`id` bigint(20) NOT NULL AUTO_INCREMENT, `time` int(11) NOT NULL, `code` char(5) NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", + 'dmarc_report' => "CREATE TABLE IF NOT EXISTS `dmarc_report` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `report_id` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `org_name` varchar(255) NOT NULL, `begin` datetime NOT NULL, `end` datetime NOT NULL, `domain` varchar(255) NOT NULL, `adkim` char(1) NOT NULL, `aspf` char(1) NOT NULL, `policy` varchar(10) NOT NULL, `subdomain_policy` varchar(10) NOT NULL, `pct` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `report_id_org_name` (`report_id`,`org_name`), KEY `begin_end` (`begin`,`end`), KEY `domain` (`domain`), CONSTRAINT `dmarc_report_ibfk_1` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON DELETE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'dmarc_report_errors' => "CREATE TABLE IF NOT EXISTS `dmarc_report_errors` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `report_id` bigint(20) unsigned NOT NULL, `error` text NOT NULL, PRIMARY KEY (`id`), KEY `report_id` (`report_id`), CONSTRAINT `dmarc_report_errors_ibfk_1` FOREIGN KEY (`id`) REFERENCES `dmarc_report` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'dmarc_report_records' => "CREATE TABLE IF NOT EXISTS `dmarc_report_records` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `report_id` bigint(20) unsigned NOT NULL, `source_ip` varchar(255) NOT NULL, `count` int(11) NOT NULL, `policy_evaluated_disposition` varchar(10) NOT NULL, `policy_evaluated_dkim` char(4) NOT NULL, `policy_evaluated_spf` char(4) NOT NULL, `identifier_envelope_to` varchar(255) NOT NULL, `identifier_envelope_from` varchar(255) NOT NULL, `identifier_header_from` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `report_id` (`report_id`), CONSTRAINT `dmarc_report_records_ibfk_2` FOREIGN KEY (`report_id`) REFERENCES `dmarc_report` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'dmarc_report_record_dkim_result' => "CREATE TABLE IF NOT EXISTS `dmarc_report_record_dkim_result` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `record_id` bigint(20) unsigned NOT NULL, `domain` varchar(255) NOT NULL, `selector` varchar(255) NOT NULL, `result` varchar(255) NOT NULL, `human_result` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `record_id` (`record_id`), CONSTRAINT `dmarc_report_record_dkim_result_ibfk_2` FOREIGN KEY (`record_id`) REFERENCES `dmarc_report_records` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'dmarc_report_record_reason' => "CREATE TABLE IF NOT EXISTS `dmarc_report_record_reason` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `record_id` bigint(20) unsigned NOT NULL, `type` varchar(255) NOT NULL, `comment` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `record_id` (`record_id`), CONSTRAINT `dmarc_report_record_reason_ibfk_1` FOREIGN KEY (`record_id`) REFERENCES `dmarc_report_records` (`id`) ON DELETE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'dmarc_report_record_spf_result' => "CREATE TABLE IF NOT EXISTS `dmarc_report_record_spf_result` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `record_id` bigint(20) unsigned NOT NULL, `domain` varchar(255) NOT NULL, `result` varchar(255) NOT NULL, `scope` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `record_id` (`record_id`), CONSTRAINT `dmarc_report_record_spf_result_ibfk_2` FOREIGN KEY (`record_id`) REFERENCES `dmarc_report_records` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;", + 'domain_admins' => "CREATE TABLE IF NOT EXISTS `domain_admins` (`username` varchar(255) NOT NULL, `domain` varchar(255) NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `id` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`), KEY `username` (`username`), KEY `active` (`active`), KEY `domain` (`domain`), CONSTRAINT `domain_admins_ibfk_1` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `domain_admins_ibfk_2` FOREIGN KEY (`username`) REFERENCES `admin` (`username`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'mailbox' => "CREATE TABLE IF NOT EXISTS `mailbox` (`username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL DEFAULT '', `quota` bigint(20) NOT NULL DEFAULT 0, `local_part` varchar(255) NOT NULL DEFAULT '', `domain` varchar(255) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT current_timestamp(), `modified` datetime NOT NULL DEFAULT current_timestamp(), `active` tinyint(1) NOT NULL DEFAULT 1, `password_hash_type` varchar(20) NOT NULL DEFAULT '', `openpgpkey_wkd` char(32) NOT NULL DEFAULT '', `pgp_key` text DEFAULT NULL, `pgp_verified` tinyint(1) NOT NULL DEFAULT 0, `tfa` tinyint(1) NOT NULL DEFAULT 0, `last_login` bigint(20) unsigned DEFAULT NULL, `enforce_tls_in` tinyint(1) NOT NULL DEFAULT 1, `enforce_tls_out` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`username`), KEY `domain` (`domain`), KEY `active` (`active`), KEY `openpgpkey_wkd` (`openpgpkey_wkd`), CONSTRAINT `mailbox_ibfk_2` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'monitored_mailboxes' => "CREATE TABLE IF NOT EXISTS `monitored_mailboxes` (`username` varchar(255) NOT NULL, `last_login` int(11) NOT NULL, PRIMARY KEY (`username`), KEY `lastlogin` (`last_login`), CONSTRAINT `monitored_mailboxes_ibfk_1` FOREIGN KEY (`username`) REFERENCES `mailbox` (`username`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'tls_report' => "CREATE TABLE IF NOT EXISTS `tls_report` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `report_id` varchar(255) NOT NULL, `organization` varchar(255) NOT NULL, `contact` varchar(255) NOT NULL, `start` datetime NOT NULL, `end` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `report_id_organization` (`report_id`,`organization`), KEY `start_end` (`start`,`end`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'tls_report_policy' => "CREATE TABLE IF NOT EXISTS `tls_report_policy` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `report_id` bigint(20) unsigned NOT NULL, `policy_type` varchar(255) NOT NULL, `policy_string` text NOT NULL, `policy_domain` varchar(255) NOT NULL, `mx_host` text NOT NULL, `success` int(11) NOT NULL, `failure` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `report_id` (`report_id`), KEY `policy_domain` (`policy_domain`), CONSTRAINT `tls_report_policy_ibfk_1` FOREIGN KEY (`report_id`) REFERENCES `tls_report` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `tls_report_policy_ibfk_3` FOREIGN KEY (`policy_domain`) REFERENCES `domain` (`domain`) ON DELETE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'tls_report_policy_failures' => "CREATE TABLE IF NOT EXISTS `tls_report_policy_failures` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `policy_id` bigint(20) unsigned NOT NULL, `result_type` varchar(255) NOT NULL, `sender_ip` varchar(255) NOT NULL, `receiver_ip` varchar(255) NOT NULL, `receiver_hostname` varchar(255) NOT NULL, `reveiver_helo` varchar(255) NOT NULL, `session_count` int(11) NOT NULL, `additional_information` text NOT NULL, `reason_code` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `policy_id` (`policy_id`), CONSTRAINT `tls_report_policy_failures_ibfk_2` FOREIGN KEY (`policy_id`) REFERENCES `tls_report_policy` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", + 'settings' => 'CREATE TABLE IF NOT EXISTS `settings` (`setting` varchar(50) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL PRIMARY KEY, `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;', +]; + try{ $stmt=$db->query("SELECT value FROM settings WHERE setting='version';"); $version=$stmt->fetch(PDO::FETCH_NUM)[0]; try { + foreach($createTableStatements as $tableName => $statement){ + if ($tableName !== 'settings') { + $db->exec($statement); + } + } $db->beginTransaction(); - $stmt=$db->prepare("UPDATE settings SET value=? WHERE setting='version';"); + $stmt=$db->prepare("UPDATE `settings` SET `value`=? WHERE `setting`='version';"); $stmt->execute([DBVERSION]); $db->commit(); if($version < DBVERSION){ @@ -39,20 +65,17 @@ try{ } catch(PDOException $e){ echo _('Error updating database:') . PHP_EOL; echo $e->getMessage() . PHP_EOL; - $db->rollBack(); + if($db->inTransaction()){ + $db->rollBack(); + } } } catch(PDOException){ //create tables try { - $db->exec("CREATE TABLE `admin` (`username` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `password` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `superadmin` tinyint(1) NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `password_hash_type` varchar(20) CHARACTER SET utf8mb4 NOT NULL DEFAULT '{ARGON2ID}', PRIMARY KEY (`username`), KEY `active` (`active`)) DEFAULT CHARSET=utf8mb4;"); - $db->exec("CREATE TABLE `domain` (`domain` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`domain`), KEY `active` (`active`)) DEFAULT CHARSET=utf8mb4;"); - $db->exec("CREATE TABLE `alias` (`address` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `goto` text CHARACTER SET utf8mb4 NOT NULL, `domain` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `enforce_tls_in` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`address`), KEY `domain` (`domain`), KEY `active` (`active`), CONSTRAINT `alias_ibfk_1` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4;"); - $db->exec("CREATE TABLE `alias_domain` (`alias_domain` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', `target_domain` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`alias_domain`), KEY `active` (`active`), KEY `target_domain` (`target_domain`)) DEFAULT CHARSET=utf8mb4;"); - $db->exec("CREATE TABLE `captcha` (`id` int(11) NOT NULL AUTO_INCREMENT, `time` int(11) NOT NULL, `code` char(5) COLLATE utf8mb4_bin NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;"); - $db->exec("CREATE TABLE `domain_admins` (`username` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `domain` varchar(255) CHARACTER SET utf8mb4 NOT NULL, `created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00', `active` tinyint(1) NOT NULL DEFAULT 1, `id` int(11) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`), KEY `username` (`username`), KEY `active` (`active`), KEY `domain` (`domain`), CONSTRAINT `domain_admins_ibfk_1` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `domain_admins_ibfk_2` FOREIGN KEY (`username`) REFERENCES `admin` (`username`) ON DELETE CASCADE ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4;"); - $db->exec("CREATE TABLE `mailbox` (`username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `quota` bigint(20) NOT NULL DEFAULT 0, `local_part` varchar(255) NOT NULL, `domain` varchar(255) NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, `active` tinyint(1) NOT NULL DEFAULT 1, `password_hash_type` varchar(20) NOT NULL, `openpgpkey_wkd` char(32) NOT NULL, `pgp_key` text DEFAULT NULL, `pgp_verified` tinyint(1) NOT NULL DEFAULT 0, `tfa` tinyint(1) NOT NULL DEFAULT 0, `last_login` bigint(20) unsigned DEFAULT NULL, `enforce_tls_in` tinyint(1) NOT NULL DEFAULT 1, `enforce_tls_out` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`username`), KEY `domain` (`domain`), KEY `active` (`active`), KEY `openpgpkey_wkd` (`openpgpkey_wkd`), CONSTRAINT `mailbox_ibfk_2` FOREIGN KEY (`domain`) REFERENCES `domain` (`domain`) ON UPDATE CASCADE) DEFAULT CHARSET=utf8mb4;"); - $db->exec('CREATE TABLE settings (setting varchar(50) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL PRIMARY KEY, value text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;'); - $stmt=$db->prepare("INSERT INTO settings (setting, value) VALUES ('version', ?);"); + foreach($createTableStatements as $statement){ + $db->exec($statement); + } + $stmt=$db->prepare("INSERT INTO `settings` (`setting`, `value`) VALUES ('version', ?);"); $stmt->execute([DBVERSION]); echo _('Database has successfully been set up.') . PHP_EOL; } catch(PDOException $e){ @@ -61,10 +84,10 @@ try{ } } try { - $stmt = $db->prepare( 'INSERT IGNORE INTO domain (domain, created, modified) VALUES (?, NOW(), NOW())' ); + $stmt = $db->prepare( 'INSERT IGNORE INTO `domain` (`domain`, `created`, `modified`) VALUES (?, NOW(), NOW())' ); $stmt->execute( [ CLEARNET_SERVER ] ); $stmt->execute( [ ONION_SERVER ] ); - $stmt = $db->prepare( 'INSERT IGNORE INTO alias_domain (alias_domain, target_domain, created, modified) VALUES (?, ?, NOW(), NOW())' ); + $stmt = $db->prepare( 'INSERT IGNORE INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`) VALUES (?, ?, NOW(), NOW())' ); $stmt->execute( [ ONION_SERVER, CLEARNET_SERVER ] ); } catch( PDOException $e ) { echo _('Error adding primary domain:') . PHP_EOL; diff --git a/tools/create_export.php b/tools/create_export.php new file mode 100644 index 0000000..1dfe3c3 --- /dev/null +++ b/tools/create_export.php @@ -0,0 +1,57 @@ +prepare('SELECT a.goto, m.domain, m.local_part, m.created, m.modified, m.last_login FROM `mailbox` AS m LEFT JOIN `alias` AS a ON (a.address=m.username) WHERE m.`username` = ?;'); +$stmt->execute([EXPORT_EMAIL]); +if($result = $stmt->fetch(PDO::FETCH_ASSOC)){ + $exportText = [ + "Forwarding addresses: ".$result['goto'], + "Created: ".$result['created']." UTC", + "Modified: ".$result['modified']." UTC", + "Last login: ".date('Y-m-d H:i:s', $result['last_login'])." UTC", + ]; + $mailboxPath = '/var/mail/vmail/'.$result['domain'].'/'.$result['local_part']; + $infoFile = $result['local_part'].'_mailbox_info.txt'; + $xmppFile = $result['local_part'].'_prosody_dump.txt'; + file_put_contents($infoFile, implode(PHP_EOL, $exportText).PHP_EOL); + try { + $dbProsody = new PDO('mysql:host=' . DBHOST_PROSODY . ';dbname=' . DBNAME_PROSODY, DBUSER_PROSODY, DBPASS_PROSODY, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $lines = [ + 'Prosody user data dump for host='.$result['domain'].' and user='.$result['local_part'], + '', + 'Table: prosody', + ]; + $stmtProsody = $dbProsody->prepare('SELECT * FROM `prosody` WHERE `host` = ? AND `user` = ?;'); + $stmtProsody->execute([$result['domain'], $result['local_part']]); + while($row = $stmtProsody->fetch(PDO::FETCH_ASSOC)){ + $lines[] = json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, 512); + } + $lines[] = ''; + $lines[] = 'Table: prosodyarchive'; + $stmtArchive = $dbProsody->prepare('SELECT * FROM `prosodyarchive` WHERE `host` = ? AND `user` = ? ORDER BY `when`;'); + $stmtArchive->execute([$result['domain'], $result['local_part']]); + while($row = $stmtArchive->fetch(PDO::FETCH_ASSOC)){ + $lines[] = json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, 512); + } + file_put_contents($xmppFile, implode(PHP_EOL, $lines).PHP_EOL); + } catch (PDOException $e) { + file_put_contents($xmppFile, "Prosody dump failed: ".$e->getMessage().PHP_EOL); + } + if(file_exists($mailboxPath)){ + exec('cp -a '.escapeshellarg($mailboxPath) . ' . && sync'); + exec('./crypt_maildir.sh '.escapeshellarg($result['local_part']) . ' decrypt && sync'); + sleep(15); + exec('tar czf '.escapeshellarg($result['local_part'].'.tar.gz') . ' ' . escapeshellarg($result['local_part']) . ' ' . escapeshellarg($infoFile) . ' ' . escapeshellarg($xmppFile)); + exec('rm -r '.escapeshellarg($result['local_part'])); + @unlink($infoFile); + @unlink($xmppFile); + } +}else { + echo "E-Mail doesn't exist\n"; +} diff --git a/tools/crypt_maildir.sh b/tools/crypt_maildir.sh index edb4e3c..d8882db 100755 --- a/tools/crypt_maildir.sh +++ b/tools/crypt_maildir.sh @@ -41,14 +41,14 @@ esac _encrypt(){ touch -r "$mailmessage" $tempfile - doveadm fs put compress gz:9:crypt:private_key_path=$private_key_path:public_key_path=$public_key_path:posix:prefix=$maildir_path/$userdir/ "$mailmessage" "$mailmessage" + doveadm -o fs=compress,crypt,posix -o fs_compress_write_method=gz -o fs/compress/fs_driver=compress -o fs/crypt/fs_driver=crypt -o fs/posix/fs_driver=posix -o crypt_private_key_file=$private_key_path -o crypt_global_private_key=main -o crypt_global_private_key/main/crypt_private_key_file=$private_key_path fs put '' "$mailmessage" "$mailmessage" touch -r $tempfile "$mailmessage" chown $uid:$gid "$mailmessage" } _decrypt(){ touch -r "$mailmessage" $tempfile - doveadm fs get compress maybe-gz:9:crypt:private_key_path=$private_key_path:public_key_path=$public_key_path:posix:prefix=$maildir_path/$userdir/ "$mailmessage" > .tempdecrypted + doveadm -o fs=compress,crypt,posix -o fs/compress/fs_driver=compress -o fs/crypt/fs_driver=crypt -o fs/posix/fs_driver=posix -o crypt_private_key_file=$private_key_path -o crypt_global_private_key=main -o crypt_global_private_key/main/crypt_private_key_file=$private_key_path fs get '' "$mailmessage" > .tempdecrypted mv .tempdecrypted "$mailmessage" touch -r $tempfile "$mailmessage" chmod 0600 "$mailmessage" diff --git a/tools/monitor_login.php b/tools/monitor_login.php new file mode 100755 index 0000000..e0e6dcd --- /dev/null +++ b/tools/monitor_login.php @@ -0,0 +1,23 @@ +prepare( "SELECT `m`.`last_login` FROM `mailbox` AS m LEFT JOIN `monitored_mailboxes` AS `mon` ON (`m`.`username`=`mon`.`username`) WHERE `m`.`username` = ? AND (ISNULL(`mon`.`last_login`) OR `mon`.`last_login` < `m`.`last_login`);" ); +$update = $db->prepare( "INSERT INTO `monitored_mailboxes` (`username`, `last_login`) VALUES(?, ?) ON DUPLICATE KEY UPDATE `last_login` = ?;" ); +$changed = ''; +foreach(MONITOR_MAILS as $mail){ + $stmt->execute([$mail]); + if($result = $stmt->fetch(PDO::FETCH_ASSOC)){ + $update->execute([$mail, $result['last_login'], $result['last_login']]); + $changed .= $mail . ' -> ' . date('Y-m-d H:i:s', $result['last_login']) . ' UTC' . PHP_EOL; + } +} +if(!empty($changed)){ + mail(NOTIFICATION_EMAIL, 'User logged in', $changed."\n\n"); +} diff --git a/tools/process_postmaster_reports.php b/tools/process_postmaster_reports.php new file mode 100755 index 0000000..1fc702b --- /dev/null +++ b/tools/process_postmaster_reports.php @@ -0,0 +1,230 @@ +db = get_db_instance(); + $this->insert_tls_report = $this->db->prepare('INSERT INTO tls_report (report_id, organization, contact, start, end) VALUES (?, ?, ?, ?, ?);'); + $this->insert_tls_policy = $this->db->prepare('INSERT INTO tls_report_policy (report_id, policy_type, policy_string, policy_domain, mx_host, success, failure) VALUES (?, ?, ?, ?, ?, ?, ?);'); + $this->insert_tls_policy_failure = $this->db->prepare('INSERT INTO tls_report_policy_failure (policy_id, result_type, sender_ip, receiver_ip, receiver_hostname, receiver_helo, session_count, additional_information, reason_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);'); + $this->insert_dmarc_report = $this->db->prepare('INSERT INTO dmarc_report (report_id, email, org_name, begin, end, domain, adkim, aspf, policy, subdomain_policy, pct) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);'); + $this->insert_dmarc_report_error = $this->db->prepare('INSERT INTO dmarc_report_errors (report_id, error) VALUES (?, ?);'); + $this->insert_dmarc_report_record = $this->db->prepare('INSERT INTO dmarc_report_records (report_id, source_ip, count, policy_evaluated_disposition, policy_evaluated_dkim, policy_evaluated_spf, identifier_envelope_to, identifier_envelope_from, identifier_header_from) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);'); + $this->insert_dmarc_report_record_reason = $this->db->prepare('INSERT INTO dmarc_report_record_reason (record_id, type, comment) VALUES (?, ?,?);'); + $this->insert_dmarc_report_record_dkim_result = $this->db->prepare('INSERT INTO dmarc_report_record_dkim_result (record_id, domain, selector, result, human_result) VALUES (?, ?, ?, ?, ?);'); + $this->insert_dmarc_report_record_spf_result = $this->db->prepare('INSERT INTO dmarc_report_record_spf_result (record_id, domain, result, scope) VALUES (?, ?, ?, ?);'); + } + + public function run() : void + { + $imap = imap_open("{localhost:993/ssl/novalidate-cert}INBOX", POSTMASTER_EMAIL, POSTMASTER_PASSWORD); + $messages = imap_sort($imap, SORTDATE, true); + foreach($messages as $message){ + $structure = imap_fetchstructure($imap, $message); + // TLS Report + if($structure && $structure->subtype === 'REPORT'){ + foreach($structure->parameters as $parameter){ + if($parameter->attribute === 'report-type' && $parameter->value === 'tlsrpt'){ + foreach($structure->parts as $part_number => $part){ + if(!in_array($part->subtype, ['TLSRPT+JSON', 'TLSRPT+GZIP'], true)){ + continue; + } + $encoded_body = imap_fetchbody($imap, $message, $part_number + 1); + if($part->encoding === ENC7BIT){ + $decoded_body = quoted_printable_decode($encoded_body); + }elseif($part->encoding === ENCBASE64){ + $decoded_body = imap_base64($encoded_body); + }elseif($part->encoding === ENCQUOTEDPRINTABLE ){ + $decoded_body = quoted_printable_decode($encoded_body); + } else { + $decoded_body = $encoded_body; + } + if($part->subtype === 'TLSRPT+GZIP'){ + $decoded_body = gzdecode($decoded_body); + } + $report = json_decode($decoded_body, true); + if(!empty($report['report-id'])){ + try { + $this->insert_tls_report->execute([$report['report-id'] ?? '', $report['organization-name'] ?? '', $report['contact-info'] ?? '', date('Y-m-d H:i:s', strtotime($report['date-range']['start-datetime'] ?? 'now')), date('Y-m-d H:i:s', strtotime($report['date-range']['end-datetime'] ?? 'now'))]); + $report_id = $this->db->lastInsertId(); + foreach($report['policies'] as $policy){ + $this->insert_tls_policy->execute([$report_id, $policy['policy']['policy-type'] ?? '', json_encode($policy['policy']['policy-string'] ?? ''), $policy['policy']['policy-domain'] ?? '', json_encode($policy['policy']['mx-host'] ?? ''), $policy['summary']['total-successful-session-count'], $policy['summary']['total-failure-session-count']]); + if(!empty($policy['failure-details'])){ + $policy_id = $this->db->lastInsertId(); + foreach($policy['failure-details'] as $failure){ + $this->insert_tls_policy_failure->execute([$policy_id, $failure['result-type'] ?? '', $failure['sending-mta-ip'] ?? '', $failure['receiving-ip'] ?? '', $failure['receiving-mx-hostname'] ?? '', $failure['receiving-mx-helo'] ?? '', $failure['failed-session-count'], $failure['additional-information'] ?? '', $failure['failure-reason-code'] ?? '']); + } + } + } + } catch(PDOException $e){ + // delete only duplicates + if($e->getCode() !== '23000'){ + continue; + } + } + imap_mail_move($imap, $message, 'Trash'); + } + break; + } + break; + } + } + // DMARC Report + } elseif($structure && in_array($structure->subtype, ['MIXED', 'RELATED'], true)){ + foreach($structure->parts as $part_number => $part){ + if(!in_array($part->subtype, ['ZIP', 'GZIP', 'XML', 'OCTET-STREAM'], true)) { + continue; + } + $encoded_body = imap_fetchbody($imap, $message, $part_number + 1); + if($part->encoding === ENC7BIT){ + $decoded_body = quoted_printable_decode($encoded_body); + }elseif($part->encoding === ENCBASE64){ + $decoded_body = imap_base64($encoded_body); + }elseif($part->encoding === ENCQUOTEDPRINTABLE ){ + $decoded_body = quoted_printable_decode($encoded_body); + } else { + $decoded_body = $encoded_body; + } + $temp = tempnam(sys_get_temp_dir(), 'attachment'); + file_put_contents($temp, $decoded_body); + if(mime_content_type($temp) === 'application/zip') { + $zip = new \ZipArchive(); + if($zip->open($temp) !== false && $zip->numFiles === 1) { + $decoded_body = $zip->getFromIndex(0); + } + } elseif(mime_content_type($temp) === 'application/gzip') { + $decoded_body = gzdecode($decoded_body); + } + unlink($temp); + try { + if($this->process_dmarc_xml($decoded_body)){ + imap_mail_move($imap, $message, 'Trash'); + break; + } + } catch(PDOException $e){ + // delete only duplicates + if($e->getCode() === '23000'){ + imap_mail_move($imap, $message, 'Trash'); + } + } + } + // DMARC Report + } elseif($structure && in_array($structure->subtype, ['ZIP', 'GZIP', 'XML'], true)){ + $encoded_body = imap_body($imap, $message); + if($structure->encoding === ENC7BIT){ + $decoded_body = quoted_printable_decode($encoded_body); + }elseif($structure->encoding === ENCBASE64){ + $decoded_body = imap_base64($encoded_body); + }elseif($structure->encoding === ENCQUOTEDPRINTABLE ){ + $decoded_body = quoted_printable_decode($encoded_body); + } else { + $decoded_body = $encoded_body; + } + $temp = tempnam(sys_get_temp_dir(), 'attachment'); + file_put_contents($temp, $decoded_body); + if(mime_content_type($temp) === 'application/zip') { + $zip = new \ZipArchive(); + if($zip->open($temp) !== false && $zip->numFiles === 1) { + $decoded_body = $zip->getFromIndex(0); + } + } elseif(mime_content_type($temp) === 'application/gzip') { + $decoded_body = gzdecode($decoded_body); + } + unlink($temp); + try { + if($this->process_dmarc_xml($decoded_body)){ + imap_mail_move($imap, $message, 'Trash'); + } + } catch(PDOException $e){ + // delete only duplicates + if($e->getCode() === '23000'){ + imap_mail_move($imap, $message, 'Trash'); + } + } + } + } + imap_expunge($imap); + imap_close($imap); + } + + private function process_dmarc_xml(string $decoded_body) : bool + { + $xml = @simplexml_load_string(str_replace('xmlns=', 'ns=', $decoded_body)); + if($xml !== false) { + $org_name = (string) ($xml->xpath('/feedback/report_metadata/org_name')[0] ?? ''); + $email = (string) ($xml->xpath('/feedback/report_metadata/email')[0] ?? ''); + $report_id = (string) ($xml->xpath('/feedback/report_metadata/report_id')[0] ?? ''); + $begin = (string) ($xml->xpath('/feedback/report_metadata/date_range/begin')[0] ?? ''); + $end = (string) ($xml->xpath('/feedback/report_metadata/date_range/end')[0] ?? ''); + $errors = $xml->xpath('/feedback/report_metadata/error'); + $domain = (string) ($xml->xpath('/feedback/policy_published/domain')[0] ?? ''); + $adkim = (string) ($xml->xpath('/feedback/policy_published/adkim')[0] ?? ''); + $aspf = (string) ($xml->xpath('/feedback/policy_published/aspf')[0] ?? ''); + $policy = (string) ($xml->xpath('/feedback/policy_published/p')[0] ?? ''); + $subdomain_policy = (string) ($xml->xpath('/feedback/policy_published/sp')[0] ?? ''); + $pct = (int) ($xml->xpath('/feedback/policy_published/pct')[0] ?? 100); + $records = $xml->xpath('/feedback/record'); + if(!empty($org_name) && !empty($report_id)){ + $this->insert_dmarc_report->execute([$report_id, $email, $org_name, date('Y-m-d H:i:s', $begin), date('Y-m-d H:i:s', $end), $domain, $adkim, $aspf, $policy, $subdomain_policy, $pct]); + $report_id = $this->db->lastInsertId(); + foreach($errors as $error){ + $this->insert_dmarc_report_error->execute([$report_id, (string) $error]); + } + foreach($records as $record){ + $source_ip = (string) ($record->xpath('row/source_ip')[0] ?? ''); + $count = (string) ($record->xpath('row/count')[0] ?? ''); + $policy_evaluated_disposition = (string) ($record->xpath('row/policy_evaluated/disposition')[0] ?? ''); + $policy_evaluated_dkim = (string) ($record->xpath('row/policy_evaluated/dkim')[0] ?? ''); + $policy_evaluated_spf = (string) ($record->xpath('row/policy_evaluated/spf')[0] ?? ''); + $policy_evaluated_reasons = $record->xpath('row/policy_evaluated/reason'); + $identifier_envelope_to = (string) ($record->xpath('identifiers/envelope_to')[0] ?? ''); + $identifier_envelope_from = (string) ($record->xpath('identifiers/envelope_from')[0] ?? ''); + $identifier_header_from = (string) ($record->xpath('identifiers/header_from')[0] ?? ''); + $this->insert_dmarc_report_record->execute([$report_id, $source_ip, $count, $policy_evaluated_disposition, $policy_evaluated_dkim, $policy_evaluated_spf, $identifier_envelope_to, $identifier_envelope_from, $identifier_header_from]); + $record_id = $this->db->lastInsertId(); + foreach($policy_evaluated_reasons as $reason){ + $policy_evaluated_reason_type = (string) ($reason->xpath('type')[0] ?? ''); + $policy_evaluated_reason_comment = (string) ($reason->xpath('comment')[0] ?? ''); + $this->insert_dmarc_report_record_reason->execute([$record_id, $policy_evaluated_reason_type, $policy_evaluated_reason_comment]); + } + $auth_results_dkim = $record->xpath('auth_results/dkim'); + foreach($auth_results_dkim as $dkim_result){ + $auth_result_dkim_domain = (string) ($dkim_result->xpath('domain')[0] ?? ''); + $auth_result_dkim_selector = (string) ($dkim_result->xpath('selector')[0] ?? ''); + $auth_result_dkim_result = (string) ($dkim_result->xpath('result')[0] ?? ''); + $auth_result_dkim_human_result = (string) ($dkim_result->xpath('human_result')[0] ?? ''); + $this->insert_dmarc_report_record_dkim_result->execute([$record_id, $auth_result_dkim_domain, $auth_result_dkim_selector, $auth_result_dkim_result, $auth_result_dkim_human_result]); + } + $auth_results_spf = $record->xpath('auth_results/spf'); + foreach($auth_results_spf as $spf_result){ + $auth_result_spf_domain = (string) ($spf_result->xpath('domain')[0] ?? ''); + $auth_result_spf_scope = (string) ($spf_result->xpath('scope')[0] ?? ''); + $auth_result_spf_result = (string) ($spf_result->xpath('result')[0] ?? ''); + $this->insert_dmarc_report_record_spf_result->execute([$record_id, $auth_result_spf_domain, $auth_result_spf_result, $auth_result_spf_scope]); + } + } + return true; + } + } + return false; + } +} +$processor = new Mail_Report_Processor(); +$processor->run();