Initial commit for Autoconfig

pull/343/head
Jacques Deguest 4 years ago
parent d81363541a
commit 7740a0d132

1
.gitignore vendored

@ -5,3 +5,4 @@
/.php_cs.cache
/.idea
/composer.lock
**/*.bbprojectd

File diff suppressed because it is too large Load Diff

@ -0,0 +1,157 @@
Installation procedure for Autodiscovery configuration tool
============================================================
* Author: [Jacques Deguest](mailto:jack@deguest.jp)
* Created: 2020-03-10
* License: Same as Postfix Admin itself
## Quick background & overview
Autodiscovery is a somewhat standardised features that makes it possible for mail client to find out the proper configuration for a mail account, and to prevent the every day user from guessing the right parameters.
Let's take the example of joe@example.com.
When creating an account on Thurderbird and other who use the same configuration, the mail client will make a http query to <https://www.example.com/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=joe%2540example.com>
See this page from Mozilla for more information: <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat>
If a dns record exist
For Outlook, the mail client will attempt a POST request to: <https://www.example.com/autodiscover/autodiscover.xml> and submit a xml-based request
For Mac mail and iOS, the user needs to download a `mobileconfig` file, which is basically an xml file, that can be signed.
Unfortunately, there is no auto discovery system for Mac/iOS mail, so you need to redirect your users to the `autoconfig.pl` cgi script under the Postfix Admin web root. You need to pass a `emailaddress` parameter for example <https://www.example.com/postfixadmin/autoconfig.pl?mac_mail=1&emailaddress=joe@example.com>
## Installation
### Dependencies
#### SQL
You need to activate the `uuid-ossp` PostgreSQL extension to use the UUID_V4. You can do that, as an admin logged on PostgreSQL, with `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`
If you cannot or do not want to do that, edit the sql script for PostgreSQL and comment line 9 and uncomment line 11, comment line 72 and uncoment line 74, comment line 84 and uncomment line 86, comment line 107 and uncomment line 109
#### Perl
The following perl modules are required. Most are standard core modules.
* strict
* IO::File
* CGI v4.44
* Email::Valid v1.202
* Email::Address v1.912
* XML::LibXML v2.0201
* XML::LibXML::PrettyPrint v0.006
* Data::Dumper v2.174
* Scalar::Util v1.50
* Data::UUID v1.224
* File::Basename v2.85
* Cwd v3.78
* File::Temp v0.2309
* File::Spec v3.78
* File::Which v1.23
* JSON v2.34
* DBI v1.642
* TryCatch v1.003002
* Devel::StackTrace v2.04
For PostgreSQL you need `DBD::Pg`. I use version 3.8.1.
For MySQL you need `DBD::mysql` any recent version should do.
For SQLite, you need `DBD::SQLite`. I used version 1.62.
You can install those module using `cpanm` (https://metacpan.org/pod/App::cpanminus) like:
`cpanm --interactive IO::File CGI Email::Valid Email::Address XML::LibXML XML::LibXML::PrettyPrint Data::Dumper Scalar::Util Data::UUID File::Basename Cwd File::Temp File::Spec File::Which JSON DBI TryCatch Devel::StackTrace`
#### Web
* jQuery v3.3.1 (loaded automatically from within the template by calling <https://code.jquery.com/jquery-3.3.1.min.js>)
#### Signature of mobileconfig files for Mac/iOS
You need to have `openssl` installed. I used version 1.0.2g. You would also need ssl certificates installed for server wide or per domain. I recommend using Let's Encrypt <https://letsencrypt.org/> by installing their command line too `certbot`
### SQL
Load the sql script `autoconfig.sql` into your Postfix Admin database. For exaple:
* PostgreSQL : psql -U postfixadmin postfixadmin < autoconfig.sql
* MySQL : mysql -u postfixadmin postfixadmin < autoconfig.sql
* SQLite : sqlite3 /path/to/database.sqlite < autoconfig.sql
This will create 4 separate tables. Rest assured that `autoconfig` does not alter in any way other areas of Postfix Admin database.
### PHP, perl and other web files
Move `AutoconfigHandler.php` under the `model` sub directory in the Postfix Admin root folder, and `autoconfig.php`, `autoconfig.pl`, `autoconfig.css`, `autoconfig.js` and `sprintf.js` under the Postfix Admin web root `public`:
```
mv ./AUTOCONFIG/autoconfig.{css,js,php,pl} ./public/
mv ./AUTOCONFIG/{autoconfig_languages.php,sprintf.js} ./public/
mv ./AUTOCONFIG/AutoconfigHandler.php ./model
mv ./AUTOCONFIG/*.tpl ./templates/
```
#### Additional notes :
`autoconfig.js` is a small file containing event handlers to make the use of the admin interface smooth, and also makes use of Ajax with jQuery 3.3.1. jQuery 3.3.1 is used, and not the latest 3.3.2, because the pseudo selector `:first` and `:last` have been deprecated and are needed here, at least until I can find an alternative solution. If you have one, please let me know!
The general use of javascript is light and only to support workflow, nothing more. Pure css is used whenever possible (such as the switch button). No other framework is used to keep things light.
FontAwesome version 5.12.1 is loaded as import in the css file
`autoconfig.pl` will guess the location of the `config.inc.php` based on the file path. You can change that, such as by specifiying `config.local.php` instead by editing the perl script and change the line `our $POSTFIXADMIN_CONF_FILE = File::Basename::dirname( __FILE__ ) . '/../config.inc.php';` for example into `our $POSTFIXADMIN_CONF_FILE = '/var/www/postfix/config.inc.php';`
`autoconfig.pl` will read the php file by converting it into a json file and save that conversion into a temporary file on the server until the modification time of `config.inc.php` changes.
### DNS
Not required, but to take full advaantage of the idea of auto discovery, you should set up the following dns record in your domain name zones:
```bind
_submission._tcp IN SRV 0 1 587 mail.example.com.
_imap._tcp IN SRV 0 0 143 mail.example.com.
_imaps._tcp IN SRV 0 0 993 mail.example.com.
_pop3._tcp IN SRV 0 0 110 mail.example.com.
```
If you want to use a dedicated autodisvoer sub domain, you could set up yoru dns zone with the following record:
```bind
_autodiscover._tcp IN SRV 0 10 443 autoconfig.example.com.
```
### Apache
Add the following tp the general config file or to the relevant Vitual Hosts. You can also add it as a conf file under `/etc/apache2/conf-available` if it exists and then issue `a2enconf autoconfig.conf` to activate it (assuming the file name was `autoconfig.conf`)
(Here I presumed Postfix Admin is installed under /var/www/postfixadmin)
```conf
Alias /autoconfig /var/www/postfixadmin/public
<Directory "/var/www/postfixadmin/public/">
AllowOverride None
Options Indexes FollowSymLinks ExecCGI
Require all granted
Allow from all
AddHandler cgi-script .cgi .pl
</Directory>
RewriteEngine On
# For Thunderbird and others
RewriteRule "^/.well-known/autoconfig/mail/config-v1.1.xml" "/autoconfig/autoconfig.pl" [PT,L]
# For Outlook; POST request
RewriteRule "^/autodiscover/autodiscover.xml" "/autoconfig/autoconfig.pl?outlook=1" [PT,L]
# For autodiscovery settings in dns
RewriteRule "^/mail/config-v1\.1\.xml(.*)$" "/autoconfig/autoconfig.pl" [PT,L]
```

@ -0,0 +1,91 @@
<tr class="{if $server.type == 'imap' || $server.type == 'pop3'}autoconfig-incoming{else}autoconfig-outgoing{/if}">
<td colspan="3">
<table class="server" width="100%">
<tr>
<td class="label" width="20%"><label>{$PALANG.pAutoconfig_type}:</label></td>
{if $server.type == "imap" || $server.type == "pop3"}
<td width="60%"><select name="type[]" class="host_type">
<option value="imap" {if !empty($server.type) && $server.type == "imap"}selected{/if}>imap</option>
<option value="pop3" {if !empty($server.type) && $server.type == "pop3"}selected{/if}>pop3</option></select></td>
{else}
<td width="20%"><em>smtp</em><input type="hidden" name="type[]" value="smtp" /></td>
{/if}
<td rowspan="2"><button class="autoconfig-command ripple autoconfig-server-add {if $server.type == 'imap' || $server.type == 'pop3'} autoconfig-incoming-server{else}autoconfig-outgoing-server{/if}" title="{$PALANG.pAutoconfig_add_new_host}"><i class="fas fa-plus fa-2x"></i></button><button class="autoconfig-command ripple autoconfig-server-remove {if $server.type == 'imap' || $server.type == 'pop3'} autoconfig-incoming-server{else}autoconfig-outgoing-server{/if}" title="{$PALANG.pAutoconfig_remove_host}"><i class="fas fa-minus fa-2x"></i></button><button class="autoconfig-command ripple autoconfig-move-up" title="{$PALANG.pAutoconfig_move_up_host}"><i class="fas fa-arrow-up fa-2x"></i></button><button class="autoconfig-command ripple autoconfig-move-down" title="{$PALANG.pAutoconfig_move_down_host}"><i class="fas fa-arrow-down fa-2x"></i></button></td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_hostname}:</label></td>
<td><input type="hidden" name="host_id[]" value="{$server.host_id}" />
<input type="text" size="40" class="flat" name="hostname[]" maxlength="255" value="{$server.hostname}" /></td>
<!-- <td>&nbsp;</td> -->
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_port}:</label></td>
<td><input type="number" class="flat" name="port[]" maxlength="255" value="{$server.port}" list="defaultPorts" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_socket_type}:</label></td>
<td><select name="socket_type[]">
<option value="" {if empty($server.socket_type) || $server.socket_type == ""}selected{/if}>{$PALANG.pAutoconfig_no_selection}</option>
<option value="SSL" {if !empty($server.socket_type) && $server.socket_type == "SSL"}selected{/if}>SSL</option>
<option value="STARTTLS" {if !empty($server.socket_type) && $server.socket_type == "STARTTLS"}selected{/if}>STARTTLS</option>
<option value="TLS" {if !empty($server.socket_type) && $server.socket_type == "TLS"}selected{/if}>TLS</option>
</select></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_auth}:</label></td>
<td><select name="auth[]">
<option value="password-cleartext" {if !empty($server.auth) && $server.auth == "password-cleartext"}selected{/if}>{$PALANG.pAutoconfig_password_cleartext}</option>
<option value="password-encrypted" {if !empty($server.auth) && $server.auth == "password-encrypted"}selected{/if}>{$PALANG.pAutoconfig_password_encrypted}</option>
<option value="NTLM" {if !empty($server.auth) && $server.auth == "NTLM"}selected{/if}>NTLM</option>
<option value="GSSAPI" {if !empty($server.auth) && $server.auth == "GSSAPI"}selected{/if}>GSSAPI</option>
<option value="client-IP-address" {if !empty($server.auth) && $server.auth == "client-IP-address"}selected{/if}>{$PALANG.pAutoconfig_client_ip_address}</option>
<option value="TLS-client-cert" {if !empty($server.auth) && $server.auth == "TLS-client-cert"}selected{/if}>{$PALANG.pAutoconfig_tls_client_cert}</option>
<option value="smtp-after-pop" {if !empty($server.auth) && $server.auth == "smtp-after-pop"}selected{/if}>{$PALANG.pAutoconfig_smtp_after_pop}</option>
<option value="ouath2" {if !empty($server.auth) && $server.auth == "ouath2"}selected{/if}>OAuth2</option>
</select></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_username}:</label></td>
<td><input type="text" class="flat" name="username[]" maxlength="255" value="{$server.username}" placeholder="%EMAILADDRESS%" /> {$PALANG.pAutoconfig_username_template}<select name="username_template" class="username_template">
<option value="">{$PALANG.pAutoconfig_no_selection}</option>
<option value="%EMAILADDRESS%">%EMAILADDRESS%</option>
<option value="%EMAILLOCALPART%">%EMAILLOCALPART%</option>
<option value="%EMAILDOMAIN%">%EMAILDOMAIN%</option>
</select></td>
<td>&nbsp;</td>
</tr>
{if $server.type == "pop3" || $server.type == "imap"}
<!-- if incoming server is a pop3 -->
{if isset( $server.host_id )}
{assign var=host_unique_id value=$server.host_id}
{else}
{assign var=host_unique_id value=10|mt_rand:20}
{/if}
<tr class="host_pop3">
<td class="label"><label for="autoconfig_leave_messages_on_server_{$host_unique_id}">{$PALANG.pAutoconfig_leave_messages_on_server}:</label></td>
<td><input type="checkbox" class="flat" name="leave_messages_on_server[]" id="autoconfig_leave_messages_on_server_{$host_unique_id}" value="1" {if !empty($server.leave_messages_on_server) && $server.leave_messages_on_server == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr class="host_pop3">
<td class="label"><label for="autoconfig_download_on_biff_{$host_unique_id}">{$PALANG.pAutoconfig_download_on_biff}:</label></td>
<td><input type="checkbox" class="flat" name="download_on_biff[]" id="autoconfig_download_on_biff_{$host_unique_id}" value="1" {if !empty($server.download_on_biff) && $server.download_on_biff == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr class="host_pop3">
<td class="label"><label for="autoconfig_days_to_leave_messages_on_server_{$host_unique_id}">{$PALANG.pAutoconfig_days_to_leave_messages_on_server}:</label></td>
<td><input type="number" class="flat" name="days_to_leave_messages_on_server[]" id="autoconfig_days_to_leave_messages_on_server_{$host_unique_id}" min="0" max="365" value="{$server.days_to_leave_messages_on_server}" /></td>
<td>&nbsp;</td>
</tr>
<tr class="host_pop3">
<td class="label"><label for="autoconfig_check_interval_{$host_unique_id}">{$PALANG.pAutoconfig_check_interval}:</label></td>
<td><input type="number" class="flat" name="check_interval[]" id="autoconfig_check_interval_{$host_unique_id}" value="{$server.check_interval}" /></td>
<td>&nbsp;</td>
</tr>
{/if}
</table>
</td>
</tr>
<!-- end if incoming server is a pop3 -->

@ -0,0 +1,116 @@
/*
Created on 2020-03-07
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
CREATE TABLE IF NOT EXISTS autoconfig (
id SERIAL
-- https://stackoverflow.com/questions/43056220/store-uuid-v4-in-mysql
,config_id CHAR(36) NOT NULL
-- If you prefer you can also use CHAR(36)
-- ,config_id CHAR(36) NOT NULL
,encoding VARCHAR(12)
,provider_id VARCHAR(255) NOT NULL
-- Nice feature but not enough standard across other db. Instead, we'll use a separate table
-- ,provider_domain VARCHAR(255)[]
,provider_name VARCHAR(255)
,provider_short VARCHAR(120)
-- enable section
,enable_status BOOLEAN
,enable_url VARCHAR(2048)
-- documentation section
,documentation_status BOOLEAN
,documentation_url VARCHAR(2048)
,webmail_login_page VARCHAR(2048)
-- webmail login page info
,lp_info_url VARCHAR(2048)
,lp_info_username VARCHAR(255)
,lp_info_username_field_id VARCHAR(255)
,lp_info_username_field_name VARCHAR(255)
,lp_info_password_field VARCHAR(255)
,lp_info_login_button_id VARCHAR(255)
,lp_info_login_button_name VARCHAR(255)
-- Mac Mail specific fields
,account_name VARCHAR(255)
-- Typically 'email'
,account_type VARCHAR(42)
-- could be empty or could be a placeholder like %EMAILADDRESS%
,email VARCHAR(255)
-- If not explicitly set, this will be guessed from host socket_type
,ssl_enabled BOOLEAN
-- Will be empty obviously unless the user enters it in the form
-- password may be provided by the user on the web interface, but not stored here
-- Used for payload_description
,description TEXT
,organisation VARCHAR(255)
-- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server
,payload_type VARCHAR(100)
,prevent_app_sheet BOOLEAN
,prevent_move BOOLEAN
,smime_enabled BOOLEAN
,payload_remove_ok BOOLEAN
-- Outlook specific fields
-- domain_required -> Not sure this should be an option; false by default
,spa BOOLEAN
-- payload_enabled
,active BOOLEAN
-- For signing of the Mac/iOS mobileconfig settings
-- none, local or global
-- none: do not sign
-- local: use this configuration's certificate information
-- global: use the server wide one in config.inc.php
,sign_option VARCHAR(7)
,cert_filepath VARCHAR(1024)
,privkey_filepath VARCHAR(1024)
,chain_filepath VARCHAR(1024)
,created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
,modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
,CONSTRAINT pk_autoconfig PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig UNIQUE (config_id)
) ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS autoconfig_domains (
id SERIAL
,config_id CHAR(36) NOT NULL
-- COLLATE here is crucial for the foreign key to work. It must be the same as the target
,domain VARCHAR(255) NOT NULL COLLATE latin1_general_ci
,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain)
,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES domain(domain) ON DELETE CASCADE
,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
) ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS autoconfig_hosts (
id SERIAL
,config_id CHAR(36) NOT NULL
-- imap, smtp, pop3
,type VARCHAR(12) NOT NULL
,hostname VARCHAR(255) NOT NULL
,port INTEGER NOT NULL
,socket_type VARCHAR(42)
,auth VARCHAR(42) DEFAULT 'none' NOT NULL
-- possibly to contain some placeholder like %EMAILADDRESS%
,username VARCHAR(255)
,leave_messages_on_server BOOLEAN DEFAULT FALSE
,download_on_biff BOOLEAN DEFAULT FALSE
,days_to_leave_messages_on_server INTEGER
,check_interval INTEGER
,priority INTEGER
,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port)
,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
) ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS autoconfig_text (
id SERIAL
,config_id CHAR(36) NOT NULL
-- instruction or documentation
,type VARCHAR(17) NOT NULL
-- iso 639 2-letters code
,lang CHAR(2) NOT NULL
,phrase TEXT
,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang)
,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
) ENGINE = InnoDB;

@ -0,0 +1,120 @@
/*
Created on 2020-03-07
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
CREATE TABLE IF NOT EXISTS autoconfig (
id SERIAL
,config_id UUID NOT NULL
-- If you prefer not using a UUID field
-- ,config_id CHAR(36) NOT NULL
,encoding VARCHAR(12)
,provider_id VARCHAR(255) NOT NULL
-- Nice feature but not enough standard across other db. Instead, we'll use a separate table
-- ,provider_domain VARCHAR(255)[]
,provider_name VARCHAR(255)
,provider_short VARCHAR(120)
-- enable section
,enable_status BOOLEAN
,enable_url VARCHAR(2048)
-- documentation section
,documentation_status BOOLEAN
,documentation_url VARCHAR(2048)
,webmail_login_page VARCHAR(2048)
-- webmail login page info
,lp_info_url VARCHAR(2048)
,lp_info_username VARCHAR(255)
,lp_info_username_field_id VARCHAR(255)
,lp_info_username_field_name VARCHAR(255)
,lp_info_password_field VARCHAR(255)
,lp_info_login_button_id VARCHAR(255)
,lp_info_login_button_name VARCHAR(255)
-- Mac Mail specific fields
,account_name VARCHAR(255)
-- Typically 'email'
,account_type VARCHAR(42)
-- could be empty or could be a placeholder like %EMAILADDRESS%
,email VARCHAR(255)
-- If not explicitly set, this will be guessed from host socket_type
,ssl_enabled BOOLEAN
-- Will be empty obviously unless the user enters it in the form
-- password may be provided by the user on the web interface, but not stored here
-- Used for payload_description
,description TEXT
,organisation VARCHAR(255)
-- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server
,payload_type VARCHAR(100)
,prevent_app_sheet BOOLEAN
,prevent_move BOOLEAN
,smime_enabled BOOLEAN
,payload_remove_ok BOOLEAN
-- Outlook specific fields
-- domain_required -> Not sure this should be an option; false by default
,spa BOOLEAN
-- payload_enabled
,active BOOLEAN
-- For signing of the Mac/iOS mobileconfig settings
-- none, local or global
-- none: do not sign
-- local: use this configuration's certificate information
-- global: use the server wide one in config.inc.php
,sign_option VARCHAR(7)
,cert_filepath VARCHAR(1024)
,privkey_filepath VARCHAR(1024)
,chain_filepath VARCHAR(1024)
,created TIMESTAMP WITH TIME ZONE DEFAULT NOW()
,modified TIMESTAMP WITH TIME ZONE DEFAULT NOW()
,CONSTRAINT pk_autoconfig PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig UNIQUE (config_id)
);
CREATE TABLE IF NOT EXISTS autoconfig_domains (
id SERIAL
,config_id UUID NOT NULL
-- If you prefer not to install uuid-ossp:
--- ,config_id CHAR(36) NOT NULL
,domain VARCHAR(255) NOT NULL
,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain)
,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES public.domain(domain) ON DELETE CASCADE
,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autoconfig_hosts (
id SERIAL
,config_id UUID NOT NULL
-- If you prefer not to install uuid-ossp:
--- ,config_id CHAR(36) NOT NULL
-- imap, smtp, pop3
,type VARCHAR(12) NOT NULL
,hostname VARCHAR(255) NOT NULL
,port INTEGER NOT NULL
,socket_type VARCHAR(42)
,auth VARCHAR(42) DEFAULT 'none' NOT NULL
-- possibly to contain some placeholder like %EMAILADDRESS%
,username VARCHAR(255)
,leave_messages_on_server BOOLEAN DEFAULT FALSE
,download_on_biff BOOLEAN DEFAULT FALSE
,days_to_leave_messages_on_server INTEGER
,check_interval INTEGER
,priority INTEGER
,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port)
,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autoconfig_text (
id SERIAL
,config_id UUID NOT NULL
-- If you prefer not to install uuid-ossp:
--- ,config_id CHAR(36) NOT NULL
-- instruction or documentation
,type VARCHAR(17) NOT NULL
-- iso 639 2-letters code
,lang CHAR(2) NOT NULL
,phrase TEXT
,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang)
,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE
);

@ -0,0 +1,112 @@
/*
Created on 2020-03-07
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
CREATE TABLE IF NOT EXISTS autoconfig (
id INTEGER NOT NULL
,config_id CHAR(36) NOT NULL
,encoding VARCHAR(12)
,provider_id VARCHAR(255) NOT NULL
-- Nice feature but not enough standard across other db. Instead, we'll use a separate table
-- ,provider_domain VARCHAR(255)[]
,provider_name VARCHAR(255)
,provider_short VARCHAR(120)
-- enable section
,enable_url VARCHAR(2048)
,enable_status BOOLEAN
-- documentation section
,documentation_status BOOLEAN
,documentation_url VARCHAR(2048)
,webmail_login_page VARCHAR(2048)
-- webmail login page info
,lp_info_url VARCHAR(2048)
,lp_info_username VARCHAR(255)
,lp_info_username_field_id VARCHAR(255)
,lp_info_username_field_name VARCHAR(255)
,lp_info_password_field VARCHAR(255)
,lp_info_login_button_id VARCHAR(255)
,lp_info_login_button_name VARCHAR(255)
-- Mac Mail specific fields
,account_name VARCHAR(255)
-- Typically 'email'
,account_type VARCHAR(42)
-- could be empty or could be a placeholder like %EMAILADDRESS%
,email VARCHAR(255)
-- If not explicitly set, this will be guessed from host socket_type
,ssl_enabled BOOLEAN
-- Will be empty obviously unless the user enters it in the form
-- password may be provided by the user on the web interface, but not stored here
-- Used for payload_description
,description TEXT
,organisation VARCHAR(255)
-- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server
,payload_type VARCHAR(100)
,prevent_app_sheet BOOLEAN
,prevent_move BOOLEAN
,smime_enabled BOOLEAN
,payload_remove_ok BOOLEAN
-- Outlook specific fields
-- domain_required -> Not sure this should be an option; false by default
,spa BOOLEAN
-- payload_enabled
,active BOOLEAN
-- For signing of the Mac/iOS mobileconfig settings
-- none, local or global
-- none: do not sign
-- local: use this configuration's certificate information
-- global: use the server wide one in config.inc.php
,sign_option VARCHAR(7)
,cert_filepath VARCHAR(1024)
,privkey_filepath VARCHAR(1024)
,chain_filepath VARCHAR(1024)
,created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
,modified TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
,CONSTRAINT pk_autoconfig PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig UNIQUE (config_id)
);
CREATE TABLE IF NOT EXISTS autoconfig_domains (
id INTEGER NOT NULL
,config_id CHAR(36) NOT NULL
,domain VARCHAR(255) NOT NULL
,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain)
,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES domain(domain) ON DELETE CASCADE
,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autoconfig_hosts (
id INTEGER NOT NULL
,config_id CHAR(36) NOT NULL
-- imap, smtp, pop3
,type VARCHAR(12) NOT NULL
,hostname VARCHAR(255) NOT NULL
,port INTEGER NOT NULL
,socket_type VARCHAR(42)
,auth VARCHAR(42) DEFAULT 'none' NOT NULL
-- possibly to contain some placeholder like %EMAILADDRESS%
,username VARCHAR(255)
,leave_messages_on_server BOOLEAN DEFAULT FALSE
,download_on_biff BOOLEAN DEFAULT FALSE
,days_to_leave_messages_on_server INTEGER
,check_interval INTEGER
,priority INTEGER
,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port)
,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autoconfig_text (
id INTEGER NOT NULL
,config_id CHAR(36) NOT NULL
-- instruction or documentation
,type VARCHAR(17) NOT NULL
-- iso 639 2-letters code
,lang CHAR(2) NOT NULL
,phrase TEXT
,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id)
,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang)
,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE
);

@ -0,0 +1,908 @@
/*
Created on 2020-03-12
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
$(document).ready(function()
{
const DEBUG = true;
// Credits to: https://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/
// Modified by Jacques Deguest to include other form elements:
// http://www.w3schools.com/tags/tag_input.asp
(function($)
{
$.fn.serializeAll = function(options)
{
return $.param(this.serializeArrayAll(options));
};
$.fn.serializeArrayAll = function (options)
{
var o = $.extend({
checkboxesAsBools: false
}, options || {});
var rselectTextarea = /select|textarea/i;
var rinput = /text|hidden|password|search|date|time|number|color|datetime|email|file|image|month|range|tel|url|week/i;
return this.map(function ()
{
return this.elements ? $.makeArray(this.elements) : this;
})
.filter(function ()
{
return this.name && !this.disabled &&
(this.checked
|| (o.checkboxesAsBools && this.type === 'checkbox')
|| rselectTextarea.test(this.nodeName)
|| rinput.test(this.type));
})
.map(function (i, elem)
{
var val = $(this).val();
return val == null ?
null :
$.isArray(val) ?
$.map(val, function (val, i)
{
return { name: elem.name, value: val };
}) :
{
name: elem.name,
value: (o.checkboxesAsBools && this.type === 'checkbox') ?
(this.checked ? 1 : 0) :
val
};
}).get();
};
})(jQuery);
window.makeMessage = function( type, mesg )
{
return( sprintf( '<div class="%s">%s</div>', type, mesg ) );
};
window.showMessage = function()
{
if( DEBUG ) console.log( "Called from " + ( arguments.callee.caller === null ? 'void' : arguments.callee.caller.name ) );
var opts = {
div: $('#message'),
append: false,
dom: null,
timeout: null,
scroll: false,
timeoutCallback: null,
};
var msgDiv = $('#message');
if( arguments.length == 3 && typeof( arguments[2] ) === 'object' )
{
var param = arguments[2];
opts.type = arguments[0];
opts.message = arguments[1];
if( typeof( param.append ) !== 'undefined' ) opts.append = param.append;
if( typeof( param.dom ) !== 'undefined' ) opts.dom = param.dom;
if( typeof( param.timeout ) !== 'undefined' ) opts.timeout = param.timeout;
if( typeof( param.timeoutCallback ) !== 'undefined' ) opts.timeoutCallback = param.timeoutCallback;
if( typeof( param.scroll ) !== 'undefined' ) opts.scroll = param.scroll;
if( typeof( param.speak ) !== 'undefined' ) opts.speak = param.speak;
}
// Backward compatibility
else if( arguments.length >= 2 )
{
opts.type = arguments[0];
opts.message = arguments[1];
if( arguments.length > 2 ) opts.append = arguments[2];
if( arguments.length > 3 ) opts.dom = arguments[3];
if( arguments.length > 4 ) opts.timeout = arguments[4];
}
else
{
msgDiv.append( makeMessage( 'warning', "showMessage called with only " + arguments.length + " parameters while 2 at minimum are required. Usage: showMessage( type, message, append, domObject, timeout ) or showMessage( type, message, options )" ) );
return( false );
}
if( typeof( opts.dom ) === 'object' && opts.dom != null )
{
msgDiv = opts.dom;
}
// Check if message is an array of messages
// https://stackoverflow.com/a/4775741/4814971
if( Array.isArray )
{
if( Array.isArray( opts.message ) )
{
opts.messages = opts.message;
}
}
else if( opts.message instanceof( Array ) )
{
opts.messages = opts.message;
}
else if( $.isArray( opts.message ) )
{
opts.messages = opts.message;
}
// messages has been set with previous check
// Make this array a list of errors
if( opts.hasOwnProperty( 'messages' ) )
{
opts.message = sprintf( "<ol>\n%s\n</ol>", opts.messages.map(function(e){ return('<li>' + e + '</li>'); }).join( "\n" ) );
}
if( opts.append )
{
msgDiv.append(makeMessage(opts.type, opts.message));
}
else
{
msgDiv.html(makeMessage(opts.type, opts.message));
}
if( opts.type == 'error' )
{
msgDiv.addClass( 'error-shake' );
setTimeout(function()
{
msgDiv.removeClass( 'error-shake' );
}, 70000);
}
if( parseInt( opts.timeout ) > 0 )
{
var thisTimeout = parseInt( opts.timeout );
setTimeout(function()
{
msgDiv.html( '' );
if( typeof( opts.timeoutCallback ) === 'function' )
{
opts.timeoutCallback();
}
},thisTimeout);
}
else
{
setTimeout(function()
{
msgDiv.html( '' );
},15000);
}
if( opts.scroll )
{
if( DEBUG ) console.log( "Scrolling to the top of the page..." );
$('html, body').animate( { scrollTop: 0 }, 500 );
}
else
{
if( DEBUG ) console.log( "No scrolling..." );
}
};
window.postfixAdminProgressBar = function()
{
var xhr = new window.XMLHttpRequest();
if( DEBUG ) console.log( "Initiating the progress bar." );
if( DEBUG ) console.log( "Called from:\n" + (new Error).stack );
$('#postfixadmin-progress').show().removeClass('done');
xhr.upload.addEventListener('progress', function(evt)
{
if( evt.lengthComputable )
{
var percentComplete = evt.loaded / evt.total;
if( DEBUG ) console.log(percentComplete);
$('#postfixadmin-progress').css({
width: percentComplete * 100 + '%' });
if( DEBUG ) console.log( "upload.addEventListener: " + percentComplete );
if( percentComplete === 1 )
{
$('#postfixadmin-progress').addClass('done').hide();
}
}
}, false);
xhr.addEventListener('progress', function(evt)
{
if( evt.lengthComputable )
{
var percentComplete = evt.loaded / evt.total;
if( DEBUG ) console.log("addEventListener: " + percentComplete);
$('#postfixadmin-progress').css({
width: percentComplete * 100 + '%' });
if( percentComplete === 1 )
{
$('#postfixadmin-progress').addClass('done').hide();
}
}
}, false);
return( xhr );
};
window.postfixAdminProgressBarStart = function()
{
// $('#postfixadmin-progress').show().addClass('done');
$('#postfixadmin-progress').show();
$({property: 0}).animate({property: 85},
{
// Arbitrary time, which should well cover the time it takes to get response from server
// Otherwise, well our progress bar will hang at 85% until we get a call to kill it
duration: 4000,
step: function()
{
var _percent = Math.round( this.property );
$('#postfixadmin-progress').css( 'width', _percent + '%' );
}
});
};
window.postfixAdminProgressBarStop = function()
{
$({property: 85}).animate({property: 105},
{
duration: 1000,
step: function()
{
var _percent = Math.round( this.property );
$('#postfixadmin-progress').css( 'width', _percent + '%' );
if( _percent == 105 )
{
$('#postfixadmin-progress').addClass('done');
}
},
complete: function()
{
$('#postfixadmin-progress').hide();
$('#postfixadmin-progress').removeClass('done');
$('#postfixadmin-progress').css( 'width', '0%' );
}
});
};
window.autoconfigShowHideArrow = function()
{
// Reset. Show them all
$('table.server .autoconfig-command').show();
// Hide first and last ones
// This requires jQUery up to v3.3. Version 3.4 onward do not support :first and :last anymore
$('.autoconfig-incoming:first .autoconfig-move-up').hide();
$('.autoconfig-incoming:last .autoconfig-move-down').hide();
$('.autoconfig-outgoing:first .autoconfig-move-up').hide();
$('.autoconfig-outgoing:last .autoconfig-move-down').hide();
};
window.autoconfig_ajax_call = function( postData )
{
var prom = $.Deferred();
$this = $(this);
$.ajax({
xhr: postfixAdminProgressBar(),
type: "POST",
url: "autoconfig.php",
dataType: "json",
data: postData,
beforeSend: function(xhr)
{
xhr.overrideMimeType( "application/json; charset=utf-8" );
postfixAdminProgressBarStart();
},
error: function(xhr, errType, ExceptionObject)
{
postfixAdminProgressBarStop();
if( DEBUG ) console.log( "Returned error " + xhr.status + " with error type " + errType + " and exception object " + JSON.stringify( ExceptionObject ) );
if( DEBUG ) console.log( "Current url is: " + xhr.responseURL );
if( DEBUG ) console.log( "Http headers are: " + xhr.getAllResponseHeaders() );
if( DEBUG ) console.log( "Response raw data is: \n" + xhr.responseText );
// There was a redirect most likely due to some timeout
// if( xhr.getResponseHeader( 'Content-Type' ).toLowerCase().indexOf( 'text/html' ) >= 0 )
// {
// window.location.reload();
// return( true );
// }
var msg = 'An unexpected error has occurred';
showMessage( 'error', msg, { scroll: true });
$this.addClass( 'error-shake' );
prom.reject();
},
success: function(data, status, xhr)
{
postfixAdminProgressBarStop();
if( data.error )
{
showMessage( 'error', data.error, { scroll: true });
$this.addClass( 'error-shake' );
setTimeout(function()
{
$this.removeClass( 'error-shake' );
},5000);
prom.reject();
}
else
{
if( data.success )
{
prom.resolve(data);
showMessage( 'success', data.success, { scroll: true });
if( DEBUG ) console.log( "save(): " + data.success );
}
else if( data.info )
{
showMessage( 'info', data.info, { scroll: true } );
prom.resolve();
}
else
{
showMessage( 'info', data.msg, { scroll: true } );
prom.resolve();
}
}
}
});
return( prom.promise() );
};
$(document).on('click','#autoconfig_save', function(e)
{
e.preventDefault();
$this = $(this);
var data = {handler: 'autoconfig_save'};
$('#autoconfig_form').serializeArrayAll().map(function(item)
{
if( data[ item.name ] !== undefined )
{
if( !data[ item.name ].push )
{
data[ item.name ] = [ data[item.name] ];
}
data[ item.name ].push( item.value );
}
else
{
data[ item.name ] = item.value;
}
});
if( DEBUG ) console.log( "serialized data is: " + JSON.stringify( data ) );
autoconfig_ajax_call( data ).done(function(data)
{
// Since this shared function is used for both saving (adding and updating) as well as deleting
// we check if those data properties are returned by the server.
// Those here are only returned if this is the result of an addition
if( data.hasOwnProperty('config_id') )
{
if( $('#autoconfig_form input[name="config_id"]').length == 0 )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find the field \"config_id\" !" );
}
if( typeof( data.config_id ) !== 'undefined' && data.config_id.length > 0 )
{
// We force trigger change, because this is a hidden field and it hidden field do not trigger change
$('#autoconfig_form input[name="config_id"]').val( data.config_id ).trigger('change');
}
}
// Do the hosts
var hostTypes = [ 'incoming', 'outgoing' ];
for( var j = 0; j < hostTypes.length; j++ )
{
var hostType = hostTypes[j];
if( DEBUG ) console.log( "Checking host of type " + hostType );
if( data.hasOwnProperty(hostType + '_server') && Array.isArray( data[hostType + '_server'] ) )
{
var thoseHosts = $('.autoconfig-' + hostType);
var dataHosts = data[hostType + '_server'];
if( thoseHosts.length == 0 )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find any hosts block in our form!" );
}
else if( dataHosts.length != thoseHosts.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Total of " + hostType + " servers returned from the server (" + dataHosts.length + ") do not match the total hosts we have in our form (" + thoseHosts.length + ")." );
}
dataHosts.forEach(function(def, index)
{
if( DEBUG ) console.log( "def contains: " + JSON.stringify( def ) );
if( !def.hasOwnProperty('config_id') ||
!def.hasOwnProperty('hostname') ||
!def.hasOwnProperty('port') )
{
if( DEBUG ) console.error( "Something is wrong. Data received is missing fields config_id, or hostname or port" );
return( false );
}
thoseHosts.each(function(offset, obj)
{
var dom = $(obj);
var hostId = dom.find('input[name="host_id[]"]');
var hostName = dom.find('input[name="hostname[]"]');
var hostPort = dom.find('input[name="port[]"]');
if( !hostId.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host id field for host type \"" + hostType + "\" at offset " + i );
}
if( !hostName.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host name field for host type \"" + hostType + "\" at offset " + i );
}
if( !hostPort.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host port field for host type \"" + hostType + "\" at offset " + i );
}
// We found our match: no id, hostname and port match
if( hostId.val().length == 0 && hostName.val() == def.hostname && hostPort.val() == def.port )
{
if( DEBUG ) console.log( "Setting host id " + def.host_id + " to host name " + hostName.val() + " with port " + hostPort.val() );
hostId.val( def.host_id );
// exit the loop
return( false );
}
});
});
}
else
{
if( DEBUG ) console.error( "Something is wrong. Data received for hosts of type " + hostType + " does not exist or is not an array." );
}
}
// Now, do the texts
var textTypes = ['instruction', 'documentation'];
for( var j = 0; j < textTypes.length; j++ )
{
var textType = textTypes[j];
if( DEBUG ) console.log( "Checking text of type " + textType );
if( data.hasOwnProperty(textType) && Array.isArray( data[textType] ) )
{
var dataTexts = data[textType];
var thoseTextIds = $('input[name="' + textType + '_id[]"]');
var thoseTextLang = $('select[name="' + textType + '_lang[]"]');
var thoseTextData = $('textarea[name="' + textType + '_text[]"]');
// The array could very well be empty
dataTexts.forEach(function(def, index)
{
if( !def.hasOwnProperty('id') ||
!def.hasOwnProperty('type') ||
!def.hasOwnProperty('lang') )
{
if( DEBUG ) console.error( "Something is wrong. Data received is missing fields id, or type or lang" );
return( false );
}
for( var k = 0; k < thoseTextIds.length; k++ )
{
var textId = thoseTextIds.eq(k);
var textLang = thoseTextLang.eq(k);
// Found a match
if( textId.val().length == 0 && textLang.val() == def.lang )
{
if( DEBUG ) console.log( "Setting text id " + def.id + " to text with type " + textType + " and language " + def.lang );
textId.val( def.id );
return( false );
}
}
});
}
else
{
if( DEBUG ) console.error( "Something is wrong. Data received for " + textType + " text does not exist or is not an array." );
}
}
}).fail(function()
{
// Nothing for now
});
});
$(document).on('click','#autoconfig_remove', function(e)
{
e.preventDefault();
var data =
{
handler: 'autoconfig_remove',
config_id: $('input[name="config_id"]').val(),
token: $('input[name="token"]').val(),
};
if( DEBUG ) console.log( "Data to be sent is: " + JSON.stringify( data ) );
autoconfig_ajax_call( data ).done(function(data)
{
// Reload the page, but without the query string at the end
window.location.href = window.location.pathname;
}).fail(function()
{
// Nothing for now
});
});
$(document).on('click', '#autoconfig_cancel', function(e)
{
e.preventDefault();
window.location.href = 'list.php?table=domain';
return( true );
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
window.getRandomInt = function(min, max)
{
return( Math.floor( Math.random() * Math.floor((max - min) + min) ) );
}
/*
I could have just created one function and called it
*/
// Add and remove hosts
$(document).on('click','.autoconfig-server-add',function(e)
{
e.preventDefault();
var row = $(this).closest('table.server').closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var clone = row.clone();
clone.find('select,input[type!="hidden"],textarea').each(function(i,item)
{
$(item).val( '' );
});
// We need to remove the host_id so it can be treated as a new host and not an update of an existing one
clone.find('input[name="host_id[]"]').val( '' );
// Set default value and trigger change, which will call an event handler that will hide/show the section on pop3
clone.find('.host_type').val('imap').trigger('change');
var optionLabels = ['leave_messages_on_server', 'download_on_biff', 'days_to_leave_messages_on_server', 'check_interval'];
optionLabels.forEach(function(fieldName, index)
{
if( DEBUG ) console.log( "Checking field name " + fieldName );
var thisField = clone.find('input[name="' + fieldName + '[]"]');
if( thisField.length > 0 )
{
var forThisField = $('label[for="' + thisField.attr('id') + '"]', clone);
if( forThisField.length > 0 )
{
if( DEBUG ) console.log( "Field is " + thisField.html() + "\nLabel field is: " + forThisField.html() );
thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000));
maxAttempt = 10;
if( DEBUG ) console.log( "Checking if generated id exists: " + thisField.attr('id') );
while( $('#' + thisField.attr('id'), clone).length > 0 && ++maxAttempt < 10 )
{
thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000));
}
forThisField.attr('for', thisField.attr('id'));
if( DEBUG ) console.log( "Final generated id is: " + thisField.attr('id') );
}
else
{
if( DEBUG ) console.error( "Unable to find label element for field name." );
}
}
});
clone.insertAfter( row );
autoconfigShowHideArrow();
$('html, body').animate( { scrollTop: clone.offset().top }, 500 );
return( true );
});
$(document).on('click','.autoconfig-server-remove',function(e)
{
e.preventDefault();
var row = $(this).closest('table.server').closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
// Check if there are at least 2 elements, so that after at least one remain
var re = new RegExp('(autoconfig-(?:incoming|outgoing))-server');
var res = $(this).attr('class').match( re );
console.log( res );
if( res == null )
{
throw( "Cannot find class \"autoconfig-incoming-server\" or class \"autoconfig-outgoing-server\" in our clicked element." );
}
// Now find how many elements we have with this class
var total = $('tr.' + res[1]).length;
if( total < 2 )
{
row.addClass('autoconfig-error-shake');
setTimeout(function()
{
row.removeClass('autoconfig-error-shake');
},1000);
return( false );
}
row.remove();
autoconfigShowHideArrow();
});
// Add and remove account enable instructions or support documentation
$(document).on('click','.autoconfig-locale-text-add',function(e)
{
e.preventDefault();
var row = $(this).closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var clone = row.clone();
clone.find('select,input[type!="hidden"],textarea').each(function(i,item)
{
$(item).val( '' );
});
// We need to remove the host_id so it can be treated as a new text and not an update of an existing one
clone.find('input[name$="_id[]"]').val( '' );
clone.insertAfter( row );
$('html, body').animate( { scrollTop: clone.offset().top }, 500 );
});
$(document).on('click','.autoconfig-locale-text-remove',function(e)
{
e.preventDefault();
var row = $(this).closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var re = new RegExp('(autoconfig-(instruction|documentation))');
var res = $(this).attr('class').match( re );
if( res == null )
{
throw( "Cannot find class \"autoconfig-instruction\" or class \"autoconfig-documentation\" in our clicked element." );
}
var textType = res[2];
var total = $('tr.' + res[1]).length;
if( DEBUG ) console.log( total + " rows found for text type " + textType );
if( total < 2 )
{
if( DEBUG ) console.log( "text remove: one left" );
if( DEBUG ) console.log( "Getting lang object with " + '[name="' + textType + '_lang[]"]' );
var textLang = row.find('[name="' + textType + '_lang[]"]');
if( DEBUG ) console.log( "Getting text object with " + '[name="' + textType + '_text[]"]' );
var textData = row.find('[name="' + textType + '_text[]"]');
if( DEBUG ) console.log( "text remove: found lang and text field? " + ( ( textLang.length > 0 && textData.length > 0 ) ? "Yes" : "No" ) );
// This is remaining default tet row and there is no more data
if( ( textLang.val() === null || ( textLang.val() !== null && textLang.val() == '' ) ) && $.trim(textData.val()) == '' )
{
if( DEBUG ) console.log( "text remove: lang and text fields are empty already, error shake it" );
row.addClass('autoconfig-error-shake');
setTimeout(function()
{
row.removeClass('autoconfig-error-shake');
},1000);
}
else
{
if( DEBUG ) console.log( "text remove: empty fields" );
textLang.val( '' );
textData.val( '' );
}
return( false );
}
row.remove();
});
$(document).on('click', '#copy_provider_value', function(e)
{
e.preventDefault();
var orgField = $('input[name="organisation"]');
var providerNameField = $('input[name="provider_name"]');
if( !orgField.length || !providerNameField.length )
{
throw( "Unable to find either the provider name field or the organisation field!" );
}
if( providerNameField.val().length == 0 )
{
return( false );
}
orgField.val( providerNameField.val() );
return( true );
});
$(document).on('click', '#autoconfig_toggle_select_all_domains', function(e)
{
e.preventDefault();
// if( DEBUG ) console.log( "provider_domain options length: " + $('#autoconfig_provider_domain option').length + " and disabled options are: " + $('#autoconfig_provider_domain option:disabled').length + " and selected options are: " + $('#autoconfig_provider_domain option:selected').length );
if( $('#autoconfig_provider_domain option:selected').length > 0 )
{
$('#autoconfig_provider_domain option').prop('selected', false);
}
else
{
if( $('#autoconfig_provider_domain option:disabled').length == $('#autoconfig_provider_domain option').length )
{
var row = $(this).closest('tr');
row.addClass('error-shake');
setTimeout(function()
{
row.removeClass('error-shake');
},500);
return( false );
}
else
{
$('#autoconfig_provider_domain option:not(:disabled)').prop('selected', true);
}
}
});
$(document).on('change', '#autoconfig_form .host_type', function(e)
{
var typeValue = $(this).val();
// We get the enclosing table object to set some limiting context to the host_pop3 selector below
var tbl = $(this).closest('table');
if( typeValue == 'imap' )
{
$('.host_pop3', tbl).hide();
}
else
{
$('.host_pop3', tbl).show();
}
});
$(document).on('change', '#autoconfig_form .username_template', function(e)
{
var usernameField = $(this).closest('.server').find('input[name="username[]"]');
if( usernameField.length == 0 )
{
throw( "Unable to find the username field!" );
}
usernameField.val( $(this).val() );
});
$(document).on('change', '#autoconfig_form select[name="jump_to"]', function(e)
{
var id = $(this).val();
var re = new RegExp( '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})' );
// This will display an empty form
if( id.length == 0 )
{
window.location.href = 'autoconfig.php';
}
else if( id.match( re ) )
{
window.location.href = 'autoconfig.php?config_id=' + encodeURIComponent( id );
}
});
// Upon change in the content of the hidden config_id, enable or disable the "Remove" buttton
// The Remove button can only be used if this is for an existing configuration obviously
$(document).on('change', '#autoconfig_form input[name="config_id"]', function(e)
{
if( DEBUG ) console.log( "Config id field contains \"" + $(this).val() + "\"." );
if( $(this).val().length == 0 || $(this).val().match( /^[[:blank:]\t]*$/ ) )
{
$('#autoconfig_remove').attr( 'disabled', true );
}
else
{
$('#autoconfig_remove').attr( 'disabled', false );
}
});
$(document).on('click', '.autoconfig-move-up', function(e)
{
e.preventDefault();
var row = $(this).closest('.server').closest('tr');
if( row.prev().length == 0 || ( !row.prev().hasClass('autoconfig-incoming') && !row.prev().hasClass('autoconfig-outgoing') ) )
{
return( false );
}
row.insertBefore( row.prev() );
$('html, body').animate( { scrollTop: row.offset().top }, 500 );
autoconfigShowHideArrow();
});
$(document).on('click', '.autoconfig-move-down', function(e)
{
e.preventDefault();
var row = $(this).closest('.server').closest('tr');
if( row.next().length == 0 || ( !row.next().hasClass('autoconfig-incoming') && !row.next().hasClass('autoconfig-outgoing') ) )
{
return( false );
}
row.insertAfter( row.next() );
$('html, body').animate( { scrollTop: row.offset().top }, 500 );
autoconfigShowHideArrow();
});
window.checkExistingTextLanguage = function(opts = {})
{
if( typeof( opts ) !== 'object' )
{
throw( "Parameters provided is not an object. Call checkExistingTextLanguage like this: checkExistingTextLanguage({ type: 'instruction', lang: 'fr', caller: $(this) })" );
}
var textType = opts.type;
var callerMenu = opts.caller;
var langToCheck = opts.lang;
if( DEBUG ) console.log( "checkExistingTextLanguage() checking text type " + textType + " for language " + langToCheck );
if( typeof( textType ) === 'undefined' )
{
throw( "No text type was provided." );
}
var langMenu = $('select[name="' + textType + '_lang[]"]');
if( DEBUG ) console.log( "checkExistingTextLanguage() Found " + langMenu.length + " language menu(s)." );
if( langMenu.length == 0 )
{
throw( "Could not find any language menu for this text type " + textType );
}
if( langMenu.length == 1 )
{
return( true );
}
var alreadySelected = false;
if( DEBUG ) console.log( "checkExistingTextLanguage() checking each existing language menu." );
langMenu.each(function(offset, menuObject)
{
if( $(menuObject).is( callerMenu ) )
{
if( DEBUG ) console.log( "checkExistingTextLanguage() skipping because this is our caller menu." );
return( true );
}
// Found a match, stop there
else if( $(menuObject).val() == langToCheck )
{
if( DEBUG ) console.log( "checkExistingTextLanguage() Found match with this menu value (" + $(menuObject).val() + ") matching the language to check \"" + langToCheck + "\"." );
alreadySelected = true;
return( false );
}
});
if( DEBUG ) console.log( "checkExistingTextLanguage() returning: " + !alreadySelected );
return( !alreadySelected );
};
// Upon selection or change of a language, we check it has not been selected already
$(document).on('change', 'select[name="instruction_lang[]"]', function(e)
{
if( !checkExistingTextLanguage({ type: 'instruction', lang: $(this).val(), caller: $(this) }) )
{
$(this).addClass('error-shake');
// <i class="fas fa-exclamation-triangle"></i>
var warning = $('<i/>',
{
class: 'fas fa-exclamation-triangle fa-2x',
style: 'color: red; font-size: 20px;',
});
warning.insertAfter( $(this) );
var that = $(this);
setTimeout(function()
{
that.removeClass('error-shake');
warning.remove();
},5000);
$(this).val('');
return( false );
}
return( true );
});
window.toggleCertFiles = function(option)
{
if( typeof( option ) === 'undefined' )
{
return( false );
}
if( option == 'local' )
{
$('.cert_files').show();
}
else
{
$('.cert_files').hide();
}
};
$(document).on('change', 'select[name="sign_option"]', function(e)
{
toggleCertFiles( $(this).val() );
});
// Need to trigger the change for the host type menu
if( $('#autoconfig_form .host_type').length > 0 )
{
$('#autoconfig_form .host_type').trigger('change');
}
if( $('#autoconfig_form input[name="config_id"]').length > 0 )
{
$('#autoconfig_form input[name="config_id"]').trigger('change');
}
// Hide useless up/down arrows
autoconfigShowHideArrow();
toggleCertFiles( $('select[name="sign_option"]').val() );
});

@ -0,0 +1,808 @@
/*
Created on 2020-03-11
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
@charset "UTF-8";
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css");
/* Progress bar */
#postfixadmin-progress
{
/* position: absolute; */
position: fixed;
display: none;
z-index: 2147483647;
top: 0px;
left: 0px;
width: 0px;
height: 2px;
-webkit-transition: width 500ms ease-out,opacity 400ms linear;
-moz-transition: width 500ms ease-out,opacity 400ms linear;
-ms-transition: width 500ms ease-out,opacity 400ms linear;
-o-transition: width 500ms ease-out,opacity 400ms linear;
transition: width 500ms ease-out,opacity 400ms linear;
-webkit-border-radius: 1px;
-moz-border-radius: 1px;
border-radius: 1px;
background: #b91f1f;
}
#postfixadmin-progress.done
{
opacity: 0;
}
#postfixadmin-progress dd,
#postfixadmin-progress dt
{
position: absolute;
top: 0px;
height: 2px;
-webkit-border-radius: 100%;
-moz-border-radius: 100%;
border-radius: 100%;
-webkit-box-shadow: #b91f1f 1px 0 6px 1px;
-moz-box-shadow: #b91f1f 1px 0 6px 1px;
-ms-box-shadow: #b91f1f 1px 0 6px 1px;
box-shadow: #b91f1f 1px 0 6px 1px;
}
#postfixadmin-progress dd
{
right: 0px;
clip: rect(-6px,22px,14px,10px);
width: 20px;
opacity: 1;
}
#postfixadmin-progress dt
{
right: -80px;
clip: rect(-6px,90px,14px,-6px);
width: 180px;
opacity: 1;
}
@-moz-keyframes legaltech-progress-pulse
{
30%
{
opacity: 1;
}
60%
{
opacity: 0;
}
100%
{
opacity: 1;
}
}
@-ms-keyframes legaltech-progress-pulse
{
30%
{
opacity: .6;
}
60%
{
opacity: 0;
}
100%
{
opacity: .6;
}
}
@-o-keyframes legaltech-progress-pulse
{
30%
{
opacity: 1;
}
60%
{
opacity: 0;
}
100%
{
opacity: 1;
}
}
@-webkit-keyframes legaltech-progress-pulse
{
30%
{
opacity: .6;
}
60%
{
opacity: 0;
}
100%
{
opacity: .6;
}
}
@keyframes legaltech-progress-pulse
{
30%
{
opacity: 1;
}
60%
{
opacity: 0;
}
100%
{
opacity: 1;
}
}
#postfixadmin-progress.waiting dd,
#postfixadmin-progress.waiting dt
{
-webkit-animation: legaltech-progress-pulse 2s ease-out 0s infinite;
-moz-animation: legaltech-progress-pulse 2s ease-out 0s infinite;
-ms-animation: legaltech-progress-pulse 2s ease-out 0s infinite;
-o-animation: legaltech-progress-pulse 2s ease-out 0s infinite;
animation: legaltech-progress-pulse 2s ease-out 0s infinite;
}
.autoconfig-command
{
position: relative;
display: inline-block;
z-index: 1;
outline: 0;
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 50%;
padding: 3px 3px;
/* padding: 0 16px; */
margin: 10px 4px;
background-color: #3f51b5;
color: #fff;
border: 0;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
text-transform: uppercase;
text-decoration: none;
font-size: 10px;
font-weight: 500;
vertical-align: middle;
cursor: pointer;
overflow: hidden;
-webkit-transition: all .15s ease-in;
transition: all .15s ease-in;
}
.autoconfig-command:hover,
.autoconfig-command:focus
{
opacity: .9;
}
@-webkit-keyframes ripple
{
0%
{
width: 0px;
height: 0px;
opacity: .5;
}
100%
{
width: 150px;
height: 150px;
opacity: 0;
}
}
@keyframes ripple
{
0%
{
width: 0px;
height: 0px;
opacity: .5;
}
100%
{
width: 150px;
height: 150px;
opacity: 0;
}
}
.ripple:before
{
content: '';
z-index: 2;
position: absolute;
visibility: hidden;
top: 50%;
left: 50%;
width: 0px;
height: 0px;
-webkit-transform: translate( -50%, -50% );
transform: translate( -50%, -50% );
border-radius: 50%;
background-color: currentColor;
}
.ripple:not(:active):before
{
-webkit-animation: ripple 0.4s cubic-bezier( 0, 0, 0.2, 1 );
animation: ripple 0.4s cubic-bezier( 0, 0, 0.2, 1 );
-webkit-transition: visibility .4s step-end;
transition: visibility .4s step-end;
}
.ripple:active:before
{
visibility: visible;
}
.autoconfig-server-add,
.autoconfig-locale-text-add
{
background-color: green;
}
.autoconfig-server-remove,
.autoconfig-locale-text-remove
{
background-color: red;
}
@-webkit-keyframes autoconfig-error
{
0% { -webkit-transform: translateX( 0px ); }
25% { -webkit-transform: translateX( 30px ); }
45% { -webkit-transform: translateX( -30px ); }
65% { -webkit-transform: translateX( 30px ); }
82% { -webkit-transform: translateX( -30px ); }
94% { -webkit-transform: translateX( 30px ); }
35%, 55%, 75%, 87%, 97%, 100% { -webkit-transform: translateX( 0px ); }
}
@keyframes autoconfig-error
{
0% { transform: translateX( 0px ); }
25% { transform: translateX( 30px ); }
45% { transform: translateX( -30px ); }
65% { transform: translateX( 30px ); }
82% { transform: translateX( -30px ); }
94% { transform: translateX( 30px ); }
35%, 55%, 75%, 87%, 97%, 100% { transform: translateX( 0px ); }
}
.autoconfig-error-shake
{
-webkit-animation: autoconfig-error 0.35s linear;
-moz-animation: autoconfig-error 0.35s linear;
animation: autoconfig-error 0.35s linear;
}
.autoconfig-error:before
{
background-position: 0 0;
}
/* Message formatting */
#message
{
max-width: 90%;
display: block;
}
/* So that any link inside a message can be clickable */
#message a
{
cursor: pointer;
pointer-events: all;
}
.success,
.error,
.info,
.warning,
.edit,
.lock,
.tip,
.download,
.chat,
.task
{
position: relative;
display: block;
clear: both;
margin-bottom: 2px;
padding: 10px 10px 10px 40px;
min-height: 20px;
font-family: "Lucida Grande", "Lucida Sans Unicode", sans-serif;
font-size: 12px;
font-weight: normal;
line-height: 20px;
-webkit-border-radius: 5px;
-khtml-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset;
-moz-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset;
-o-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset;
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset;
pointer-events: none;
cursor: pointer;
}
.success:before,
.error:before,
.info:before,
.warning:before,
.edit:before,
.lock:before,
.tip:before,
.download:before,
.chat:before,
.task:before
{
content: "";
position: absolute;
top: 14px;
left: 16px;
width: 14px;
height: 15px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAACWCAYAAADufbn/AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5QkZCREFGMkQ5NDFFMTExQUFEMUYxOUI5RkM4REUzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyM0E3OTNDMjQ2NUYxMUUxQTMxRkIyNkE5NjhGNzcwOSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyM0E3OTNDMTQ2NUYxMUUxQTMxRkIyNkE5NjhGNzcwOSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozM0NDRDBCOTVENDZFMTExOTJFNEYxQUZFREQ5ODZCMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QkZCREFGMkQ5NDFFMTExQUFEMUYxOUI5RkM4REUzOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pmz0zwcAAAlJSURBVHja7JgLUBRHGoCnZ2Zhl+WdZYGFhUOkFA4C6CW+lYRTcyGA5nEmPjg9E8ipJ4gFxkRFTVQOJV6qLoUYS3MqnuQiENEk5PCxQixNTorkAAkJMQiiiEAE5bHs7tzfyyyZnZ3dnSUmd1V3XfVv93b3N//03393/9OIYRiiIiw0iyCIdJBN879ufpfgJWifANkpkCsgKdBHiz4ePy4T/uRz+q3gwkYIIQ3BML7GCoQqoJxAkhLJWp6CQ9B5uSCEE8PMByaMVD39TAGiKK0A/JoFBMnj4ej35zY0tlFvX7jwtba7u7+3vm4GdKI4fR4HceVDU0+UbIFiOwk/t8O35BSqFy95Q0CzEHTVOFRsVTYpa156Mafz/LlVfEiqUn02R1O13ASZgdgQoLGK0et9LNQhZIBhLANrHzMDBa1nmfTsHBph0hrkEhys4Y0ZG+4w9F9sBBFJvseH3KOiSmZVns0MXvH7rVbgIBKefI0PTSsp2wzFmgkbXnmHD9Nubq1BKSkDoIyJqpo/7xNwPebiwuQT8D8Cj5sjisbcXRsrJoQNVU6Kab7zaXUy1NGmxui+r75aJACNwr2Njc9BPgfECddx51ECMmzDqmbtXNChRIvtuG53BPbdAyCnQTJosRAi0GkYrRT+rgEZIEVAT3AgY5LLvCJJEdBJLuQidf9macKfdtNsh5WQ4X0lZ29WwwBblwTQ+wBJftDk2ZT27P4MtV9kNcrIC38N6t4wmphAn0LHuVDEmorNIa/G1GcL1wX5RZ7B00KTiMoyMPqR7YRgZgDwGdbOhVxdvBtWLTqU4a8IO2+aS3JSRMJfECJ1o3sRwURaQL89mA7QOa4DUCeKqq923W3T3exsmgaYmbHc5Iov05ccS/d9aJwG/urM1jbrOf7FH29efbmudAPDGGgTtPaFo+kKr6AqdhGbJ44j+xVX5Gxfvyfq/rZ98Rc7u1vioI6y4vQWvuoHEgPShdejoKYf6+QkMcY06uSz3yzMgWwryPcXMtO8HNEYx+ae2HMcWY9b3aXSvZODAjTsareduCbedroypr69Yx6U1damwWw6YHzB8Ax8mMYhgmjRZKaF2dl/CBKgaMhrTWP0dJH1YluJMU4ZRZKtXi6yL3FFmFJxCW8NYsCYTb95LLenf+BhXDEtJKgOslt2QZizu7kVmkHjOSihb09Sqzr2VV3WiprHIZ1ulnF8Mtm3q4o/2PG3z2uP2psSkwNgxyZu9fZNHdAOK1+c8ehh+PuIGAf43vSqaTOn7HgmNrId/taJAZcnRE58fsO8ORjoB/kcpNcWyD903Nm1aDc9mEOnan0gjjhwLLMQJICtvgFSCrJ9Vn5bp4VGgHDnQhBjuIIoyV3jItAPe7B9MZQGcOkoCNAyHBSMPg2gKTlXluDy5W2TizgwTikAH6EBCoJwpYDgjJWSSHskcu+PTGUdF0RoPzAakqSd/giQnLdGDZAZhS1zG6WYAc9BSQ6bFJFPkQa9NsRRzqDThpBw4IxlIimacpa36QZ6x5k9cXjAn51TeCvK32Lypa7XSZnPuHMWG5hBLwPrXcFiLPPjV8UvNGTA7Jf2IYruMx87PTg5+9xqLLhs1gZ9A+NeLqB9YpO/6Gupyb1RffB1MPXI+kTEcN3+JfWmMlZiCnj9py7N84lJqjW5nE9z2ZZVNy8eWcfxEj0n1DRq8p+ekh+6YHsBjuPNYvL77Q0zr53akQD5r3RD94xGoZ1db8pVEf8MSdz0odw/HB+yt4WWFV6TahAc+Jq86T5IB0jrmILA0p0on92bnl74KnOXFAmlQpY58hGDiu0erABQICtnp1RrZG4BNSMuI52I662eRiU7CArkOAhTupM80tfVNPvMO9GnbjSW4EgZCY7R+ESCKAJZ9MMyJP+6YKN+DxS/ARmkrUCHuRBFy7omzHi1jLvX0gLQIZDFXChq7t6skNi0SsFdjgMt40KxCQfWqX+5GG9Q9yzODhY6YAZJXDpZ6AQf4mrE0HIuNPmpdzMCwp8rY48Ey9MKa1OGzG1EaOQziJLIO1io1BpketWP3BQRZyPjd693lvvWPZJclM5CNo9zVLqT6oMd8I5v6BMJ0xd96MkGf4N2/bDj209eACeIB5HZi234cY4zPs0d3lq5LpeWlpbJXl0EgVwHeauwsPBNm0EgQK/gqwuapl2VSqUG5/g/Wy+sERplCKFuJyennuzs7NWBgYEtbW1twXl5eW9rtVov6OMDmi09RyKRREOj1MfH5wJA+BalBue+vr5ncT2kCMFXVSgUeBqIwcHBPs6+Mjw0NNRhjH88PS0CX5Samvpn1iD2EjZUxqhGSCtFBe+8fqRxrxY5dQ/kK+A/ACJ85SJmgLx+pEqlqhAD8vthl8NeESqCbQZpeDBB4MltMd6QPQ+CYx4XkC/YrbI2KadW0Abog63R4yGDTsxMXtMAGGQNbCtFAA9ZehIl2WwJGWMPHG3sgTMjWXA64IlJNr67POHBfxAGDXoPG59soJAKFAQpiazDxrTrJTL364Kg3Ft93mrAJnHuUoQ8elwQVEcn7neWe9dbuhg5rAydfjAifm2ZIBg6bdkl76DY4xDxmp0TMg//SzHJWwucXRV3rK2OAXdl2EmICK9xPdFNGVoukbq3Gq8/rC0rvW6oBV7NbPvTDd5rsgaNulx7fQWCaTFpwy7GaPt7SLsLGSzLmIK9kRsWhoJpYuyCqoh5/e5+E/9BO7nchFcewlPkqYpoEnPoIDb4G8/GprfYtTf4036UuS04avaUvrKl6L9oe/zZQeSafESUWfmGIqEiSQSUJPSq5bZgtq3c2hgFYWuQxQ0SSCIeMxZctnuDxEuJprf4SS4Qfn4HoKdkeM+BfBe+dRLJnAXJpklEvmdgDEoHlD1OkfQB0kFoZIenKP8xjRHhff5/YCGTriMHi0PJxdmtlcrL3XOnub1BDVFEv7NE2m0SCs5HnX7YlY0wtK4yj2u4XuHue/XlJzfuwk6OGyNGbxfY1HOvU5b61pMbrnc2PwZv9d2Z3O9+xzbhc7Te1tcMRCRM7Ir8+L/HZaub+e1iPofCXz+2JkvMQhZKStNVxYNbyL+OesjmE/jttKmi8l9dSbZgbjuUzZy83M7bmbWj+EhvhweJNWLjJI7BNuVj1khgkN21Ex0oE//XaEPj2IOHMTvAWJfVvwUYAKkVWW9BNMncAAAAAElFTkSuQmCC);
background-repeat: no-repeat;
}
.success:after,
.error:after,
.info:after,
.warning:after,
.edit:after,
.lock:after,
.tip:after,
.download:after,
.chat:after,
.task:after
{
content: "x";
position: absolute;
top: 10px;
right: 10px;
width: 5px;
height: 6px;
cursor: pointer;
font: normal normal 13px/20px "Lucida Grande", "Lucida Sans Unicode", sans-serif;
font-size: 13px;
-webkit-text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
-moz-text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
/*
Awesome trick !
http://stackoverflow.com/questions/7478336/only-detect-click-event-on-pseudo-element
http://jsfiddle.net/ZWw3Z/70/
*/
cursor: pointer;
pointer-events: all;
}
.success
{
border: 1px solid #accc5d;
color: #70892b;
background-color: #c8e185;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #d0e98e), color-stop(100%, #c1da7f));
background-image: -webkit-linear-gradient( #d0e98e, #c1da7f );
background-image: -moz-linear-gradient( #d0e98e, #c1da7f );
background-image: -o-linear-gradient( #d0e98e, #c1da7f );
background-image: linear-gradient( #d0e98e, #c1da7f );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 );
}
.success:before
{
background-position: 0 -15px;
}
@-webkit-keyframes error
{
0% { -webkit-transform: translateX( 0px ); }
25% { -webkit-transform: translateX( 30px ); }
45% { -webkit-transform: translateX( -30px ); }
65% { -webkit-transform: translateX( 30px ); }
82% { -webkit-transform: translateX( -30px ); }
94% { -webkit-transform: translateX( 30px ); }
35%, 55%, 75%, 87%, 97%, 100% { -webkit-transform: translateX( 0px ); }
}
.error
{
border: 1px solid #dc4e4d;
color: #b52525;
background-color: #ec8282;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f48888), color-stop(100%, #e17575));
background-image: -webkit-linear-gradient( #f48888, #e17575 );
background-image: -moz-linear-gradient( #f48888, #e17575 );
background-image: -o-linear-gradient( #f48888, #e17575 );
background-image: linear-gradient( #f48888, #e17575 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-webkit-animation: error 0.35s linear;
-moz-animation: error 0.35s linear;
}
.error-shake
{
-webkit-animation: error 0.35s linear;
-moz-animation: error 0.35s linear;
animation: error 0.35s linear;
}
.error:before
{
background-position: 0 0;
}
.info
{
border: 1px solid #69c0ca;
color: #3d8d98;
background-color: #8aced6;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #99e2eb), color-stop(100%, #79c6cd));
background-image: -webkit-linear-gradient( #99e2eb, #79c6cd );
background-image: -moz-linear-gradient( #99e2eb, #79c6cd );
background-image: -o-linear-gradient( #99e2eb, #79c6cd );
background-image: linear-gradient( #99e2eb, #79c6cd );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.info:before
{
background-position: 0 -30px;
}
.warning
{
color: #c2721b;
border: 1px solid #f9b516;
background-color: #fbb160;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffd57f), color-stop(100%, #ffa544));
background-image: -webkit-linear-gradient( #ffd57f, #ffa544 );
background-image: -moz-linear-gradient( #ffd57f, #ffa544 );
background-image: -o-linear-gradient( #ffd57f, #ffa544 );
background-image: linear-gradient( #ffd57f, #ffa544 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.warning:before
{
background-position: 0 -45px;
}
.edit
{
color: #ae8500;
border: 1px solid #e9c95f;
background-color: #f3dc8f;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffeaa7), color-stop(100%, #f3d573));
background-image: -webkit-linear-gradient( #ffeaa7, #f3d573 );
background-image: -moz-linear-gradient( #ffeaa7, #f3d573 );
background-image: -o-linear-gradient( #ffeaa7, #f3d573 );
background-image: linear-gradient( #ffeaa7, #f3d573 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.edit:before
{
background-position: 0 -60px;
}
.lock
{
border: 1px solid #CCC;
color: #666;
background-color: #e8e8e8;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #e0e0e0));
background-image: -webkit-linear-gradient( #f4f4f4, #e0e0e0 );
background-image: -moz-linear-gradient( #f4f4f4, #e0e0e0 );
background-image: -o-linear-gradient( #f4f4f4, #e0e0e0 );
background-image: linear-gradient( #f4f4f4, #e0e0e0 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.lock:before
{
background-position: 0 -75px;
}
.tip
{
border: 1px solid #e6b96f;
color: #b1802f;
background-color: #f5dcb2;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffedcf), color-stop(100%, #f8d69e));
background-image: -webkit-linear-gradient( #ffedcf, #f8d69e );
background-image: -moz-linear-gradient( #ffedcf, #f8d69e );
background-image: -o-linear-gradient( #ffedcf, #f8d69e );
background-image: linear-gradient( #ffedcf, #f8d69e );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.tip:before
{
background-position: 0 -90px;
}
.download
{
border: 1px solid #3178c0;
color: #0c4fa3;
background-color: #6dacea;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8ed0fa), color-stop(100%, #4e95dc));
background-image: -webkit-linear-gradient( #8ed0fa, #4e95dc );
background-image: -moz-linear-gradient( #8ed0fa, #4e95dc );
background-image: -o-linear-gradient( #8ed0fa, #4e95dc );
background-image: linear-gradient( #8ed0fa, #4e95dc );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.download:before
{
background-position: 0 -105px;
}
.chat
{
color: #366f11;
border: 1px solid #5d902f;
background-color: #89bc5a;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8dcb3d), color-stop(100%, #74a547));
background-image: -webkit-linear-gradient( #8dcb3d, #74a547 );
background-image: -moz-linear-gradient( #8dcb3d, #74a547 );
background-image: -o-linear-gradient( #8dcb3d, #74a547 );
background-image: linear-gradient( #8dcb3d, #74a547 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.chat:before
{
background-position: 0 -120px;
}
.task
{
color: #432c12;
border: 1px solid #71502b;
background-color: #92724e;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a58868), color-stop(100%, #886640));
background-image: -webkit-linear-gradient( #a58868, #886640 );
background-image: -moz-linear-gradient( #a58868, #886640 );
background-image: -o-linear-gradient( #a58868, #886640 );
background-image: linear-gradient( #a58868, #886640 );
-webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
-moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 );
}
.task:before
{
background-position: 0 -135px;
}
#autoconfig_save:hover,
#autoconfig_remove:hover,
#autoconfig_cancel:hover
{
cursor: pointer;
}
#autoconfig_provider_domain option:disabled:hover,
#autoconfig_remove:disabled
{
cursor: not-allowed;
}
/* If this move up button is in the first host block, then deactivate it */
.autoconfig-incoming:first-child .autoconfig-move-up,
.autoconfig-incoming:last-child .autoconfig-move-down,
.autoconfig-outgoing:first-child .autoconfig-move-up,
.autoconfig-outgoing:last-child .autoconfig-move-down
{
display: none;
}
#autoconfig_form > table
{
border-collapse: collapse;
}
.autoconfig-incoming,
.autoconfig-outgoing,
.autoconfig-instruction,
.autoconfig-documentation
{
border-bottom: 1pt solid black;
}
/* CSS switch designed and credit to Thibaut Courouble http://thibaut.me */
.switch
{
position: relative;
display: inline-block;
width: 56px;
height: 20px;
padding: 3px;
vertical-align: top;
background-color: white;
border-radius: 18px;
box-shadow: inset 0 -1px white, inset 0 1px 1px rgba(0, 0, 0, 0.05);
cursor: pointer;
background-image: -webkit-linear-gradient(top, #eeeeee, white 25px);
background-image: -moz-linear-gradient(top, #eeeeee, white 25px);
background-image: -o-linear-gradient(top, #eeeeee, white 25px);
background-image: linear-gradient(to bottom, #eeeeee, white 25px);
}
.switch-input
{
/*
position: absolute;
top: 0;
left: 0;
opacity: 0;
*/
display: none;
}
.switch-label
{
position: relative;
display: block;
height: inherit;
font-size: 10px;
text-transform: uppercase;
background: #eceeef;
border-radius: inherit;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12), inset 0 0 2px rgba(0, 0, 0, 0.15);
-webkit-transition: 0.15s ease-out;
-moz-transition: 0.15s ease-out;
-o-transition: 0.15s ease-out;
transition: 0.15s ease-out;
-webkit-transition-property: opacity background;
-moz-transition-property: opacity background;
-o-transition-property: opacity background;
transition-property: opacity background;
}
.switch-label:before,
.switch-label:after
{
position: absolute;
top: 50%;
margin-top: -.5em;
line-height: 1;
-webkit-transition: inherit;
-moz-transition: inherit;
-o-transition: inherit;
transition: inherit;
}
.switch-label:before
{
content: attr(data-off);
right: 11px;
color: #aaa;
text-shadow: 0 1px rgba(255, 255, 255, 0.5);
}
.switch-label:after
{
content: attr(data-on);
left: 11px;
color: white;
text-shadow: 0 1px rgba(0, 0, 0, 0.2);
opacity: 0;
}
input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label,
input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label
{
background: #47a8d8;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 0 3px rgba(0, 0, 0, 0.2);
}
input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label:before,
input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label:before
{
opacity: 0;
}
input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label:after,
input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label:after
{
opacity: 1;
}
.switch-handle
{
position: absolute;
top: 4px;
left: 4px;
width: 18px;
height: 18px;
background: white;
border-radius: 10px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
background-image: -webkit-linear-gradient(top, white 40%, #f0f0f0);
background-image: -moz-linear-gradient(top, white 40%, #f0f0f0);
background-image: -o-linear-gradient(top, white 40%, #f0f0f0);
background-image: linear-gradient(to bottom, white 40%, #f0f0f0);
-webkit-transition: left 0.15s ease-out;
-moz-transition: left 0.15s ease-out;
-o-transition: left 0.15s ease-out;
transition: left 0.15s ease-out;
}
.switch-handle:before
{
content: '';
position: absolute;
top: 50%;
left: 50%;
margin: -6px 0 0 -6px;
width: 12px;
height: 12px;
background: #f9f9f9;
border-radius: 6px;
box-shadow: inset 0 1px rgba(0, 0, 0, 0.02);
background-image: -webkit-linear-gradient(top, #eeeeee, white);
background-image: -moz-linear-gradient(top, #eeeeee, white);
background-image: -o-linear-gradient(top, #eeeeee, white);
background-image: linear-gradient(to bottom, #eeeeee, white);
}
input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-handle,
input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-handle
{
left: 40px;
box-shadow: -1px 1px 5px rgba(0, 0, 0, 0.2);
}
input[name="enable_status"] ~ table tr.autoconfig-instruction,
input[name="documentation_status"] ~ table tr.autoconfig-documentation
{
display: none;
}
input[name="enable_status"]:checked ~ table tr.autoconfig-instruction,
input[name="documentation_status"]:checked ~ table tr.autoconfig-documentation
{
display: table-row;
}

@ -0,0 +1,910 @@
/*
Created on 2020-03-12
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
$(document).ready(function()
{
const DEBUG = false;
// Credits to: https://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/
// Modified by Jacques Deguest to include other form elements:
// http://www.w3schools.com/tags/tag_input.asp
(function($)
{
$.fn.serializeAll = function(options)
{
return $.param(this.serializeArrayAll(options));
};
$.fn.serializeArrayAll = function (options)
{
var o = $.extend({
checkboxesAsBools: false
}, options || {});
var rselectTextarea = /select|textarea/i;
var rinput = /text|hidden|password|search|date|time|number|color|datetime|email|file|image|month|range|tel|url|week/i;
return this.map(function ()
{
return this.elements ? $.makeArray(this.elements) : this;
})
.filter(function ()
{
return this.name && !this.disabled &&
(this.checked
|| (o.checkboxesAsBools && this.type === 'checkbox')
|| rselectTextarea.test(this.nodeName)
|| rinput.test(this.type));
})
.map(function (i, elem)
{
var val = $(this).val();
return val == null ?
null :
$.isArray(val) ?
$.map(val, function (val, i)
{
return { name: elem.name, value: val };
}) :
{
name: elem.name,
value: (o.checkboxesAsBools && this.type === 'checkbox') ?
(this.checked ? 1 : 0) :
val
};
}).get();
};
})(jQuery);
window.makeMessage = function( type, mesg )
{
return( sprintf( '<div class="%s">%s</div>', type, mesg ) );
};
window.showMessage = function()
{
if( DEBUG ) console.log( "Called from " + ( arguments.callee.caller === null ? 'void' : arguments.callee.caller.name ) );
var opts = {
div: $('#message'),
append: false,
dom: null,
timeout: null,
scroll: false,
timeoutCallback: null,
};
var msgDiv = $('#message');
if( arguments.length == 3 && typeof( arguments[2] ) === 'object' )
{
var param = arguments[2];
opts.type = arguments[0];
opts.message = arguments[1];
if( typeof( param.append ) !== 'undefined' ) opts.append = param.append;
if( typeof( param.dom ) !== 'undefined' ) opts.dom = param.dom;
if( typeof( param.timeout ) !== 'undefined' ) opts.timeout = param.timeout;
if( typeof( param.timeoutCallback ) !== 'undefined' ) opts.timeoutCallback = param.timeoutCallback;
if( typeof( param.scroll ) !== 'undefined' ) opts.scroll = param.scroll;
if( typeof( param.speak ) !== 'undefined' ) opts.speak = param.speak;
}
// Backward compatibility
else if( arguments.length >= 2 )
{
opts.type = arguments[0];
opts.message = arguments[1];
if( arguments.length > 2 ) opts.append = arguments[2];
if( arguments.length > 3 ) opts.dom = arguments[3];
if( arguments.length > 4 ) opts.timeout = arguments[4];
}
else
{
msgDiv.append( makeMessage( 'warning', "showMessage called with only " + arguments.length + " parameters while 2 at minimum are required. Usage: showMessage( type, message, append, domObject, timeout ) or showMessage( type, message, options )" ) );
return( false );
}
if( typeof( opts.dom ) === 'object' && opts.dom != null )
{
msgDiv = opts.dom;
}
// Check if message is an array of messages
// https://stackoverflow.com/a/4775741/4814971
if( Array.isArray )
{
if( Array.isArray( opts.message ) )
{
opts.messages = opts.message;
}
}
else if( opts.message instanceof( Array ) )
{
opts.messages = opts.message;
}
else if( $.isArray( opts.message ) )
{
opts.messages = opts.message;
}
// messages has been set with previous check
// Make this array a list of errors
if( opts.hasOwnProperty( 'messages' ) )
{
opts.message = sprintf( "<ol>\n%s\n</ol>", opts.messages.map(function(e){ return('<li>' + e + '</li>'); }).join( "\n" ) );
}
if( opts.append )
{
msgDiv.append(makeMessage(opts.type, opts.message));
}
else
{
msgDiv.html(makeMessage(opts.type, opts.message));
}
if( opts.type == 'error' )
{
msgDiv.addClass( 'error-shake' );
setTimeout(function()
{
msgDiv.removeClass( 'error-shake' );
}, 70000);
}
if( parseInt( opts.timeout ) > 0 )
{
var thisTimeout = parseInt( opts.timeout );
setTimeout(function()
{
msgDiv.html( '' );
if( typeof( opts.timeoutCallback ) === 'function' )
{
opts.timeoutCallback();
}
},thisTimeout);
}
else
{
setTimeout(function()
{
msgDiv.html( '' );
},15000);
}
if( opts.scroll )
{
if( DEBUG ) console.log( "Scrolling to the top of the page..." );
$('html, body').animate( { scrollTop: 0 }, 500 );
}
else
{
if( DEBUG ) console.log( "No scrolling..." );
}
};
window.postfixAdminProgressBar = function()
{
var xhr = $.ajaxSettings.xhr();
if( DEBUG ) console.log( "Initiating the progress bar." );
if( DEBUG ) console.log( "Called from:\n" + (new Error).stack );
$('#postfixadmin-progress').css( 'width', '0%' ).show().removeClass('done');
xhr.upload.onprogress = function(evt)
{
if( evt.lengthComputable )
{
var percentComplete = evt.loaded / evt.total;
if( DEBUG ) console.log(percentComplete);
$('#postfixadmin-progress').css({
width: percentComplete * 100 + '%' });
if( DEBUG ) console.log( "upload.addEventListener: " + percentComplete );
if( percentComplete === 1 )
{
// $('#postfixadmin-progress').addClass('done').hide();
}
}
};
xhr.onprogress = function(evt)
{
if( evt.lengthComputable )
{
var percentComplete = evt.loaded / evt.total;
if( DEBUG ) console.log("addEventListener: " + percentComplete);
$('#postfixadmin-progress').css({
width: percentComplete * 100 + '%' });
if( percentComplete === 1 )
{
// $('#postfixadmin-progress').addClass('done').hide();
}
}
};
return( xhr );
};
window.postfixAdminProgressBarStart = function()
{
$({property: 0}).animate({property: 85},
{
// Arbitrary time, which should well cover the time it takes to get response from server
// Otherwise, well our progress bar will hang at 85% until we get a call to kill it
duration: 4000,
step: function()
{
var _percent = Math.round( this.property );
$('#postfixadmin-progress').css( 'width', _percent + '%' );
}
});
};
window.postfixAdminProgressBarStop = function()
{
$({property: 85}).animate({property: 105},
{
duration: 1000,
step: function()
{
var _percent = Math.round( this.property );
$('#postfixadmin-progress').css( 'width', _percent + '%' );
if( _percent == 105 )
{
$('#postfixadmin-progress').addClass('done');
}
},
complete: function()
{
$('#postfixadmin-progress').hide();
$('#postfixadmin-progress').css( 'width', '0%' );
}
});
};
window.autoconfigShowHideArrow = function()
{
// Reset. Show them all
$('table.server .autoconfig-command').show();
// Hide first and last ones
// This requires jQUery up to v3.3. Version 3.4 onward do not support :first and :last anymore
$('.autoconfig-incoming:first .autoconfig-move-up').hide();
$('.autoconfig-incoming:last .autoconfig-move-down').hide();
$('.autoconfig-outgoing:first .autoconfig-move-up').hide();
$('.autoconfig-outgoing:last .autoconfig-move-down').hide();
};
window.autoconfig_ajax_call = function( postData )
{
var prom = $.Deferred();
$this = $(this);
$.ajax({
xhr: postfixAdminProgressBar,
type: "POST",
url: "autoconfig.php",
dataType: "json",
data: postData,
beforeSend: function(xhr)
{
xhr.overrideMimeType( "application/json; charset=utf-8" );
if( DEBUG ) console.log( "Initiating the progress bar." );
if( DEBUG ) console.log( "Called from:\n" + (new Error).stack );
// $('#postfixadmin-progress').show().removeClass('done');
postfixAdminProgressBarStart();
}
}).fail(function(xhr, errType, ExceptionObject)
{
postfixAdminProgressBarStop();
if( DEBUG ) console.log( "Returned error " + xhr.status + " with error type " + errType + " and exception object " + JSON.stringify( ExceptionObject ) );
if( DEBUG ) console.log( "Current url is: " + xhr.responseURL );
if( DEBUG ) console.log( "Http headers are: " + xhr.getAllResponseHeaders() );
if( DEBUG ) console.log( "Response raw data is: \n" + xhr.responseText );
// There was a redirect most likely due to some timeout
if( typeof( xhr ) !== 'undefined' )
{
if( DEBUG ) console.log( "Xhr is: " + JSON.stringify( xhr ) );
if( xhr.getResponseHeader( 'Content-Type' ).toLowerCase().indexOf( 'text/html' ) >= 0 )
{
window.location.reload();
return( true );
}
}
var msg = 'An unexpected error has occurred';
showMessage( 'error', msg, { scroll: true });
$this.addClass( 'error-shake' );
prom.reject();
}).done(function(data, status, xhr)
{
postfixAdminProgressBarStop();
if( data.error )
{
showMessage( 'error', data.error, { scroll: true });
$this.addClass( 'error-shake' );
setTimeout(function()
{
$this.removeClass( 'error-shake' );
},5000);
prom.reject();
}
else
{
if( data.success )
{
prom.resolve(data);
showMessage( 'success', data.success, { scroll: true });
if( DEBUG ) console.log( "save(): " + data.success );
}
else if( data.info )
{
showMessage( 'info', data.info, { scroll: true } );
prom.resolve();
}
else
{
showMessage( 'info', data.msg, { scroll: true } );
prom.resolve();
}
}
});
return( prom.promise() );
};
$(document).on('click','#autoconfig_save', function(e)
{
e.preventDefault();
$this = $(this);
var data = {handler: 'autoconfig_save'};
$('#autoconfig_form').serializeArrayAll().map(function(item)
{
if( data[ item.name ] !== undefined )
{
if( !data[ item.name ].push )
{
data[ item.name ] = [ data[item.name] ];
}
data[ item.name ].push( item.value );
}
else
{
data[ item.name ] = item.value;
}
});
if( DEBUG ) console.log( "serialized data is: " + JSON.stringify( data ) );
autoconfig_ajax_call( data ).done(function(data)
{
// Since this shared function is used for both saving (adding and updating) as well as deleting
// we check if those data properties are returned by the server.
// Those here are only returned if this is the result of an addition
if( data.hasOwnProperty('config_id') )
{
if( $('#autoconfig_form input[name="config_id"]').length == 0 )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find the field \"config_id\" !" );
}
if( typeof( data.config_id ) !== 'undefined' && data.config_id.length > 0 )
{
// We force trigger change, because this is a hidden field and it hidden field do not trigger change
$('#autoconfig_form input[name="config_id"]').val( data.config_id ).trigger('change');
}
}
// Do the hosts
var hostTypes = [ 'incoming', 'outgoing' ];
for( var j = 0; j < hostTypes.length; j++ )
{
var hostType = hostTypes[j];
if( DEBUG ) console.log( "Checking host of type " + hostType );
if( data.hasOwnProperty(hostType + '_server') && Array.isArray( data[hostType + '_server'] ) )
{
var thoseHosts = $('.autoconfig-' + hostType);
var dataHosts = data[hostType + '_server'];
if( thoseHosts.length == 0 )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find any hosts block in our form!" );
}
else if( dataHosts.length != thoseHosts.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Total of " + hostType + " servers returned from the server (" + dataHosts.length + ") do not match the total hosts we have in our form (" + thoseHosts.length + ")." );
}
dataHosts.forEach(function(def, index)
{
if( DEBUG ) console.log( "def contains: " + JSON.stringify( def ) );
if( !def.hasOwnProperty('config_id') ||
!def.hasOwnProperty('hostname') ||
!def.hasOwnProperty('port') )
{
if( DEBUG ) console.error( "Something is wrong. Data received is missing fields config_id, or hostname or port" );
return( false );
}
thoseHosts.each(function(offset, obj)
{
var dom = $(obj);
var hostId = dom.find('input[name="host_id[]"]');
var hostName = dom.find('input[name="hostname[]"]');
var hostPort = dom.find('input[name="port[]"]');
if( !hostId.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host id field for host type \"" + hostType + "\" at offset " + i );
}
if( !hostName.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host name field for host type \"" + hostType + "\" at offset " + i );
}
if( !hostPort.length )
{
showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true });
throw( "Unable to find host port field for host type \"" + hostType + "\" at offset " + i );
}
// We found our match: no id, hostname and port match
if( hostId.val().length == 0 && hostName.val() == def.hostname && hostPort.val() == def.port )
{
if( DEBUG ) console.log( "Setting host id " + def.host_id + " to host name " + hostName.val() + " with port " + hostPort.val() );
hostId.val( def.host_id );
// exit the loop
return( false );
}
});
});
}
else
{
if( DEBUG ) console.error( "Something is wrong. Data received for hosts of type " + hostType + " does not exist or is not an array." );
}
}
// Now, do the texts
var textTypes = ['instruction', 'documentation'];
for( var j = 0; j < textTypes.length; j++ )
{
var textType = textTypes[j];
if( DEBUG ) console.log( "Checking text of type " + textType );
if( data.hasOwnProperty(textType) && Array.isArray( data[textType] ) )
{
var dataTexts = data[textType];
var thoseTextIds = $('input[name="' + textType + '_id[]"]');
var thoseTextLang = $('select[name="' + textType + '_lang[]"]');
var thoseTextData = $('textarea[name="' + textType + '_text[]"]');
// The array could very well be empty
dataTexts.forEach(function(def, index)
{
if( !def.hasOwnProperty('id') ||
!def.hasOwnProperty('type') ||
!def.hasOwnProperty('lang') )
{
if( DEBUG ) console.error( "Something is wrong. Data received is missing fields id, or type or lang" );
return( false );
}
for( var k = 0; k < thoseTextIds.length; k++ )
{
var textId = thoseTextIds.eq(k);
var textLang = thoseTextLang.eq(k);
// Found a match
if( textId.val().length == 0 && textLang.val() == def.lang )
{
if( DEBUG ) console.log( "Setting text id " + def.id + " to text with type " + textType + " and language " + def.lang );
textId.val( def.id );
return( false );
}
}
});
}
else
{
if( DEBUG ) console.error( "Something is wrong. Data received for " + textType + " text does not exist or is not an array." );
}
}
}).fail(function()
{
// Nothing for now
});
});
$(document).on('click','#autoconfig_remove', function(e)
{
e.preventDefault();
var data =
{
handler: 'autoconfig_remove',
config_id: $('input[name="config_id"]').val(),
token: $('input[name="token"]').val(),
};
if( DEBUG ) console.log( "Data to be sent is: " + JSON.stringify( data ) );
autoconfig_ajax_call( data ).done(function(data)
{
// Reload the page, but without the query string at the end
window.location.href = '/' + window.location.pathname;
}).fail(function()
{
// Nothing for now
});
});
$(document).on('click', '#autoconfig_cancel', function(e)
{
e.preventDefault();
window.location.href = 'list.php?table=domain';
return( true );
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
window.getRandomInt = function(min, max)
{
return( Math.floor( Math.random() * Math.floor((max - min) + min) ) );
}
/*
I could have just created one function and called it
*/
// Add and remove hosts
$(document).on('click','.autoconfig-server-add',function(e)
{
e.preventDefault();
var row = $(this).closest('table.server').closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var clone = row.clone();
clone.find('select,input[type!="hidden"],textarea').each(function(i,item)
{
$(item).val( '' );
});
// We need to remove the host_id so it can be treated as a new host and not an update of an existing one
clone.find('input[name="host_id[]"]').val( '' );
// Set default value and trigger change, which will call an event handler that will hide/show the section on pop3
clone.find('.host_type').val('imap').trigger('change');
var optionLabels = ['leave_messages_on_server', 'download_on_biff', 'days_to_leave_messages_on_server', 'check_interval'];
optionLabels.forEach(function(fieldName, index)
{
if( DEBUG ) console.log( "Checking field name " + fieldName );
var thisField = clone.find('input[name="' + fieldName + '[]"]');
if( thisField.length > 0 )
{
var forThisField = $('label[for="' + thisField.attr('id') + '"]', clone);
if( forThisField.length > 0 )
{
if( DEBUG ) console.log( "Field is " + thisField.html() + "\nLabel field is: " + forThisField.html() );
thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000));
maxAttempt = 10;
if( DEBUG ) console.log( "Checking if generated id exists: " + thisField.attr('id') );
while( $('#' + thisField.attr('id'), clone).length > 0 && ++maxAttempt < 10 )
{
thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000));
}
forThisField.attr('for', thisField.attr('id'));
if( DEBUG ) console.log( "Final generated id is: " + thisField.attr('id') );
}
else
{
if( DEBUG ) console.error( "Unable to find label element for field name." );
}
}
});
clone.insertAfter( row );
autoconfigShowHideArrow();
$('html, body').animate( { scrollTop: clone.offset().top }, 500 );
return( true );
});
$(document).on('click','.autoconfig-server-remove',function(e)
{
e.preventDefault();
var row = $(this).closest('table.server').closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
// Check if there are at least 2 elements, so that after at least one remain
var re = new RegExp('(autoconfig-(?:incoming|outgoing))-server');
var res = $(this).attr('class').match( re );
console.log( res );
if( res == null )
{
throw( "Cannot find class \"autoconfig-incoming-server\" or class \"autoconfig-outgoing-server\" in our clicked element." );
}
// Now find how many elements we have with this class
var total = $('tr.' + res[1]).length;
if( total < 2 )
{
row.addClass('autoconfig-error-shake');
setTimeout(function()
{
row.removeClass('autoconfig-error-shake');
},1000);
return( false );
}
row.remove();
autoconfigShowHideArrow();
});
// Add and remove account enable instructions or support documentation
$(document).on('click','.autoconfig-locale-text-add',function(e)
{
e.preventDefault();
var row = $(this).closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var clone = row.clone();
clone.find('select,input[type!="hidden"],textarea').each(function(i,item)
{
$(item).val( '' );
});
// We need to remove the host_id so it can be treated as a new text and not an update of an existing one
clone.find('input[name$="_id[]"]').val( '' );
clone.insertAfter( row );
$('html, body').animate( { scrollTop: clone.offset().top }, 500 );
});
$(document).on('click','.autoconfig-locale-text-remove',function(e)
{
e.preventDefault();
var row = $(this).closest('tr');
if( !row.length )
{
throw( "Unable to find the current enclosing row." );
}
var re = new RegExp('(autoconfig-(instruction|documentation))');
var res = $(this).attr('class').match( re );
if( res == null )
{
throw( "Cannot find class \"autoconfig-instruction\" or class \"autoconfig-documentation\" in our clicked element." );
}
var textType = res[2];
var total = $('tr.' + res[1]).length;
if( DEBUG ) console.log( total + " rows found for text type " + textType );
if( total < 2 )
{
if( DEBUG ) console.log( "text remove: one left" );
if( DEBUG ) console.log( "Getting lang object with " + '[name="' + textType + '_lang[]"]' );
var textLang = row.find('[name="' + textType + '_lang[]"]');
if( DEBUG ) console.log( "Getting text object with " + '[name="' + textType + '_text[]"]' );
var textData = row.find('[name="' + textType + '_text[]"]');
if( DEBUG ) console.log( "text remove: found lang and text field? " + ( ( textLang.length > 0 && textData.length > 0 ) ? "Yes" : "No" ) );
// This is remaining default tet row and there is no more data
if( ( textLang.val() === null || ( textLang.val() !== null && textLang.val() == '' ) ) && $.trim(textData.val()) == '' )
{
if( DEBUG ) console.log( "text remove: lang and text fields are empty already, error shake it" );
row.addClass('autoconfig-error-shake');
setTimeout(function()
{
row.removeClass('autoconfig-error-shake');
},1000);
}
else
{
if( DEBUG ) console.log( "text remove: empty fields" );
textLang.val( '' );
textData.val( '' );
}
return( false );
}
row.remove();
});
$(document).on('click', '#copy_provider_value', function(e)
{
e.preventDefault();
var orgField = $('input[name="organisation"]');
var providerNameField = $('input[name="provider_name"]');
if( !orgField.length || !providerNameField.length )
{
throw( "Unable to find either the provider name field or the organisation field!" );
}
if( providerNameField.val().length == 0 )
{
return( false );
}
orgField.val( providerNameField.val() );
return( true );
});
$(document).on('click', '#autoconfig_toggle_select_all_domains', function(e)
{
e.preventDefault();
// if( DEBUG ) console.log( "provider_domain options length: " + $('#autoconfig_provider_domain option').length + " and disabled options are: " + $('#autoconfig_provider_domain option:disabled').length + " and selected options are: " + $('#autoconfig_provider_domain option:selected').length );
if( $('#autoconfig_provider_domain option:selected').length > 0 )
{
$('#autoconfig_provider_domain option').prop('selected', false);
}
else
{
if( $('#autoconfig_provider_domain option:disabled').length == $('#autoconfig_provider_domain option').length )
{
var row = $(this).closest('tr');
row.addClass('error-shake');
setTimeout(function()
{
row.removeClass('error-shake');
},500);
return( false );
}
else
{
$('#autoconfig_provider_domain option:not(:disabled)').prop('selected', true);
}
}
});
$(document).on('change', '#autoconfig_form .host_type', function(e)
{
var typeValue = $(this).val();
// We get the enclosing table object to set some limiting context to the host_pop3 selector below
var tbl = $(this).closest('table');
if( typeValue == 'imap' )
{
$('.host_pop3', tbl).hide();
}
else
{
$('.host_pop3', tbl).show();
}
});
$(document).on('change', '#autoconfig_form .username_template', function(e)
{
var usernameField = $(this).closest('.server').find('input[name="username[]"]');
if( usernameField.length == 0 )
{
throw( "Unable to find the username field!" );
}
usernameField.val( $(this).val() );
});
$(document).on('change', '#autoconfig_form select[name="jump_to"]', function(e)
{
var id = $(this).val();
var re = new RegExp( '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})' );
// This will display an empty form
if( id.length == 0 )
{
window.location.href = 'autoconfig.php';
}
else if( id.match( re ) )
{
window.location.href = 'autoconfig.php?config_id=' + encodeURIComponent( id );
}
});
// Upon change in the content of the hidden config_id, enable or disable the "Remove" buttton
// The Remove button can only be used if this is for an existing configuration obviously
$(document).on('change', '#autoconfig_form input[name="config_id"]', function(e)
{
if( DEBUG ) console.log( "Config id field contains \"" + $(this).val() + "\"." );
if( $(this).val().length == 0 || $(this).val().match( /^[[:blank:]\t]*$/ ) )
{
$('#autoconfig_remove').attr( 'disabled', true );
}
else
{
$('#autoconfig_remove').attr( 'disabled', false );
}
});
$(document).on('click', '.autoconfig-move-up', function(e)
{
e.preventDefault();
var row = $(this).closest('.server').closest('tr');
if( row.prev().length == 0 || ( !row.prev().hasClass('autoconfig-incoming') && !row.prev().hasClass('autoconfig-outgoing') ) )
{
return( false );
}
row.insertBefore( row.prev() );
$('html, body').animate( { scrollTop: row.offset().top }, 500 );
autoconfigShowHideArrow();
});
$(document).on('click', '.autoconfig-move-down', function(e)
{
e.preventDefault();
var row = $(this).closest('.server').closest('tr');
if( row.next().length == 0 || ( !row.next().hasClass('autoconfig-incoming') && !row.next().hasClass('autoconfig-outgoing') ) )
{
return( false );
}
row.insertAfter( row.next() );
$('html, body').animate( { scrollTop: row.offset().top }, 500 );
autoconfigShowHideArrow();
});
window.checkExistingTextLanguage = function(opts = {})
{
if( typeof( opts ) !== 'object' )
{
throw( "Parameters provided is not an object. Call checkExistingTextLanguage like this: checkExistingTextLanguage({ type: 'instruction', lang: 'fr', caller: $(this) })" );
}
var textType = opts.type;
var callerMenu = opts.caller;
var langToCheck = opts.lang;
if( DEBUG ) console.log( "checkExistingTextLanguage() checking text type " + textType + " for language " + langToCheck );
if( typeof( textType ) === 'undefined' )
{
throw( "No text type was provided." );
}
var langMenu = $('select[name="' + textType + '_lang[]"]');
if( DEBUG ) console.log( "checkExistingTextLanguage() Found " + langMenu.length + " language menu(s)." );
if( langMenu.length == 0 )
{
throw( "Could not find any language menu for this text type " + textType );
}
if( langMenu.length == 1 )
{
return( true );
}
var alreadySelected = false;
if( DEBUG ) console.log( "checkExistingTextLanguage() checking each existing language menu." );
langMenu.each(function(offset, menuObject)
{
if( $(menuObject).is( callerMenu ) )
{
if( DEBUG ) console.log( "checkExistingTextLanguage() skipping because this is our caller menu." );
return( true );
}
// Found a match, stop there
else if( $(menuObject).val() == langToCheck )
{
if( DEBUG ) console.log( "checkExistingTextLanguage() Found match with this menu value (" + $(menuObject).val() + ") matching the language to check \"" + langToCheck + "\"." );
alreadySelected = true;
return( false );
}
});
if( DEBUG ) console.log( "checkExistingTextLanguage() returning: " + !alreadySelected );
return( !alreadySelected );
};
// Upon selection or change of a language, we check it has not been selected already
$(document).on('change', 'select[name="instruction_lang[]"]', function(e)
{
if( !checkExistingTextLanguage({ type: 'instruction', lang: $(this).val(), caller: $(this) }) )
{
$(this).addClass('error-shake');
// <i class="fas fa-exclamation-triangle"></i>
var warning = $('<i/>',
{
class: 'fas fa-exclamation-triangle fa-2x',
style: 'color: red; font-size: 20px;',
});
warning.insertAfter( $(this) );
var that = $(this);
setTimeout(function()
{
that.removeClass('error-shake');
warning.remove();
},5000);
$(this).val('');
return( false );
}
return( true );
});
window.toggleCertFiles = function(option)
{
if( typeof( option ) === 'undefined' )
{
return( false );
}
if( option == 'local' )
{
$('.cert_files').show();
}
else
{
$('.cert_files').hide();
}
};
$(document).on('change', 'select[name="sign_option"]', function(e)
{
toggleCertFiles( $(this).val() );
});
// Need to trigger the change for the host type menu
if( $('#autoconfig_form .host_type').length > 0 )
{
$('#autoconfig_form .host_type').trigger('change');
}
if( $('#autoconfig_form input[name="config_id"]').length > 0 )
{
$('#autoconfig_form input[name="config_id"]').trigger('change');
}
// Hide useless up/down arrows
autoconfigShowHideArrow();
toggleCertFiles( $('select[name="sign_option"]').val() );
});

@ -0,0 +1,307 @@
<?php
/**
* Postfix Admin Auto Discovery Configuration
*
* LICENSE
* This source file is subject to the GPL license that is bundled with
* this package in the file LICENSE.TXT.
*
* Further details on the project are available at http://postfixadmin.sf.net
*
* @version $Id$
* @license GNU GPL v2 or later.
*
* File: autoconfig.php
*
* Allows admin to configure Autodiscovery settings for mail domain names and
* for Mac users or admins to generate the .mobile configuration file for Mac Mail.
*
* Template File: autoconfig.tpl, autoconfig-host-settings.tpl
*
* Template Variables:
*
* Form POST \ GET Variables:
*/
require_once( 'common.php' );
require_once( 'autoconfig_languages.php' );
const DEBUG = false;
authentication_require_role('admin');
$fUsername = authentication_get_username(); # enforce login
$Return_url = "list.php?table=domain";
mb_internal_encoding( 'UTF-8' );
// $smarty->error_reporting = E_ALL & ~E_NOTICE;
/*
if( authentication_has_role('admin') )
{
$Admin_role = 1 ;
$fDomain = safeget('domain');
// $fUsername = safeget('username');
// list(null $fDomain) = explode('@', $fUsername);
$Return_url = "list-virtual.php?domain=" . urlencode( $fDomain );
if( $fDomain == '' || !check_owner( authentication_get_username(), $fDomain ) )
{
die( "Invalid username!" ); # TODO: better error message
}
}
else
{
$Admin_role = 0 ;
$Return_url = "main.php";
authentication_require_role('user');
}
*/
// is autoconfig support enabled in $CONF ?
if( $CONF['autoconfig'] == 'NO' || !array_key_exists( 'autoconfig', $CONF ) )
{
header( "Location: $Return_url" );
exit( 0 );
}
date_default_timezone_set( @date_default_timezone_get() ); # Suppress date.timezone warnings
$error = 0;
$fDomain = safeget('domain');
$ah = new AutoconfigHandler( $fUsername );
$ah->debug = DEBUG;
$config_id = safeget('config_id');
if( !empty( $fDomain ) && empty( $config_id ) )
{
$config_id = $ah->get_id_by_domain( $fDomain );
}
// if( !$config_id )
// {
// flash_error( $PALANG['pAutoconfig_no_config_found'] );
// $error = 1;
// }
$form = array();
if( count( $ah->all_domains ) == 0 )
{
if( authentication_has_role( 'global-admin' ) )
{
flash_error( $PALANG['no_domains_exist'] );
}
else
{
flash_error( $PALANG['no_domains_for_this_admin'] );
}
header( "Location: list.php?table=domain" ); # no domains (for this admin at least) - redirect to domain list
exit;
}
else
{
$form['provider_domain_options'] = $ah->all_domains;
}
if( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) )
{
if( DEBUG ) error_log( "config id submitted is: '$config_id'." );
if( !empty( $config_id ) )
{
if( DEBUG ) error_log( "Getting configuration details with get_details()" );
$form = $ah->get_details( $config_id );
if( DEBUG ) error_log( "get_details() returned: " . print_r( $form, true ) );
}
if( empty( $form['account_type'] ) )
{
$form['account_type'] = 'imap';
}
if( empty( $form['ssl_enabled'] ) )
{
$form['ssl_enabled'] = 1;
}
if( empty( $form['active'] ) )
{
$form['active'] = 1;
}
$form['placeholder'] = array(
'provider_id' => $ah->all_domains[0],
'provider_name' => $PALANG['pAutoconfig_placeholder_provider_name'],
);
$form['config_options'] = $ah->get_config_ids();
if( DEBUG ) error_log( "config_options is: " . print_r( $form['config_options'], true ) );
// $config_id could be null
$form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id );
if( DEBUG ) error_log( "provider_domain_disabled is: " . print_r( $form['provider_domain_disabled'], true ) );
// Get defaults
if( count( $form['enable']['instruction'] ) == 0 )
{
$form['enable']['instruction'] = array(
array( 'lang' => 'en', 'phrase' => '' )
);
if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 0;
}
else
{
if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 1;
}
if( count( $form['documentation']['description'] ) == 0 )
{
$form['documentation']['description'] = array(
array( 'lang' => 'en', 'phrase' => '' )
);
if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 0;
}
else
{
if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 1;
}
showAutoconfigForm( $form );
exit( 0 );
}
elseif( $_SERVER['REQUEST_METHOD'] == "POST" )
{
if( safepost('token') != $_SESSION['PFA_token'] )
{
die('Invalid token!');
}
if( !isset( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) ||
strtolower( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) != 'xmlhttprequest' )
{
if( DEBUG ) error_log( "This request is not using Ajax." );
flash_error( "Request is not using Ajax" );
showAutoconfigForm( $_POST );
exit( 0 );
}
if( isset( $_POST['config_id'] ) && !empty( $_POST['config_id'] ) )
{
if( DEBUG ) error_log( "Got config_id: " . $_POST['config_id'] );
if( !$ah->config_id( $_POST['config_id'] ) )
{
json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_config_id_not_found'], $_POST['config_id'] ) ) );
exit( 0 );
}
}
$handler = null;
if( isset( $_POST['handler'] ) )
{
if( preg_match( '/^[a-z][a-z_]+$/', $_POST['handler'] ) )
{
$handler = $_POST['handler'];
}
else
{
if( DEBUG ) error_log( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." );
json_reply( array( 'error' => "Bad handler provided." ) );
exit( 0 );
}
}
if( DEBUG ) error_log( "handler is \"$handler\"." );
if( $handler == 'autoconfig_save' )
{
if( DEBUG ) error_log( "Got here saving configuration." );
if( !( $form = $ah->save_config( $_POST ) ) )
{
if( DEBUG ) error_log( "Failed to save config: " . $ah->error_as_string() );
json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) );
}
else
{
if( DEBUG ) error_log( "Ok, config saved." );
// We return the newly created ids so the user can perform a follow-on update
// The Ajax script will take care of setting those values in the hidden fields
json_reply( array(
'success' => $PALANG['pAutoconfig_config_saved'],
'config_id' => $form['config_id'],
'incoming_server' => $form['incoming_server'],
'outgoing_server' => $form['outgoing_server'],
'instruction' => $form['enable']['instruction'],
'documentation' => $form['documentation']['description'],
) );
}
}
elseif( $handler == 'autoconfig_remove' )
{
if( DEBUG ) error_log( "Got here removing configuration id " . $_POST['config_id'] );
if( empty( $_POST['config_id'] ) )
{
json_reply( array( 'error' => $PALANG['pAutoconfig_no_config_yet_to_remove'] ) );
exit( 0 );
}
if( !$ah->remove_config( $_POST['config_id'] ) )
{
if( DEBUG ) error_log( "Failed to remove config: " . $ah->error_as_string() );
json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) );
}
else
{
if( DEBUG ) error_log( "Ok, config removed." );
json_reply( array( 'success' => $PALANG['pAutoconfig_config_removed'] ) );
}
exit( 0 );
}
else
{
json_reply( array( 'error' => 'Unknown handler provided "' . $handler . '".' ) );
}
exit( 0 );
}
function json_reply( $data )
{
if( !array_key_exists( 'error', $data ) &&
!array_key_exists( 'info', $data ) &&
!array_key_exists( 'success', $data ) )
{
error_log( "json_reply() missing message type: error, info or success" );
return( false );
}
$allowed_domain = 'http'
. ( ( array_key_exists( 'HTTPS', $_SERVER )
&& $_SERVER[ 'HTTPS' ]
&& strtolower( $_SERVER[ 'HTTPS' ] ) !== 'off' )
? 's'
: null )
. '://' . $_SERVER[ 'HTTP_HOST' ];
header( "Access-Control-Allow-Origin: $allowed_domain" );
header( 'Content-Type: application/json;charset=utf-8' );
if( DEBUG ) error_log( "Returning to client the payload: " . json_encode( $data ) );
echo json_encode( $data );
return( true );
}
function showAutoconfigForm( &$form )
{
global $PALANG, $CONF, $languages, $smarty;
if( DEBUG ) error_log( "showAutoconfigForm() received form data: " + print_r( $form, true ) );
if( $form == null ) $form = array();
if( array_key_exists( 'enable', $form ) )
{
if( array_key_exists( 'instruction', $form['enable'] ) )
{
if( count( $form['enable']['instruction'] ) == 0 )
{
$form['enable']['instruction'][] = array( 'lang' => 'en' );
}
}
}
if( array_key_exists( 'documentation', $form ) )
{
if( array_key_exists( 'description', $form['documentation'] ) )
{
if( count( $form['documentation']['description'] ) == 0 )
{
$form['documentation']['description'][] = array( 'lang' => 'en' );
}
}
}
$smarty->assign( 'form', $form );
$smarty->assign( 'language_options', $languages );
$smarty->assign( 'default_lang', 'en' );
$smarty->assign( 'smarty_template', 'autoconfig' );
$smarty->display( 'index.tpl');
exit( 0 );
}
/* vim: set expandtab softtabstop=3 tabstop=3 shiftwidth=3: */
?>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,302 @@
{literal}
<link rel="stylesheet" type="text/css" href="autoconfig.css" />
<script language="JavaScript" src="https://code.jquery.com/jquery-3.3.1.min.js" type="text/javascript"></script>
<script language="JavaScript" src="sprintf.js" type="text/javascript"></script>
<script language="JavaScript" src="autoconfig.js" type="text/javascript"></script>
{/literal}
<script id="host-template" type="text/template">
{include file='autoconfig-host-settings.tpl'}
</script>
<div id="postfixadmin-progress" class="waiting">
<dt></dt>
<dd></dd>
</div>
<div id="message"></div>
<div id="edit_form">
<form id="autoconfig_form" name="edit-autoconfig" method="post" action=''>
<input class="flat" type="hidden" name="token" value="{$smarty.session.PFA_token|escape:"url"}" />
<input class="flat" type="hidden" name="config_id" value="{$form.config_id}" />
<input type="checkbox" class="switch-input" name="enable_status" id="enable_status" value="1" {if !empty($form.enable_status) && $form.enable_status == 1}checked="checked"{/if} />
<input type="checkbox" class="switch-input" name="documentation_status" id="documentation_status" value="1" {if !empty($form.documentation_status) && $form.documentation_status == 1}checked="checked"{/if} />
<datalist id="defaultPorts">
<option value="25">
<option value="143">
<option value="465">
<option value="587">
<option value="993">
<option value="995">
<option value="2525">
</datalist>
<table>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_page_title}</th>
</tr>
<tr class="autoconfig_jump">
<td class="label"><label for="autoconfig_active">{$PALANG.pAutoconfig_jump_to}:</label></td>
<td><select name="jump_to">
<option value="">{$PALANG.pAutoconfig_new_configuration}</option>
{html_options options=$form.config_options selected=$form.config_id}
</select></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_active">{$PALANG.pAutoconfig_active}:</label></td>
<td><input type="checkbox" class="flat" name="active" id="autoconfig_active" value="1" {if !empty($form.active) && $form.active == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_encoding}:</label></td>
<td><input type="hidden" class="flat" name="encoding" value="utf-8" /><em>utf-8</em></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_provider_id}:</label></td>
<td><input type="text" size="40" class="flat" name="provider_id" maxlength="255" value="{$form.provider_id}" placeholder="{$form.placeholder.provider_id}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_provider_domain}:</label></td>
<td><select class="flat" name="provider_domain[]" id="autoconfig_provider_domain" size="10" multiple="multiple">
{foreach from=$form.provider_domain_options item=domain}
<option value="{$domain}"{if in_array($domain, $form.provider_domain)} selected="selected"{elseif in_array($domain, $form.provider_domain_disabled)} disabled="true"{/if}>{$domain}</option>
{/foreach}
</select><br /><input type="button" id="autoconfig_toggle_select_all_domains" value="{$PALANG.pAutoconfig_toggle_select_all}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_provider_name}:</label></td>
<td><input type="text" size="40" class="flat" name="provider_name" maxlength="255" value="{$form.provider_name}" placeholder="{$form.placeholder.provider_name}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_provider_short}:</label></td>
<td><input type="text" size="40" class="flat" name="provider_short" maxlength="120" value="{$form.provider_short}" /></td>
<td>&nbsp;</td>
</tr>
<!-- looping for each incoming server -->
<tr>
<th colspan="3">{$PALANG.pAutoconfig_incoming_server}</th>
</tr>
{if count($form.incoming_server) == 0}
{assign var="server" value=['type' => 'imap'] scope="global"}
{include file='autoconfig-host-settings.tpl' server=$server}
{else}
{foreach name=outer item=server from=$form.incoming_server}
{include file='autoconfig-host-settings.tpl' server=$server}
{/foreach}
{/if}
<!-- end looping for each incoming server -->
<!-- looping for each outgoing server -->
<tr>
<th colspan="3">{$PALANG.pAutoconfig_outgoing_server}</th>
</tr>
{if count($form.outgoing_server) == 0}
{assign var="server" value=['type' => 'smtp'] scope="global"}
{include file='autoconfig-host-settings.tpl' server=$server}
{else}
{foreach name=outer item=server from=$form.outgoing_server}
{include file='autoconfig-host-settings.tpl' server=$server}
{/foreach}
{/if}
<!-- end looping for each outgoing server -->
<tr>
<th colspan="3">{$PALANG.pAutoconfig_enable}
<label class="switch" for="enable_status">
<span class="switch-label" data-on="{$PALANG.pAutoconfig_on}" data-off="{$PALANG.pAutoconfig_off}"></span>
<span class="switch-handle"></span>
</label>
<br />{$PALANG.pAutoconfig_not_supported} (<a href="https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo" title="Autoconfig: How to create a configuration file" target="_new">?</a>)</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_enable_url}:</label></td>
<td><input type="text" size="40" class="flat" name="enable_url" maxlength="2048" value="{$form.enable.url}" /></td>
<td>&nbsp;</td>
</tr>
{foreach name=support item=text from=$form.enable.instruction}
<tr class="autoconfig-instruction">
<td class="label"><label>{$PALANG.pAutoconfig_enable_instruction}:</label></td>
<td><input type="hidden" name="instruction_id[]" value="{$text.id}" />
{html_options name="instruction_lang[]" options=$language_options selected=$text.lang}<br />
<textarea class="flat" rows="5" cols="50" name="instruction_text[]" >{$text.phrase}</textarea></td>
<td><button class="autoconfig-command autoconfig-instruction autoconfig-locale-text-add ripple" title="{$PALANG.pAutoconfig_add_new_text}"><i class="fas fa-plus fa-2x"></i></button><button class="autoconfig-command autoconfig-instruction autoconfig-locale-text-remove ripple" title="{$PALANG.pAutoconfig_remove_text}"><i class="fas fa-minus fa-2x"></i></button></td>
</tr>
{/foreach}
<tr>
<th colspan="3">{$PALANG.pAutoconfig_documentation}
<label class="switch" for="documentation_status">
<span class="switch-label" data-on="{$PALANG.pAutoconfig_on}" data-off="{$PALANG.pAutoconfig_off}"></span>
<span class="switch-handle"></span>
</label>
</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_documentation_url}:</label></td>
<td><input type="text" size="40" class="flat" name="documentation_url" maxlength="2048" value="{$form.documentation.url}" /></td>
<td>&nbsp;</td>
</tr>
{foreach name=support item=text from=$form.documentation.description}
<tr class="autoconfig-documentation">
<td class="label"><label>{$PALANG.pAutoconfig_documentation_desc}:</label></td>
<td><input type="hidden" name="documentation_id[]" value="{$text.id}" />
{html_options name="documentation_lang[]" options=$language_options selected=$text.lang}<br />
<textarea class="flat" rows="5" cols="50" name="documentation_text[]" >{$text.phrase}</textarea></td>
<td><button class="autoconfig-command autoconfig-documentation autoconfig-locale-text-add ripple" title="{$PALANG.pAutoconfig_add_new_text}"><i class="fas fa-plus fa-2x"></i></button><button class="autoconfig-command autoconfig-documentation autoconfig-locale-text-remove ripple" title="{$PALANG.pAutoconfig_remove_text}"><i class="fas fa-minus fa-2x"></i></button></td>
</tr>
{/foreach}
<tr>
<th colspan="3">{$PALANG.pAutoconfig_webmail}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_webmail_login_page}:</label></td>
<td><input type="text" size="40" class="flat" name="webmail_login_page" maxlength="2048" value="{$form.webmail_login_page}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_webmail_login_page_info}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_lp_info_url}:</label></td>
<td><input type="text" size="40" class="flat" name="lp_info_url" maxlength="2048" value="{$form.lp_info_url}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_lp_info_username}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_lp_info_username_field_id}:</label></td>
<td><input type="text" class="flat" name="lp_info_username_field_id" maxlength="255" value="{$form.lp_info_username_field_id}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_lp_info_username_field_name}:</label></td>
<td><input type="text" class="flat" name="lp_info_username_field_name" maxlength="255" value="{$form.lp_info_username_field_name}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_lp_info_login_button}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_lp_info_login_button_id}:</label></td>
<td><input type="text" class="flat" name="lp_info_login_button_id" maxlength="255" value="{$form.lp_info_login_button_id}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_lp_info_login_button_name}:</label></td>
<td><input type="text" class="flat" name="lp_info_login_button_name" maxlength="255" value="{$form.lp_info_login_button_name}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_mac_specific_settings}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_account_name}:</label></td>
<td><input type="text" size="40" class="flat" name="account_name" maxlength="255" value="{$form.account_name}" placeholder="John Doe" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_account_type}:</label></td>
<td><select name="account_type">
<option value="imap" {if !empty($form.account_type) && $form.account_type == "imap"}selected{/if}>imap</option>
<option value="pop3" {if !empty($form.account_type) && $form.account_type == "pop3"}selected{/if}>pop3</option>
</select></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_email}:</label></td>
<td><input type="text" size="40" class="flat" name="email" maxlength="255" value="{$form.email}" placeholder="john.doe@example.org" /></td>
<td>&nbsp;</td>
</tr>
<!-- Not sure this field should stay there, because it is automatically computed based on the sock_type, ie SSL, TLS, STARTTLS -->
<tr>
<td class="label"><label for="autoconfig_ssl">{$PALANG.pAutoconfig_ssl}:</label></td>
<td><input type="checkbox" class="flat" name="ssl_enabled" id="autoconfig_ssl" value="1" {if !empty($form.ssl_enabled) && $form.ssl_enabled == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<!--
<tr>
<td class="label"><label>{$PALANG.Autoconfig_password}:</label></td>
<td><input type="text" class="flat" name="password" maxlength="255" value="{$form.password}" /></td>
<td>&nbsp;</td>
</tr>
-->
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_description}:</label></td>
<td><textarea class="flat" rows="5" cols="60" name="description" >{$form.description}</textarea></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_organisation}:</label></td>
<td><input type="text" size="40" class="flat" name="organisation" maxlength="255" value="{$form.organisation}" /><input type="button" id="copy_provider_value" value="{$PALANG.pAutoconfig_copy_provider_name}" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_payload_type}:</label></td>
<td><select name="payload_type">
<option value="com.apple.mail.managed" {if !empty($form.payload_type) && $form.payload_type == "com.apple.mail.managed"}selected{/if}>Mail account</option>
<option value="com.apple.eas.account" {if !empty($form.payload_type) && $form.payload_type == "com.apple.eas.account"}selected{/if}>Microsoft Exchange</option>
</select></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_prevent_app_sheet">{$PALANG.pAutoconfig_prevent_app_sheet}:</label></td>
<td><input type="checkbox" class="flat" name="prevent_app_sheet" id="autoconfig_prevent_app_sheet" value="1" {if !empty($form.prevent_app_sheet) && $form.prevent_app_sheet == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_prevent_move">{$PALANG.pAutoconfig_prevent_move}:</label></td>
<td><input type="checkbox" class="flat" name="prevent_move" id="autoconfig_prevent_move" value="1" {if !empty($form.prevent_move) && $form.prevent_move == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_smime_enabled" >{$PALANG.pAutoconfig_smime_enabled}:</label></td>
<td><input type="checkbox" class="flat" name="smime_enabled" id="autoconfig_smime_enabled" value="1" {if !empty($form.smime_enabled) && $form.smime_enabled == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_payload_remove_ok">{$PALANG.pAutoconfig_payload_remove_ok}:</label></td>
<td><input type="checkbox" class="flat" name="payload_remove_ok" id="autoconfig_payload_remove_ok" value="1" {if !empty($form.payload_remove_ok) && $form.payload_remove_ok == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="label"><label for="autoconfig_spa">{$PALANG.pAutoconfig_spa}:</label></td>
<td><input type="checkbox" class="flat" name="spa" id="autoconfig_spa" value="1" {if !empty($form.spa) && $form.spa == 1}checked="checked"{/if} /></td>
<td>&nbsp;</td>
</tr>
<tr>
<th colspan="3">{$PALANG.pAutoconfig_cert_sign}</th>
</tr>
<tr>
<td class="label"><label>{$PALANG.pAutoconfig_cert_option}:</label></td>
<td><select name="sign_option">
<option value="none" {if !empty($form.sign_option) && $form.sign_option == "none"}selected{/if}>{$PALANG.pAutoconfig_cert_none}</option>
<option value="local" {if !empty($form.sign_option) && $form.sign_option == "local"}selected{/if}>{$PALANG.pAutoconfig_cert_local}</option>
<option value="global" {if !empty($form.sign_option) && $form.sign_option == "global"}selected{/if}>{$PALANG.pAutoconfig_cert_global}</option>
</select></td>
<td>&nbsp;</td>
</tr>
<tr class="cert_files">
<td class="label"><label>{$PALANG.pAutoconfig_cert_filepath}:</label></td>
<td><input type="text" size="70" class="flat" name="cert_filepath" maxlength="1024" value="{$form.cert_filepath}" placeholder="/etc/letsencrypt/live/example.com/cert.pm" /></td>
<td>&nbsp;</td>
</tr>
<tr class="cert_files">
<td class="label"><label>{$PALANG.pAutoconfig_privkey_filepath}:</label></td>
<td><input type="text" size="70" class="flat" name="privkey_filepath" maxlength="1024" value="{$form.privkey_filepath}" placeholder="/etc/letsencrypt/live/example.com/privkey.pm" /></td>
<td>&nbsp;</td>
</tr>
<tr class="cert_files">
<td class="label"><label>{$PALANG.pAutoconfig_chain_filepath}:</label></td>
<td><input type="text" size="70" class="flat" name="chain_filepath" maxlength="1024" value="{$form.chain_filepath}" placeholder="/etc/letsencrypt/live/example.com/chain.pm" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input class="button" id="autoconfig_save" type="submit" name="fChange" value="{$PALANG.pEdit_autoconfig_set}" />
<input class="button" id="autoconfig_remove" type="submit" name="fBack" value="{$PALANG.pEdit_autoconfig_remove}" />
<input class="button" id="autoconfig_cancel" type="submit" name="fCancel" value="{$PALANG.exit}" />
</td>
</tr>
</table>
</form>
</div>

@ -0,0 +1,193 @@
<?php
/*
Created on 2020-03-10
Copyright 2020 Jacques Deguest
Distributed under the same licence as Postfix Admin
*/
$languages = array(
'ab' => "Abkhaz",
'aa' => "Afar",
'af' => "Afrikaans",
'ak' => "Akan",
'sq' => "Albanian",
'am' => "Amharic",
'ar' => "Arabic",
'an' => "Aragonese",
'hy' => "Armenian",
'as' => "Assamese",
'av' => "Avaric",
'ae' => "Avestan",
'ay' => "Aymara",
'az' => "Azerbaijani",
'bm' => "Bambara",
'ba' => "Bashkir",
'eu' => "Basque",
'be' => "Belarusian",
'bn' => "Bengali, Bangla",
'bh' => "Bihari",
'bi' => "Bislama",
'bs' => "Bosnian",
'br' => "Breton",
'bg' => "Bulgarian",
'my' => "Burmese",
'ca' => "Catalan",
'ch' => "Chamorro",
'ce' => "Chechen",
'ny' => "Chichewa, Chewa, Nyanja",
'zh' => "Chinese",
'cv' => "Chuvash",
'kw' => "Cornish",
'co' => "Corsican",
'cr' => "Cree",
'hr' => "Croatian",
'cs' => "Czech",
'da' => "Danish",
'dv' => "Divehi, Dhivehi, Maldivian",
'nl' => "Dutch",
'dz' => "Dzongkha",
'en' => "English",
'eo' => "Esperanto",
'et' => "Estonian",
'ee' => "Ewe",
'fo' => "Faroese",
'fj' => "Fijian",
'fi' => "Finnish",
'fr' => "French",
'ff' => "Fula, Fulah, Pulaar, Pular",
'gl' => "Galician",
'lg' => "Ganda",
'ka' => "Georgian",
'de' => "German",
'el' => "Greek (modern)",
'gn' => "Guaraní",
'gu' => "Gujarati",
'ht' => "Haitian, Haitian Creole",
'ha' => "Hausa",
'he' => "Hebrew (modern)",
'hz' => "Herero",
'hi' => "Hindi",
'ho' => "Hiri Motu",
'hu' => "Hungarian",
'is' => "Icelandic",
'io' => "Ido",
'ig' => "Igbo",
'id' => "Indonesian",
'ia' => "Interlingua",
'ie' => "Interlingue",
'iu' => "Inuktitut",
'ik' => "Inupiaq",
'ga' => "Irish",
'it' => "Italian",
'ja' => "Japanese",
'jv' => "Javanese",
'kl' => "Kalaallisut, Greenlandic",
'kn' => "Kannada",
'kr' => "Kanuri",
'ks' => "Kashmiri",
'kk' => "Kazakh",
'km' => "Khmer",
'ki' => "Kikuyu, Gikuyu",
'rw' => "Kinyarwanda",
'rn' => "Kirundi",
'kv' => "Komi",
'kg' => "Kongo",
'ko' => "Korean",
'ku' => "Kurdish",
'kj' => "Kwanyama, Kuanyama",
'ky' => "Kyrgyz",
'lo' => "Lao",
'la' => "Latin",
'lv' => "Latvian",
'li' => "Limburgish, Limburgan, Limburger",
'ln' => "Lingala",
'lt' => "Lithuanian",
'lu' => "Luba-Katanga",
'lb' => "Luxembourgish, Letzeburgesch",
'mk' => "Macedonian",
'mg' => "Malagasy",
'ms' => "Malay",
'ml' => "Malayalam",
'mt' => "Maltese",
'gv' => "Manx",
'mr' => "Marathi (Marāṭhī)",
'mh' => "Marshallese",
'mn' => "Mongolian",
'mi' => "Māori",
'na' => "Nauruan",
'nv' => "Navajo, Navaho",
'ng' => "Ndonga",
'ne' => "Nepali",
'nd' => "Northern Ndebele",
'se' => "Northern Sami",
'no' => "Norwegian",
'nb' => "Norwegian Bokmål",
'nn' => "Norwegian Nynorsk",
'ii' => "Nuosu",
'oc' => "Occitan",
'oj' => "Ojibwe, Ojibwa",
'cu' => "Old Church Slavonic, Church Slavonic, Old Bulgarian",
'or' => "Oriya",
'om' => "Oromo",
'os' => "Ossetian, Ossetic",
'pa' => "Panjabi, Punjabi",
'ps' => "Pashto, Pushto",
'fa' => "Persian (Farsi)",
'pl' => "Polish",
'pt' => "Portuguese",
'pi' => "Pāli",
'qu' => "Quechua",
'ro' => "Romanian",
'rm' => "Romansh",
'ru' => "Russian",
'sm' => "Samoan",
'sg' => "Sango",
'sa' => "Sanskrit (Saṁskṛta)",
'sc' => "Sardinian",
'gd' => "Scottish Gaelic, Gaelic",
'sr' => "Serbian",
'sn' => "Shona",
'sd' => "Sindhi",
'si' => "Sinhala, Sinhalese",
'sk' => "Slovak",
'sl' => "Slovene",
'so' => "Somali",
'nr' => "Southern Ndebele",
'st' => "Southern Sotho",
'es' => "Spanish",
'su' => "Sundanese",
'sw' => "Swahili",
'ss' => "Swati",
'sv' => "Swedish",
'tl' => "Tagalog",
'ty' => "Tahitian",
'tg' => "Tajik",
'ta' => "Tamil",
'tt' => "Tatar",
'te' => "Telugu",
'th' => "Thai",
'bo' => "Tibetan Standard, Tibetan, Central",
'ti' => "Tigrinya",
'to' => "Tonga (Tonga Islands)",
'ts' => "Tsonga",
'tn' => "Tswana",
'tr' => "Turkish",
'tk' => "Turkmen",
'tw' => "Twi",
'uk' => "Ukrainian",
'ur' => "Urdu",
'ug' => "Uyghur",
'uz' => "Uzbek",
've' => "Venda",
'vi' => "Vietnamese",
'vo' => "Volapük",
'wa' => "Walloon",
'cy' => "Welsh",
'fy' => "Western Frisian",
'wo' => "Wolof",
'xh' => "Xhosa",
'yi' => "Yiddish",
'yo' => "Yoruba",
'za' => "Zhuang, Chuang",
'zu' => "Zulu",
);
?>

@ -0,0 +1,212 @@
/**
* JavaScript printf/sprintf functions.
* http://hexmen.com/blog/2007/03/printf-sprintf/
*
* This code is unrestricted: you are free to use it however you like.
*
* The functions should work as expected, performing left or right alignment,
* truncating strings, outputting numbers with a required precision etc.
*
* For complex cases these functions follow the Perl implementations of
* (s)printf, allowing arguments to be passed out-of-order, and to set
* precision and output-length from other argument
*
* See http://perldoc.perl.org/functions/sprintf.html for more information.
*
* Implemented flags:
*
* - zero or space-padding (default: space)
* sprintf("%4d", 3) -> " 3"
* sprintf("%04d", 3) -> "0003"
*
* - left and right-alignment (default: right)
* sprintf("%3s", "a") -> " a"
* sprintf("%-3s", "b") -> "b "
*
* - out of order arguments (good for templates & message formats)
* sprintf("Estimate: %2$d units total: %1$.2f total", total, quantity)
*
* - binary, octal and hex prefixes (default: none)
* sprintf("%b", 13) -> "1101"
* sprintf("%#b", 13) -> "0b1101"
* sprintf("%#06x", 13) -> "0x000d"
*
* - positive number prefix (default: none)
* sprintf("%d", 3) -> "3"
* sprintf("%+d", 3) -> "+3"
* sprintf("% d", 3) -> " 3"
*
* - min/max width (with truncation); e.g. "%9.3s" and "%-9.3s"
* sprintf("%5s", "catfish") -> "catfish"
* sprintf("%.5s", "catfish") -> "catfi"
* sprintf("%5.3s", "catfish") -> " cat"
* sprintf("%-5.3s", "catfish") -> "cat "
*
* - precision (see note below); e.g. "%.2f"
* sprintf("%.3f", 2.1) -> "2.100"
* sprintf("%.3e", 2.1) -> "2.100e+0"
* sprintf("%.3g", 2.1) -> "2.10"
* sprintf("%.3p", 2.1) -> "2.1"
* sprintf("%.3p", '2.100') -> "2.10"
*
* Deviations from perl spec:
* - %n suppresses an argument
* - %p and %P act like %g, but without over-claiming accuracy:
* Compare:
* sprintf("%.3g", "2.1") -> "2.10"
* sprintf("%.3p", "2.1") -> "2.1"
*
* @version 2011.09.23
* @author Ash Searle
*/
function sprintf() {
function pad(str, len, chr, leftJustify) {
var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
return leftJustify ? str + padding : padding + str;
}
function justify(value, prefix, leftJustify, minWidth, zeroPad) {
var diff = minWidth - value.length;
if (diff > 0) {
if (leftJustify || !zeroPad) {
value = pad(value, minWidth, ' ', leftJustify);
} else {
value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
}
}
return value;
}
var a = arguments, i = 0, format = a[i++];
return format.replace(sprintf.regex, function(substring, valueIndex, flags, minWidth, _, precision, type) {
if (substring == '%%') return '%';
// parse flags
var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false;
for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) {
case ' ': positivePrefix = ' '; break;
case '+': positivePrefix = '+'; break;
case '-': leftJustify = true; break;
case '0': zeroPad = true; break;
case '#': prefixBaseX = true; break;
}
// parameters may be null, undefined, empty-string or real valued
// we want to ignore null, undefined and empty-string values
if (!minWidth) {
minWidth = 0;
} else if (minWidth == '*') {
minWidth = +a[i++];
} else if (minWidth.charAt(0) == '*') {
minWidth = +a[minWidth.slice(1, -1)];
} else {
minWidth = +minWidth;
}
// Note: undocumented perl feature:
if (minWidth < 0) {
minWidth = -minWidth;
leftJustify = true;
}
if (!isFinite(minWidth)) {
throw new Error('sprintf: (minimum-)width must be finite');
}
if (precision && precision.charAt(0) == '*') {
precision = +a[(precision == '*') ? i++ : precision.slice(1, -1)];
if (precision < 0) {
precision = null;
}
}
if (precision == null) {
precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
} else {
precision = +precision;
}
// grab value using valueIndex if required?
var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];
var prefix, base;
switch (type) {
case 'c': value = String.fromCharCode(+value);
case 's': {
// If you'd rather treat nulls as empty-strings, uncomment next line:
// if (value == null) return '';
value = String(value);
if (precision != null) {
value = value.slice(0, precision);
}
prefix = '';
break;
}
case 'b': base = 2; break;
case 'o': base = 8; break;
case 'u': base = 10; break;
case 'x': case 'X': base = 16; break;
case 'i':
case 'd': {
var number = parseInt(+value);
if (isNaN(number)) {
return '';
}
prefix = number < 0 ? '-' : positivePrefix;
value = prefix + pad(String(Math.abs(number)), precision, '0', false);
break;
}
case 'e': case 'E':
case 'f': case 'F':
case 'g': case 'G':
case 'p': case 'P':
{
var number = +value;
if (isNaN(number)) {
return '';
}
prefix = number < 0 ? '-' : positivePrefix;
var method;
if ('p' != type.toLowerCase()) {
method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
} else {
// Count significant-figures, taking special-care of zeroes ('0' vs '0.00' etc.)
var sf = String(value).replace(/[eE].*|[^\d]/g, '');
sf = (number ? sf.replace(/^0+/,'') : sf).length;
precision = precision ? Math.min(precision, sf) : precision;
method = (!precision || precision <= sf) ? 'toPrecision' : 'toExponential';
}
var number_str = Math.abs(number)[method](precision);
// number_str = thousandSeparation ? thousand_separate(number_str): number_str;
value = prefix + number_str;
break;
}
case 'n': return '';
default: return substring;
}
if (base) {
// cast to non-negative integer:
var number = value >>> 0;
prefix = prefixBaseX && base != 10 && number && ['0b', '0', '0x'][base >> 3] || '';
value = prefix + pad(number.toString(base), precision || 0, '0', false);
}
var justified = justify(value, prefix, leftJustify, minWidth, zeroPad);
return ('EFGPX'.indexOf(type) > -1) ? justified.toUpperCase() : justified;
});
}
sprintf.regex = /%%|%(\d+\$)?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegpEGP])/g;
/**
* Trival printf implementation, probably only useful during page-load.
* Note: you may as well use "document.write(sprintf(....))" directly
*/
function printf() {
// delegate the work to sprintf in an IE5 friendly manner:
var i = 0, a = arguments, args = Array(arguments.length);
while (i < args.length) args[i] = 'a[' + (i++) + ']';
document.write(eval('sprintf(' + args + ')'));
}

@ -692,6 +692,17 @@ $CONF['xmlrpc_enabled'] = false;
//More details in README.password_expiration
$CONF['password_expiration'] = 'YES';
// Autodiscovery configuration (autoconfig)
// You can leave it blank to not sign the Mac Mail mobileconfig system setting, or
// You can provide default values here and override them with each configuration specific values
// You can set this to true or false to simply activate or deactivate signing the mobileconfig file
// This will apply to all, including per configuration settings
$CONF['autoconfig'] = 'YES';
$CONF['autoconfig_sign'] = true;
$CONF['autoconfig_cert'] = null;
$CONF['autoconfig_privkey'] = null;
$CONF['autoconfig_chain'] = null;
// If you want to keep most settings at default values and/or want to ensure
// that future updates work without problems, you can use a separate config
// file (config.local.php) instead of editing this file and override some

@ -2027,6 +2027,21 @@ function db_where_clause(array $condition, array $struct, $additional_raw_where
return $query;
}
function db_begin()
{
return( db_query( "BEGIN" ) );
}
function db_commit()
{
return( db_query( "COMMIT" ) );
}
function db_rollback()
{
return( db_query( "ROLLBACK" ) );
}
/**
* Convert a programmatic db table name into what may be the actual name.
*

@ -412,6 +412,129 @@ $PALANG['pFetchmail_desc_mda'] = 'Mail Delivery Agent';
$PALANG['pFetchmail_desc_date'] = 'Date of last polling/configuration change';
$PALANG['pFetchmail_desc_returned_text'] = 'Text message from last polling';
// Autoconfig
$PALANG['pAutoconfig_page_title'] = 'Mail Autodiscovery Configuration';
$PALANG['pEdit_autoconfig_set'] = 'Save Configuration';
$PALANG['pEdit_autoconfig_remove'] = 'Remove Configuration';
$PALANG['pAutoconfig_encoding'] = 'Encoding';
$PALANG['pAutoconfig_provider_id'] = 'Provider ID';
$PALANG['pAutoconfig_provider_domain'] = 'Domain names';
$PALANG['pAutoconfig_provider_name'] = 'Provider name';
$PALANG['pAutoconfig_provider_short'] = 'Provider short name';
$PALANG['pAutoconfig_incoming_server'] = 'Incoming servers';
$PALANG['pAutoconfig_outgoing_server'] = 'Outgoing servers';
$PALANG['pAutoconfig_type'] = 'Type';
$PALANG['pAutoconfig_hostname'] = 'Host name';
$PALANG['pAutoconfig_port'] = 'Port';
$PALANG['pAutoconfig_socket_type'] = 'Socket type';
$PALANG['pAutoconfig_auth'] = 'Authentication scheme';
$PALANG['pAutoconfig_username'] = 'Username';
$PALANG['pAutoconfig_leave_messages_on_server']= 'Leave message on server?';
$PALANG['pAutoconfig_download_on_biff'] = 'Download on biff?';
$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'Number of days to leave messages on server';
$PALANG['pAutoconfig_check_interval'] = 'Mail check interval in minute';
$PALANG['pAutoconfig_enable'] = 'Login Enable Instruction';
$PALANG['pAutoconfig_enable_url'] = 'URL to enable login';
$PALANG['pAutoconfig_enable_instruction'] = 'Enabling instruction';
$PALANG['pAutoconfig_documentation'] = 'Support documentation';
$PALANG['pAutoconfig_documentation_url'] = 'Support documentation url';
$PALANG['pAutoconfig_documentation_desc'] = 'Support documentation description';
$PALANG['pAutoconfig_webmail_login_page'] = 'Webmail login page';
$PALANG['pAutoconfig_webmail_login_page_info']= 'Webmail information page';
$PALANG['pAutoconfig_lp_info_url'] = 'Login page info url';
$PALANG['pAutoconfig_lp_info_username'] = 'Login page info username';
$PALANG['pAutoconfig_lp_info_username_field_id']= 'Login page info username field id';
$PALANG['pAutoconfig_lp_info_username_field_name']= 'Login page info username field name';
$PALANG['pAutoconfig_lp_info_login_button'] = 'Login page info button';
$PALANG['pAutoconfig_lp_info_login_button_id']= 'Login page info button id';
$PALANG['pAutoconfig_lp_info_login_button_name']= 'Login page info button name';
$PALANG['pAutoconfig_mac_specific_settings']= 'Mac specific settings';
$PALANG['pAutoconfig_account_name'] = 'Account name (you can leave it blank)';
$PALANG['pAutoconfig_account_type'] = 'Account type';
$PALANG['pAutoconfig_email'] = 'e-mail address (you can leave it blank)';
$PALANG['pAutoconfig_ssl'] = 'Use of SSL?';
$PALANG['pAutoconfig_password'] = 'Password (you can leave it blank)';
$PALANG['pAutoconfig_description'] = 'Description';
$PALANG['pAutoconfig_organisation'] = 'Organisation';
$PALANG['pAutoconfig_payload_type'] = 'Payload type';
$PALANG['pAutoconfig_prevent_app_sheet'] = 'Prevent app sheet';
$PALANG['pAutoconfig_prevent_move'] = 'Prevent move';
$PALANG['pAutoconfig_smime_enabled'] = 'S/MIME enabled?';
$PALANG['pAutoconfig_payload_remove_ok'] = 'Payload removal allowed?';
$PALANG['pAutoconfig_spa'] = 'Microsoft SPA (Secure Password Authentication)';
$PALANG['pAutoconfig_active'] = 'Status';
// Error messages
$PALANG['pAutoconfig_invalid_encoding'] = 'Invalid encoding';
$PALANG['pAutoconfig_empty_provider_id'] = 'No provider id provided';
$PALANG['pAutoconfig_host_no_type_provided']= 'No type provided';
$PALANG['pAutoconfig_host_invalid_type_value'] = 'Invalid type value';
$PALANG['pAutoconfig_host_no_hostname_provided'] = 'No host name provided';
$PALANG['pAutoconfig_host_no_port_provided']= 'No port provided';
$PALANG['pAutoconfig_host_port_is_not_an_integer'] = 'Port provided is not an integer';
// sprintf
$PALANG['pAutoconfig_host_invalid_socket_type'] = 'Invalid connection type \"%s\"';
// sprintf
$PALANG['pAutoconfig_host_invalid_auth_scheme'] = 'Invalid authentication scheme \"%s\"';
$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'Value provided for days to remain on server is not an integer';
$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = 'Check interval provided is not an integer';
$PALANG['pAutoconfig_duplicate_host'] = 'You have already defined a host "%1$s" of type %2$s and with port number %3$d';
$PALANG['pAutoconfig_text_type_not_provided'] = 'No type provided for this text';
// sprintf
$PALANG['pAutoconfig_text_type_invalid'] = 'Text type is invalid';
$PALANG['pAutoconfig_text_lang_not_provided'] = 'No language is provided';
// sprintf
$PALANG['pAutoconfig_text_lang_invalid'] = 'Language code provided is invalid';
$PALANG['pAutoconfig_text_text_not_provided'] = 'No text was provided';
$PALANG['pAutoconfig_no_config_found'] = 'No configuration found';
$PALANG['pAutoconfig_save_no_data_provided']= 'No data was provided to save';
$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'You lack permission to modify this configuration with id "%s"';
$PALANG['pAutoconfig_text_id_is_not_an_integer'] = 'The text id provided "%s" is not an integer';
$PALANG['pAutoconfig_config_id_not_found'] = 'No configuration could be found with id "%s"';
$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = 'Configuration id submitted "%s" is not the one to be used for this configuration';
$PALANG['pAutoconfig_no_config_id_declared']= 'No configuration id was declared';
$PALANG['pAutoconfig_no_config_id_provded']= 'No configuration id was provided';
$PALANG['pAutoconfig_unauthorised_domains'] = 'The following domain names are not authorised to the admin: %s';
$PALANG['pAutoconfig_data_provided_is_not_array'] = 'Data submitted is not an array';
$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'No domain name have been selected. If you want to remove this configuration, simply click on "Remove"';
$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'Domain names data provided is not an array';
$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'No domain name have been authorised for this admin';
$PALANG['pAutoconfig_host_id_is_not_an_integer'] = 'Host id provided "%s" is not an integer';
$PALANG['pAutoconfig_text_language_already_used'] = 'You have already selected the language "%s". Please choose a different one.';
$PALANG['pAutoconfig_no_config_yet_to_remove'] = 'No configuration id yet provided to remove';
$PALANG['pAutoconfig_config_removed'] = 'Configuration has been removed';
$PALANG['pAutoconfig_config_saved'] = 'Configuration has been saved';
$PALANG['pAutoconfig_failed_to_add_config'] = 'Failed to add configuration';
$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = 'Failed to add domain name "%s" to configuration.';
$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'Failed to add host configuration for host "%s"';
$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'Failed to add the text that starts with: "%s"';
$PALANG['pAutoconfig_placeholder_provider_name'] = 'Your Organisation, Inc';
$PALANG['pAutoconfig_password_cleartext'] = 'Plain password';
$PALANG['pAutoconfig_password_encrypted'] = 'MD5 encrypted password';
$PALANG['pAutoconfig_client_ip_address'] = 'Client IP address';
$PALANG['pAutoconfig_tls_client_cert'] = 'TLS client certificate';
$PALANG['pAutoconfig_smtp_after_pop'] = 'SMTP after POP3';
$PALANG['pAutoconfig_copy_provider_name'] = 'Copy provider name';
$PALANG['pAutoconfig_toggle_select_all'] = 'Toggle select all';
$PALANG['pAutoconfig_username_template'] = 'Use template:';
$PALANG['pAutoconfig_no_selection'] = 'None';
$PALANG['pAutoconfig_jump_to'] = 'Jump to configuration:';
$PALANG['pAutoconfig_new_configuration'] = 'New configuration';
$PALANG['pAutoconfig_add_new_host'] = 'Add new host';
$PALANG['pAutoconfig_remove_host'] = 'Remove this host';
$PALANG['pAutoconfig_move_up_host'] = 'Move this host up in priority';
$PALANG['pAutoconfig_move_down_host'] = 'Move this host down in priority';
$PALANG['pAutoconfig_add_new_text'] = 'Add new text';
$PALANG['pAutoconfig_remove_text'] = 'Remove text';
$PALANG['pAutoconfig_not_supported'] = 'Not currently supported by Thunderbird';
$PALANG['pAutoconfig_cert_sign'] = 'Mac mobile config signature';
$PALANG['pAutoconfig_cert_option'] = 'How to sign';
$PALANG['pAutoconfig_cert_none'] = 'No signature';
$PALANG['pAutoconfig_cert_local'] = 'Configuration certificate';
$PALANG['pAutoconfig_cert_global'] = 'Global certificate';
$PALANG['pAutoconfig_on'] = 'On';
$PALANG['pAutoconfig_off'] = 'Off';
$PALANG['pAutoconfig_server_side_error'] = 'A server side error has occured, please check the web server log for details. %s';
$PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your local date format, but make sure it's a valid PostgreSQL date format
$PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format
$PALANG['password_expiration'] = 'Pass expires';

@ -407,6 +407,131 @@ $PALANG['dateformat_mysql'] = '%d-%m-%Y';
$PALANG['password_expiration'] = 'Expiration du mot de passe';
$PALANG['password_expiration_desc'] = 'Date when password will expire'; # XXX
// Autoconfig
$PALANG['pAutoconfig_page_title'] = 'Configuration de l\'auto découverte';
$PALANG['pEdit_autoconfig_set'] = 'Enregistrer la configuration';
$PALANG['pEdit_autoconfig_remove'] = 'Supprimer la configuration';
$PALANG['pAutoconfig_encoding'] = 'Encodage';
$PALANG['pAutoconfig_provider_id'] = 'Identifiant du fournisseur';
$PALANG['pAutoconfig_provider_domain'] = 'Noms de domaine';
$PALANG['pAutoconfig_provider_name'] = 'Nom du fournisseur';
$PALANG['pAutoconfig_provider_short'] = 'Nom court du fournisseur';
$PALANG['pAutoconfig_incoming_server'] = 'Serveurs entrants';
$PALANG['pAutoconfig_outgoing_server'] = 'Serveurs sortants';
$PALANG['pAutoconfig_type'] = 'Type';
$PALANG['pAutoconfig_hostname'] = 'No d\'hôte';
$PALANG['pAutoconfig_port'] = 'Port';
$PALANG['pAutoconfig_socket_type'] = 'Type de socket';
$PALANG['pAutoconfig_auth'] = 'Méthode d\'autentification';
$PALANG['pAutoconfig_username'] = 'Nom d\'utilisateur';
$PALANG['pAutoconfig_leave_messages_on_server']= 'Laisser les message sur le serveur ?';
$PALANG['pAutoconfig_download_on_biff'] = 'Télécharger sur biff ?';
$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'Nom de jours des messages sur le serveur';
$PALANG['pAutoconfig_check_interval'] = 'Interval de relevée de courriels en minute';
$PALANG['pAutoconfig_enable'] = 'Instruction d\'activation du login';
$PALANG['pAutoconfig_enable_url'] = 'Adresse URL pour activer le login';
$PALANG['pAutoconfig_enable_instruction'] = 'Tnstruction d\'activation';
$PALANG['pAutoconfig_documentation'] = 'Documentation de support';
$PALANG['pAutoconfig_documentation_url'] = 'Adresse url de la documentation support';
$PALANG['pAutoconfig_documentation_desc'] = 'Description de la documentation support';
$PALANG['pAutoconfig_webmail_login_page'] = 'Page de login du Webmail';
$PALANG['pAutoconfig_webmail_login_page_info']= 'Page d\'information du Webmail';
$PALANG['pAutoconfig_lp_info_url'] = 'Adresse url de la page d\'information login';
$PALANG['pAutoconfig_lp_info_username'] = 'Nom d\'utilisateur de la page info login';
$PALANG['pAutoconfig_lp_info_username_field_id']= 'ID du champs login de la page info';
$PALANG['pAutoconfig_lp_info_username_field_name']= 'Nom du champs login de la page info';
$PALANG['pAutoconfig_lp_info_login_button'] = 'Bouton de la page info login';
$PALANG['pAutoconfig_lp_info_login_button_id']= 'ID du bouton de la page info login';
$PALANG['pAutoconfig_lp_info_login_button_name']= 'Nom du bouton de la page info login';
$PALANG['pAutoconfig_mac_specific_settings']= 'Réglages spécifiques au Mac';
$PALANG['pAutoconfig_account_name'] = 'Nom du compte (vous pouvez le laisser vide)';
$PALANG['pAutoconfig_account_type'] = 'Type de compte';
$PALANG['pAutoconfig_email'] = 'Adresse e-mail (vous pouvez le laisser vide)';
$PALANG['pAutoconfig_ssl'] = 'Utilisation de la SSL ?';
$PALANG['pAutoconfig_password'] = 'Mot de passe (vous pouvez le laisser vide)';
$PALANG['pAutoconfig_description'] = 'Description';
$PALANG['pAutoconfig_organisation'] = 'Organisation';
$PALANG['pAutoconfig_payload_type'] = 'Type de payload';
// https://stackoverflow.com/questions/19385776/preventappsheet-use-only-in-mail-ios-mail-payload-configuration
// https://developer.apple.com/documentation/devicemanagement/mail
$PALANG['pAutoconfig_prevent_app_sheet'] = 'Empêcher app sheet';
$PALANG['pAutoconfig_prevent_move'] = 'EMpêcher le déplacement';
$PALANG['pAutoconfig_smime_enabled'] = 'S/MIME activé ?';
$PALANG['pAutoconfig_payload_remove_ok'] = 'Autoriser la suppresion du payload ?';
$PALANG['pAutoconfig_spa'] = 'Microsoft SPA (Secure Password Authentication)';
$PALANG['pAutoconfig_active'] = 'Etat';
// Error messages
$PALANG['pAutoconfig_invalid_encoding'] = 'Encodage invalide';
$PALANG['pAutoconfig_empty_provider_id'] = 'Aucun idenitifant de fournisseurn\'a été fourni';
$PALANG['pAutoconfig_host_no_type_provided']= 'Aucun type spécifié';
$PALANG['pAutoconfig_host_invalid_type_value'] = 'Incorrecte valeur de type';
$PALANG['pAutoconfig_host_no_hostname_provided'] = 'Aucun hôte n\'a été fourni';
$PALANG['pAutoconfig_host_no_port_provided']= 'Aucun port n\'a été spécifié';
$PALANG['pAutoconfig_host_port_is_not_an_integer'] = 'Le port fourni n\'est pas un entier';
// sprintf
$PALANG['pAutoconfig_host_invalid_socket_type'] = 'Type de connection \"%s\" invalide';
// sprintf
$PALANG['pAutoconfig_host_invalid_auth_scheme'] = 'Méthode d\'autentification \"%s\" invaide';
$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'La valeur fournie pour le nombre de jours à rester sur le serveur n\'est pas un entier';
$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = 'L\'intervale fourni n\'est pas un entier';
$PALANG['pAutoconfig_duplicate_host'] = 'Vou avez déjà défini un hôte "%1$s" de type %2$s et avec le port numéro %3$d';
$PALANG['pAutoconfig_text_type_not_provided'] = 'Aucun type n\'a été fourni pour ce texte';
// sprintf
$PALANG['pAutoconfig_text_type_invalid'] = 'Type de texte est invalide';
$PALANG['pAutoconfig_text_lang_not_provided'] = 'AUcune langue n\'a été spécifiée';
// sprintf
$PALANG['pAutoconfig_text_lang_invalid'] = 'Le code langue fourni est invalide';
$PALANG['pAutoconfig_text_text_not_provided'] = 'Aucun text n\'a été fourni';
$PALANG['pAutoconfig_no_config_found'] = 'Aucune configuration n\'a été trouvée';
$PALANG['pAutoconfig_save_no_data_provided']= 'Aucune donnée à enregistrer n\'a été fournie';
$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'Vous n\'avez pas les permissions de modifier cette configuration avec L\'id "%s"';
$PALANG['pAutoconfig_text_id_is_not_an_integer'] = 'L\'id fourni du texte "%s" n\'est pas un entier';
$PALANG['pAutoconfig_config_id_not_found'] = 'Aucune configuration n\'a pu être trouvée avec l\'id "%s"';
$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = 'L\'id de la configuration soumis "%s" n\'est pas celui de cette configuration';
$PALANG['pAutoconfig_no_config_id_declared']= 'Aucun id de configuration n\'a été déclaré';
$PALANG['pAutoconfig_no_config_id_provded']= 'Aucun id de configuration id n\'a été fourni';
$PALANG['pAutoconfig_unauthorised_domains'] = 'Les noms de domaine suivants ne sont pas autorisés pour cet admin : %s';
$PALANG['pAutoconfig_data_provided_is_not_array'] = 'Les données soumises ne sont pas un array';
$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'Aucun nom de domaine n\'a été selectionné. Si vous voulez supprimer cette configuration, cliquez simplement sur "Supprimer"';
$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'Les données des noms de domaine fournis n\'est pas un array';
$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'Aucun nom de domaine n\'a été autorisé pour cet admin';
$PALANG['pAutoconfig_host_id_is_not_an_integer'] = 'L\'id fourni de cet hôte "%s" n\'est pas un entier';
$PALANG['pAutoconfig_text_language_already_used'] = 'Vous avez selectionné la langue "%s". Veuillez en choisir un autre.';
$PALANG['pAutoconfig_no_config_yet_to_remove'] = 'Aucune id de configuration à supprimer n\' été fournie';
$PALANG['pAutoconfig_config_removed'] = 'La configuration a été supprimée';
$PALANG['pAutoconfig_config_saved'] = 'La configuration a été enregistrée';
$PALANG['pAutoconfig_failed_to_add_config'] = 'Echec d\'ajout de la configuration';
$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = 'Echec d\'ajout du nom de domaine "%s" à cette configuration.';
$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'Echec de l\'ajout de la configuration pour l\'hôte "%s"';
$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'Echec d\'ajout du texte qui commence par : "%s"';
$PALANG['pAutoconfig_placeholder_provider_name'] = 'Votre organisation S.A.';
$PALANG['pAutoconfig_password_cleartext'] = 'Mot de passe simple';
$PALANG['pAutoconfig_password_encrypted'] = 'Mot de passe MD5 encrypté';
$PALANG['pAutoconfig_client_ip_address'] = 'Adresse IP client';
$PALANG['pAutoconfig_tls_client_cert'] = 'Certificat client TLS';
$PALANG['pAutoconfig_smtp_after_pop'] = 'SMTP après POP3';
$PALANG['pAutoconfig_copy_provider_name'] = 'Copier le nom du fournisseur';
$PALANG['pAutoconfig_toggle_select_all'] = 'Tout selectionner';
$PALANG['pAutoconfig_username_template'] = 'Utiliser le modèle:';
$PALANG['pAutoconfig_no_selection'] = 'Aucun';
$PALANG['pAutoconfig_jump_to'] = 'Suter à la configuration:';
$PALANG['pAutoconfig_new_configuration'] = 'Nouvelle configuration';
$PALANG['pAutoconfig_add_new_host'] = 'Ajouter un nouvel hôte';
$PALANG['pAutoconfig_remove_host'] = 'Supprimer cet hôte';
$PALANG['pAutoconfig_move_up_host'] = 'Augmenter la priorité de cet hôte';
$PALANG['pAutoconfig_move_down_host'] = 'Réduire la priorité de cet hôte';
$PALANG['pAutoconfig_add_new_text'] = 'Ajouter un nouveau texte';
$PALANG['pAutoconfig_remove_text'] = 'Supprimer le texte';
$PALANG['pAutoconfig_not_supported'] = 'Pas reconnu pour l\'instant par Thunderbird';
$PALANG['pAutoconfig_cert_sign'] = 'Signature de la config mobile Mac';
$PALANG['pAutoconfig_cert_option'] = 'Comment signer';
$PALANG['pAutoconfig_cert_none'] = 'Pas de signature';
$PALANG['pAutoconfig_cert_local'] = 'Certificat de la configuration';
$PALANG['pAutoconfig_cert_global'] = 'Certificat global';
$PALANG['pAutoconfig_on'] = 'On';
$PALANG['pAutoconfig_off'] = 'Off';
$PALANG['pAutoconfig_server_side_error'] = 'Un erreur serveur s\'est produite. Veuillez voir les logs du serveur web pour plus d\'information. %s';
$PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh
/* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */
?>

@ -412,6 +412,128 @@ $PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your loc
$PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format
$PALANG['password_expiration'] = 'Pass expires'; # XXX
$PALANG['password_expiration_desc'] = 'Date when password will expire'; # XXX
// Autoconfig
$PALANG['pAutoconfig_page_title'] = 'メールオトディスカバリーの設定';
$PALANG['pEdit_autoconfig_set'] = '設定を保存';
$PALANG['pEdit_autoconfig_remove'] = '設定を削除';
$PALANG['pAutoconfig_encoding'] = 'エンコディング';
$PALANG['pAutoconfig_provider_id'] = 'プロバイダID';
$PALANG['pAutoconfig_provider_domain'] = 'ドメイン名';
$PALANG['pAutoconfig_provider_name'] = 'プロバイダ名';
$PALANG['pAutoconfig_provider_short'] = 'プロバイダ省略';
$PALANG['pAutoconfig_incoming_server'] = '受信サーバー';
$PALANG['pAutoconfig_outgoing_server'] = '送信サーバー';
$PALANG['pAutoconfig_type'] = 'タイプ';
$PALANG['pAutoconfig_hostname'] = 'ホスト名';
$PALANG['pAutoconfig_port'] = 'ポート';
$PALANG['pAutoconfig_socket_type'] = 'ソケットタイプ';
$PALANG['pAutoconfig_auth'] = '認証方法';
$PALANG['pAutoconfig_username'] = 'ユーザー名';
$PALANG['pAutoconfig_leave_messages_on_server']= 'メッセージをサーバーで残す';
$PALANG['pAutoconfig_download_on_biff'] = 'ビフでダウンロード?';
$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'サーバーで残す日間';
$PALANG['pAutoconfig_check_interval'] = 'メール確認の頻度(分)';
$PALANG['pAutoconfig_enable'] = 'ログインを有効化の説明';
$PALANG['pAutoconfig_enable_url'] = 'ログイン有効化のURL';
$PALANG['pAutoconfig_enable_instruction'] = '有効化の説明';
$PALANG['pAutoconfig_documentation'] = 'サポート情報';
$PALANG['pAutoconfig_documentation_url'] = 'サポート情報URL';
$PALANG['pAutoconfig_documentation_desc'] = 'サポート情報の記述';
$PALANG['pAutoconfig_webmail_login_page'] = 'Webmailログインページ';
$PALANG['pAutoconfig_webmail_login_page_info']= 'Webmail情報ページ';
$PALANG['pAutoconfig_lp_info_url'] = 'ログイン情報ページのログイン情報ページのURL';
$PALANG['pAutoconfig_lp_info_username'] = 'ログイン情報ページのユーザー名';
$PALANG['pAutoconfig_lp_info_username_field_id']= 'ログイン情報ページのユーザー名のフィルドID';
$PALANG['pAutoconfig_lp_info_username_field_name']= 'ログイン情報ページのユーザー名のフィルド名';
$PALANG['pAutoconfig_lp_info_login_button'] = 'ログイン情報ページのユーザー名';
$PALANG['pAutoconfig_lp_info_login_button_id']= 'ログイン情報ページのボタンID';
$PALANG['pAutoconfig_lp_info_login_button_name']= 'ログイン情報ページのボタン名';
$PALANG['pAutoconfig_mac_specific_settings']= 'Mac専用設定';
$PALANG['pAutoconfig_account_name'] = 'アカウント名(空欄可能)';
$PALANG['pAutoconfig_account_type'] = 'アカウントタイプ';
$PALANG['pAutoconfig_email'] = '電子メールアドレス(空欄可能)';
$PALANG['pAutoconfig_ssl'] = 'SSLを利用するか?';
$PALANG['pAutoconfig_password'] = 'パスワード(空欄可能)';
$PALANG['pAutoconfig_description'] = '記述';
$PALANG['pAutoconfig_organisation'] = '法人';
$PALANG['pAutoconfig_payload_type'] = 'ペイロードタイプ';
$PALANG['pAutoconfig_prevent_app_sheet'] = 'アップシートを防ぐ';
$PALANG['pAutoconfig_prevent_move'] = '移動を防ぐ';
$PALANG['pAutoconfig_smime_enabled'] = 'S/MIMEは有効ですか';
$PALANG['pAutoconfig_payload_remove_ok'] = 'ペイロード削除は可能?';
$PALANG['pAutoconfig_spa'] = 'マイクロソフトSPASecure Password Authentication';
$PALANG['pAutoconfig_active'] = 'ステータス';
// Error messages
$PALANG['pAutoconfig_invalid_encoding'] = '無効なエンコディング';
$PALANG['pAutoconfig_empty_provider_id'] = 'プロバイダIDは指定おらず';
$PALANG['pAutoconfig_host_no_type_provided']= 'タイプは指定おらず';
$PALANG['pAutoconfig_host_invalid_type_value'] = '無効タイプの価値';
$PALANG['pAutoconfig_host_no_hostname_provided'] = 'ホスト名は指定おらず';
$PALANG['pAutoconfig_host_no_port_provided']= 'ポートは未設定';
$PALANG['pAutoconfig_host_port_is_not_an_integer'] = '指定したポートは整数ではあります。';
// sprintf
$PALANG['pAutoconfig_host_invalid_socket_type'] = '無効な接続タイプ「%s」';
// sprintf
$PALANG['pAutoconfig_host_invalid_auth_scheme'] = '無効な認証方法「%s」';
$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'サーバーでメッセージを残す日間の価値は整数ではありません。';
$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = '確認頻度の価値は整数ではありません。';
$PALANG['pAutoconfig_duplicate_host'] = 'すでにポート「%3$d」のある%2$sタイプのホスト「%1$s」を定義しました。';
$PALANG['pAutoconfig_text_type_not_provided'] = 'このテキストのためのタイプは指定おらず';
// sprintf
$PALANG['pAutoconfig_text_type_invalid'] = '無効なテキストタイプ';
$PALANG['pAutoconfig_text_lang_not_provided'] = '言語は指定おらず';
// sprintf
$PALANG['pAutoconfig_text_lang_invalid'] = '指定した言語コードは無効です。';
$PALANG['pAutoconfig_text_text_not_provided'] = 'テキストは入力されていません。';
$PALANG['pAutoconfig_no_config_found'] = '設定を見つかりません。';
$PALANG['pAutoconfig_save_no_data_provided']= '保存データはありません。';
$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'ID「%s」のある設定を変更する権限は足りません。';
$PALANG['pAutoconfig_text_id_is_not_an_integer'] = '指定したテキストID「$s」は整数ではありません。';
$PALANG['pAutoconfig_config_id_not_found'] = 'ID「%s」のある設定は見つかりません。';
$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = '送信した設定ID「%s」とこの設定IDと異なっています。';
$PALANG['pAutoconfig_no_config_id_declared']= '設定IDは指定されていません。';
$PALANG['pAutoconfig_no_config_id_provded']= '設定IDは指定されていません。';
$PALANG['pAutoconfig_unauthorised_domains'] = 'この管理者はこのドメイン名へのアクセス許可はありません:%s';
$PALANG['pAutoconfig_data_provided_is_not_array'] = '送信したデータはarrayではありません。';
$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'ドメイン名を選定していません。もしこの設定の削除をご希望であれば単純に「削除」ボタンをクリックください。';
$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'ドメイン名のデータはarrayではありません。';
$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'この管理者は一つのドメイン名でもアクセスがありません。';
$PALANG['pAutoconfig_host_id_is_not_an_integer'] = '指定のホストID「%s」は整数ではありません。';
$PALANG['pAutoconfig_text_language_already_used'] = 'すでに、「言語」を選定しました。他の言語を選んでください。';
$PALANG['pAutoconfig_no_config_yet_to_remove'] = '削除すべきの設定のIDは送信されませんでした。';
$PALANG['pAutoconfig_config_removed'] = '設定は削除されました。';
$PALANG['pAutoconfig_config_saved'] = '設定は保存されました。';
$PALANG['pAutoconfig_failed_to_add_config'] = '設定の追加を失敗しました。';
$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = '設定にドメイン名「%s」の追加を失敗しました。';
$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'ホスト「%s」の設定を追加できませんでした。';
$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'この部分で始まるテキストを追加できませんでした。';
$PALANG['pAutoconfig_placeholder_provider_name'] = '太郎法人株式会社';
$PALANG['pAutoconfig_password_cleartext'] = '未暗号化パスワード';
$PALANG['pAutoconfig_password_encrypted'] = 'MD5で暗号化パスワード';
$PALANG['pAutoconfig_client_ip_address'] = 'クライエントIPアドレス';
$PALANG['pAutoconfig_tls_client_cert'] = 'TLSクライエント証明書';
$PALANG['pAutoconfig_smtp_after_pop'] = 'POP3後のSMTP';
$PALANG['pAutoconfig_copy_provider_name'] = 'プロバイダ名をコピー';
$PALANG['pAutoconfig_toggle_select_all'] = '全て選択を切り替える';
$PALANG['pAutoconfig_username_template'] = 'テンプレートを適用';
$PALANG['pAutoconfig_no_selection'] = 'なし';
$PALANG['pAutoconfig_jump_to'] = '設定へジャンプ:';
$PALANG['pAutoconfig_new_configuration'] = '新規設定';
$PALANG['pAutoconfig_add_new_host'] = '新ホストを追加';
$PALANG['pAutoconfig_remove_host'] = 'このホストを削除';
$PALANG['pAutoconfig_move_up_host'] = 'このホストの優先順位を上がる';
$PALANG['pAutoconfig_move_down_host'] = 'このホストの優先順位を下がる';
$PALANG['pAutoconfig_add_new_text'] = '新テキストを追加';
$PALANG['pAutoconfig_remove_text'] = 'テキストを削除';
$PALANG['pAutoconfig_not_supported'] = '現在Thunderbirdではサポートなし';
$PALANG['pAutoconfig_cert_sign'] = 'Macのmobileconfigの認証';
$PALANG['pAutoconfig_cert_option'] = '認証方法';
$PALANG['pAutoconfig_cert_none'] = '認証なし';
$PALANG['pAutoconfig_cert_local'] = 'この設定の認証';
$PALANG['pAutoconfig_cert_global'] = 'グロバール設定による認証';
$PALANG['pAutoconfig_on'] = 'オン';
$PALANG['pAutoconfig_off'] = 'オフ';
$PALANG['pAutoconfig_server_side_error'] = 'サーバー側、予期せぬエラーが発生してしまいました。詳細情報のためサーバーログをご参照ください。%s';
$PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh
/* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */

Loading…
Cancel
Save