├── .gitignore ├── docker ├── php │ ├── timezone.ini │ ├── xdebug.ini │ └── Dockerfile └── nginx │ └── default.conf ├── vexim ├── sorttable.js ├── images │ ├── check.gif │ └── trashcan.gif ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── es │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── hu │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── it │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ ├── ro │ │ └── LC_MESSAGES │ │ │ └── messages.mo │ └── ru │ │ └── LC_MESSAGES │ │ └── messages.mo ├── config │ ├── i18n.php │ ├── httpheaders.php │ ├── authuser.php │ ├── Tests │ │ └── functionsValidatePasswordTest.php │ ├── authsite.php │ ├── authpostmaster.php │ └── variables.php.example ├── logout.php ├── adminfaildelete.php ├── admingroupaddsubmit.php ├── sitepasswordsubmit.php ├── admingroupchangesubmit.php ├── admingroupcontentaddsubmit.php ├── admingroupcontentdeletesubmit.php ├── userblocksubmit.php ├── admincatchallsubmit.php ├── adminfailchangesubmit.php ├── admingroupadd.php ├── sitepassword.php ├── adminlists.php ├── index.php ├── adminuserblocksubmit.php ├── adminfailaddsubmit.php ├── admincatchalladd.php ├── api │ └── change-user-password.php ├── adminfailadd.php ├── adminfail.php ├── admin.php ├── admingroup.php ├── admincatchall.php ├── adminaliasdelete.php ├── login.php ├── adminfailchange.php ├── scripts.js ├── admingroupdelete.php ├── adminalias.php ├── adminaliasaddsubmit.php ├── adminaliaschangesubmit.php ├── style.css ├── userchangesubmit.php ├── adminaliasadd.php ├── adminuserdelete.php ├── sitechangesubmit.php ├── sitedelete.php ├── adminuser.php ├── siteaddsubmit.php ├── adminuseraddsubmit.php ├── site.php ├── admingroupchange.php └── adminaliaschange.php ├── setup └── migrations │ ├── vexim_1.3_to_1.5_mysql.sql │ ├── vexim_2.0.1_to_2.2_mysql.sql │ ├── vexim_2.3_to_2.3.1_mysql.sql │ ├── vexim_1.5_to_2.0.1_mysql.sql │ └── vexim_2.2_to_2.3_mysql.sql ├── .gitattributes ├── docs ├── debian-conf.d │ ├── transport │ │ ├── 30_vexim_virtual_vacation_delivery │ │ ├── 30_vexim_mailman │ │ ├── 30_vexim_virtual_ditch_spam_transport │ │ └── 30_vexim_virtual_delivery │ ├── router │ │ ├── 240_vexim_mailman │ │ └── 250_vexim_virtual_domains │ ├── auth │ │ └── 30_vexim_authenticators │ └── main │ │ └── 00_vexim_listmacrosdefs ├── vexim-acl-check-helo.conf ├── vexim-group.txt ├── clients │ ├── dovecot.txt │ └── courierimap.txt ├── vexim-acl-check-mime.conf ├── vexim-acl-check-content.conf ├── vexim-acl-check-rcpt.conf └── checklist.txt ├── LICENSE ├── docker-compose.yml └── TODO /.gitignore: -------------------------------------------------------------------------------- 1 | vexim/config/variables.php 2 | -------------------------------------------------------------------------------- /docker/php/timezone.ini: -------------------------------------------------------------------------------- 1 | date.timezone = 'UTC' 2 | -------------------------------------------------------------------------------- /vexim/sorttable.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/sorttable.js -------------------------------------------------------------------------------- /vexim/images/check.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/images/check.gif -------------------------------------------------------------------------------- /vexim/images/trashcan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/images/trashcan.gif -------------------------------------------------------------------------------- /vexim/locale/de/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/de/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /vexim/locale/es/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/es/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /vexim/locale/hu/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/hu/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /vexim/locale/it/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/it/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /vexim/locale/ro/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/ro/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /vexim/locale/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vexim/vexim2/HEAD/vexim/locale/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /docker/php/xdebug.ini: -------------------------------------------------------------------------------- 1 | [xdebug] 2 | xdebug.client_host=host.docker.internal 3 | xdebug.start_with_request=yes 4 | xdebug.mode=debug 5 | -------------------------------------------------------------------------------- /vexim/config/i18n.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /setup/migrations/vexim_1.3_to_1.5_mysql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- MySQL script to upgrade Vexim database schema from Vexim 1.1, 1.2, 1.2.1 and 1.3 to Vexim 1.5 3 | -- 4 | 5 | ALTER TABLE `users` ADD COLUMN `status` tinyint(1) NOT NULL DEFAULT '1' AFTER `admin`; 6 | -------------------------------------------------------------------------------- /vexim/config/httpheaders.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /vexim/logout.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.php text 7 | *.conf text 8 | *.txt text 9 | *.sql text 10 | *.pl text 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.gif binary 14 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3-fpm-alpine 2 | 3 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ 4 | 5 | RUN addgroup -S -g 90 vexim && \ 6 | adduser -S -u 90 -G vexim -h /var/vmail vexim && \ 7 | cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini && \ 8 | install-php-extensions pdo pdo_mysql gettext xdebug 9 | 10 | WORKDIR /srv/app/ 11 | -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | error_log /dev/stderr debug; 5 | access_log /dev/stdout; 6 | 7 | root /srv/app; 8 | index index.php; 9 | try_files $uri $uri/ /index.php?$query_string; 10 | 11 | location ~ \.php$ { 12 | fastcgi_pass php:9000; 13 | include fastcgi_params; 14 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /setup/migrations/vexim_2.0.1_to_2.2_mysql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- MySQL script to upgrade Vexim database schema from Vexim 2.0.1 to Vexim 2.2 3 | -- 4 | 5 | ALTER TABLE `users` ADD COLUMN `unseen` bool DEFAULT '0' AFTER `forward`; 6 | 7 | CREATE TABLE `groups` ( 8 | `id` int(10) AUTO_INCREMENT, 9 | `domain_id` mediumint(8) UNSIGNED NOT NULL, 10 | `name` varchar(64) NOT NULL, 11 | `is_public` char(1) NOT NULL DEFAULT 'Y', 12 | `enabled` bool NOT NULL DEFAULT '1', 13 | PRIMARY KEY (`id`), 14 | UNIQUE KEY `group_name`(`domain_id`, `name`) 15 | ); 16 | CREATE TABLE `group_contents` ( 17 | `group_id` int(10) NOT NULL, 18 | `member_id` int(10) NOT NULL, 19 | PRIMARY KEY (`group_id`, `member_id`) 20 | ); 21 | -------------------------------------------------------------------------------- /docs/debian-conf.d/transport/30_vexim_virtual_vacation_delivery: -------------------------------------------------------------------------------- 1 | 2 | ### transport/30_vexim_virtual_vacation_delivery 3 | ####################################### 4 | 5 | virtual_vacation_delivery: 6 | driver = autoreply 7 | from = "${local_part}@${domain}" 8 | to = ${sender_address} 9 | subject = "Autoreply from ${local_part}@${domain}" 10 | headers = "Content-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable" 11 | text = ${lookup mysql{select vacation from users,domains \ 12 | where domain='${quote_mysql:$domain}' \ 13 | and localpart='${quote_mysql:$local_part}' \ 14 | and users.domain_id=domains.domain_id}} 15 | -------------------------------------------------------------------------------- /vexim/adminfaildelete.php: -------------------------------------------------------------------------------- 1 | prepare($query); 11 | $success = $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 12 | if ($success) { 13 | header ("Location: adminfail.php?deleted={$_GET['localpart']}"); 14 | } else { 15 | header ("Location: adminfail.php?faildeleted={$_GET['localpart']}"); 16 | die; 17 | } 18 | ?> 19 | 20 | -------------------------------------------------------------------------------- /docs/debian-conf.d/transport/30_vexim_mailman: -------------------------------------------------------------------------------- 1 | 2 | ### transport/30_vexim_mailman 3 | ############################## 4 | 5 | # This transport delivers messages to Mailman lists. 6 | # The if def:local_part_suffix section selects whether the suffix is used 7 | # as the mailman command, or whether there is no suffix and so post is 8 | # passed as a command. 9 | # The sg phrase strips the VERP information (if any) from the suffix, 10 | 11 | .ifdef VEXIM_HAVE_MAILMAN 12 | mailman_transport: 13 | driver = pipe 14 | command = MAILMAN_WRAP \ 15 | '${if def:local_part_suffix \ 16 | {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \ 17 | {post}}' \ 18 | $local_part 19 | current_directory = MAILMAN_HOME 20 | home_directory = MAILMAN_HOME 21 | user = MAILMAN_USER 22 | group = MAILMAN_GROUP 23 | .endif 24 | -------------------------------------------------------------------------------- /docs/debian-conf.d/router/240_vexim_mailman: -------------------------------------------------------------------------------- 1 | 2 | ### router/240_vexim_mailman 3 | ############################ 4 | 5 | # This router picks up all the addresses going to the Mailman lists. Initially 6 | # it selects only the domains that may have lists in them, then selects where 7 | # local_part matches a list name (ie you can see a list config file). The 8 | # suffixes pick up all the Mailman admin addresses 9 | 10 | .ifdef VEXIM_HAVE_MAILMAN 11 | mailman_router: 12 | driver = accept 13 | domains = +local_domains 14 | require_files = MAILMAN_LISTCHK 15 | local_part_suffix_optional 16 | local_part_suffix = -admin : \ 17 | -bounces : -bounces+* : \ 18 | -confirm : -confirm+* : \ 19 | -join : -leave : \ 20 | -owner : -request : \ 21 | -subscribe : -unsubscribe 22 | transport = mailman_transport 23 | .endif 24 | -------------------------------------------------------------------------------- /docs/vexim-acl-check-helo.conf: -------------------------------------------------------------------------------- 1 | # These ACL's allow filtering by the HELO argument. 2 | # The two ACL's overlap. You may want to comment out one of them. 3 | 4 | accept hosts = : 5 | accept hosts = +relay_from_hosts 6 | 7 | # DROP all messages with raw IP address as HELO (not allowed, it must be enclosed in []) 8 | drop 9 | condition = ${if isip{$sender_helo_name}} 10 | message = Access denied - Invalid HELO name (See RFC2821 4.1.3) 11 | 12 | # Drop all messages where the HELO is _our_ IP address 13 | drop condition = ${if eq{[$interface_address]}{$sender_helo_name}} 14 | message = $interface_address is _my_ address 15 | 16 | # DROP all messages with same hostname as *ours* 17 | drop message = "REJECTED - Bad HELO - Host impersonating [$sender_helo_name]" 18 | condition = ${if match{$sender_helo_name}{$primary_hostname}} 19 | 20 | # Accept all others 21 | accept 22 | -------------------------------------------------------------------------------- /docs/debian-conf.d/transport/30_vexim_virtual_ditch_spam_transport: -------------------------------------------------------------------------------- 1 | 2 | ### transport/30_vexim_virtual_ditch_spam_transport 3 | ####################################### 4 | 5 | virtual_ditch_spam_transport: 6 | driver = appendfile 7 | envelope_to_add 8 | return_path_add 9 | mode = 0600 10 | maildir_format = true 11 | create_directory = true 12 | user = ${lookup mysql{select users.uid from users,domains \ 13 | where localpart = '${quote_mysql:$local_part}' \ 14 | and domain = '${quote_mysql:$domain}' \ 15 | and users.domain_id = domains.domain_id}} 16 | group = ${lookup mysql{select users.gid from users,domains \ 17 | where localpart = '${quote_mysql:$local_part}' \ 18 | and domain = '${quote_mysql:$domain}' \ 19 | and users.domain_id = domains.domain_id}} 20 | maildir_use_size_file = false 21 | -------------------------------------------------------------------------------- /docs/vexim-group.txt: -------------------------------------------------------------------------------- 1 | # Things to do : 2 | 3 | major 4 | 5 | - translation 6 | - the create SQL statements are missing for postgres in create_db.pl 7 | 8 | minor 9 | 10 | - find a way to garbage members (user or alias) of a group that has been deleted 11 | - improvement : could add spam processing and virus checking at the group level 12 | - improvement : it is not possible to add member that is not a alias nor a user 13 | if you want to put a group in a group the work around is to create 14 | an alias for the internal group 15 | if you want to add a member that is not a user or an alias in the 16 | domain the work around is to create an alias for this user 17 | 18 | trivial 19 | 20 | - when added the same user twice there is no clean error message 21 | - a confirmation message is missing when removing a member from a group 22 | - move information messages to config/header.php 23 | -------------------------------------------------------------------------------- /setup/migrations/vexim_2.3_to_2.3.1_mysql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- MySQL script to upgrade Vexim database schema from Vexim 2.3 to Vexim 2.3.1. 3 | -- The only change so far is that of character set from utf8mb3 to utf8mb4. 4 | -- 5 | 6 | -- 7 | -- Table: `domains` 8 | -- 9 | ALTER TABLE `domains` ROW_FORMAT=DYNAMIC; 10 | ALTER TABLE `domains` CONVERT TO CHARACTER SET utf8mb4; 11 | 12 | -- 13 | -- Table: `users` 14 | -- 15 | ALTER TABLE `users` ROW_FORMAT=DYNAMIC; 16 | ALTER TABLE `users` CONVERT TO CHARACTER SET utf8mb4; 17 | 18 | -- 19 | -- Table: `blocklists` 20 | -- 21 | ALTER TABLE `blocklists` ROW_FORMAT=DYNAMIC; 22 | ALTER TABLE `blocklists` CONVERT TO CHARACTER SET utf8mb4; 23 | 24 | -- 25 | -- Table: `domainalias` 26 | -- 27 | ALTER TABLE `domainalias` ROW_FORMAT=DYNAMIC; 28 | ALTER TABLE `domainalias` CONVERT TO CHARACTER SET utf8mb4; 29 | 30 | -- 31 | -- Table: `groups` 32 | -- 33 | ALTER TABLE `groups` ROW_FORMAT=DYNAMIC; 34 | ALTER TABLE `groups` CONVERT TO CHARACTER SET utf8mb4; 35 | 36 | -- 37 | -- Table: `group_contents` 38 | -- 39 | ALTER TABLE `group_contents` ROW_FORMAT=DYNAMIC; 40 | ALTER TABLE `group_contents` CONVERT TO CHARACTER SET utf8mb4; 41 | -------------------------------------------------------------------------------- /vexim/admingroupaddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 22 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], ':domain_id'=>$_SESSION['domain_id'])); 23 | 24 | if ($success) { 25 | header ("Location: admingroup.php?group_added={$_POST['localpart']}"); 26 | } else { 27 | header ("Location: admingroup.php?group_failadded={$_POST['localpart']}"); 28 | } 29 | ?> 30 | 31 | 32 | -------------------------------------------------------------------------------- /vexim/sitepasswordsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 15 | $success = $sth->execute(array(':crypt'=>$cryptedpassword)); 16 | if ($success) { 17 | $_SESSION['crypt'] = $cryptedpassword; 18 | header ("Location: site.php?sitepass=success"); 19 | die; 20 | } else { 21 | header ("Location: site.php?sitepass=fail"); 22 | die; 23 | } 24 | } else { 25 | header ("Location: site.php?badpass=siteadmin"); 26 | } 27 | ?> 28 | 29 | -------------------------------------------------------------------------------- /vexim/config/authuser.php: -------------------------------------------------------------------------------- 1 | prepare($query); 22 | $success = $sth->execute(array(':user_id'=>$_SESSION['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 23 | if(!$success || !$sth->rowCount()) { 24 | header ("Location: index.php?login=failed&2"); 25 | die(); 26 | } 27 | $row = $sth->fetch(); 28 | 29 | # confirm the crypted password in the session matches the crypted password in the database for the user 30 | if ($row['crypt'] !== $_SESSION['crypt']) { 31 | header ("Location: index.php?login=failed&3"); 32 | die(); 33 | } 34 | 35 | ?> 36 | -------------------------------------------------------------------------------- /vexim/config/Tests/functionsValidatePasswordTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(validate_password("", "")); 14 | } 15 | 16 | public function testSendSamePasswords() 17 | { 18 | $this->assertTrue(validate_password("password", "password")); 19 | } 20 | 21 | public function testSendDifferentPasswords() 22 | { 23 | $this->assertFalse(validate_password("password", "pass")); 24 | } 25 | 26 | public function testSendNullValues() 27 | { 28 | $this->assertFalse(validate_password(null, null)); 29 | } 30 | 31 | public function testSendBoolValues() 32 | { 33 | $this->assertFalse(validate_password(true, true)); 34 | $this->assertFalse(validate_password(false, false)); 35 | } 36 | 37 | public function testSendIntegerValues() 38 | { 39 | $this->assertFalse(validate_password(10, 10)); 40 | } 41 | 42 | public function testSendZeroValues() 43 | { 44 | $this->assertFalse(validate_password(0, 0)); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /vexim/admingroupchangesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 19 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], ':enabled'=>$_POST['enabled'], 20 | ':is_public'=>$_POST['is_public'], ':group_id'=>$_POST['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 21 | if ($success) { 22 | header ("Location: admingroupchange.php?group_id={$_POST['group_id']}&group_updated={$_POST['localpart']}"); 23 | } else { 24 | header ("Location: admingroupchange.php?group_id={$_POST['group_id']}&group_failupdated={$_POST['localpart']}"); 25 | } 26 | ?> 27 | -------------------------------------------------------------------------------- /docs/debian-conf.d/transport/30_vexim_virtual_delivery: -------------------------------------------------------------------------------- 1 | 2 | ### transport/30_vexim_virtual_delivery 3 | ####################################### 4 | 5 | virtual_delivery: 6 | driver = appendfile 7 | envelope_to_add 8 | return_path_add 9 | mode = 0600 10 | maildir_format = true 11 | create_directory = true 12 | directory = ${extract{smtp}{$address_data}} 13 | user = ${extract{uid}{$address_data}} 14 | group = ${extract{gid}{$address_data}} 15 | quota = ${extract{quota}{$address_data}{${value}M}} 16 | quota_is_inclusive = false 17 | #quota_size_regex = ,S=(\d+): 18 | quota_warn_threshold = 75% 19 | # enable if you want to create quota files 20 | maildir_use_size_file = false 21 | # create directory on first mail recipient 22 | #create_directory = true 23 | quota_warn_message = "To: $local_part@$domain\n\ 24 | Subject: Mailbox quota warning\n\n\ 25 | This message was automatically generated by the mail delivery software.\n\n\ 26 | You are now using over 75% of your allocated mail storage quota.\n\n\ 27 | If your mailbox fills completely, further incoming messages will be automatically\n\ 28 | returned to their senders.\n\n\ 29 | Please take note of this and remove unwanted mail from your mailbox.\n" 30 | -------------------------------------------------------------------------------- /vexim/admingroupcontentaddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 9 | $sth->execute(array(':group_id'=>$_POST['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 10 | if (!$sth->rowCount()) { 11 | header ("Location: admingroupchange.php?group_id={$_POST['group_id']}&group_failupdated={$_POST['localpart']}"); 12 | die(); 13 | } 14 | 15 | # validate user_id and group_id 16 | if (!isset($_POST['usertoadd']) or !isset($_POST['group_id'])) { 17 | header("Location: admingroup.php?badname={$_POST['usertoadd']}"); 18 | die; 19 | } 20 | $query = "INSERT INTO group_contents (group_id, member_id) VALUES (:group_id, :usertoadd)"; 21 | $sth = $dbh->prepare($query); 22 | $success = $sth->execute(array(':group_id'=>$_POST['group_id'], ':usertoadd'=>$_POST['usertoadd'])); 23 | if ($success) { 24 | header ("Location: admingroupchange.php?group_id={$_POST['group_id']}&group_updated={$_POST['localpart']}"); 25 | } else { 26 | header ("Location: admingroupchange.php?group_id={$_POST['group_id']}&group_failupdated={$_POST['localpart']}"); 27 | die; 28 | } 29 | ?> 30 | -------------------------------------------------------------------------------- /vexim/admingroupcontentdeletesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 9 | $sth->execute(array(':group_id'=>$_REQUEST['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 10 | if (!$sth->rowCount()) { 11 | header ("Location: admingroupchange.php?group_id={$_REQUEST['group_id']}&group_failupdated={$_REQUEST['localpart']}"); 12 | die(); 13 | } 14 | 15 | # validate user_id and group_id 16 | if (!isset($_REQUEST['member_id']) or !isset($_REQUEST['group_id'])) { 17 | header("Location: admingroup.php?badname={$_REQUEST_['member_id']}"); 18 | die; 19 | } 20 | $query = "DELETE FROM group_contents 21 | WHERE group_id=:group_id 22 | AND member_id=:member_id"; 23 | $sth = $dbh->prepare($query); 24 | $success = $sth->execute(array(':group_id'=>$_REQUEST['group_id'], ':member_id'=>$_REQUEST['member_id'])); 25 | if ($success) { 26 | header ("Location: admingroupchange.php?group_id={$_REQUEST['group_id']}&group_updated={$_REQUEST['localpart']}"); 27 | } else { 28 | header ("Location: admingroupchange.php?group_failupdated={$_REQUEST['localpart']}"); 29 | } 30 | ?> 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Virtual Exim Copyright Notice 2 | 3 | Copyright 2003 Avleen Vig and Virtual Exim Development Team. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE VEXIM PROJECT ``AS IS'' AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE VEXIM PROJECT OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /vexim/userblocksubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 10 | $success = $sth->execute(array(':block_id'=>$_GET['block_id'])); 11 | if ($success) { 12 | header ("Location: userchange.php?updated"); 13 | } else { 14 | header ("Location: userchange.php?failed"); 15 | } 16 | } 17 | 18 | # Finally 'the rest' which is handled by the profile form 19 | if (preg_match("/^\s*$/",$_POST['blockval'])) { header("Location: userchange.php"); die; } 20 | $query = "INSERT INTO blocklists (domain_id, user_id, blockhdr, blockval, color) values ( 21 | :domain_id, :user_id, :blockhdr, :blockval, :color)"; 22 | $sth = $dbh->prepare($query); 23 | $success = $sth->execute(array(':domain_id'=>$_SESSION['domain_id'], ':user_id'=>$_SESSION['user_id'], 24 | ':blockhdr'=>$_POST['blockhdr'], ':blockval'=>$_POST['blockval'], ':color'=>$_POST['color'])); 25 | if ($success) { 26 | header ("Location: userchange.php?updated"); 27 | } else { 28 | header ("Location: userchange.php?failed"); 29 | } 30 | ?> 31 | 32 | -------------------------------------------------------------------------------- /vexim/admincatchallsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 12 | $success = $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 13 | if(!$success) { 14 | header ("Location: adminalias.php?failupdated=Catchall"); 15 | } else { 16 | $query = "INSERT INTO users (localpart, username, domain_id, smtp, 17 | pop, uid, gid, realname, type, enabled) SELECT '*', 18 | :domain, :domain_id, :smtp, :smtp, uid, gid, 'CatchAll', 'catch', 19 | '1' FROM domains WHERE domains.domain_id=:domain_id"; 20 | $sth = $dbh->prepare($query); 21 | $success = $sth->execute(array(':domain'=>'*@'.$_SESSION['domain'], ':domain_id'=>$_SESSION['domain_id'], ':smtp'=>$_POST['smtp'])); 22 | 23 | if ($success) { 24 | header ("Location: adminalias.php?updated=Catchall"); 25 | } else { 26 | header ("Location: adminalias.php?failupdated=Catchall"); 27 | } 28 | } 29 | ?> 30 | 31 | -------------------------------------------------------------------------------- /vexim/adminfailchangesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 24 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], 25 | ':username'=>$_POST['localpart'].'@'.$_SESSION['domain'], 26 | ':user_id'=>$_POST['user_id'], ':domain_id'=>$_SESSION['domain_id'], 27 | ':smtp'=>$_POST['smtp'], ':realname'=>$_POST['realname'] 28 | )); 29 | if ($success) { 30 | header ("Location: adminfail.php?updated={$_POST['localpart']}"); 31 | } else { 32 | header ("Location: adminfail.php?failupdated={$_POST['localpart']}"); 33 | die; 34 | } 35 | ?> 36 | 37 | -------------------------------------------------------------------------------- /vexim/admingroupadd.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | <?php echo _('Virtual Exim') . ': ' . _('Add Group'); ?> 10 | 11 | 12 | 13 | 14 | 19 |
20 |
21 | 22 | 23 | 24 | 28 | 29 | 30 | 34 | 35 |
: 25 | @ 26 | 27 |
31 | 33 |
36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | restart: on-failure 7 | ports: 8 | - "${MYSQL_PORT:-3306}:3306" 9 | environment: 10 | MYSQL_ROOT_PASSWORD: '' 11 | MYSQL_DATABASE: 'vexim' 12 | MYSQL_USER: 'vexim' 13 | MYSQL_PASSWORD: 'CHANGE' 14 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 15 | volumes: 16 | - db_data:/var/lib/mysql:rw 17 | - ./setup/mysql.sql:/docker-entrypoint-initdb.d/mysql.sql:ro,cached 18 | 19 | php: 20 | build: docker/php 21 | restart: on-failure 22 | user: '${UID:-1000}' 23 | volumes: 24 | - mail_dir:/var/vmail:rw 25 | - ./docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro,cached 26 | - ./docker/php/timezone.ini:/usr/local/etc/php/conf.d/timezone.ini:ro,cached 27 | - ./vexim:/srv/app:ro,cached 28 | - ./vexim/config/variables.php.example:/srv/app/config/variables.php:ro,cached 29 | extra_hosts: 30 | - host.docker.internal:host-gateway 31 | environment: 32 | PHP_IDE_CONFIG: '${PHP_IDE_CONFIG:-serverName=vexim}' 33 | depends_on: 34 | - db 35 | links: 36 | - db 37 | 38 | nginx: 39 | image: nginx:1-alpine-slim 40 | restart: on-failure 41 | ports: 42 | - "${WEB_PORT:-80}:80" 43 | volumes: 44 | - ./vexim:/srv/app:ro,cached 45 | - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro,cached 46 | depends_on: 47 | - php 48 | links: 49 | - php 50 | 51 | volumes: 52 | db_data: 53 | mail_dir: 54 | -------------------------------------------------------------------------------- /vexim/sitepassword.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _("Virtual Exim") . ": " . _("Manage Sites"); ?> 11 | 12 | 13 | 14 | 15 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
:
:
:
">
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /vexim/adminlists.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | <?php echo _('Virtual Exim') . ': ' . _('Mailing List Administration'); ?> 14 | 15 | 16 | 17 | 18 | 23 |
24 |
25 | 26 | 27 | 30 | 31 | 32 | 35 | 36 | 37 | 41 | 42 |
28 | : 29 |
33 | 34 |
38 | 40 |
43 |
44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /vexim/config/authsite.php: -------------------------------------------------------------------------------- 1 | prepare($query); 40 | $success = $sth->execute(array(':user_id'=>$_SESSION['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 41 | if(!$success || ($sth->rowCount()!=1)) { 42 | header ("Location: index.php?login=failed"); 43 | die(); 44 | } 45 | 46 | $row = $sth->fetch(); 47 | 48 | # confirm the crypted password in the session matches the crypted password in the database for the siteadmin 49 | if ($row['crypt'] !== $_SESSION['crypt']) { 50 | header ("Location: index.php?login=failed"); 51 | die(); 52 | } 53 | -------------------------------------------------------------------------------- /vexim/index.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | <?php echo _('Virtual Exim'); ?> 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 |
: 23 | 26 |
:
34 | " class="longbutton"> 36 |
39 |
40 |
41 | " . _("Login failed") . ""; 44 | } 45 | ?> 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /vexim/adminuserblocksubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 13 | $success = $sth->execute(array(':block_id'=>$_GET['block_id'], 14 | ':domain_id'=>$_SESSION['domain_id'], ':user_id'=>$_GET['user_id'])); 15 | if ($success) { 16 | header ("Location: adminuser.php?updated={$_GET['localpart']}"); 17 | die; 18 | } else { 19 | header ("Location: adminuser.php?failed={$_GET['localpart']}"); 20 | die; 21 | } 22 | } 23 | 24 | # Finally 'the rest' which is handled by the profile form 25 | if (preg_match("/^\s*$/",$_POST['blockval'])) { 26 | header("Location: adminuser.php"); 27 | die; 28 | } 29 | $query = "INSERT INTO blocklists 30 | (domain_id, user_id, blockhdr, blockval, color) 31 | VALUES (:domain_id, :user_id, :blockhdr, :blockval, :color)"; 32 | $sth = $dbh->prepare($query); 33 | $success = $sth->execute(array(':domain_id'=>$_SESSION['domain_id'], 34 | ':user_id'=>$_POST['user_id'], 35 | ':blockhdr'=>$_POST['blockhdr'], 36 | ':blockval'=>$_POST['blockval'], 37 | ':color'=>$_POST['color'])); 38 | if ($success) { 39 | header ("Location: adminuser.php?updated={$_POST['localpart']}"); 40 | } else { 41 | header ("Location: adminuser.php?failed={$_POST['localpart']}"); 42 | } 43 | ?> 44 | 45 | -------------------------------------------------------------------------------- /vexim/adminfailaddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 38 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], 39 | ':username'=>$_POST['localpart'].'@'.$_SESSION['domain'], 40 | ':domain_id'=>$_SESSION['domain_id'], 41 | ':smtp' => $_POST['smtp'], 42 | ':realname' => $_POST['realname'] 43 | )); 44 | 45 | if ($success) { 46 | header ("Location: adminfail.php?added={$_POST['localpart']}"); 47 | } else { 48 | header ("Location: adminfail.php?failadded={$_POST['localpart']}"); 49 | } 50 | ?> 51 | 52 | -------------------------------------------------------------------------------- /docs/clients/dovecot.txt: -------------------------------------------------------------------------------- 1 | Configuring Dovecot to work with Virtual Exim 2 | 3 | This short manual is based on my experience setting up Dovecot 1.2.15 4 | to work with Vexim under Debian 6.0 (Squeeze). Any additions or 5 | improvements to this document are very welcome. 6 | 7 | Dovecot is an open source IMAP and POP3 email server for Linux/UNIX-like 8 | systems, written with security primarily in mind. 9 | 10 | Setting Dovecot up to work with Vexim is indeed rather trivial: 11 | 12 | 1. Edit /etc/dovecot/dovecot.conf. It is documented rather extensively, 13 | at least in Debian, so I won't go through everything. The things I had to 14 | change for Vexim auth to work were the following: 15 | * 'first_valid_uid' must be low enough for vexim user to be able to log in. 16 | For example: 17 | 18 | first_valid_uid = 100 19 | 20 | * comment out the 'passdb pam' block, and uncomment the 'passdb sql' block 21 | instead. My `passdb sql' looks like this: 22 | 23 | passdb sql { 24 | # Path for SQL configuration file 25 | args = /etc/dovecot/dovecot-sql.conf 26 | } 27 | 28 | * I have also uncommented the 'userdb prefetch' block: 29 | 30 | userdb prefetch { 31 | } 32 | 33 | 2. Edit /etc/dovecot/dovecot-sql.conf. Again, it is well documeted, so I 34 | won't go into detail. With comments stripped out, my dovecot-sql.conf 35 | looks like following: 36 | 37 | driver = mysql 38 | connect = host=/var/run/mysqld/mysqld.sock dbname=vexim user=vexim password=CHANGE 39 | default_pass_scheme = CRYPT #PLAIN 40 | password_query = \ 41 | SELECT `username` AS `user`, `crypt` AS `password`, \ 42 | `pop` AS `userdb_home`, `uid` AS `userdb_uid`, `gid` AS `userdb_gid` \ 43 | FROM `users` WHERE `username` = '%u' 44 | 45 | Dovecot expects the select to return a set of columns with particular 46 | names, hence usage of the 'AS' keyword above. 47 | 48 | 3. Just restart Dovecot, and it should work. 49 | -------------------------------------------------------------------------------- /docs/clients/courierimap.txt: -------------------------------------------------------------------------------- 1 | Installing Courier-IMAP 2.x on FreeBSD: 2 | 3 | * We are going to be performing configuration for both IMAP and POP3 4 | below. If you'd like to not run one of these two daemons, you can 5 | simply ignore the steps for its configuration file. 6 | 7 | cd /usr/ports/mail/courier-imap && make -DWITH_MYSQL install clean 8 | cd /usr/local/etc/courier-imap/ 9 | cp imapd.dist imapd 10 | vi imapd 11 | 12 | Installing Courier-IMAP on Debian: 13 | apt-get install courier-imap courier-imap-ssl courier-pop courier-pop-ssl courier-authlib-mysql courier-authdaemon 14 | 15 | 16 | General Instructions: 17 | Now, replace the following lines: 18 | > Replace: AUTHMODULES="authdaemon" 19 | > with: AUTHMODULES="authmysql" 20 | AND: 21 | > Replace: AUTHMODULES_ORIG="authdaemon" 22 | > with: AUTHMODULES_ORIG="" 23 | Make the same changes to the pop3d.dist file, after copying it to 24 | pop3d. 25 | 26 | Now create a file in that directory called authmysqlrc, with the 27 | following contents: 28 | MYSQL_SERVER localhost 29 | MYSQL_USERNAME vexim 30 | MYSQL_PASSWORD CHANGE 31 | MYSQL_SOCKET /tmp/mysql.sock 32 | ** Debian: /var/run/mysqld/mysqld.sock 33 | MYSQL_PORT 3306 34 | MYSQL_OPT 0 35 | MYSQL_DATABASE vexim 36 | MYSQL_USER_TABLE users 37 | MYSQL_CRYPT_PWFIELD crypt 38 | MYSQL_UID_FIELD uid 39 | MYSQL_GID_FIELD gid 40 | MYSQL_LOGIN_FIELD username 41 | MYSQL_HOME_FIELD pop 42 | MYSQL_NAME_FIELD realname 43 | 44 | FreeBSD: 45 | Finally, perform these steps to move the startup scripts into 46 | place, and start the daemons: 47 | cd /usr/local/etc/rc.d 48 | mv imapd.sh.dist imapd.sh 49 | mv pop3d.sh.dist pop3d.sh 50 | /usr/local/etc/rc.d/imapd.sh start 51 | /usr/local/etc/rc.d/pop3d.sh start 52 | 53 | Debian: 54 | Restart the daemons 55 | for i in /etc/init.d/courier*; do $i restart; done -------------------------------------------------------------------------------- /vexim/admincatchalladd.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 11 | 12 | 13 | 14 | 15 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 43 | 44 |
:
:*@
:
35 | ! 36 |
40 | 42 |
45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/vexim-acl-check-mime.conf: -------------------------------------------------------------------------------- 1 | # Decode MIME parts to disk. This will support virus scanners later. 2 | deny 3 | decode = default 4 | condition = ${if > {$mime_anomaly_level}{2} \ 5 | {true}{false}} 6 | message = This message contains a MIME error ($mime_anomaly_text) 7 | log_message = DENY: MIME Error ($mime_anomaly_text) 8 | 9 | # File extension filtering. 10 | deny 11 | condition = ${if match \ 12 | {${lc:$mime_filename}} \ 13 | {\N(?i)\.(exe|com|ade|adep|adp|bas|bat|chm|cmd|cnf|com|cpl|crt|dll|hlp|hta|inf|ins|isp|js|jse|lnk|mad|maf|mag|mam|maq|mar|mas|matmav|maw|ocx|pcd|pif|reg|scf|scr|sct|vbe|vbs|wsc|wsf|wsh|url|xnk)$\N} \ 14 | {1}{0}} 15 | message = Blacklisted file extension detected in "$mime_filename". If you legitimately need to send these files please use a compressed archive or a file exchange provider. 16 | log_message = DENY: Blacklisted extension ("$mime_filename") 17 | 18 | # Check zip files for suspicious mail extensions 19 | # from: http://www.gossamer-threads.com/lists/exim/users/98336#98336 20 | # 21 | # deny message = A .zip attachment contains a Windows-executable file - \ 22 | # blocked because we are afraid of new viruses \ 23 | # not recognized [yet] by antiviruses. 24 | # condition = ${if match{$mime_filename}{\N(?i)\.zip$\N}} 25 | # condition = ${if def:sender_host_address} 26 | # !authenticated = * 27 | # decode = default 28 | # log_message = forbidden binary in attachment: filename=$mime_filename, \ 29 | # recipients=$recipients 30 | # condition = ${if match{${run{/usr/bin/unzip -l \ 31 | # $mime_decoded_filename}}}\ 32 | # {\N(?i)\.(exe|com|ade|adep|adp|bas|bat|chm|cmd|cnf|com|cpl|crt|dll|hlp|hta|inf|ins|isp|js|jse|lnk|mad|maf|mag|mam|maq|mar|mas|matmav|maw|ocx|pcd|pif|reg|scf|scr|sct|vbe|vbs|wsc|wsf|wsh|url|xnk)\n\N}} 33 | 34 | # Further checks can be implemented, especially if older email clients are used: https://github.com/Exim/exim/wiki/ExiscanBugBlocking 35 | -------------------------------------------------------------------------------- /docs/vexim-acl-check-content.conf: -------------------------------------------------------------------------------- 1 | # Reject virus infested messages. 2 | # 3 | warn message = This message contains malware ($malware_name) 4 | malware = * 5 | log_message = This message contains malware ($malware_name) 6 | 7 | # Reject messages with an "X-Spam-Flag: YES" header. 8 | # 9 | deny message = Sender openly considers message as spam (X-Spam-Flag header with a positive value was found). 10 | condition = ${if bool{$h_x-spam-flag:}{true}{false}} 11 | 12 | # Reject messages containing "viagra" in all kinds of whitespace/case combinations 13 | # WARNING: this is an example ! 14 | # 15 | # deny message = This message matches a blacklisted regular expression ($regex_match_string) 16 | # regex = [Vv] *[Ii] *[Aa] *[Gg] *[Rr] *[Aa] 17 | 18 | # Mails larger than 300 KB are accepted without spam-checking to save resources. 19 | # Message scanning time grows exponentially in the size of the message and goes timeout 20 | # after 2~5 minutes, while typical spam has only a few KB in size. 21 | # 22 | accept condition = ${if >={$message_size}{300k}{yes}{no}} 23 | remove_header = X-Spam-Flag : X-Spam-Score : X-Spam-Status : X-Spam-Report 24 | 25 | # Pass missing spam score and report data to routers and transports, but do not reject anything yet. 26 | # Also, if set, remove X-Spam-* headers here: we will add our own ones later. 27 | # 28 | warn spam = VEXIM_SA_USERNAME:true 29 | set acl_m_spam_score = $spam_score 30 | set acl_m_spam_bar = $spam_bar 31 | set acl_m_spam_report = $spam_report 32 | # $spam_score_int persists by itself 33 | remove_header = X-Spam-Flag : X-Spam-Score : X-Spam-Status : X-Spam-Report 34 | 35 | # Reject all mails with more than 15 spam points, REGARDLESS OF INDIVIDUAL USER SETTINGS. 36 | # 37 | # deny message = This message has been rejected due to spam. 38 | # condition = ${if >{$spam_score_int}{150}{true}{false}} 39 | 40 | accept hosts = 127.0.0.1:+relay_from_hosts 41 | accept authenticated = * 42 | -------------------------------------------------------------------------------- /vexim/api/change-user-password.php: -------------------------------------------------------------------------------- 1 | prepare($query); 30 | $success = $sth->execute(array(':username'=>$_POST['user'])); 31 | if(!$success) { 32 | http_response_code(500); 33 | if(ini_get('display_errors')) { 34 | print_r($sth->errorInfo()); 35 | } 36 | die('internal server error'); 37 | } 38 | if ($sth->rowCount()!=1) { 39 | http_response_code(403); 40 | die('invalid user or password'); 41 | } 42 | 43 | $row = $sth->fetch(); 44 | $cryptedpass = crypt_password($_POST['curpass'], $row['crypt']); 45 | 46 | if ($cryptedpass !== $row['crypt']) { 47 | http_response_code(403); 48 | die('invalid user or password'); 49 | } 50 | 51 | if (!password_strengthcheck($_POST['newpass'])) { 52 | http_response_code(422); 53 | die("week password"); 54 | } 55 | 56 | $cryptedpass = crypt_password($_POST['newpass']); 57 | 58 | $query = "UPDATE users SET crypt=:crypt WHERE user_id=:user_id"; 59 | $sth = $dbh->prepare($query); 60 | $success = $sth->execute(array(':crypt'=>$cryptedpass, ':user_id'=>$row['user_id'])); 61 | if (!$success) { 62 | http_response_code(500); 63 | if(ini_get('display_errors')) { 64 | print_r($sth->errorInfo()); 65 | } 66 | die('internal server error'); 67 | } 68 | -------------------------------------------------------------------------------- /vexim/config/authpostmaster.php: -------------------------------------------------------------------------------- 1 | prepare($query); 30 | $success = $sth->execute(array(':user_id'=>$_SESSION['user_id'], ':domain_id'=>$domain_id)); 31 | if(!$success || ($sth->rowCount()!=1)) { 32 | header ("Location: index.php?login=failed"); 33 | die(); 34 | } 35 | $row = $sth->fetch(); 36 | 37 | # confirm the crypted password in the session matches the crypted password in the database for the user 38 | if ($row['crypt'] != $_SESSION['crypt']) { 39 | header ("Location: index.php?login=failed"); 40 | die(); 41 | } 42 | 43 | if (isset($siteadminManageDomains) 44 | && $siteadminManageDomains 45 | && isset($_SESSION['username']) 46 | && 'siteadmin' === $_SESSION['username'] 47 | && isset($_GET['manage_domain_id']) 48 | ) 49 | { 50 | $query = "SELECT domain FROM domains WHERE domain_id=:domain_id;"; 51 | $sth = $dbh->prepare($query); 52 | $success = $sth->execute(array(':domain_id'=>$_GET['manage_domain_id'])); 53 | if(!$success || ($sth->rowCount()!=1)) { 54 | header ("Location: index.php?login=failed"); 55 | die(); 56 | } 57 | $row = $sth->fetch(); 58 | $_SESSION['domain_id'] = $_GET['manage_domain_id']; 59 | $_SESSION['domain'] = $row['domain']; 60 | } 61 | -------------------------------------------------------------------------------- /docs/debian-conf.d/auth/30_vexim_authenticators: -------------------------------------------------------------------------------- 1 | 2 | ### 30_vexim_authenticators 3 | ########################### 4 | 5 | plain_virtual_exim: 6 | driver = plaintext 7 | public_name = PLAIN 8 | server_condition = ${if crypteq{$auth3}{${lookup mysql{ \ 9 | SELECT crypt FROM users \ 10 | WHERE username = '${quote_mysql:$auth2}' \ 11 | AND enabled = 1 \ 12 | }}}{1}{0}} 13 | server_set_id = $auth2 14 | server_advertise_condition = ${if or{\ 15 | {!eq{$tls_cipher}{}}\ 16 | {match_ip {$sender_host_address}{@[]}}\ 17 | }\ 18 | {*}{}} 19 | 20 | login_virtual_exim: 21 | driver = plaintext 22 | public_name = LOGIN 23 | server_prompts = "Username:: : Password::" 24 | 25 | server_condition = ${if crypteq{$auth2}{${lookup mysql{ \ 26 | SELECT crypt FROM users \ 27 | WHERE username = '${quote_mysql:$auth1}' \ 28 | AND enabled = 1 \ 29 | }}}{1}{0}} 30 | server_set_id = $auth1 31 | server_advertise_condition = ${if or{\ 32 | {!eq{$tls_cipher}{}}\ 33 | {match_ip {$sender_host_address}{@[]}}\ 34 | }\ 35 | {*}{}} 36 | 37 | 38 | # You can use the authenticator of your IMAP server (Dovecot or Courier) to authenticate 39 | # users in Exim. To do that, comment out the lines above and uncomment either the Dovecot 40 | # section below or the Courier section in 30_exim4-config_examples file. 41 | 42 | # Authenticate against Dovecot SASL 43 | # Based on: http://wiki2.dovecot.org/HowTo/EximAndDovecotSASL 44 | # 45 | # login_dovecot_sasl: 46 | # driver = dovecot 47 | # public_name = LOGIN 48 | # server_socket = /var/run/dovecot/auth-client 49 | # server_set_id = $auth1 50 | # server_advertise_condition = ${if or{\ 51 | # {!eq{$tls_cipher}{}}\ 52 | # {match_ip {$sender_host_address}{@[]}}\ 53 | # }\ 54 | # {*}{}} 55 | # 56 | # plain_dovecot_sasl: 57 | # driver = dovecot 58 | # public_name = PLAIN 59 | # server_socket = /var/run/dovecot/auth-client 60 | # server_set_id = $auth1 61 | # server_advertise_condition = ${if or{\ 62 | # {!eq{$tls_cipher}{}}\ 63 | # {match_ip {$sender_host_address}{@[]}}\ 64 | # }\ 65 | # {*}{}} 66 | -------------------------------------------------------------------------------- /vexim/adminfailadd.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 11 | 12 | 13 | 14 | 15 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 46 | 47 | 48 | 52 | 53 |
:
: 30 | @ 31 | 32 |
: 37 | 38 |
42 | 43 | with return code 551 and the specified address will be returned as part of the reject message.
44 | Otherwise, generic return code 550 will be used.'); ?> 45 |
49 | 51 |
54 |
55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /vexim/adminfail.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 11 | 12 | 13 | 14 | 15 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | prepare($query); 34 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 35 | if ($sth->rowCount()) { 36 | while ($row = $sth->fetch()) { 37 | print '' 38 | . ''; 47 | print ''; 53 | print ''; 61 | print ''; 62 | print ''; 63 | } 64 | } 65 | ?> 66 |
 
' 39 | . '' 48 | . '' 51 | . $row['realname'] 52 | . '' 54 | . '' 57 | . $row['localpart'] 58 | . '@' 59 | . htmlspecialchars($_SESSION['domain']) 60 | . '' . ($row['smtp'] === ':fail:' ? _('None') : $row['smtp']) . '
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /vexim/admin.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _('Virtual Exim'); ?> 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 25 | 26 | 27 | 34 | 35 | 36 | 41 | 42 | 43 | 48 | 49 | '; 52 | } 53 | ?> 54 | 55 | 58 | 59 | 60 | 61 | prepare($query); 66 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 67 | 68 | if($sth->rowCount()) { 69 | print ''; 70 | while ($row = $sth->fetch()) { 71 | print ''; 74 | } 75 | } 76 | ?> 77 |
19 | 20 | 23 | 24 |
28 | 29 | 32 | 33 |
37 | 38 | 39 | 40 |
44 | 45 | 46 | 47 |
' . _('Manage mailing lists') . '
56 | 57 |
Domain data:
'; 72 | print "{$row['alias']} is an alias of {$_SESSION['domain']}"; 73 | print '
78 |
79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /vexim/admingroup.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | <?php echo _('Virtual Exim') . ': ' . _('List groups'); ?> 10 | 11 | 12 | 13 | 14 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | prepare($query); 33 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 34 | while ($row = $sth->fetch()) { 35 | if($row['enabled'] === 0) print ''; else print ''; 36 | ?> 37 | 43 | 48 | 54 | 60 | 61 | 64 |
 
38 | 39 | trashcan 41 | 42 | 44 | 46 | 47 | 49 | 50 | 52 | 53 | 55 | 56 | 58 | 59 |
65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /vexim/admincatchall.php: -------------------------------------------------------------------------------- 1 | prepare($query); 9 | $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 10 | if ($sth->rowCount()) { 11 | $row = $sth->fetch(); 12 | } 13 | ?> 14 | 15 | 16 | 17 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 18 | 19 | 20 | 21 | 22 | 27 |
28 | rowCount()) { 31 | echo '
'; 32 | printf(_("Invalid catchall userid '%s' for domain '%s'"), htmlentities($_GET['user_id']), htmlentities($_SESSION['domain'])); 33 | echo '
'; 34 | }else{ 35 | ?> 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 59 | 63 | 64 |
:
:*@
: 49 |
51 | ! 52 |
56 | 58 | 60 | 62 |
65 |
66 | 70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/vexim-acl-check-rcpt.conf: -------------------------------------------------------------------------------- 1 | # Use spfquery to perform a pair of SPF checks (for details, see 2 | # http://www.openspf.org/) 3 | # This check has been copied from a stock Debian config and has not been 4 | # tested, that's why it's commented out. 5 | # 6 | # This is quite costly in terms of DNS lookups (~6 lookups per mail). Do not 7 | # enable if that's an issue. Also note that if you enable this, you must 8 | # install "spf-tools-perl" which provides the spfquery command. 9 | # Missing spf-tools-perl will trigger the "Unexpected error in 10 | # SPF check" warning. 11 | # deny 12 | # message = [SPF] $sender_host_address is not allowed to send mail from \ 13 | # ${if def:sender_address_domain {$sender_address_domain}{$sender_helo_name}}. \ 14 | # Please see \ 15 | # http://www.openspf.org/Why?scope=${if def:sender_address_domain \ 16 | # {mfrom}{helo}};identity=${if def:sender_address_domain \ 17 | # {$sender_address}{$sender_helo_name}};ip=$sender_host_address 18 | # log_message = SPF check failed. 19 | # !acl = acl_local_deny_exceptions 20 | # condition = ${run{/usr/bin/spfquery.mail-spf-perl --ip \ 21 | # \"$sender_host_address\" --identity \ 22 | # ${if def:sender_address_domain \ 23 | # {--scope mfrom --identity \"$sender_address\"}\ 24 | # {--scope helo --identity \"$sender_helo_name\"}}}\ 25 | # {no}{${if eq {$runrc}{1}{yes}{no}}}} 26 | # defer 27 | # message = Temporary DNS error while checking SPF record. Try again later. 28 | # !acl = acl_local_deny_exceptions 29 | # condition = ${if eq {$runrc}{5}{yes}{no}} 30 | # 31 | # warn 32 | # condition = ${if <={$runrc}{6}{yes}{no}} 33 | # message = Received-SPF: ${if eq {$runrc}{0}{pass}\ 34 | # {${if eq {$runrc}{2}{softfail}\ 35 | # {${if eq {$runrc}{3}{neutral}\ 36 | # {${if eq {$runrc}{4}{permerror}\ 37 | # {${if eq {$runrc}{6}{none}{error}}}}}}}}}\ 38 | # } client-ip=$sender_host_address; \ 39 | # ${if def:sender_address_domain \ 40 | # {envelope-from=${sender_address}; }{}}\ 41 | # helo=$sender_helo_name 42 | # 43 | # warn 44 | # log_message = Unexpected error in SPF check. 45 | # condition = ${if >{$runrc}{6}{yes}{no}} 46 | 47 | 48 | 49 | # deny hosts = ! +relay_from_hosts 50 | # condition = ${if eq {${lookup mysql{select count(*) from domains \ 51 | # where domain = '${quote_mysql:$domain}' \ 52 | # and spamassassin='1'}}}{1} {yes}{no}} 53 | # !acl = spf_rcpt_acl 54 | 55 | 56 | .ifdef CHECK_RCPT_REVERSE_DNS 57 | warn message = Warning - Reverse DNS lookup failed for host $sender_host_address. 58 | !authenticated = * 59 | !verify = reverse_host_lookup 60 | .endif 61 | 62 | 63 | # Deny unless the sender address can be verified. 64 | # require verify = sender 65 | 66 | 67 | # To use a DNSBL the best would be to set up a caching dns server (e.g. unbound) because there is 68 | # often a rate limit that you will hit when using public DNS servers. 69 | deny message = DNSBL listed at $dnslist_domain\n$dnslist_text 70 | dnslists = zen.spamhaus.org 71 | -------------------------------------------------------------------------------- /docs/debian-conf.d/main/00_vexim_listmacrosdefs: -------------------------------------------------------------------------------- 1 | 2 | ### main/00_vexim_listmacrosdefs 3 | ################################# 4 | 5 | hide mysql_servers = localhost::(/var/run/mysqld/mysqld.sock)/vexim/vexim/CHANGE 6 | 7 | # domains 8 | VEXIM_VIRTUAL_DOMAINS = SELECT DISTINCT domain FROM domains WHERE type = 'local' AND enabled = '1' AND domain = '${quote_mysql:$domain}' 9 | VEXIM_RELAY_DOMAINS = SELECT DISTINCT domain FROM domains WHERE type = 'relay' AND domain = '${quote_mysql:$domain}' 10 | VEXIM_ALIAS_DOMAINS = SELECT DISTINCT alias FROM domainalias WHERE alias = '${quote_mysql:$domain}' 11 | 12 | # domains and relay networks 13 | MAIN_LOCAL_DOMAINS = MAIN_LOCAL_DOMAINS : ${lookup mysql{VEXIM_VIRTUAL_DOMAINS}} : ${lookup mysql{VEXIM_ALIAS_DOMAINS}} 14 | MAIN_RELAY_TO_DOMAINS = MAIN_RELAY_TO_DOMAINS : ${lookup mysql{VEXIM_RELAY_DOMAINS}} 15 | 16 | # primary hostname 17 | #MAIN_HARDCODE_PRIMARY_HOSTNAME=myhostname 18 | 19 | # add vexim system user 20 | #MAIN_TRUSTED_USERS = www-data 21 | 22 | # enable TLS 23 | #MAIN_TLS_ENABLE = true 24 | 25 | # enable av scanner 26 | #av_scanner = clamd:/var/run/clamav/clamd.ctl 27 | 28 | # use spamassassing 29 | #spamd_address = 127.0.0.1 783 30 | 31 | .ifdef MAIN_KEEP_ENVIRONMENT 32 | keep_environment = MAIN_KEEP_ENVIRONMENT 33 | .else 34 | keep_environment = 35 | .endif 36 | .ifdef MAIN_ADD_ENVIRONMENT 37 | add_environment = MAIN_ADD_ENVIRONMENT 38 | .endif 39 | 40 | # validation of sending mailserver 41 | #CHECK_RCPT_REVERSE_DNS = true 42 | #CHECK_RCPT_SPF = true 43 | 44 | 45 | # Mailman-related options. Uncomment VEXIM_HAVE_MAILMAN and adjust the macros 46 | # below if want to use Mailman with this installation. 47 | # NOTE: we only have integration with Mailman 2 ready at the moment. 48 | 49 | #VEXIM_HAVE_MAILMAN = yes 50 | 51 | 52 | # Home dir for your Mailman installation - aka Mailman's prefix 53 | # directory. 54 | # On a Red Hat/Fedora system using RPM, use "/var/mailman" 55 | # On Debian using the deb package use "/var/lib/mailman" 56 | # This is normally the same as ~mailman 57 | 58 | MAILMAN_HOME=/usr/local/mailman 59 | 60 | 61 | # User and group for Mailman, should match your --with-mail-gid 62 | # switch to Mailman's configure script. 63 | # Value is normally "mailman" 64 | 65 | MAILMAN_USER=mailman 66 | MAILMAN_GROUP=mailman 67 | 68 | 69 | # The path of the Mailman mail wrapper script 70 | 71 | MAILMAN_WRAP=MAILMAN_HOME/mail/mailman 72 | 73 | 74 | # The path of the list config file (used as a required file when 75 | # verifying list addresses) 76 | 77 | MAILMAN_LISTCHK = MAILMAN_HOME/lists/${lc::$local_part}/config.pck 78 | 79 | 80 | # If the local-part suffix is used, mails to user+whatevertext@example.org will be delivered to user@example.org. 81 | # Comment this line if you want to disable it. You can also use a different separator instead of `+`. 82 | VEXIM_LOCALPART_SUFFIX = +* 83 | 84 | CHECK_RCPT_LOCAL_ACL_FILE = /etc/exim4/vexim-acl-check-rcpt.conf 85 | CHECK_DATA_LOCAL_ACL_FILE = /etc/exim4/vexim-acl-check-content.conf 86 | 87 | # Exim will put the detailed spam report into an X-Spam-Report header by default. 88 | # This report is really huge by default, but its template can be tweaked to make 89 | # it look almost exactly like contents of the X-Spam-Status header, which 90 | # SpamAssassin adds when scanning messages externally, and which is a much more 91 | # compact version of the report. If you tweak your template this way, you may 92 | # as well want to change the header name here. 93 | VEXIM_SPAM_REPORT_HEADER_NAME = X-Spam-Report 94 | -------------------------------------------------------------------------------- /vexim/adminaliasdelete.php: -------------------------------------------------------------------------------- 1 | prepare($query); 14 | $success = $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 15 | if ($success) { 16 | header ("Location: adminalias.php?deleted={$_GET['localpart']}"); 17 | die; 18 | } else { 19 | header ("Location: adminalias.php?faildeleted={$_GET['localpart']}"); 20 | die; 21 | } 22 | } else if ($_GET['confirm'] == 'cancel') { 23 | header ("Location: adminalias.php?faildeleted={$_GET['localpart']}"); 24 | die; 25 | } 26 | } 27 | ?> 28 | 29 | 30 | 31 | <?php echo _('Virtual Exim') . ': ' . _('Confirm Delete'); ?> 32 | 33 | 34 | 35 | 36 | 41 |
42 |
43 | 44 | 45 | 51 | 52 | 53 | 60 | 61 | 62 | 69 | 70 | 71 | 81 | 82 |
46 | : 50 |
54 | 55 | 59 |
63 | 64 | 68 |
72 | '> 74 | '> 76 | '> 78 | '> 80 |
83 |
84 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /vexim/login.php: -------------------------------------------------------------------------------- 1 | prepare($query); 23 | $success = $sth->execute(array(':username'=>$_POST['username'])); 24 | if(!$success) { 25 | print_r($sth->errorInfo()); 26 | die(); 27 | } 28 | if ($sth->rowCount()!=1) { 29 | header ('Location: index.php?login=failed'); 30 | die(); 31 | } 32 | 33 | $row = $sth->fetch(); 34 | $cryptedpass = crypt_password($_POST['crypt'], $row['crypt']); 35 | 36 | // Some debugging prints. They help when you don't know why auth is failing. 37 | /* 38 | print $query. "
\n";; 39 | print $row['username']. "
\n"; 40 | print $_POST['username'] . "
\n"; 41 | print "Posted crypt: " .$_POST['crypt'] . "
\n"; 42 | print $row['crypt'] . "
\n"; 43 | print $cryptscheme . "
\n"; 44 | print $cryptedpass . "
\n"; 45 | */ 46 | 47 | # if they have the wrong password bail out 48 | if ($cryptedpass !== $row['crypt']) { 49 | header ('Location: index.php?login=failed'); 50 | die(); 51 | } 52 | if($row['username']!=='siteadmin') { 53 | if (($row['userenabled'] === '0')) { 54 | header ('Location: index.php?userdisabled'); 55 | die(); 56 | } 57 | if (($row['domainenabled'] === '0')) { 58 | header ('Location: index.php?domaindisabled'); 59 | die(); 60 | } 61 | } 62 | 63 | # populate session variables from what was retrieved from the database (NOT what they posted) 64 | $_SESSION['username'] = $row['username']; 65 | $_SESSION['localpart'] = $row['localpart']; 66 | $_SESSION['domain_id'] = $row['domain_id']; 67 | $_SESSION['domain'] = $row['domain']; 68 | $_SESSION['crypt'] = $row['crypt']; 69 | $_SESSION['user_id'] = $row['user_id']; 70 | 71 | if (isset($siteadminManageDomains) 72 | && $siteadminManageDomains 73 | && 'siteadmin' === $row['username'] 74 | ) { 75 | $_SESSION['siteadmin_domain_id'] = $row['domain_id']; 76 | $_SESSION['siteadmin_domain'] = $row['domain']; 77 | } 78 | 79 | # redirect the user to the correct starting page 80 | if (($row['admin'] == '1') && ($row['type'] == 'site')) { 81 | header ('Location: site.php'); 82 | die(); 83 | } 84 | 85 | if ($row['admin'] == '1') { 86 | header ('Location: admin.php'); 87 | die(); 88 | } 89 | 90 | # must be a user, send them to edit their own details, if User-Login is permitted 91 | if($AllowUserLogin===1) { 92 | header ('Location: userchange.php'); 93 | die(); 94 | } 95 | header ('Location: index.php?login=disabled'); 96 | ?> 97 | 98 | -------------------------------------------------------------------------------- /vexim/adminfailchange.php: -------------------------------------------------------------------------------- 1 | prepare($query); 9 | $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 10 | if($sth->rowCount()) { 11 | $row = $sth->fetch(); 12 | } 13 | ?> 14 | 15 | 16 | 17 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 18 | 19 | 20 | 21 | 22 | 28 | 29 | rowCount()) { 32 | echo '
'; 33 | echo "Invalid fail userid '" . htmlentities($_GET['user_id']) . "' for domain '" . htmlentities($_SESSION['domain']). "'"; 34 | echo '
'; 35 | }else{ 36 | ?> 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 72 | 73 |
:
: 46 | @ 48 | 49 | 51 |
: 56 | 57 |
61 | 62 | with return code 551 and the specified address will be returned as part of the reject message.
63 | Otherwise, generic return code 550 will be used.'); ?> 64 |
69 | 71 |
74 |
75 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /vexim/scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a new password, which may then be copied to the form 3 | * with suggestPasswordCopy(). 4 | * 5 | * Function copied from phpMyAdmin 6 | * 7 | * @param string name of the field that should be filled 8 | * 9 | * @return string the generated password 10 | */ 11 | function suggestPassword(fieldName) { 12 | // restrict the password to just letters and numbers to avoid problems: 13 | // "editors and viewers regard the password as multiple words and 14 | // things like double click no longer work" 15 | var pwchars = "23456789abcdefhjmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWYXZ"; 16 | var passwordlength = 12; // do we want that to be dynamic? no, keep it simple :) 17 | var passwd = ''; 18 | 19 | for ( i = 0; i < passwordlength; i++ ) { 20 | passwd += pwchars.charAt( Math.floor( Math.random() * pwchars.length ) ) 21 | } 22 | 23 | if((fieldName != '') && (field = document.getElementById(fieldName))) { 24 | field.value = passwd; 25 | } 26 | return passwd; 27 | } 28 | 29 | 30 | /** 31 | * Copy the generated password (or anything in the field) to the form 32 | * 33 | * Function copied from phpMyAdmin 34 | * 35 | * @param string name of the field to copy the password from 36 | * @param string name of the field to copy the password to 37 | * @param string name of the field to copy the password to 38 | * 39 | * @return boolean always true 40 | */ 41 | function copyPassword(sourceFieldName, target1FieldName, target2FieldName) { 42 | document.getElementById(target1FieldName).value = document.getElementById(sourceFieldName).value; 43 | document.getElementById(target2FieldName).value = document.getElementById(sourceFieldName).value; 44 | return true; 45 | } 46 | 47 | 48 | /** 49 | * Enables forward-destination field and selection box only if forwarding is enabled 50 | */ 51 | function fwform() { 52 | document.getElementById('forward').disabled = !document.getElementById('on_forward').checked; 53 | if (document.getElementById('forwardmenu') != null) { //userchange.php has no box 54 | document.getElementById('forwardmenu').disabled = !document.getElementById('on_forward').checked; 55 | } 56 | return true; 57 | } 58 | 59 | 60 | /** 61 | * If item on forwarding destination is selected from a list of destinations 62 | * the forwarding text box is updated 63 | */ 64 | function boxadd() { 65 | var exstring = document.getElementById('forward').value; 66 | var box = document.getElementById('forwardmenu'); 67 | var selectitem = box.options[box.selectedIndex].value; 68 | if (!exstring.match(/\S/)) { 69 | document.getElementById('forward').value=selectitem; 70 | } else { 71 | document.getElementById('forward').value += "," + selectitem; 72 | } 73 | } 74 | 75 | 76 | /** 77 | * Add event listener 78 | */ 79 | document.addEventListener('DOMContentLoaded', function () { 80 | if (document.getElementById('pwcopy') != null) { 81 | document.getElementById('pwcopy').addEventListener('click', function() { copyPassword('suggest', 'clear', 'vclear')}); 82 | } 83 | if (document.getElementById('pwgenerate') != null) { 84 | document.getElementById('pwgenerate').addEventListener('click', function() { suggestPassword('suggest') }); 85 | } 86 | if (document.getElementById('on_forward') != null) { 87 | document.getElementById('on_forward').addEventListener('change', function() { fwform() }); 88 | } 89 | if (document.getElementById('forwardmenu') != null) { 90 | document.getElementById('forwardmenu').addEventListener('change', function() { boxadd() }); 91 | } 92 | 93 | // settings at page load 94 | if (document.getElementById('forward') != null) { 95 | fwform(); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 1. Site Controls 2 | a. Add/Modify/Delete domain [Done] 3 | b. Domain can add admin [Done] 4 | c. Procmail available switch [Done] 5 | d. SpamAssassin available [Done] 6 | e. Virus scanner available [Done] 7 | f. Mailing list availability [For later release] 8 | g. Quota can be set [Done] 9 | -> Need exim config [Done] 10 | h. Blacklists and Whitelists can be set 11 | -> [Blacklisting done] 12 | i. UID/GID [Done] 13 | -> Let siteadmin decide if these are domain admin tunable [Done] 14 | j. Domain tag line can be added for outbound mail [Unsure] 15 | k. Add/Modify/Delete domain admin [Done] 16 | l. Add/Modify/Delete relay_domains [Done] 17 | m. Password complexity requirements [For later release] 18 | o. Add the ability to choose any filesystem directory [Done] 19 | -> Check the permissions to make sure Exim can create it 20 | p. Add limits on how many accounts each domain can have [Done] 21 | q. Add a "junior site admin" with different abilities 22 | 23 | 2. Domain Controls (depends on site settings) 24 | a. Add/delete/modify users [Done] 25 | b. Add/delete/modify admins [Done] 26 | c. Add/delete/modify aliases [Done] 27 | d. Quota can be set [Done] 28 | e. SpamAssassin settings [Done] 29 | f. Virus Scanner [Done] 30 | g. Blacklists and Whitelists 31 | -> tables are ready, almost code required 32 | h. Mailing lists [For later release] 33 | i. UID/GID [Done] 34 | j. Domain tag line attached to all outgoing mail [Unsure] 35 | k. Allow any user or alias to act as domain admin [Done] 36 | l. Password complexity requirements [For later release] 37 | m. Allow domain admin to tweak any user settings [Done] 38 | n. Multiple levels of 'admin': 39 | -> Some who can one add users, some who can only change 40 | passwords, others who can only create aliases, etc 41 | Eg, when an admin tries to create an alias, assign the 42 | variable '$task=addalias', and query the db to see if the 43 | admin has the privilege to do this. This requires a 44 | 'flags' column in the db. [For later release] 45 | 46 | 3. User Controls (depends on site and domain settings) 47 | a. Change password [Done] 48 | b. SpamAssassin settings [Done] 49 | c. Virus scanner settings [Done] 50 | d. Personal block lists [Done] 51 | e. Add alias (?) 52 | f. Vacation response [Done] 53 | g. Forward incoming mail [Done] 54 | h. See current quota limit [Done] 55 | -> Later, need a way to see current mailbox size 56 | This is dependent on the maildirsize option getting fixed in Exim 57 | i. Code to intergrate whitelists to webpages 58 | 59 | 4. System Controls 60 | a. Choose from multiple databases: MySQL, PostgreSQL, LDAP [MySQL, PostgreSQL done] 61 | b. Shell scripts to perform all the same work the webpages do. 62 | Possibly in PHP or Perl. Probably PHP for simpler DB 63 | integration/package maintenance. 64 | 65 | 5. Setup 66 | a. Scripts used to configure vexim the first time. 67 | -> These scripts should be deleted (for security purposes) 68 | after configuration is completed. 69 | The scripts will create the basic database (with site-wide 70 | settings), various tables, and the variables.php file from 71 | the site admin's input. Would it be wise to require a 72 | user/password with enough privileges to create a database 73 | and grant privileges on it? I've seen this done with other 74 | programs. It certainly makes things easier, but some 75 | people might have a problem with entering the mysql god 76 | password. 77 | b. Reduce the overall number of PHP files in the package, by 78 | consolidating common functions. 79 | -> Various 'submit' pages can be consolidated to one or two, 80 | etc. 81 | c. Add Wash's docs on using tpop3d to CVS [Done] 82 | d. Make APOP work! 83 | 84 | -------------------------------------------------------------------------------- /vexim/config/variables.php.example: -------------------------------------------------------------------------------- 1 | 'SET NAMES UTF8'); 13 | 14 | try { 15 | $dbh = new PDO($dsn, $sqluser, $sqlpass, $dboptions); 16 | $dbh->setAttribute($dbh::ATTR_DEFAULT_FETCH_MODE, $dbh::FETCH_ASSOC); 17 | } catch (PDOException $e) { 18 | die($e->getMessage()); 19 | } 20 | 21 | /* We use this IMAP server to check user quotas */ 22 | $imapquotaserver = "{mail.CHANGE.com:143/imap/notls}"; 23 | $imap_to_check_quota = "no"; 24 | 25 | /* Setting this to 0 if only admins should be allowed to login */ 26 | $AllowUserLogin = 1; 27 | 28 | /* Choose whether to break up domain and user lists alphabetically */ 29 | $alphadomains = 1; 30 | $alphausers = 1; 31 | 32 | /* Set to either "sha512" or "bcrypt" (only on *BSD) for advanced 33 | pw-hash functions, "des" and "md5" (kept for compatibility 34 | to older setups), or "clear" for clear-text passwords. 35 | It is not recommended to use the "clear" option 36 | Alternatively, you can specify custom salt prefix here, e.g. 37 | SHA-512 with 10000 rounds -> $cryptscheme='$6$rounds=10000$' 38 | or bcrypt with complexity 2^12 -> $cryptscheme='$2a$12$' */ 39 | $cryptscheme = 'sha512'; 40 | 41 | /* Guess domain name from hostname and allow login based on 42 | local part only. It is off by default, set this value to 1 43 | in order to enable this function 44 | and set a string which will be cut off the left side of 45 | the hostname*/ 46 | $domainguess = 0; 47 | $domainguess_lefttrim = "mail|vexim"; 48 | 49 | /* Enable password strength check 50 | To disable this check set $passwordstrengthcheck = 0; 51 | */ 52 | $passwordstrengthcheck = 1; 53 | 54 | /* Enable Content Security Policy (CSP) to prevent cross-site scripting 55 | and other code-injection attacks. */ 56 | $CSPenabled = false; 57 | 58 | /* The UID's and GID's control the default UID and GID for new domains 59 | and if postmasters can define their own. 60 | THE UID AND GID MUST BE NUMERIC! */ 61 | $uid = "90"; 62 | $gid = "90"; 63 | $postmasteruidgid = "yes"; 64 | 65 | /* Allow the siteadmin user to manage domains? */ 66 | $siteadminManageDomains = true; 67 | 68 | /* The location of your mailstore for new domains. 69 | Make sure the directory belongs to the configured $uid/$gid! */ 70 | $mailroot = "/var/vmail/"; 71 | 72 | /* Check if mail store specified above exists when creating a new domain. 73 | Generally, this shouldn't be disabled, but there may be special cases 74 | when our check doesn't work and should be disabled (e.g. the parent 75 | directory of mail store is inaccessible to your web server). */ 76 | $testmailroot = true; 77 | 78 | /* path to Mailman */ 79 | $mailmanroot = "http://www.EXAMPLE.com/mailman"; 80 | 81 | /* sa_tag is the default value to offer when we create new domains for SpamAssassin tagging 82 | sa_refuse is the default value to offer when we create new domains for SpamAssassin dropping */ 83 | $sa_tag = "2"; 84 | $sa_refuse = "5"; 85 | 86 | /* Welcome message, sent to new POP/IMAP accounts */ 87 | @$welcome_message = "Welcome, {$_POST['realname']} !\n\n" 88 | . "Your new E-mail account is all ready for you.\n\n" 89 | . "Here are some settings you might find useful:\n\n" 90 | . "Username: {$_POST['localpart']}@{$_SESSION['domain']}\n" 91 | . "POP3 server: mail.{$_SESSION['domain']}\n" 92 | . "SMTP server: mail.{$_SESSION['domain']}\n"; 93 | 94 | /* Welcome message, sent to new domains */ 95 | @$welcome_newdomain = "Welcome, and thank you for registering your e-mail domain\n" 96 | . "{$_POST['domain']} with us.\n\nIf you have any questions, please\n" 97 | . "don't hesitate to ask your account representitive.\n"; 98 | ?> 99 | -------------------------------------------------------------------------------- /docs/checklist.txt: -------------------------------------------------------------------------------- 1 | Checklist to follow after updating code, to confirm functionality 2 | ================================================================= 3 | 4 | [x] Logging in as Siteadmin 5 | Site management: 6 | [ ] Adding domains: local and relay 7 | [ ] With and without max accounts 8 | [ ] With and without quotas 9 | [ ] With and without maximum message size 10 | [ ] Different SpamAssassin scores 11 | [ ] With and without SpamAssassin 12 | [ ] With and without Anti-Virus 13 | [ ] With and without Piping mail option 14 | [ ] With and without 'Domain enabled' 15 | [ ] Changing domain admin password 16 | [ ] Changing UID/GID flags 17 | [ ] Changing Boolean flags has impact on admin/users 18 | [ ] Changing 'Enabled' works 19 | [ ] Deleting domains 20 | [ ] Adding domain alias 21 | [ ] Deleting domain alias 22 | [ ] Adding relay domain 23 | [ ] Deleting relay domain 24 | 25 | Siteadmin Management: 26 | [ ] Change siteadmin password 27 | [ ] Set siteadmin password to 'CHANGE', at next login you must change password. 28 | 29 | Account Additions: 30 | [ ] Adding aliases works 31 | [ ] Adding Catchall: 32 | [ ] Does the 'Add Catchall' option disappear if a Catchall exists? 33 | [ ] Adding POP/IMAP accounts: 34 | [ ] If the SpamAssassin/Anti-Virus/Piping options are disabled, 35 | do they still show up? 36 | [ ] Can you enable/disable the availible options? 37 | [ ] Can you change SpamAssassin scores? 38 | [ ] Can you change if spam mails are moved/deleted? 39 | [ ] Does adding :fail:'s work? 40 | [ ] If you have a maximum number of accounts: 41 | [ ] Is this displayed? 42 | [ ] Is the current number of accounts displayed? 43 | [ ] Does adding more than max accounts fail? 44 | 45 | Account Management: 46 | As Admin: 47 | [ ] Update Aliases: 48 | [ ] Can you update the name? 49 | [ ] Can you update the 'to' ? 50 | [ ] Can you update the final destination? 51 | [ ] Can you enable/disable Anti-Virus? 52 | [ ] Can you enable/disable SpamAssassin? 53 | [ ] Can you update SpamAssassin scores? 54 | [ ] Can you change if spam mails are forwarded/deleted? 55 | [ ] Update Catchall: 56 | [ ] Can you update the final destination? 57 | [ ] Update POP/IMAP accounts - name, password, UID, GID, procmail, admin 58 | [ ] Can you update the name? 59 | [ ] Can you update the password? 60 | [ ] Can you update the UID? 61 | [ ] Can you update the GID? 62 | [ ] Can you update the piping value? 63 | [ ] Can you update the admin flag? 64 | [ ] Can you update the SpamAssassin flag? 65 | [ ] Can you update the Anti-Virus flag? 66 | [ ] Can you update the SpamAssassin scores? 67 | [ ] Can you update the Spam-mail handling (move/delete)? 68 | [ ] Can you update the enabled flag? 69 | [ ] Can you add and remove header filters? 70 | [ ] Update :fail:'s 71 | [ ] Can you update the name? 72 | As User: 73 | [ ] Can you update your name? 74 | [ ] Can you change your password? (Try to re-login!) 75 | [ ] Can you enable/disable Anti-Virus? 76 | [ ] Can you enable/disable Spamassassin? 77 | [ ] Can you change SpamAssassin scores? 78 | [ ] Can you change the Spam-mail handling (move/delete)? 79 | [ ] Can you add/remove header filters? 80 | 81 | [ ] Logging in as Postmaster, a second Admin user, and a regular user 82 | [ ] Can you log in if the domain is disabled? Should work as postmaster, should not work as user 83 | [ ] Can you log in if the account is disabled? Should fail 84 | 85 | Account Deletions: 86 | [ ] Does deleting Aliases work? 87 | [ ] Does deleting Catchall work? 88 | [ ] Does deleteing POP/IMAP accounts work? 89 | [ ] Does deleting :fail:'s work? 90 | 91 | Testing Failures: 92 | [ ] Adding duplicate account 93 | [ ] Adding alias using existing account name 94 | [ ] Adding duplicate alias 95 | [ ] Email disabled accounts 96 | [ ] Going over quota 97 | [ ] Quota warnings 98 | [ ] Update mis-matched passwords as admin 99 | [ ] Update mis-matched passwords as user 100 | [ ] Creating POP/IMAP accounts with blank values 101 | [ ] Creating alias accounts with blank values 102 | -------------------------------------------------------------------------------- /vexim/admingroupdelete.php: -------------------------------------------------------------------------------- 1 | prepare($query); 11 | $sth->execute(array(':group_id'=>$_GET['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 12 | if (!$sth->rowCount()) { 13 | header ("Location: admingroup.php?group_faildeleted={$_GET['localpart']}"); 14 | die(); 15 | } 16 | 17 | # delete group member first 18 | $query = "DELETE FROM group_contents WHERE group_id=:group_id"; 19 | $sth = $dbh->prepare($query); 20 | $success = $sth->execute(array(':group_id'=>$_GET['group_id'])); 21 | if ($success) { 22 | # delete group 23 | $query = "DELETE FROM groups WHERE id=:group_id AND domain_id=:domain_id"; 24 | $sth = $dbh->prepare($query); 25 | $success = $sth->execute(array(':group_id'=>$_GET['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 26 | if ($success) { 27 | header ("Location: admingroup.php?group_deleted={$_GET['localpart']}"); 28 | die; 29 | } else { 30 | header ("Location: admingroup.php?group_faildeleted={$_GET['localpart']}"); 31 | die; 32 | } 33 | } else { 34 | header ("Location: admingroup.php?group_faildeleted={$_GET['localpart']}"); 35 | die; 36 | } 37 | } else if ($_GET['confirm'] == 'cancel') { 38 | header ("Location: admingroup.php?group_faildeleted={$_GET['localpart']}"); 39 | die; 40 | } 41 | } 42 | ?> 43 | 44 | 45 | 46 | <?php echo _('Virtual Exim') . ': ' . _('Confirm Delete'); ?> 47 | 48 | 49 | 50 | 51 | 56 |
57 |
58 | 59 | 60 | 66 | 67 | 68 | 75 | 76 | 77 | 84 | 85 | 86 | 96 | 97 |
61 | : 65 |
69 | 70 | 74 |
78 | 79 | 83 |
87 | '> 89 | '> 91 | '> 93 | '> 95 |
98 |
99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /vexim/adminalias.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 11 | 12 | 13 | 14 | 15 | 16 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | rowCount()) { 42 | $row = $sth->fetch(); 43 | print ''; 53 | print ''; 59 | print ''; 60 | print ''; 61 | print ''; 63 | } 64 | $query = "SELECT user_id,localpart,smtp,realname,type,admin,enabled 65 | FROM users 66 | WHERE domain_id=:domain_id AND type='alias' 67 | ORDER BY localpart;"; 68 | $sth = $dbh->prepare($query); 69 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 70 | if ($sth->rowCount()) { 71 | while ($row = $sth->fetch()) { 72 | if($row['enabled'] === 0) print ''; else print ''; 73 | print ''; 82 | print ''; 88 | print ''; 89 | print ''; 90 | print ''; 97 | } 98 | } 99 | ?> 100 |
 
' 44 | . '' 49 | . '' 54 | . '' 57 | . $row['realname'] 58 | . '*' . $row['smtp'] . ''; 62 | print '
' 74 | . ''; 83 | print '' 86 | . $row['realname'] 87 | . '' . $row['localpart'] . '' . $row['smtp'] . ''; 91 | if ($row['admin'] == "1") { 92 | print ''; 95 | } 96 | print '
101 |

: 102 | ' 105 | . _('It will catch and forward all email that does not get delivered to a specific mailbox.'); 106 | ?> 107 |

108 |
109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /vexim/adminaliasaddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 21 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 22 | $row = $sth->fetch(); 23 | if ((isset($_POST['on_avscan'])) && ($row['avscan'] == 1)) { 24 | $_POST['on_avscan'] = 1; 25 | } else { 26 | $_POST['on_avscan'] = 0; 27 | } 28 | if ((isset($_POST['on_spamassassin'])) && ($row['spamassassin'] == 1)) { 29 | $_POST['on_spamassassin'] = 1; 30 | } else { 31 | $_POST['on_spamassassin'] = 0; 32 | } 33 | # If a password wasn't specified, create a randomised 128bit password 34 | if (($_POST['clear'] === "") && ($_POST['vclear'] === "")) { 35 | $junk = md5(rand().time().rand()); 36 | $_POST['clear'] = $junk; 37 | $_POST['vclear'] = $junk; 38 | } 39 | 40 | # aliases must have a localpart defined 41 | if ($_POST['localpart']==''){ 42 | header("Location: adminalias.php?badname={$_POST['localpart']}"); 43 | die; 44 | } 45 | 46 | check_mail_address( 47 | $_POST['localpart'],$_SESSION['domain_id'],'adminalias.php' 48 | ); 49 | 50 | # check_user_exists() will die if a user account already exists with the same localpart and domain id 51 | check_user_exists( 52 | $dbh,$_POST['localpart'],$_SESSION['domain_id'],'adminalias.php' 53 | ); 54 | 55 | if(!isset($_POST['realname']) || $_POST['realname']==='') { 56 | $_POST['realname']=$_POST['localpart']; 57 | } 58 | 59 | if ((preg_match("/['@%!\/\|\" ']/",$_POST['localpart'])) 60 | || preg_match("/^\s*$/",$_POST['realname'])) { 61 | header("Location: adminalias.php?badname={$_POST['localpart']}"); 62 | die; 63 | } 64 | $forwardto=explode(",",$_POST['smtp']); 65 | for($i=0; $iprepare($query); 86 | $success = $sth->execute(array( 87 | ':localpart' => $_POST['localpart'], 88 | ':username' => $_POST['localpart'] . '@' . $_SESSION['domain'], 89 | ':domain_id' => $_SESSION['domain_id'], 90 | ':crypt' => crypt_password($_POST['clear']), 91 | ':smtp' => $aliasto, 92 | ':pop' => $aliasto, 93 | ':realname' => $_POST['realname'], 94 | ':admin' => $_POST['admin'], 95 | ':on_avscan' => $_POST['on_avscan'], 96 | ':on_spamassassin' => $_POST['on_spamassassin'], 97 | ':sa_tag'=>(isset($_POST['sa_tag']) ? $_POST['sa_tag'] : $sa_tag), 98 | ':sa_refuse'=>(isset($_POST['sa_refuse']) ? $_POST['sa_refuse'] : $sa_refuse), 99 | ':spam_drop'=>(isset($_POST['spam_drop']) ? $_POST['spam_drop'] : 0), 100 | ':enabled' => $_POST['enabled'] 101 | )); 102 | 103 | 104 | if ($success) { 105 | header ("Location: adminalias.php?added={$_POST['localpart']}"); 106 | } else { 107 | header ("Location: adminalias.php?failadded={$_POST['localpart']}"); 108 | } 109 | } else { 110 | header ("Location: adminalias.php?badaliaspass={$_POST['localpart']}"); 111 | } 112 | ?> 113 | 114 | -------------------------------------------------------------------------------- /vexim/adminaliaschangesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 12 | $sth->execute(array(':user_id'=>$_POST['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 13 | if (!$sth->rowCount()) { 14 | header ("Location: adminalias.php?failupdated={$_POST['localpart']}"); 15 | die(); 16 | } 17 | 18 | # Fix the boolean values 19 | if (isset($_POST['admin'])) { 20 | $_POST['admin'] = 1; 21 | } else { 22 | $_POST['admin'] = 0; 23 | } 24 | if (isset($_POST['enabled'])) { 25 | $_POST['enabled'] = 1; 26 | } else { 27 | $_POST['enabled'] = 0; 28 | } 29 | $query = "SELECT avscan,spamassassin from domains 30 | WHERE domain_id=:domain_id"; 31 | $sth = $dbh->prepare($query); 32 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 33 | $row = $sth->fetch(); 34 | if ((isset($_POST['on_avscan'])) && ($row['avscan'] == 1)) { 35 | $_POST['on_avscan'] = 1; 36 | } else { 37 | $_POST['on_avscan'] = 0; 38 | } 39 | if ((isset($_POST['on_spamassassin'])) && ($row['spamassassin'] == 1)) { 40 | $_POST['on_spamassassin'] = 1; 41 | } else { 42 | $_POST['on_spamassassin'] = 0; 43 | } 44 | 45 | # Update the password, if the password was given 46 | if(isset($_POST['password']) && $_POST['password']!=='' ){ 47 | if (validate_password($_POST['password'], $_POST['vpassword'])) { 48 | if (!password_strengthcheck($_POST['password'])) { 49 | header ("Location: adminalias.php?weakpass={$_POST['localpart']}"); 50 | die; 51 | } 52 | $cryptedpassword = crypt_password($_POST['password']); 53 | $query = "UPDATE users SET crypt=:crypt WHERE user_id=:user_id AND domain_id=:domain_id AND type='alias'"; 54 | $sth = $dbh->prepare($query); 55 | $success = $sth->execute(array(':crypt'=>$cryptedpassword, ':user_id'=>$_POST['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 56 | 57 | if ($success) { 58 | if ($_POST['localpart'] == $_SESSION['localpart']) { 59 | $_SESSION['crypt'] = $cryptedpassword; 60 | } 61 | } else { 62 | header ('Location: adminalias.php?failedupdated=' . $_POST['localpart']); 63 | die(); 64 | } 65 | } else { 66 | header ('Location: adminalias.php?badaliaspass'); 67 | die(); 68 | } 69 | } 70 | 71 | # update the actual alias in the users table 72 | $forwardto=explode(",",$_POST['target']); 73 | for($i=0; $iprepare($query); 89 | $success = $sth->execute(array( 90 | ':localpart'=>$_POST['localpart'], 91 | ':username'=>$_POST['localpart'].'@'.$_SESSION['domain'], 92 | ':smtp'=>$aliasto, 93 | ':pop'=>$aliasto, 94 | ':realname'=>$_POST['realname'], 95 | ':admin'=>$_POST['admin'], 96 | ':on_avscan'=>$_POST['on_avscan'], 97 | ':on_spamassassin'=>$_POST['on_spamassassin'], 98 | ':sa_tag'=>(isset($_POST['sa_tag']) ? $_POST['sa_tag'] : $sa_tag), 99 | ':sa_refuse'=>(isset($_POST['sa_refuse']) ? $_POST['sa_refuse'] : $sa_refuse), 100 | ':spam_drop'=>(isset($_POST['spam_drop']) ? $_POST['spam_drop'] : 0), 101 | ':enabled'=>$_POST['enabled'], 102 | ':user_id'=>$_POST['user_id'], 103 | ':domain_id'=>$_SESSION['domain_id'] 104 | )); 105 | if ($success) { 106 | header ("Location: adminalias.php?updated={$_POST['localpart']}"); 107 | } else { 108 | header ("Location: adminalias.php?failupdated={$_POST['localpart']}"); 109 | } 110 | ?> 111 | -------------------------------------------------------------------------------- /vexim/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Layout and CSS tricks obtained from 3 | http://www.bluerobot.com/web/layouts/ 4 | */ 5 | 6 | 7 | body { 8 | font-size: 10pt; 9 | margin:0px; 10 | padding:0px; 11 | font-family:verdana, arial, helvetica, sans-serif; 12 | color:#333; 13 | background-color:white; 14 | } 15 | h1 { 16 | margin:0px 0px 15px 0px; 17 | padding:0px; 18 | font-size:28px; 19 | line-height:28px; 20 | font-weight:900; 21 | color:#ccc; 22 | } 23 | p { 24 | font:11px/20px verdana, arial, helvetica, sans-serif; 25 | margin:0px 0px 16px 0px; 26 | padding:0px; 27 | } 28 | #Content>p {margin:0px;} 29 | #Content>p+p {text-indent:30px;} 30 | 31 | a { 32 | color:#09c; 33 | font-size:11px; 34 | text-decoration:none; 35 | font-weight:600; 36 | font-family:verdana, arial, helvetica, sans-serif; 37 | } 38 | 39 | p.alpha { 40 | padding-bottom: 2em; 41 | } 42 | 43 | a.alpha { 44 | font-size: 110%; 45 | } 46 | 47 | table,tr,td,th { 48 | border: hidden; 49 | font-size: 10pt; 50 | text-align: left; 51 | } 52 | 53 | table.zebra tr:nth-child(even) { 54 | background-color: #f0f0f0; 55 | } 56 | 57 | td.check { 58 | text-align: center; 59 | } 60 | 61 | td.button { 62 | padding-top: 1em; 63 | text-align: center; 64 | } 65 | 66 | td.trash { 67 | padding-right: 3px; 68 | } 69 | 70 | td.padafter { 71 | padding-bottom:1em; 72 | } 73 | 74 | td.padbefore { 75 | padding-top:1em; 76 | } 77 | 78 | tr.disabled a { 79 | color:#666; 80 | } 81 | 82 | div.row { 83 | clear: both; 84 | padding-top: 10px; 85 | } 86 | 87 | div.button { 88 | clear: both; 89 | padding-top: 10px; 90 | text-align: center; 91 | } 92 | 93 | div.row span.label { 94 | float: left; 95 | width: 125px; 96 | text-align: right; 97 | } 98 | 99 | div.row span.formw { 100 | position: relative; 101 | width: 300px; 102 | text-align: left; 103 | } 104 | 105 | .textfield { border-color: #000000; 106 | border-width: 1px; } 107 | 108 | a:link {color:#09c;} 109 | a:visited {color:#07a;} 110 | a:hover {background-color:#eee;} 111 | 112 | #Header { 113 | font-size: 12pt; 114 | margin:50px 0px 10px 0px; 115 | padding:10px 0px 2px 20px; 116 | /* For IE5/Win's benefit height = [correct height] + [top padding] + [top and bottom border widths] */ 117 | height:40px; /* 14px + 17px + 2px = 33px */ 118 | border-style:solid; 119 | border-color:black; 120 | border-width:1px 0px; /* top and bottom borders: 1px; left and right borders: 0px */ 121 | line-height:11px; 122 | background-color:#eee; 123 | voice-family: "\"}\""; 124 | voice-family:inherit; 125 | height:28px; /* the correct height */ 126 | } 127 | body>#Header {height:28px;} 128 | 129 | #Header>a:link, #Header>a:visited { 130 | font-family:verdana, arial, helvetica, sans-serif; 131 | font-size: 12pt; 132 | color: #000; 133 | } 134 | 135 | #Header>a:hover { 136 | font-family:verdana, arial, helvetica, sans-serif; 137 | font-size: 12pt; 138 | color: #09c; 139 | } 140 | 141 | #SiteadminHome { 142 | float: right; 143 | margin-right: 20px; 144 | } 145 | 146 | #Content { 147 | margin: 0px 50px 50px 250px; 148 | padding:10px; 149 | overflow: auto; 150 | } 151 | 152 | #forms { 153 | margin: 15px 200px 50px 250px; 154 | width: 450px; 155 | border: 1px dashed #999; 156 | padding: 10px; 157 | overflow: visible; 158 | } 159 | 160 | form.login { 161 | margin-top:3em; 162 | } 163 | 164 | #Centered { 165 | position:absolute; 166 | left:50%; 167 | width:500px; 168 | margin-top:50px; 169 | margin-left:-266px; 170 | padding:15px; 171 | border:1px dashed #333; 172 | background-color:#eee; 173 | } 174 | 175 | #Menu { 176 | position:absolute; 177 | top:100px; 178 | left:20px; 179 | width:172px; 180 | padding:10px; 181 | background-color:#eee; 182 | border:1px dashed #999; 183 | line-height:17px; 184 | /* Again, the ugly brilliant hack. */ 185 | voice-family: "\"}\""; 186 | voice-family:inherit; 187 | width:150px; 188 | } 189 | /* Again, "be nice to Opera 5". */ 190 | body>#Menu {width:150px;} 191 | 192 | #Status { 193 | position: absolute; 194 | bottom: 2em; 195 | font-weight: bold; 196 | left: 10%; 197 | text-align: center; 198 | border: 1px dashed #999; 199 | padding-top: .5em; 200 | padding-bottom: .5em; 201 | padding-left: 2em; 202 | padding-right: 2em; 203 | /* Again, the ugly brilliant hack. */ 204 | voice-family: "\"}\""; 205 | voice-family:inherit; 206 | width:500px; 207 | } 208 | /* Again, "be nice to Opera 5". */ 209 | body>#Status {width:500px;} 210 | 211 | img.trash { border:0; 212 | width:10px; 213 | height:16px 214 | } 215 | img.check { border:0; 216 | width:13px; 217 | height:12px 218 | } 219 | -------------------------------------------------------------------------------- /vexim/userchangesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 28 | $sth->execute(array(':user_id'=>$_SESSION['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 29 | $account = $sth->fetch(); 30 | $_POST['forward']=$account['forward']; 31 | } 32 | 33 | # Do some checking, to make sure the user is ALLOWED to make these changes 34 | $query = "SELECT avscan,spamassassin,maxmsgsize from domains WHERE domain_id=:domain_id"; 35 | $sth = $dbh->prepare($query); 36 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 37 | $row = $sth->fetch(); 38 | $_POST['on_avscan'] = (isset($_POST['on_avscan']) && $row['avscan'] == '1') ? 1 : 0; 39 | $_POST['on_spamassassin'] = (isset($_POST['on_spamassassin']) && $row['spamassassin'] == '1') ? 1 : 0; 40 | if (isset($_POST['maxmsgsize']) && $row['maxmsgsize']!=='0') { 41 | if ($_POST['maxmsgsize']<=0 || $_POST['maxmsgsize']>$row['maxmsgsize']) { 42 | $_POST['maxmsgsize']=$row['maxmsgsize']; 43 | } 44 | } else { 45 | $_POST['maxmsgsize']=0; 46 | } 47 | 48 | if (isset($_POST['realname']) && $_POST['realname']!=="") { 49 | $query = "UPDATE users SET realname=:realname 50 | WHERE user_id=:user_id"; 51 | $sth = $dbh->prepare($query); 52 | $sth->execute(array(':realname'=>$_POST['realname'], ':user_id'=>$_SESSION['user_id'])); 53 | } 54 | 55 | # Update the password, if the password was given 56 | if (isset($_POST['clear']) && $_POST['clear']!=='') { 57 | if (validate_password($_POST['clear'], $_POST['vclear'])) { 58 | if (!password_strengthcheck($_POST['clear'])) { 59 | header ("Location: userchange.php?weakpass"); 60 | die; 61 | } 62 | $cryptedpassword = crypt_password($_POST['clear']); 63 | $query = "UPDATE users SET crypt=:crypt WHERE user_id=:user_id"; 64 | $sth = $dbh->prepare($query); 65 | $success = $sth->execute(array(':crypt'=>$cryptedpassword, ':user_id'=>$_SESSION['user_id'])); 66 | if ($success) { 67 | $_SESSION['crypt'] = $cryptedpassword; 68 | header ("Location: userchange.php?userupdated"); 69 | die; 70 | } 71 | } 72 | header ("Location: userchange.php?badpass"); 73 | die; 74 | } 75 | 76 | #If the realname was changed in the upper form, don't run the rest of the script 77 | if (isset($_POST['realname'])) { 78 | header ("Location: userchange.php?userupdated"); 79 | die; 80 | } 81 | 82 | if (isset($_POST['vacation']) && is_string($_POST['vacation'])) { 83 | $vacation = trim($_POST['vacation']); 84 | $vacation = quoted_printable_encode($vacation); 85 | } else { 86 | $vacation = ''; 87 | } 88 | 89 | # Finally 'the rest' which is handled by the profile form 90 | $query = "UPDATE users SET on_avscan=:on_avscan, 91 | on_spamassassin=:on_spamassassin, sa_tag=:sa_tag, 92 | sa_refuse=:sa_refuse, on_vacation=:on_vacation, 93 | vacation=:vacation, on_forward=:on_forward, 94 | forward=:forward, maxmsgsize=:maxmsgsize, 95 | unseen=:unseen, spam_drop=:spam_drop 96 | WHERE user_id=:user_id"; 97 | $sth = $dbh->prepare($query); 98 | $success = $sth->execute(array(':on_avscan'=>$_POST['on_avscan'], 99 | ':on_spamassassin'=>$_POST['on_spamassassin'], 100 | ':sa_tag'=>(isset($_POST['sa_tag']) ? $_POST['sa_tag'] : $sa_tag), 101 | ':sa_refuse'=>(isset($_POST['sa_refuse']) ? $_POST['sa_refuse'] : $sa_refuse), 102 | ':on_vacation'=>$_POST['on_vacation'], 103 | ':vacation'=>$vacation, 104 | ':on_forward'=>$_POST['on_forward'], ':forward'=>$_POST['forward'], 105 | ':maxmsgsize'=>$_POST['maxmsgsize'], ':unseen'=>$_POST['unseen'], 106 | ':spam_drop'=>(isset($_POST['spam_drop']) ? $_POST['spam_drop'] : 0), 107 | ':user_id'=>$_SESSION['user_id'] 108 | )); 109 | if ($success) { 110 | header ("Location: userchange.php?userupdated"); 111 | die; 112 | } 113 | header ("Location: userchange.php?userfailed"); 114 | die; 115 | 116 | -------------------------------------------------------------------------------- /vexim/adminaliasadd.php: -------------------------------------------------------------------------------- 1 | prepare($query); 9 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 10 | if ($sth->rowCount()) { $row = $sth->fetch(); } 11 | ?> 12 | 13 | 14 | 15 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 16 | 17 | 18 | 19 | 20 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 97 | 98 | 99 | 100 | 106 | 107 | 108 | 109 | 110 | 113 | 114 | 115 | 118 | 119 |
:
: 35 | @ 36 | 37 |
41 | 43 |
:
: 52 | 53 |
57 | () 59 |
: 64 | 65 |
:
:
:
: 88 |
: 94 | 96 |
: 101 | 102 |
103 | 104 |
105 |
: 111 | 112 |
116 | 117 |
120 |
121 |
122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /vexim/adminuserdelete.php: -------------------------------------------------------------------------------- 1 | prepare($query); 12 | $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 13 | if (!$sth->rowCount()) { 14 | header ("Location: adminuser.php?faildeleted={$_GET['localpart']}"); 15 | die(); 16 | } 17 | if(!isset($_GET['confirm'])) { $_GET['confirm'] = null; } 18 | 19 | if ($_GET['confirm'] == '1') { 20 | # prevent deleting the last admin 21 | $query = "SELECT COUNT(user_id) AS count FROM users 22 | WHERE admin=1 AND domain_id=:domain_id 23 | AND (type='local' OR type='piped') 24 | AND user_id!=:user_id"; 25 | $sth = $dbh->prepare($query); 26 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'], ':user_id'=>$_GET['user_id'])); 27 | $row = $sth->fetch(); 28 | if ($row['count'] == "0") { 29 | header ("Location: adminuser.php?lastadmin={$_GET['localpart']}"); 30 | die; 31 | } 32 | 33 | $query = "DELETE FROM users 34 | WHERE user_id=:user_id 35 | AND domain_id=:domain_id 36 | AND (type='local' OR type='piped')"; 37 | $sth = $dbh->prepare($query); 38 | $success = $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 39 | if ($success) { 40 | $query = "DELETE FROM group_contents WHERE member_id=:user_id"; 41 | $sth = $dbh->prepare($query); 42 | $sth->execute(array(':user_id'=>$_GET['user_id'])); 43 | header ("Location: adminuser.php?deleted={$_GET['localpart']}"); 44 | } else { 45 | header ("Location: adminuser.php?faildeleted={$_GET['localpart']}"); 46 | } 47 | die; 48 | } else if ($_GET['confirm'] == "cancel") { 49 | header ("Location: adminuser.php?faildeleted={$_GET['localpart']}"); 50 | die; 51 | } else { 52 | $query = "SELECT COUNT(user_id) AS count FROM users 53 | WHERE admin=1 AND domain_id=:domain_id 54 | AND (type='local' OR type='piped') 55 | AND user_id!=:user_id"; 56 | $sth = $dbh->prepare($query); 57 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'], ':user_id'=>$_GET['user_id'])); 58 | $row = $sth->fetch(); 59 | if ($row['count'] == "0") { 60 | header ("Location: adminuser.php?lastadmin={$_GET['localpart']}"); 61 | die; 62 | } 63 | $query = "SELECT localpart FROM users WHERE user_id=:user_id"; 64 | $sth = $dbh->prepare($query); 65 | $sth->execute(array(':user_id'=>$_GET['user_id'])); 66 | if ($sth->rowCount()) { $row = $sth->fetch(); } 67 | } 68 | ?> 69 | 70 | 71 | 72 | <?php echo _('Virtual Exim') . ': ' . _('Confirm Delete'); ?> 73 | 74 | 75 | 76 | 77 | 82 |
83 |
84 | 85 | 86 | 92 | 93 | 94 | 101 | 102 | 103 | 110 | 111 | 112 | 122 | 123 |
87 | : 91 |
95 | 96 | 100 |
104 | 105 | 109 |
113 | '> 115 | '> 117 | '> 119 | '> 121 |
124 |
125 |
126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /vexim/sitechangesubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 29 | $success = $sth->execute(array( 30 | ':domain_id' => $_POST['allusers'], 31 | ':on_spamassassin' => $_POST['on_spamassassin'])); 32 | } 33 | 34 | // Process Anti-Virus 35 | if (isset($_POST['on_avscan']) && $success && ($_POST['on_avscan'] == '1' || $_POST['on_avscan'] == '0')) { 36 | $query = "UPDATE users SET on_avscan=:on_avscan WHERE domain_id=:domain_id"; 37 | $sth = $dbh->prepare($query); 38 | $success = $sth->execute(array( 39 | ':domain_id' => $_POST['allusers'], 40 | ':on_avscan' => $_POST['on_avscan'])); 41 | } 42 | break; 43 | 44 | case 'adminpass': 45 | if (validate_password($_POST['clear'], $_POST['vclear'])) { 46 | if (!password_strengthcheck($_POST['clear'])) { 47 | header ("Location: site.php?weakpass={$_POST['domain']}"); 48 | die; 49 | } 50 | $query = "UPDATE users SET crypt=:crypt WHERE localpart=:localpart AND domain_id=:domain_id"; 51 | $sth = $dbh->prepare($query); 52 | $success = $sth->execute(array( 53 | ':crypt' => crypt_password($_POST['clear']), 54 | ':localpart' => $_POST['localpart'], 55 | ':domain_id' => $_POST['domain_id'])); 56 | } else { 57 | header("Location: site.php?badpass={$_POST['domain']}"); 58 | die; 59 | } 60 | break; 61 | 62 | default: 63 | if (isset($_POST['avscan'])) { 64 | $_POST['avscan'] = 1; 65 | } else { 66 | $_POST['avscan'] = 0; 67 | } 68 | 69 | if (isset($_POST['spamassassin'])) { 70 | $_POST['spamassassin'] = 1; 71 | } else { 72 | $_POST['spamassassin'] = 0; 73 | } 74 | 75 | if (isset($_POST['enabled'])) { 76 | $_POST['enabled'] = 1; 77 | } else { 78 | $_POST['enabled'] = 0; 79 | } 80 | 81 | if (isset($_POST['pipe'])) { 82 | $_POST['pipe'] = 1; 83 | } else { 84 | $_POST['pipe'] = 0; 85 | } 86 | 87 | if (!isset($_POST['max_accounts']) || $_POST['max_accounts'] == '') { 88 | $_POST['max_accounts'] = '0'; 89 | } 90 | 91 | // User can specify either UID, or username, the former being preferred. 92 | // Using posix_getpwuid/posix_getgrgid even when we have an UID is so we 93 | // are sure the UID exists. 94 | if (isset ($_POST['uid'])) { 95 | $uid = $_POST['uid']; 96 | } 97 | if (isset ($_POST['gid'])) { 98 | $gid = $_POST['gid']; 99 | } 100 | 101 | if ($userinfo = @posix_getpwuid($uid)) { 102 | $uid = $userinfo['uid']; 103 | } elseif ($userinfo = @posix_getpwnam($uid)) { 104 | $uid = $userinfo['uid']; 105 | } else { 106 | header("Location: site.php?failuidguid={$_POST['domain']}"); 107 | die; 108 | } 109 | 110 | if ($groupinfo = @posix_getgrgid($gid)) { 111 | $gid = $groupinfo['gid']; 112 | } elseif ($groupinfo = @posix_getgrnam($gid)) { 113 | $gid = $groupinfo['gid']; 114 | } else { 115 | header("Location: site.php?failuidguid={$_POST['domain']}"); 116 | die; 117 | } 118 | $query = "UPDATE domains SET uid=:uid, gid=:gid, avscan=:avscan, 119 | maxmsgsize=:maxmsgsize, pipe=:pipe, max_accounts=:max_accounts, 120 | quotas=:quotas, sa_tag=:sa_tag, sa_refuse=:sa_refuse, 121 | spamassassin=:spamassassin, enabled=:enabled 122 | WHERE domain_id=:domain_id"; 123 | $sth = $dbh->prepare($query); 124 | $success = $sth->execute(array(':uid' => $uid, ':gid' => $gid, 125 | ':avscan' => $_POST['avscan'], ':maxmsgsize' => $_POST['maxmsgsize'], 126 | ':pipe' => $_POST['pipe'], ':max_accounts' => $_POST['max_accounts'], 127 | ':quotas' => $_POST['quotas'], 128 | ':sa_tag' => ((isset($_POST['sa_tag'])) ? $_POST['sa_tag'] : $sa_tag), 129 | ':sa_refuse' => ((isset($_POST['sa_refuse'])) ? $_POST['sa_refuse'] : $sa_refuse), 130 | ':spamassassin' => $_POST['spamassassin'], ':enabled' => $_POST['enabled'], 131 | ':domain_id' => $_POST['domain_id'], 132 | )); 133 | } 134 | 135 | if ($success) { 136 | header("Location: site.php?updated={$_POST['domain']}"); 137 | die; 138 | } else { 139 | // Just-in-case catchall 140 | header("Location: site.php?failupdated={$_POST['domain']}"); 141 | } 142 | -------------------------------------------------------------------------------- /vexim/sitedelete.php: -------------------------------------------------------------------------------- 1 | prepare($usrdelquery); 21 | $usrdelsuccess = $usrdelsth->execute(array(':domain_id'=>$_POST['domain_id'])); 22 | // if we were successful, delete the domain's blocklists 23 | if ($usrdelsuccess) { 24 | $usrdelquery = "DELETE FROM blocklists WHERE domain_id=:domain_id"; 25 | $usrdelsth = $dbh->prepare($usrdelquery); 26 | $usrdelsuccess = $usrdelsth->execute(array(':domain_id'=>$_POST['domain_id'])); 27 | // if we were successful, delete the domain's aliases 28 | if($usrdelsuccess) { 29 | $aliasdelquery = "DELETE FROM domainalias WHERE domain_id=:domain_id"; 30 | $aliasdelsth = $dbh->prepare($aliasdelquery); 31 | $aliasdelsuccess = $aliasdelsth->execute(array(':domain_id'=>$_POST['domain_id'])); 32 | // if we were successful, delete the domain itself 33 | if ($aliasdelsuccess) { 34 | $domdelquery = "DELETE FROM domains WHERE domain_id=:domain_id"; 35 | $domdelsth = $dbh->prepare($domdelquery); 36 | $domdelsuccess = $domdelsth->execute(array(':domain_id'=>$_POST['domain_id'])); 37 | // If everything went well, redirect to a success page. 38 | if ($domdelsuccess) { 39 | header ("Location: site.php?deleted={$_POST['domain']}"); 40 | die; 41 | } 42 | } 43 | } 44 | } else { 45 | header ("Location: site.php?faildeleted={$_POST['domain']}"); 46 | die; 47 | } 48 | } else if (($_POST['confirm'] == "1") && ($_POST['type'] == "alias")) { 49 | $aliasdeletequery = "DELETE FROM domainalias WHERE alias=:domain"; 50 | $sth = $dbh->prepare($aliasdeletequery); 51 | $success = $sth->execute(array(':domain'=>$_POST['domain'])); 52 | if ($success) { 53 | header ("Location: site.php?deleted={$_POST['domain']}"); 54 | die; 55 | } else { 56 | header ("Location: site.php?faildeleted={$_POST['domain']}"); 57 | die; 58 | } 59 | } else if ($_POST['confirm'] == "cancel") { 60 | header ("Location: site.php?canceldelete={$_POST['domain']}"); 61 | die; 62 | } 63 | 64 | if ($_GET['type'] != "alias") { 65 | $query = "SELECT COUNT(*) AS count, domain, domains.type FROM users,domains 66 | WHERE (domains.domain_id=:domain_id 67 | AND users.domain_id=domains.domain_id) 68 | GROUP BY domain,domains.type"; 69 | $sth = $dbh->prepare($query); 70 | $sth->execute(array(':domain_id'=>$_GET['domain_id'])); 71 | $row = $sth->fetch(); 72 | } 73 | ?> 74 | 75 | 76 | 77 | <?php echo _("Virtual Exim") . ": " . _("Confirm Delete"); ?> 78 | 79 | 80 | 81 | 82 | 87 |
88 |
89 | 90 | 91 | "; 95 | } 96 | ?> 97 | 98 | 99 | 103 |
:
"; 93 | printf (ngettext("There is currently %1\$d account in domain %2\$s", "There are currently %1\$d accounts in domain %2\$s", $row['count']), $row['count'], htmlspecialchars($_GET['domain'])); 94 | print "
'> 100 | '> 101 | '> 102 |
104 |
105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /vexim/adminuser.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 23 | 24 | 25 | 26 | 27 | 28 | 49 |
50 | 53 |
54 | : 55 | 57 | 58 | 66 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | $_SESSION['domain_id']); 81 | if ($alphausers AND $letter != '') { 82 | $query .= " AND lower(localpart) LIKE lower(:letter)"; 83 | $queryParams[':letter'] = $letter.'%'; 84 | } elseif ($_POST['searchfor'] != '') { 85 | $query .= ' AND ' . $dbh->quote($_POST['field']) . ' LIKE :searchfor'; 86 | $queryParams[':searchfor'] = '%'.$_POST['searchfor'].'%'; 87 | } 88 | $query .= ' ORDER BY realname, localpart'; 89 | $sth = $dbh->prepare($query); 90 | $sth->execute($queryParams); 91 | while ($row = $sth->fetch()) { 92 | if($row['enabled'] === 0) print ''; else print ''; 93 | print ''; 101 | print ''; 109 | print ''; 117 | print '\n"; 125 | } 126 | ?> 127 |
 
'; 98 | print 'trashcan' 107 | . $row['realname'] 108 | . '' 115 | . $row['localpart'] .'@'. htmlspecialchars($_SESSION['domain']) 116 | . ''; 118 | if ($row['admin'] == 1) { 119 | print ''; 123 | } 124 | print "
128 |
129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /setup/migrations/vexim_1.5_to_2.0.1_mysql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- MySQL script to upgrade Vexim database schema from Vexim 1.5 to Vexim 2.0.1 3 | -- 4 | 5 | -- Create the `domains` table 6 | CREATE TABLE `domains` ( 7 | `domain_id` mediumint(8) UNSIGNED AUTO_INCREMENT NOT NULL, 8 | `domain` varchar(64) NOT NULL DEFAULT '', 9 | `maildir` varchar(128) NOT NULL DEFAULT '', 10 | `uid` smallint(5) UNSIGNED NOT NULL DEFAULT '65534', 11 | `gid` smallint(5) UNSIGNED NOT NULL DEFAULT '65534', 12 | `max_accounts` int(10) UNSIGNED NOT NULL DEFAULT '0', 13 | `quotas` int(10) UNSIGNED NOT NULL DEFAULT '0', 14 | `type` varchar(5) DEFAULT NULL, 15 | `avscan` bool NOT NULL DEFAULT '0', 16 | `blocklists` bool NOT NULL DEFAULT '0', 17 | `complexpass` bool NOT NULL DEFAULT '0', 18 | `enabled` bool NOT NULL DEFAULT '1', 19 | `mailinglists` bool NOT NULL DEFAULT '0', 20 | `maxmsgsize` mediumint(8) UNSIGNED NOT NULL DEFAULT '0', 21 | `pipe` bool NOT NULL DEFAULT '0', 22 | `spamassassin` bool NOT NULL DEFAULT '0', 23 | `sa_tag` smallint(5) UNSIGNED NOT NULL DEFAULT '0', 24 | `sa_refuse` smallint(5) UNSIGNED NOT NULL DEFAULT '0', 25 | PRIMARY KEY (`domain_id`), 26 | UNIQUE KEY `domain` (`domain`), 27 | KEY `domain_id` (`domain_id`), 28 | KEY `domains` (`domain`) 29 | 30 | ); 31 | -- Insert the siteadmin domain 32 | INSERT INTO `domains` (`domain_id`, `domain`) VALUES ('1', 'admin'); 33 | -- Insert other domains 34 | INSERT INTO `domains` (`domain`, `type`) SELECT DISTINCT `domain`, 'local' FROM `users` WHERE `domain`!='admin'; 35 | -- Set domain maildirs 36 | UPDATE `domains` SET `maildir`=( 37 | SELECT SUBSTRING_INDEX(`pophome`, `local_part`, 1) FROM `users` 38 | WHERE `users`.`domain`=`domains`.`domain` 39 | AND SUBSTRING(`users`.`pophome`, 1, 1)='/' 40 | LIMIT 1 41 | ) WHERE `domain_id` != '1'; 42 | 43 | -- Update the structure and contents of the `users` table 44 | ALTER TABLE `users` DROP INDEX `username`; 45 | ALTER TABLE `users` ADD COLUMN `user_id` int(10) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY FIRST; 46 | ALTER TABLE `users` ADD COLUMN `domain_id` mediumint(8) UNSIGNED NOT NULL AFTER `user_id`; 47 | UPDATE `users` SET `domain_id`=(SELECT `domain_id` FROM `domains` WHERE `domains`.`domain`=`users`.`domain`); 48 | ALTER TABLE `users` CHANGE COLUMN `local_part` `localpart` varchar(192) NOT NULL DEFAULT '' AFTER `domain_id`; 49 | ALTER TABLE `users` MODIFY COLUMN `username` varchar(255) NOT NULL DEFAULT '' AFTER `localpart`; 50 | ALTER TABLE `users` CHANGE COLUMN `cpassword` `clear` varchar(255) DEFAULT NULL AFTER `username`; 51 | ALTER TABLE `users` CHANGE COLUMN `password` `crypt` varchar(48) DEFAULT NULL AFTER `clear`; 52 | -- Set default uid and gid values where these are not numeric 53 | UPDATE `users` SET `uid`='65535' WHERE `type`='admin' AND `admin`='site' AND `uid` NOT REGEXP '^[0-9]+$'; 54 | UPDATE `users` SET `uid`='65534' WHERE `uid` NOT REGEXP '^[0-9]+$'; 55 | ALTER TABLE `users` MODIFY COLUMN `uid` smallint(5) UNSIGNED NOT NULL DEFAULT '65534' AFTER `crypt`; 56 | UPDATE `users` SET `gid`='65535' WHERE `type`='admin' AND `admin`='site' AND `gid` NOT REGEXP '^[0-9]+$'; 57 | UPDATE `users` SET `gid`='65534' WHERE `gid` NOT REGEXP '^[0-9]+$'; 58 | ALTER TABLE `users` MODIFY COLUMN `gid` smallint(5) UNSIGNED NOT NULL DEFAULT '65534' AFTER `uid`; 59 | ALTER TABLE `users` CHANGE COLUMN `smtphome` `smtp` varchar(255) DEFAULT NULL AFTER `gid`; 60 | ALTER TABLE `users` CHANGE COLUMN `pophome` `pop` varchar(255) DEFAULT NULL AFTER `smtp`; 61 | -- Reset invalid type values to 'local' 62 | UPDATE `users` SET `type`='local' WHERE `type` NOT IN ('local','alias','catch','fail','piped','admin','site'); 63 | ALTER TABLE `users` MODIFY COLUMN `type` enum('local','alias','catch','fail','piped','admin','site') NOT NULL DEFAULT 'local' AFTER `pop`; 64 | UPDATE `users` SET `type`='site',`localpart`=`username` WHERE `type`='admin' AND `admin`='site'; 65 | UPDATE `users` SET `admin`='1' WHERE `admin` IS NOT NULL AND `admin`!=''; 66 | UPDATE `users` SET `admin`='0' WHERE `admin`!='1' OR `admin` IS NULL; 67 | ALTER TABLE `users` MODIFY COLUMN `admin` bool NOT NULL DEFAULT '0' AFTER `type`; 68 | ALTER TABLE `users` ADD COLUMN `on_avscan` bool NOT NULL DEFAULT '0' AFTER `admin`; 69 | ALTER TABLE `users` ADD COLUMN `on_blocklist` bool NOT NULL DEFAULT '0' AFTER `on_avscan`; 70 | ALTER TABLE `users` ADD COLUMN `on_complexpass` bool NOT NULL DEFAULT '0' AFTER `on_blocklist`; 71 | ALTER TABLE `users` ADD COLUMN `on_forward` bool NOT NULL DEFAULT '0' AFTER `on_complexpass`; 72 | ALTER TABLE `users` ADD COLUMN `on_piped` bool NOT NULL DEFAULT '0' AFTER `on_forward`; 73 | UPDATE `users` SET `on_piped`='1' WHERE `smtp` LIKE '"|%'; 74 | ALTER TABLE `users` ADD COLUMN `on_spamassassin` bool NOT NULL DEFAULT '0' AFTER `on_piped`; 75 | ALTER TABLE `users` ADD COLUMN `on_vacation` bool NOT NULL DEFAULT '0' AFTER `on_spamassassin`; 76 | ALTER TABLE `users` CHANGE COLUMN `status` `enabled` bool NOT NULL DEFAULT '1' AFTER `on_vacation`; 77 | ALTER TABLE `users` ADD COLUMN `flags` varchar(16) DEFAULT NULL AFTER `enabled`; 78 | ALTER TABLE `users` ADD COLUMN `forward` varchar(255) DEFAULT NULL AFTER `flags`; 79 | ALTER TABLE `users` ADD COLUMN `maxmsgsize` mediumint(8) UNSIGNED NOT NULL DEFAULT '0' AFTER `forward`; 80 | ALTER TABLE `users` ADD COLUMN `quota` int(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `maxmsgsize`; 81 | ALTER TABLE `users` MODIFY COLUMN `realname` varchar(255) DEFAULT NULL AFTER `quota`; 82 | ALTER TABLE `users` ADD COLUMN `sa_tag` smallint(5) UNSIGNED NOT NULL DEFAULT '0' AFTER `realname`; 83 | ALTER TABLE `users` ADD COLUMN `sa_refuse` smallint(5) UNSIGNED NOT NULL DEFAULT '0' AFTER `sa_tag`; 84 | ALTER TABLE `users` ADD COLUMN `tagline` varchar(255) DEFAULT NULL AFTER `sa_refuse`; 85 | ALTER TABLE `users` ADD COLUMN `vacation` varchar(255) DEFAULT NULL AFTER `tagline`; 86 | ALTER TABLE `users` DROP COLUMN `domain`; 87 | ALTER TABLE `users` ADD UNIQUE INDEX `username` (`localpart`, `domain_id`); 88 | ALTER TABLE `users` ADD INDEX `local` (`localpart`); 89 | 90 | -- Create the `blocklists` table 91 | CREATE TABLE `blocklists` ( 92 | `block_id` int(10) UNSIGNED AUTO_INCREMENT NOT NULL, 93 | `domain_id` mediumint(8) UNSIGNED NOT NULL, 94 | `user_id` int(10) UNSIGNED DEFAULT NULL, 95 | `blockhdr` varchar(192) NOT NULL DEFAULT '', 96 | `blockval` varchar(192) NOT NULL DEFAULT '', 97 | `color` varchar(8) NOT NULL DEFAULT '', 98 | PRIMARY KEY (`block_id`) 99 | ); 100 | 101 | -- Create the `domainalias` table 102 | CREATE TABLE `domainalias` ( 103 | `domain_id` mediumint(8) UNSIGNED NOT NULL, 104 | `alias` varchar(64) DEFAULT NULL 105 | ); 106 | -------------------------------------------------------------------------------- /vexim/siteaddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 80 | $sth->execute(array(':domain_id'=>$_POST['aliasdest'], ':alias'=>$_POST['domain'])); 81 | if ($sth->rowCount()!==1) { 82 | header ("Location: site.php?failaddeddomerr={$_POST['domain']}"); 83 | die; 84 | } else { 85 | header ("Location: site.php?added={$_POST['domain']}" . 86 | "&type={$_POST['type']}"); 87 | die; 88 | } 89 | } else { // local or relay 90 | if ($_POST['type'] === "local") { 91 | if (!validate_password($_POST['clear'], $_POST['vclear'])) { 92 | header ("Location: site.php?failaddedpassmismatch={$_POST['domain']}"); 93 | die; 94 | } 95 | if (!password_strengthcheck($_POST['clear'])) { 96 | header ("Location: site.php?weakpass={$_POST['domain']}"); 97 | die; 98 | } 99 | } 100 | $query = "INSERT INTO domains 101 | (domain, spamassassin, sa_tag, sa_refuse, avscan, 102 | max_accounts, quotas, maildir, pipe, enabled, uid, gid, 103 | type, maxmsgsize) 104 | VALUES (:domain, :spamassassin, :sa_tag, :sa_refuse, 105 | :avscan, :max_accounts, :quotas, :maildir, :pipe, :enabled, 106 | :uid, :gid, :type, :maxmsgsize)"; 107 | $sth = $dbh->prepare($query); 108 | $success = $sth->execute(array(':domain'=>$_POST['domain'], 109 | ':spamassassin'=>$_POST['spamassassin'], 110 | ':sa_tag'=>((isset($_POST['sa_tag'])) ? $_POST['sa_tag'] : $sa_tag), 111 | ':sa_refuse'=>((isset($_POST['sa_refuse'])) ? $_POST['sa_refuse'] : $sa_refuse), 112 | ':avscan'=>$_POST['avscan'], ':max_accounts'=>$_POST['max_accounts'], 113 | ':quotas'=>((isset($_POST['quotas'])) ? $_POST['quotas'] : 0), 114 | ':maildir'=>((isset($_POST['maildir'])) ? $domainpath : ''), 115 | ':pipe'=>$_POST['pipe'], ':enabled'=>$_POST['enabled'], 116 | ':uid'=>$uid, ':gid'=>$gid, ':type'=>$_POST['type'], 117 | ':maxmsgsize'=>((isset($_POST['maxmsgsize'])) ? $_POST['maxmsgsize'] : 0) 118 | )); 119 | if ($success) { 120 | if ($_POST['type'] == "local") { 121 | $query = "INSERT INTO users 122 | (domain_id, localpart, username, crypt, uid, gid, smtp, pop, realname, type, admin) 123 | SELECT domain_id, :localpart, :username, :crypt, :uid, :gid, :smtp, :pop, 'Domain Admin', 'local', 1 124 | FROM domains 125 | WHERE domains.domain=:domain"; 126 | $sth = $dbh->prepare($query); 127 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], 128 | ':username'=>$_POST['localpart'].'@'.$_POST['domain'], 129 | ':crypt'=>crypt_password($_POST['clear']), 130 | ':uid'=>$uid, ':gid'=>$gid, ':smtp'=>$smtphomepath, 131 | ':pop'=>$pophomepath, 132 | ':domain'=>$_POST['domain'], 133 | )); 134 | if (!$success) { 135 | header ("Location: site.php?failaddedusrerr={$_POST['domain']}"); 136 | die; 137 | } else { 138 | header ("Location: site.php?added={$_POST['domain']}" . 139 | "&type={$_POST['type']}"); 140 | mail("{$_POST['localpart']}@{$_POST['domain']}", 141 | vexim_encode_header(_("Welcome Domain Admin!")), 142 | "$welcome_newdomain", 143 | "From: {$_POST['localpart']}@{$_POST['domain']}\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n"); 144 | die; 145 | } 146 | } else { 147 | header ("Location: site.php?added={$_POST['domain']}" . 148 | "&type={$_POST['type']}"); 149 | die; 150 | } 151 | } 152 | header ("Location: site.php?failaddeddomerr={$_POST['domain']}"); 153 | } 154 | ?> 155 | 156 | -------------------------------------------------------------------------------- /vexim/adminuseraddsubmit.php: -------------------------------------------------------------------------------- 1 | prepare($query); 15 | $success = $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 16 | if ($success) { 17 | $domrow = $sth->fetch(); 18 | if (!$domrow['allowed']) { 19 | header ("Location: adminuser.php?maxaccounts=true"); 20 | die(); 21 | } 22 | } 23 | 24 | # Strip off leading and trailing spaces 25 | $_POST['localpart'] = preg_replace("/^\s+/","",$_POST['localpart']); 26 | $_POST['localpart'] = preg_replace("/\s+$/","",$_POST['localpart']); 27 | 28 | # get the settings for the domain 29 | $query = "SELECT avscan,spamassassin,pipe,uid,gid,quotas,maxmsgsize FROM domains 30 | WHERE domain_id=:domain_id"; 31 | $sth = $dbh->prepare($query); 32 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 33 | if ($sth->rowCount()) { 34 | $row = $sth->fetch(); 35 | } 36 | 37 | # Fix the boolean values 38 | if (isset($_POST['admin'])) { 39 | $_POST['admin'] = 1; 40 | } else { 41 | $_POST['admin'] = 0; 42 | } 43 | if (isset($_POST['enabled'])) { 44 | $_POST['enabled'] = 1; 45 | } else { 46 | $_POST['enabled'] = 0; 47 | } 48 | if ($postmasteruidgid == "yes"){ 49 | if(!isset($_POST['uid'])) { 50 | $_POST['uid'] = $row['uid']; 51 | } 52 | if(!isset($_POST['gid'])) { 53 | $_POST['gid'] = $row['gid']; 54 | } 55 | }else{ 56 | # customisation of the uid and gid is not permitted for postmasters, use the domain defaults 57 | $_POST['uid'] = $row['uid']; 58 | $_POST['gid'] = $row['gid']; 59 | } 60 | if(!isset($_POST['quota'])) { 61 | $_POST['quota'] = $row['quotas']; 62 | } 63 | if($row['quotas'] != "0") { 64 | if (($_POST['quota'] > $row['quotas']) || ($_POST['quota'] === "0")) { 65 | header ("Location: adminuser.php?quotahigh={$row['quotas']}"); 66 | die; 67 | } 68 | } 69 | if($row['maxmsgsize'] !== "0") { 70 | if (($_POST['maxmsgsize'] > $row['maxmsgsize']) || ($_POST['maxmsgsize'] === "0")) { 71 | $_POST['maxmsgsize']=$row['maxmsgsize']; 72 | } 73 | } 74 | 75 | # Do some checking, to make sure the user is ALLOWED to make these changes 76 | if ((isset($_POST['on_piped'])) && ($row['pipe'] == 1)) { 77 | $_POST['on_piped'] = 1; 78 | } else { 79 | $_POST['on_piped'] = 0; 80 | } 81 | if ((isset($_POST['on_avscan'])) && ($row['avscan'] == 1)) { 82 | $_POST['on_avscan'] = 1; 83 | } else { 84 | $_POST['on_avscan'] = 0; 85 | } 86 | if ((isset($_POST['on_spamassassin'])) && ($row['spamassassin'] == 1)) { 87 | $_POST['on_spamassassin'] = 1; 88 | } else { 89 | $_POST['on_spamassassin'] = 0; 90 | } 91 | 92 | check_mail_address( 93 | $_POST['localpart'],$_SESSION['domain_id'],'adminuser.php' 94 | ); 95 | 96 | check_user_exists( 97 | $dbh,$_POST['localpart'],$_SESSION['domain_id'],'adminuser.php' 98 | ); 99 | 100 | if (preg_match("/^\s*$/",$_POST['realname'])) { 101 | $_POST['realname']=$_POST['localpart']; 102 | } 103 | 104 | if (preg_match("/['@%!\/\| ']/",$_POST['localpart']) 105 | || preg_match("/^\s*$/",$_POST['localpart'])) { 106 | header("Location: adminuser.php?badname={$_POST['localpart']}"); 107 | die; 108 | } 109 | 110 | $query = "SELECT maildir FROM domains WHERE domain_id=:domain_id"; 111 | $sth = $dbh->prepare($query); 112 | $sth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 113 | if ($sth->rowCount()) { $row = $sth->fetch(); } 114 | if (($_POST['on_piped'] == 1) && ($_POST['smtp'] != '')) { 115 | $smtphomepath = $_POST['smtp']; 116 | $pophomepath = "{$row['maildir']}/{$_POST['localpart']}"; 117 | $_POST['type'] = 'piped'; 118 | } else { 119 | $smtphomepath = "{$row['maildir']}/{$_POST['localpart']}/Maildir"; 120 | $pophomepath = "{$row['maildir']}/{$_POST['localpart']}"; 121 | $_POST['type'] = 'local'; 122 | } 123 | 124 | if (validate_password($_POST['clear'], $_POST['vclear'])) { 125 | if (!password_strengthcheck($_POST['clear'])) { 126 | header ("Location: adminuser.php?weakpass={$_POST['localpart']}"); 127 | die; 128 | } 129 | $query = "INSERT INTO users (localpart, username, domain_id, crypt, 130 | smtp, pop, uid, gid, realname, type, admin, on_avscan, on_piped, 131 | on_spamassassin, sa_tag, sa_refuse, spam_drop, maxmsgsize, enabled, quota) 132 | VALUES (:localpart, :username, :domain_id, :crypt, :smtp, :pop, :uid, :gid, 133 | :realname, :type, :admin, :on_avscan, :on_piped, :on_spamassassin, 134 | :sa_tag, :sa_refuse, :spam_drop, :maxmsgsize, :enabled, :quota)"; 135 | $sth = $dbh->prepare($query); 136 | $success = $sth->execute(array(':localpart'=>$_POST['localpart'], 137 | ':localpart'=>$_POST['localpart'], 138 | ':username'=>$_POST['localpart'].'@'.$_SESSION['domain'], 139 | ':domain_id'=>$_SESSION['domain_id'], 140 | ':crypt'=>crypt_password($_POST['clear']), 141 | ':smtp'=>$smtphomepath, 142 | ':pop'=>$pophomepath, 143 | ':uid'=>$_POST['uid'], 144 | ':gid'=>$_POST['gid'], 145 | ':realname'=>$_POST['realname'], 146 | ':type'=>$_POST['type'], 147 | ':admin'=>$_POST['admin'], 148 | ':on_avscan'=>$_POST['on_avscan'], 149 | ':on_piped'=>$_POST['on_piped'], 150 | ':on_spamassassin'=>$_POST['on_spamassassin'], 151 | ':sa_tag'=>(isset($_POST['sa_tag']) ? $_POST['sa_tag'] : $sa_tag), 152 | ':sa_refuse'=>(isset($_POST['sa_refuse']) ? $_POST['sa_refuse'] : $sa_refuse), 153 | ':spam_drop'=>(isset($_POST['spam_drop']) ? $_POST['spam_drop'] : 0), 154 | ':maxmsgsize'=>$_POST['maxmsgsize'], 155 | ':enabled'=>$_POST['enabled'], 156 | ':quota'=>$_POST['quota'], 157 | )); 158 | 159 | if ($success) { 160 | header ("Location: adminuser.php?added={$_POST['localpart']}"); 161 | mail("{$_POST['localpart']}@{$_SESSION['domain']}", 162 | vexim_encode_header(sprintf(_("Welcome %s!"), $_POST['realname'])), 163 | "$welcome_message", 164 | "From: {$_SESSION['localpart']}@{$_SESSION['domain']}\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n"); 165 | die; 166 | } else { 167 | header ("Location: adminuser.php?failadded={$_POST['localpart']}"); 168 | die; 169 | } 170 | } else { 171 | header ("Location: adminuser.php?badpass={$_POST['localpart']}"); 172 | die; 173 | } 174 | ?> 175 | 176 | -------------------------------------------------------------------------------- /docs/debian-conf.d/router/250_vexim_virtual_domains: -------------------------------------------------------------------------------- 1 | 2 | ### router/250_vexim_virtual_domains 3 | ################################# 4 | 5 | virtual_vacation: 6 | driver = accept 7 | domains = +local_domains 8 | condition = ${if and { {!match {$h_precedence:}{(?i)junk|bulk|list}} \ 9 | {eq {${lookup mysql{select users.on_vacation from users,domains \ 10 | where localpart = '${quote_mysql:$local_part}' \ 11 | and domain = '${quote_mysql:$domain}' \ 12 | and users.on_vacation = '1' \ 13 | and users.domain_id=domains.domain_id}}}{1} }} {yes}{no} } 14 | no_verify 15 | no_expn 16 | unseen 17 | transport = virtual_vacation_delivery 18 | 19 | virtual_forward: 20 | driver = redirect 21 | domains = +local_domains 22 | check_ancestor 23 | unseen = ${if eq {${lookup mysql{select unseen from users,domains \ 24 | where localpart = '${quote_mysql:$local_part}' \ 25 | and domain = '${quote_mysql:$domain}' \ 26 | and users.on_forward = '1' \ 27 | and users.domain_id=domains.domain_id}}}{1} {yes}{no}} 28 | data = ${lookup mysql{select forward from users,domains \ 29 | where localpart='${quote_mysql:$local_part}' \ 30 | and domain='${quote_mysql:$domain}' \ 31 | and users.domain_id=domains.domain_id \ 32 | and on_forward = '1'}} 33 | # We explicitly make this condition NOT forward mailing list mail! 34 | condition = ${if and { {!match {$h_precedence:}{(?i)junk}} \ 35 | {eq {${lookup mysql{select users.on_forward from users,domains \ 36 | where localpart = '${quote_mysql:$local_part}' \ 37 | and domain = '${quote_mysql:$domain}' \ 38 | and users.on_forward = '1' \ 39 | and users.domain_id=domains.domain_id}}}{1} }} {yes}{no} } 40 | 41 | virtual_domains: 42 | driver = redirect 43 | domains = +local_domains 44 | address_data = ${lookup mysql{\ 45 | select smtp, users.sa_tag*10 AS sa_tag, users.on_spamassassin AND domains.spamassassin AS on_spamassassin, \ 46 | users.uid AS uid, users.gid AS gid, quota \ 47 | from users,domains \ 48 | where localpart = '${quote_mysql:$local_part}' \ 49 | and domain = '${quote_mysql:$domain}' \ 50 | and domains.enabled = '1' \ 51 | and users.enabled = '1' \ 52 | and users.domain_id = domains.domain_id}{$value}fail} 53 | allow_fail 54 | data = ${extract{smtp}{$address_data}} 55 | headers_add = ${if and { \ 56 | {match{$domain}{$original_domain}} \ 57 | {match{$local_part}{$original_local_part}} \ 58 | {>={$spam_score_int}{${extract{sa_tag}{$address_data}}}} \ 59 | {eq{1}{${extract{on_spamassassin}{$address_data}}}} \ 60 | } {X-Spam-Flag: YES\nX-Spam-Score: $acl_m_spam_score\nVEXIM_SPAM_REPORT_HEADER_NAME: $acl_m_spam_report}{} } 61 | # using local_part_suffixes enables possibility to use user-"something" localparts 62 | # which could cause you trouble if you're creating email-adresses with dashes in between. 63 | .ifdef VEXIM_LOCALPART_SUFFIX 64 | local_part_suffix = VEXIM_LOCALPART_SUFFIX 65 | local_part_suffix_optional 66 | .endif 67 | retry_use_local_part 68 | file_transport = virtual_delivery 69 | reply_transport = address_reply 70 | pipe_transport = address_pipe 71 | 72 | # A group is a list of users 73 | # 74 | # if a group is flaged public 75 | # then anyone on the internet can write to it 76 | # else only members can write to it 77 | # 78 | # If not public non member sender will receive a "550 Unknown user" message 79 | virtual_dom_groups: 80 | driver = redirect 81 | domains = +local_domains 82 | allow_fail 83 | senders = ${if eq{Y}{${lookup mysql{select g.is_public \ 84 | from groups g, domains d \ 85 | where d.enabled = '1' and d.domain = '${quote_mysql:$domain}' and \ 86 | d.domain_id = g.domain_id and g.enabled = '1' and \ 87 | g.name = '${quote_mysql:$local_part}'}}} \ 88 | {$sender_address} \ 89 | {${lookup mysql{select concat_ws('@', u.localpart, d.domain) \ 90 | from domains d, groups g, group_contents c, users u \ 91 | where d.enabled = '1' and d.domain = '${quote_mysql:$domain}' and \ 92 | d.domain_id = g.domain_id and g.name = '${quote_mysql:$local_part}' and \ 93 | g.enabled = '1' and \ 94 | g.is_public = 'N' and c.member_id = u.user_id and \ 95 | d.domain_id = u.domain_id and u.enabled = '1' \ 96 | and u.username = '${quote_mysql:$sender_address}' limit 1}}}} 97 | data = ${lookup mysql{ \ 98 | select concat_ws('@', u.localpart, d.domain) \ 99 | from domains d, groups g, group_contents c, users u \ 100 | where d.enabled = '1' and \ 101 | d.domain = '${quote_mysql:$domain}' and \ 102 | d.domain_id = g.domain_id and \ 103 | g.enabled = '1' and \ 104 | g.id = c.group_id and \ 105 | c.member_id = u.user_id and \ 106 | d.domain_id = u.domain_id and \ 107 | u.enabled = '1' and \ 108 | g.name = '${quote_mysql:$local_part}'} } 109 | # using local_part_suffixes enables possibility to use user-"something" localparts 110 | # which could cause you trouble if you're creating email-adresses with dashes in between. 111 | .ifdef VEXIM_LOCALPART_SUFFIX 112 | local_part_suffix = VEXIM_LOCALPART_SUFFIX 113 | local_part_suffix_optional 114 | .endif 115 | retry_use_local_part 116 | reply_transport = address_reply 117 | pipe_transport = address_pipe 118 | 119 | virtual_domains_catchall: 120 | driver = redirect 121 | domains = +local_domains 122 | allow_fail 123 | data = ${lookup mysql{select smtp from users,domains where localpart = '*' \ 124 | and domain = '${quote_mysql:$domain}' \ 125 | and users.domain_id = domains.domain_id}} 126 | retry_use_local_part 127 | file_transport = virtual_delivery 128 | reply_transport = address_reply 129 | pipe_transport = address_pipe_catchall 130 | 131 | virtual_domain_alias: 132 | driver = redirect 133 | domains = +local_domains 134 | allow_fail 135 | data = ${lookup mysql{select concat('${quote_mysql:$local_part}@', domain) \ 136 | from domains,domainalias where domainalias.alias = '${quote_mysql:$domain}' \ 137 | and domainalias.domain_id = domains.domain_id}} 138 | retry_use_local_part 139 | -------------------------------------------------------------------------------- /vexim/site.php: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | <?php echo _('Virtual Exim') . ': ' . _('Manage Sites'); ?> 20 | 21 | 22 | 23 | 24 | 31 |
32 | 35 |
36 | : 39 | 41 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | prepare($query); 67 | $sth->execute($queryParams); 68 | while ($row = $sth->fetch()) { 69 | if($row['enabled'] === 0) print ''; else print ''; 70 | ?> 71 | 79 | 84 | 87 | 88 | 91 | 92 | 95 | 96 | 99 | 100 | 103 | 104 | 107 | 108 | 109 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | query($query); 127 | while ($row = $sth->fetch()) { 128 | ?> 129 | 130 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 147 | query($query); 151 | while ($row = $sth->fetch()) { 152 | ?> 153 | 154 | 162 | 165 | 166 | '; 171 | if($AllowUserLogin){ 172 | echo ''; 173 | }else{ 174 | echo ''; 175 | } 176 | ?> 177 |
72 | 75 | trashcan 77 | 78 | 80 | 83 | manage
110 | : 111 | 115 |
131 | 134 | trashcan 136 | 137 |
155 | 158 | trashcan 160 | 161 | 163 | 164 |
 
'._('Standard user accounts are currently able to login and change their own personal details').'.
'._('The system is currently configured to prevent standard users logging in to change their own personal details.').'
178 |
179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /vexim/admingroupchange.php: -------------------------------------------------------------------------------- 1 | 6 | prepare($query); 9 | $sth->execute(array(':group_id'=>$_GET['group_id'], ':domain_id'=>$_SESSION['domain_id'])); 10 | if($sth->rowCount()) { 11 | $row = $sth->fetch(); 12 | $grouplocalpart = $row['name']; 13 | } 14 | ?> 15 | 16 | 17 | 18 | <?php echo _('Virtual Exim') . ': ' . _('Edit group'); ?> 19 | 20 | 21 | 22 | 23 | 29 |
30 | rowCount()) { 33 | echo '
'; 34 | echo "Invalid groupid '" . htmlentities($_GET['group_id']) . "' for domain '" . htmlentities($_SESSION['domain']). "'"; 35 | echo '
'; 36 | }else{ 37 | ?> 38 | 39 | 41 | 42 | 43 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 131 | 132 | 133 | 134 | 135 | 137 | 138 | 139 | 162 | 163 | 164 | 168 | 169 | 170 |
: 44 | @ 46 | 47 | 49 |
54 | 56 | class="textfield"> 57 |
62 | 64 | class="textfield"> 65 |
69 | 70 |
 
78 | prepare($query); 84 | $sth->execute(array(':group_id'=>$_GET['group_id'])); 85 | if ($sth->rowCount()) { 86 | ?> 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | fetch()) { 96 | ?> 97 | 98 | 109 | 110 | 111 | 120 | 121 | 124 |
 
99 | 103 | trashcan 107 | 108 | 112 | 115 | 116 | 119 |
125 | 130 |
 
140 | 142 | 144 | 161 |
165 | 167 |
171 | 175 |
176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /setup/migrations/vexim_2.2_to_2.3_mysql.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- MySQL script to upgrade Vexim database schema from Vexim 2.2RC1, 2.2 and 2.2.1 to Vexim 2.3 3 | -- 4 | -- Uncomment the section at the bottom of this file if you used Arne Schirmacher's patch for Vexim 2.2RC1 5 | -- from https://www.schirmacher.de/display/INFO/improved+Vexim+frontend+and+bug+fixes 6 | -- 7 | -- The following defines new salt prefix for password hashes. 8 | -- Before dropping the `clear` field, we will re-hash passwords for all users whose cleartext and encrypted 9 | -- passwords match using a secure hashing scheme (SHA-512 by default, supported by Linux, FreeBSD and Solaris). 10 | -- Below you may change this prefix if you prefer a different hashing scheme, e.g. '$2a$10$' for bcrypt 11 | -- (*BSD-only Blowfish hashing scheme): 12 | SET @NEW_PW_PREFIX='$6$'; 13 | -- Comment out this definition if you do not want your password hashes updated. Be advised that doing so would 14 | -- leave these passwords less secure, and you will not be able to re-hash them later. 15 | 16 | -- 17 | -- Table: `domains` 18 | -- 19 | ALTER TABLE `domains` ENGINE=InnoDB; 20 | ALTER TABLE `domains` CONVERT TO CHARACTER SET utf8; 21 | ALTER TABLE `domains` MODIFY COLUMN `domain_id` int(10) unsigned NOT NULL AUTO_INCREMENT; 22 | ALTER TABLE `domains` MODIFY COLUMN `domain` varchar(255) NOT NULL DEFAULT ''; 23 | ALTER TABLE `domains` MODIFY COLUMN `maildir` varchar(4096) NOT NULL DEFAULT ''; 24 | ALTER TABLE `domains` MODIFY COLUMN `uid` smallint(5) unsigned NOT NULL DEFAULT '65534'; 25 | ALTER TABLE `domains` MODIFY COLUMN `gid` smallint(5) unsigned NOT NULL DEFAULT '65534'; 26 | ALTER TABLE `domains` DROP COLUMN `complexpass`; 27 | ALTER TABLE `domains` DROP KEY `domain_id`; 28 | ALTER TABLE `domains` DROP KEY `domains`; 29 | 30 | -- 31 | -- Table: `users` 32 | -- 33 | ALTER TABLE `users` ENGINE=InnoDB; 34 | ALTER TABLE `users` CONVERT TO CHARACTER SET utf8; 35 | ALTER TABLE `users` MODIFY COLUMN `domain_id` int(10) unsigned NOT NULL; 36 | ALTER TABLE `users` MODIFY COLUMN `localpart` varchar(64) NOT NULL DEFAULT ''; 37 | ALTER TABLE `users` MODIFY COLUMN `crypt` varchar(255) DEFAULT NULL; 38 | -- Re-hash passwords using the more secure scheme. 39 | UPDATE `users` 40 | SET `crypt` = COALESCE(ENCRYPT(`clear`, CONCAT(@NEW_PW_PREFIX, MD5(RAND()))), `crypt`) 41 | WHERE `crypt` = ENCRYPT(`clear`, `crypt`) AND @NEW_PW_PREFIX IS NOT NULL; 42 | -- Passwords are now re-hashed. 43 | SET @NEW_PW_PREFIX=NULL; 44 | ALTER TABLE `users` DROP COLUMN `clear`; 45 | ALTER TABLE `users` MODIFY COLUMN `uid` smallint(5) unsigned NOT NULL DEFAULT '65534'; 46 | ALTER TABLE `users` MODIFY COLUMN `gid` smallint(5) unsigned NOT NULL DEFAULT '65534'; 47 | ALTER TABLE `users` MODIFY COLUMN `smtp` varchar(4096) DEFAULT NULL; 48 | ALTER TABLE `users` MODIFY COLUMN `pop` varchar(4096) DEFAULT NULL; 49 | ALTER TABLE `users` DROP COLUMN `on_complexpass`; 50 | ALTER TABLE `users` ADD COLUMN `spam_drop` tinyint(1) NOT NULL DEFAULT '0' AFTER `on_vacation`; 51 | ALTER TABLE `users` MODIFY COLUMN `forward` varchar(4096) DEFAULT NULL; 52 | ALTER TABLE `users` MODIFY COLUMN `unseen` tinyint(1) NOT NULL DEFAULT '0'; 53 | ALTER TABLE `users` MODIFY COLUMN `vacation` text DEFAULT NULL; 54 | ALTER TABLE `users` ADD KEY `fk_users_domain_id_idx` (`domain_id`); 55 | ALTER TABLE `users` ADD CONSTRAINT `fk_users_domain_id` 56 | FOREIGN KEY (`domain_id`) 57 | REFERENCES `domains` (`domain_id`) 58 | ON DELETE CASCADE 59 | ON UPDATE CASCADE; 60 | 61 | -- 62 | -- Table: `blocklists` 63 | -- 64 | ALTER TABLE `blocklists` ENGINE=InnoDB; 65 | ALTER TABLE `blocklists` CONVERT TO CHARACTER SET utf8; 66 | ALTER TABLE `blocklists` MODIFY COLUMN `domain_id` int(10) unsigned NOT NULL; 67 | ALTER TABLE `blocklists` MODIFY COLUMN `blockval` varchar(255) NOT NULL DEFAULT ''; 68 | ALTER TABLE `blocklists` ADD KEY `fk_blocklists_domain_id_idx` (`domain_id`); 69 | ALTER TABLE `blocklists` ADD KEY `fk_blocklists_user_id_idx` (`user_id`); 70 | ALTER TABLE `blocklists` ADD CONSTRAINT `fk_blocklists_domain_id` 71 | FOREIGN KEY (`domain_id`) 72 | REFERENCES `domains` (`domain_id`) 73 | ON DELETE CASCADE 74 | ON UPDATE CASCADE; 75 | ALTER TABLE `blocklists` ADD CONSTRAINT `fk_blocklists_user_id` 76 | FOREIGN KEY (`user_id`) 77 | REFERENCES `users` (`user_id`) 78 | ON DELETE CASCADE 79 | ON UPDATE CASCADE; 80 | 81 | -- 82 | -- Table: `domainalias` 83 | -- 84 | ALTER TABLE `domainalias` ENGINE=InnoDB; 85 | ALTER TABLE `domainalias` CONVERT TO CHARACTER SET utf8; 86 | ALTER TABLE `domainalias` MODIFY COLUMN `domain_id` int(10) unsigned NOT NULL; 87 | ALTER TABLE `domainalias` MODIFY COLUMN `alias` varchar(255) NOT NULL; 88 | ALTER TABLE `domainalias` ADD PRIMARY KEY (`alias`); 89 | ALTER TABLE `domainalias` ADD KEY `fk_domainalias_domain_id_idx` (`domain_id`); 90 | ALTER TABLE `domainalias` ADD CONSTRAINT `fk_domainalias_domain_id` 91 | FOREIGN KEY (`domain_id`) 92 | REFERENCES `domains` (`domain_id`) 93 | ON DELETE CASCADE 94 | ON UPDATE CASCADE; 95 | 96 | -- 97 | -- Table: `groups` 98 | -- 99 | ALTER TABLE `groups` ENGINE=InnoDB; 100 | ALTER TABLE `groups` CONVERT TO CHARACTER SET utf8; 101 | ALTER TABLE `groups` MODIFY COLUMN `id` int(10) unsigned NOT NULL AUTO_INCREMENT; 102 | ALTER TABLE `groups` MODIFY COLUMN `domain_id` int(10) unsigned NOT NULL; 103 | ALTER TABLE `groups` ADD KEY `fk_groups_domain_id_idx` (`domain_id`); 104 | ALTER TABLE `groups` ADD CONSTRAINT `fk_groups_domain_id` 105 | FOREIGN KEY (`domain_id`) 106 | REFERENCES `domains` (`domain_id`) 107 | ON DELETE CASCADE 108 | ON UPDATE CASCADE; 109 | 110 | -- 111 | -- Table: `group_contents` 112 | -- 113 | ALTER TABLE `group_contents` ENGINE=InnoDB; 114 | ALTER TABLE `group_contents` CONVERT TO CHARACTER SET utf8; 115 | ALTER TABLE `group_contents` MODIFY COLUMN `group_id` int(10) unsigned NOT NULL; 116 | ALTER TABLE `group_contents` MODIFY COLUMN `member_id` int(10) unsigned NOT NULL; 117 | ALTER TABLE `group_contents` ADD KEY `fk_group_contents_group_id_idx` (`group_id`); 118 | ALTER TABLE `group_contents` ADD KEY `fk_group_contents_member_id_idx` (`member_id`); 119 | ALTER TABLE `group_contents` ADD CONSTRAINT `fk_group_contents_group_id` 120 | FOREIGN KEY (`group_id`) 121 | REFERENCES `groups` (`id`) 122 | ON DELETE CASCADE 123 | ON UPDATE CASCADE; 124 | ALTER TABLE `group_contents` ADD CONSTRAINT `fk_group_contents_member_id` 125 | FOREIGN KEY (`member_id`) 126 | REFERENCES `users` (`user_id`) 127 | ON DELETE CASCADE 128 | ON UPDATE CASCADE; 129 | 130 | -- 131 | -- Uncomment the following section if you used Arne Schirmacher's patch for Vexim 2.2RC1 132 | -- 133 | 134 | -- UPDATE `users` SET spam_drop=1 WHERE movedelete=2; 135 | -- ALTER TABLE `users` DROP COLUMN `movedelete`; 136 | -- ALTER TABLE `users` DROP COLUMN `on_rewritesubject`; 137 | -------------------------------------------------------------------------------- /vexim/adminaliaschange.php: -------------------------------------------------------------------------------- 1 | prepare($query); 10 | $sth->execute(array(':user_id'=>$_GET['user_id'], ':domain_id'=>$_SESSION['domain_id'])); 11 | if ($sth->rowCount()) { 12 | $row = $sth->fetch(); 13 | } 14 | $domquery = "SELECT avscan,spamassassin,quotas,pipe FROM domains 15 | WHERE domain_id=:domain_id"; 16 | $domsth = $dbh->prepare($domquery); 17 | $domsth->execute(array(':domain_id'=>$_SESSION['domain_id'])); 18 | if ($domsth->rowCount()) { 19 | $domrow = $domsth->fetch(); 20 | } 21 | ?> 22 | 23 | 24 | 25 | 26 | 27 | <?php echo _('Virtual Exim') . ': ' . _('Manage Users'); ?> 28 | 29 | 30 | 31 | 32 | 38 |
39 | rowCount()) { 42 | echo '
'; 43 | echo "Invalid alias userid '" . htmlentities($_GET['user_id']) . "' for domain '" . htmlentities($_SESSION['domain']). "'"; 44 | echo '
'; 45 | }else{ 46 | ?> 47 |
48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 63 | 64 | 65 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 95 | 96 | 98 | 101 | 102 | 103 | 104 | 110 | 111 | 114 | 115 | 116 | 122 | 123 | 127 | 128 | 129 | 135 | 136 | 137 | 138 | 142 | 143 | 144 | 145 | 149 | 150 | 151 | 152 | 160 | 161 | 164 | 165 | 166 | 172 | 173 | 174 | 178 | 179 |
: 52 | 54 |
: 59 | 61 | @ 62 |
66 | 68 |
72 | 75 |
: 80 | 82 |
: 87 | 88 |
92 | () 94 |
: 99 | 100 |
: 105 | class="textfield"> 109 |
: 117 | class="textfield"> 121 |
: 130 | class="textfield"> 134 |
: 139 | 141 |
: 146 | 148 |
: 153 | > 155 |
156 | > 158 |
159 |
: 167 | class="textfield"> 171 |
175 | 177 |
180 |
181 | 185 |
186 | 187 | 188 | 189 | --------------------------------------------------------------------------------