diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 018ad9af3..a6cd3b9a5 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -1,1445 +1,1455 @@ 'r', // 'cache_index' => 'r', // 'cache_thread' => 'r', // 'cache_messages' => 'r', ]; // It is possible to specify database variable values e.g. some limits here. // Use them if your server is not MySQL or for better performance. // For example Roundcube uses max_allowed_packet value (in bytes) // which limits query size for database cache operations. $config['db_max_allowed_packet'] = null; // ---------------------------------- // LOGGING/DEBUGGING // ---------------------------------- // log driver: 'syslog', 'stdout' or 'file'. $config['log_driver'] = 'file'; // date format for log entries // (read http://php.net/manual/en/function.date.php for all format characters) $config['log_date_format'] = 'd-M-Y H:i:s O'; // length of the session ID to prepend each log line with // set to 0 to avoid session IDs being logged. $config['log_session_id'] = 8; // Default extension used for log file name $config['log_file_ext'] = '.log'; // Syslog ident string to use, if using the 'syslog' log driver. $config['syslog_id'] = 'roundcube'; // Syslog facility to use, if using the 'syslog' log driver. // For possible values see installer or http://php.net/manual/en/function.openlog.php $config['syslog_facility'] = LOG_USER; // Activate this option if logs should be written to per-user directories. // Data will only be logged if a directory // exists and is writable. $config['per_user_logging'] = false; // Log sent messages to /sendmail.log or to syslog $config['smtp_log'] = true; // Log successful/failed logins to /userlogins.log or to syslog $config['log_logins'] = false; // Log session debug information/authentication errors to /session.log or to syslog $config['session_debug'] = false; // Log SQL queries to /sql.log or to syslog $config['sql_debug'] = false; // Log IMAP conversation to /imap.log or to syslog $config['imap_debug'] = false; // Log LDAP conversation to /ldap.log or to syslog $config['ldap_debug'] = false; // Log SMTP conversation to /smtp.log or to syslog $config['smtp_debug'] = false; // Log Memcache conversation to /memcache.log or to syslog $config['memcache_debug'] = false; // Log APC conversation to /apc.log or to syslog $config['apc_debug'] = false; // Log Redis conversation to /redis.log or to syslog $config['redis_debug'] = false; // ---------------------------------- // IMAP // ---------------------------------- // The IMAP host chosen to perform the log-in. // Leave blank to show a textbox at login, give a list of hosts // to display a pulldown menu or set one host as string. // Enter hostname with prefix ssl:// to use Implicit TLS, or use // prefix tls:// to use STARTTLS. // Supported replacement variables: // %n - hostname ($_SERVER['SERVER_NAME']) // %t - hostname without the first part // %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part) // %s - domain name after the '@' from e-mail address provided at login screen // For example %n = mail.domain.tld, %t = domain.tld // WARNING: After hostname change update of mail_host column in users table is // required to match old user data records with the new host. $config['default_host'] = 'localhost'; // TCP port used for IMAP connections $config['default_port'] = 143; // IMAP authentication method (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or null). // Use 'IMAP' to authenticate with IMAP LOGIN command. // By default the most secure method (from supported) will be selected. $config['imap_auth_type'] = null; // IMAP socket context options // See http://php.net/manual/en/context.ssl.php // The example below enables server certificate validation +// +// proxy_protocol is used to inject HAproxy style headers in the TCP stream +// See http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt //$config['imap_conn_options'] = [ // 'ssl' => [ // 'verify_peer' => true, // 'verify_depth' => 3, // 'cafile' => '/etc/openssl/certs/ca.crt', // ], +// 'proxy_protocol' => 1 | 2 | [ // required (either version number (1|2) or array with 'version' key) +// 'version' => 1 | 2, // required, if array +// 'remote_addr' => $_SERVER['REMOTE_ADDR'], +// 'remote_port' => $_SERVER['REMOTE_PORT'], +// 'local_addr' => $_SERVER['SERVER_ADDR'], +// 'local_port' => $_SERVER['SERVER_PORT'], +// ], // ]; // Note: These can be also specified as an array of options indexed by hostname $config['imap_conn_options'] = null; // IMAP connection timeout, in seconds. Default: 0 (use default_socket_timeout) $config['imap_timeout'] = 0; // Optional IMAP authentication identifier to be used as authorization proxy $config['imap_auth_cid'] = null; // Optional IMAP authentication password to be used for imap_auth_cid $config['imap_auth_pw'] = null; // If you know your imap's folder delimiter, you can specify it here. // Otherwise it will be determined automatically $config['imap_delimiter'] = null; // If you know your imap's folder vendor, you can specify it here. // Otherwise it will be determined automatically. Use lower-case // identifiers, e.g. 'dovecot', 'cyrus', 'gimap', 'hmail', 'uw-imap'. $config['imap_vendor'] = null; // If IMAP server doesn't support NAMESPACE extension, but you're // using shared folders or personal root folder is non-empty, you'll need to // set these options. All can be strings or arrays of strings. // Note: Folders need to be ended with directory separator, e.g. "INBOX." // (special directory "~" is an exception to this rule) // Note: These can be used also to overwrite server's namespaces // Note: Set these to FALSE to disable access to specified namespace $config['imap_ns_personal'] = null; $config['imap_ns_other'] = null; $config['imap_ns_shared'] = null; // By default IMAP capabilities are read after connection to IMAP server // In some cases, e.g. when using IMAP proxy, there's a need to refresh the list // after login. Set to True if you've got this case. $config['imap_force_caps'] = false; // By default list of subscribed folders is determined using LIST-EXTENDED // extension if available. Some servers (dovecot 1.x) returns wrong results // for shared namespaces in this case. https://github.com/roundcube/roundcubemail/issues/2474 // Enable this option to force LSUB command usage instead. // Deprecated: Use imap_disabled_caps = ['LIST-EXTENDED'] $config['imap_force_lsub'] = false; // Some server configurations (e.g. Courier) doesn't list folders in all namespaces // Enable this option to force listing of folders in all namespaces $config['imap_force_ns'] = false; // Some servers return hidden folders (name starting with a dot) // from user home directory. IMAP RFC does not forbid that. // Enable this option to hide them and disable possibility to create such. $config['imap_skip_hidden_folders'] = false; // Some servers do not support folders with both folders and messages inside // If your server supports that use true, if it does not, use false. // By default it will be determined automatically (once per user session). $config['imap_dual_use_folders'] = null; // List of disabled imap extensions. // Use if your IMAP server has broken implementation of some feature // and you can't remove it from CAPABILITY string on server-side. // For example UW-IMAP server has broken ESEARCH. // Note: Because the list is cached, re-login is required after change. $config['imap_disabled_caps'] = []; // Log IMAP session identifiers after each IMAP login. // This is used to relate IMAP session with Roundcube user sessions $config['imap_log_session'] = false; // Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'. $config['imap_cache'] = null; // Enables messages cache. Only 'db' cache is supported. // This requires an IMAP server that supports QRESYNC and CONDSTORE // extensions (RFC7162). See synchronize() in program/lib/Roundcube/rcube_imap_cache.php // for further info, or if you experience syncing problems. $config['messages_cache'] = false; // Lifetime of IMAP indexes cache. Possible units: s, m, h, d, w $config['imap_cache_ttl'] = '10d'; // Lifetime of messages cache. Possible units: s, m, h, d, w $config['messages_cache_ttl'] = '10d'; // Maximum cached message size in kilobytes. // Note: On MySQL this should be less than (max_allowed_packet - 30%) $config['messages_cache_threshold'] = 50; // ---------------------------------- // SMTP // ---------------------------------- // SMTP server host (for sending mails). // Enter hostname with prefix ssl:// to use Implicit TLS, or use // prefix tls:// to use STARTTLS. // Supported replacement variables: // %h - user's IMAP hostname // %n - hostname ($_SERVER['SERVER_NAME']) // %t - hostname without the first part // %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part) // %z - IMAP domain (IMAP hostname without the first part) // For example %n = mail.domain.tld, %t = domain.tld // To specify different SMTP servers for different IMAP hosts provide an array // of IMAP host (no prefix or port) and SMTP server e.g. ['imap.example.com' => 'smtp.example.net'] $config['smtp_server'] = 'localhost'; // SMTP port. Use 25 for cleartext, 465 for Implicit TLS, or 587 for STARTTLS (default) $config['smtp_port'] = 587; // SMTP username (if required) if you use %u as the username Roundcube // will use the current username for login $config['smtp_user'] = '%u'; // SMTP password (if required) if you use %p as the password Roundcube // will use the current user's password for login $config['smtp_pass'] = '%p'; // SMTP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use // best server supported one) $config['smtp_auth_type'] = null; // Optional SMTP authentication identifier to be used as authorization proxy $config['smtp_auth_cid'] = null; // Optional SMTP authentication password to be used for smtp_auth_cid $config['smtp_auth_pw'] = null; // Pass the username (XCLIENT LOGIN) to the server $config['smtp_xclient_login'] = false; // Pass the remote IP (XCLIENT ADDR) to the server $config['smtp_xclient_addr'] = false; // SMTP HELO host // Hostname to give to the remote server for SMTP 'HELO' or 'EHLO' messages // Leave this blank and you will get the server variable 'server_name' or // localhost if that isn't defined. $config['smtp_helo_host'] = ''; // SMTP connection timeout, in seconds. Default: 0 (use default_socket_timeout) // Note: There's a known issue where using ssl connection with // timeout > 0 causes connection errors (https://bugs.php.net/bug.php?id=54511) $config['smtp_timeout'] = 0; // SMTP socket context options // See http://php.net/manual/en/context.ssl.php // The example below enables server certificate validation, and // requires 'smtp_timeout' to be non zero. // $config['smtp_conn_options'] = [ // 'ssl' => [ // 'verify_peer' => true, // 'verify_depth' => 3, // 'cafile' => '/etc/openssl/certs/ca.crt', // ], // ]; // Note: These can be also specified as an array of options indexed by hostname $config['smtp_conn_options'] = null; // ---------------------------------- // OAuth // ---------------------------------- // Enable OAuth2 by defining a provider. Use 'generic' here $config['oauth_provider'] = null; // Provider name to be displayed on the login button $config['oauth_provider_name'] = 'Google'; // Mandatory: OAuth client ID for your Roundcube installation $config['oauth_client_id'] = null; // Mandatory: OAuth client secret $config['oauth_client_secret'] = null; // Mandatory: URI for OAuth user authentication (redirect) $config['oauth_auth_uri'] = null; // Mandatory: Endpoint for OAuth authentication requests (server-to-server) $config['oauth_token_uri'] = null; // Optional: Endpoint to query user identity if not provided in auth response $config['oauth_identity_uri'] = null; // Optional: disable SSL certificate check on HTTP requests to OAuth server // See http://docs.guzzlephp.org/en/stable/request-options.html#verify for possible values $config['oauth_verify_peer'] = true; // Mandatory: OAuth scopes to request (space-separated string) $config['oauth_scope'] = null; // Optional: additional query parameters to send with login request (hash array) $config['oauth_auth_parameters'] = []; // Optional: array of field names used to resolve the username within the identity information $config['oauth_identity_fields'] = null; // Boolean: automatically redirect to OAuth login when opening Roundcube without a valid session $config['oauth_login_redirect'] = false; ///// Example config for Gmail // Register your service at https://console.developers.google.com/ // - use https:///index.php/login/oauth as redirect URL // $config['default_host'] = 'ssl://imap.gmail.com'; // $config['oauth_provider'] = 'google'; // $config['oauth_provider_name'] = 'Google'; // $config['oauth_client_id'] = ""; // $config['oauth_client_secret'] = ""; // $config['oauth_auth_uri'] = "https://accounts.google.com/o/oauth2/auth"; // $config['oauth_token_uri'] = "https://oauth2.googleapis.com/token"; // $config['oauth_identity_uri'] = 'https://www.googleapis.com/oauth2/v1/userinfo'; // $config['oauth_scope'] = "email profile openid https://mail.google.com/"; // $config['oauth_auth_parameters'] = ['access_type' => 'offline', 'prompt' => 'consent']; ///// Example config for Outlook.com (Office 365) // Register your OAuth client at https://portal.azure.com // - use https:///index.php/login/oauth as redirect URL // - grant permissions to Microsoft Graph API "IMAP.AccessAsUser.All", "SMTP.Send", "User.Read" and "offline_access" // $config['default_host'] = 'ssl://outlook.office365.com'; // $config['smtp_server'] = 'ssl://smtp.office365.com'; // $config['oauth_provider'] = 'outlook'; // $config['oauth_provider_name'] = 'Outlook.com'; // $config['oauth_client_id'] = ""; // $config['oauth_client_secret'] = ""; // $config['oauth_auth_uri'] = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; // $config['oauth_token_uri'] = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; // $config['oauth_identity_uri'] = "https://graph.microsoft.com/v1.0/me"; // $config['oauth_identity_fields'] = ['email', 'userPrincipalName']; // $config['oauth_scope'] = "https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/SMTP.Send User.Read offline_access"; // $config['oauth_auth_parameters'] = ['nonce' => mt_rand()]; // ---------------------------------- // LDAP // ---------------------------------- // Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'. $config['ldap_cache'] = 'db'; // Lifetime of LDAP cache. Possible units: s, m, h, d, w $config['ldap_cache_ttl'] = '10m'; // ---------------------------------- // CACHE(S) // ---------------------------------- // Use these hosts for accessing memcached // Define any number of hosts in the form of hostname:port or unix:///path/to/socket.file // Example: ['localhost:11211', '192.168.1.12:11211', 'unix:///var/tmp/memcached.sock']; $config['memcache_hosts'] = null; // Controls the use of a persistent connections to memcache servers // See http://php.net/manual/en/memcache.addserver.php $config['memcache_pconnect'] = true; // Value in seconds which will be used for connecting to the daemon // See http://php.net/manual/en/memcache.addserver.php $config['memcache_timeout'] = 1; // Controls how often a failed server will be retried (value in seconds). // Setting this parameter to -1 disables automatic retry. // See http://php.net/manual/en/memcache.addserver.php $config['memcache_retry_interval'] = 15; // Use these hosts for accessing Redis. // Currently only one host is supported. Cluster support may come in a future release. // You can pass 4 fields, host, port (optional), database (optional) and password (optional). // Unset fields will be set to the default values host=127.0.0.1, port=6379. // Examples: // ['localhost:6379']; // ['192.168.1.1:6379:1:secret']; // ['unix:///var/run/redis/redis-server.sock:1:secret']; $config['redis_hosts'] = null; // Maximum size of an object in memcache (in bytes). Default: 2MB $config['memcache_max_allowed_packet'] = '2M'; // Maximum size of an object in APC cache (in bytes). Default: 2MB $config['apc_max_allowed_packet'] = '2M'; // Maximum size of an object in Redis cache (in bytes). Default: 2MB $config['redis_max_allowed_packet'] = '2M'; // ---------------------------------- // SYSTEM // ---------------------------------- // THIS OPTION WILL ALLOW THE INSTALLER TO RUN AND CAN EXPOSE SENSITIVE CONFIG DATA. // ONLY ENABLE IT IF YOU'RE REALLY SURE WHAT YOU'RE DOING! $config['enable_installer'] = false; // don't allow these settings to be overridden by the user $config['dont_override'] = []; // List of disabled UI elements/actions $config['disabled_actions'] = []; // define which settings should be listed under the 'advanced' block // which is hidden by default $config['advanced_prefs'] = []; // provide an URL where a user can get support for this Roundcube installation // PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE! $config['support_url'] = ''; // Location of the blank (watermark) frame page. By default it is the watermark.html // file from the currently selected skin. Prepend name/path with a slash to use // current skin folder. Remove the slash to point to a file in the Roundcube // root directory. It can be also a full URL. $config['blankpage_url'] = '/watermark.html'; // Logo image replacement. Specifies location of the image as: // - URL relative to the document root of this Roundcube installation // - full URL with http:// or https:// prefix // - URL relative to the current skin folder (when starts with a '/') // // An array can be used to specify different logos for specific template files // The array key specifies the place(s) the logo should be applied to and // is made up of (up to) 3 parts: // - skin name prefix (always with colon, can be replaced with *) // - template name (or * for all templates) // - logo type - it is used for logos used on multiple templates // the available types include '[favicon]' for favicon, '[print]' for logo on all print // templates (e.g. messageprint, contactprint) and '[small]' for small screen logo in supported skins // '[dark]' and '[small-dark]' for dark mode logo in supported skins // // Example config for skin_logo /* [ // show the image /images/logo_login_small.png for the Login screen in the Elastic skin on small screens "elastic:login[small]" => "/images/logo_login_small.png", // show the image /images/logo_login.png for the Login screen in the Elastic skin "elastic:login" => "/images/logo_login.png", // show the image /images/logo_small.png in the Elastic skin "elastic:*[small]" => "/images/logo_small.png", // show the image /images/larry.png in the Larry skin "larry:*" => "/images/larry.png", // show the image /images/logo_login.png on the login template in all skins "login" => "/images/logo_login.png", // show the image /images/logo_print.png for all print type logos in all skins "[print]" => "/images/logo_print.png", ]; */ $config['skin_logo'] = null; // Automatically register user in Roundcube database on successful (IMAP) logon. // Set to false if only registered users should be allowed to the webmail. // Note: If disabled you have to create records in Roundcube users table by yourself. // Note: Roundcube does not manage/create users on a mail server. $config['auto_create_user'] = true; // Enables possibility to log in using email address from user identities $config['user_aliases'] = false; // use this folder to store log files // must be writeable for the user who runs PHP process (Apache user if mod_php is being used) // This is used by the 'file' log driver. $config['log_dir'] = RCUBE_INSTALL_PATH . 'logs/'; // Location of temporary saved files such as attachments and cache files // must be writeable for the user who runs PHP process (Apache user if mod_php is being used) $config['temp_dir'] = RCUBE_INSTALL_PATH . 'temp/'; // expire files in temp_dir after 48 hours // possible units: s, m, h, d, w $config['temp_dir_ttl'] = '48h'; // Enforce connections over https // With this option enabled, all non-secure connections will be redirected. // It can be also a port number, hostname or hostname:port if they are // different than default HTTP_HOST:443 $config['force_https'] = false; // tell PHP that it should work as under secure connection // even if it doesn't recognize it as secure ($_SERVER['HTTPS'] is not set) // e.g. when you're running Roundcube behind a https proxy // this option is mutually exclusive to 'force_https' and only either one of them should be set to true. $config['use_https'] = false; // Allow browser-autocompletion on login form. // 0 - disabled, 1 - username and host only, 2 - username, host, password $config['login_autocomplete'] = 0; // Forces conversion of logins to lower case. // 0 - disabled, 1 - only domain part, 2 - domain and local part. // If users authentication is case-insensitive this must be enabled. // Note: After enabling it all user records need to be updated, e.g. with query: // UPDATE users SET username = LOWER(username); $config['login_lc'] = 2; // Maximum length (in bytes) of logon username and password. $config['login_username_maxlen'] = 1024; $config['login_password_maxlen'] = 1024; // Logon username filter. Regular expression for use with preg_match(). // Use special value 'email' if you accept only full email addresses as user logins. // Example: '/^[a-z0-9_@.-]+$/' $config['login_username_filter'] = null; // Brute-force attacks prevention. // The value specifies maximum number of failed logon attempts per minute. $config['login_rate_limit'] = 3; // Includes should be interpreted as PHP files $config['skin_include_php'] = false; // display product name and software version on login screen // 0 - hide product name and version number, 1 - show product name only, 2 - show product name and version number $config['display_product_info'] = 1; // Session lifetime in minutes $config['session_lifetime'] = 10; // Session domain: .example.org $config['session_domain'] = ''; // Session name. Default: 'roundcube_sessid' $config['session_name'] = null; // Session authentication cookie name. Default: 'roundcube_sessauth' $config['session_auth_name'] = null; // Session path. Defaults to PHP session.cookie_path setting. $config['session_path'] = null; // Session samesite. Defaults to PHP session.cookie_samesite setting. // Requires PHP >= 7.3.0, see https://wiki.php.net/rfc/same-site-cookie for more info // Possible values: null (default), 'Lax', or 'Strict' $config['session_samesite'] = null; // Backend to use for session storage. Can either be 'db' (default), 'redis', 'memcache', or 'php' // // If set to 'memcache' or 'memcached', a list of servers need to be specified in 'memcache_hosts' // Make sure the Memcache extension (https://pecl.php.net/package/memcache) version >= 2.0.0 // or the Memcached extension (https://pecl.php.net/package/memcached) version >= 2.0.0 is installed. // // If set to 'redis', a server needs to be specified in 'redis_hosts' // Make sure the Redis extension (https://pecl.php.net/package/redis) version >= 2.0.0 is installed. // // Setting this value to 'php' will use the default session save handler configured in PHP $config['session_storage'] = 'db'; // List of trusted proxies // X_FORWARDED_* and X_REAL_IP headers are only accepted from these IPs $config['proxy_whitelist'] = []; // List of trusted host names // Attackers can modify Host header of the HTTP request causing $_SERVER['SERVER_NAME'] // or $_SERVER['HTTP_HOST'] variables pointing to a different host, that could be used // to collect user names and passwords. Some server configurations prevent that, but not all. // An empty list accepts any host name. The list can contain host names // or PCRE patterns (without // delimiters, that will be added automatically). $config['trusted_host_patterns'] = []; // check client IP in session authorization $config['ip_check'] = false; // X-Frame-Options HTTP header value sent to prevent from Clickjacking. // Possible values: sameorigin|deny|allow-from . // Set to false in order to disable sending the header. $config['x_frame_options'] = 'sameorigin'; // This key is used for encrypting purposes, like storing of imap password // in the session. For historical reasons it's called DES_key, but it's used // with any configured cipher_method (see below). // For the default cipher_method a required key length is 24 characters. $config['des_key'] = 'rcmail-!24ByteDESkey*Str'; // Encryption algorithm. You can use any method supported by OpenSSL. // Default is set for backward compatibility to DES-EDE3-CBC, // but you can choose e.g. AES-256-CBC which we consider a better choice. $config['cipher_method'] = 'DES-EDE3-CBC'; // Automatically add this domain to user names for login // Only for IMAP servers that require full e-mail addresses for login // Specify an array with 'host' => 'domain' values to support multiple hosts // Supported replacement variables: // %h - user's IMAP hostname // %n - hostname ($_SERVER['SERVER_NAME']) // %t - hostname without the first part // %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part) // %z - IMAP domain (IMAP hostname without the first part) // For example %n = mail.domain.tld, %t = domain.tld $config['username_domain'] = ''; // Force domain configured in username_domain to be used for login. // Any domain in username will be replaced by username_domain. $config['username_domain_forced'] = false; // This domain will be used to form e-mail addresses of new users // Specify an array with 'host' => 'domain' values to support multiple hosts // Supported replacement variables: // %h - user's IMAP hostname // %n - http hostname ($_SERVER['SERVER_NAME']) // %d - domain (http hostname without the first part) // %z - IMAP domain (IMAP hostname without the first part) // For example %n = mail.domain.tld, %t = domain.tld $config['mail_domain'] = ''; // Password character set, to change the password for user // authentication or for password change operations $config['password_charset'] = 'UTF-8'; // How many seconds must pass between emails sent by a user $config['sendmail_delay'] = 0; // Message size limit. Note that SMTP server(s) may use a different value. // This limit is verified when user attaches files to a composed message. // Size in bytes (possible unit suffix: K, M, G) $config['max_message_size'] = '100M'; // Maximum number of recipients per message (including To, Cc, Bcc). // Default: 0 (no limit) $config['max_recipients'] = 0; // Maximum number of recipients per message excluding Bcc header. // This is a soft limit, which means we only display a warning to the user. // Default: 5 $config['max_disclosed_recipients'] = 5; // Maximum allowed number of members of an address group. Default: 0 (no limit) // If 'max_recipients' is set this value should be less or equal $config['max_group_members'] = 0; // Name your service. This is displayed on the login screen and in the window title $config['product_name'] = 'Roundcube Webmail'; // Add this user-agent to message headers when sending. Default: not set. $config['useragent'] = null; // try to load host-specific configuration // see https://github.com/roundcube/roundcubemail/wiki/Configuration:-Multi-Domain-Setup // for more details $config['include_host_config'] = false; // path to a text file which will be added to each sent message // paths are relative to the Roundcube root folder $config['generic_message_footer'] = ''; // path to a text file which will be added to each sent HTML message // paths are relative to the Roundcube root folder $config['generic_message_footer_html'] = ''; // add a received header to outgoing mails containing the creators IP and hostname $config['http_received_header'] = false; // Whether or not to encrypt the IP address and the host name // these could, in some circles, be considered as sensitive information; // however, for the administrator, these could be invaluable help // when tracking down issues. $config['http_received_header_encrypt'] = false; // number of chars allowed for line when wrapping text. // text wrapping is done when composing/sending messages $config['line_length'] = 72; // send plaintext messages as format=flowed $config['send_format_flowed'] = true; // According to RFC2298, return receipt envelope sender address must be empty. // If this option is true, Roundcube will use user's identity as envelope sender for MDN responses. $config['mdn_use_from'] = false; // Set identities access level: // 0 - many identities with possibility to edit all params // 1 - many identities with possibility to edit all params but not email address // 2 - one identity with possibility to edit all params // 3 - one identity with possibility to edit all params but not email address // 4 - one identity with possibility to edit only signature $config['identities_level'] = 0; // Maximum size of uploaded image in kilobytes // Images (in html signatures) are stored in database as data URIs $config['identity_image_size'] = 64; // Mimetypes supported by the browser. // Attachments of these types will open in a preview window. // Either a comma-separated list or an array. Default list includes: // text/plain,text/html, // image/jpeg,image/gif,image/png,image/bmp,image/tiff,image/webp, // application/x-javascript,application/pdf,application/x-shockwave-flash $config['client_mimetypes'] = null; // Path to a local mime magic database file for PHPs finfo extension. // Set to null if the default path should be used. $config['mime_magic'] = null; // Absolute path to a local mime.types mapping table file. // This is used to derive mime-types from the filename extension or vice versa. // Such a file is usually part of the apache webserver. If you don't find a file named mime.types on your system, // download it from http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types $config['mime_types'] = null; // path to imagemagick identify binary (if not set we'll use Imagick or GD extensions) $config['im_identify_path'] = null; // path to imagemagick convert binary (if not set we'll use Imagick or GD extensions) $config['im_convert_path'] = null; // Size of thumbnails from image attachments displayed below the message content. // Note: whether images are displayed at all depends on the 'inline_images' option. // Set to 0 to display images in full size. $config['image_thumbnail_size'] = 240; // maximum size of uploaded contact photos in pixel $config['contact_photo_size'] = 160; // Enable DNS checking for e-mail address validation $config['email_dns_check'] = false; // Disables saving sent messages in Sent folder (like gmail) (Default: false) // Note: useful when SMTP server stores sent mail in user mailbox $config['no_save_sent_messages'] = false; // Improve system security by using special URL with security token. // This can be set to a number defining token length. Default: 16. // Warning: This requires http server configuration. Sample: // RewriteRule ^/roundcubemail/[a-zA-Z0-9]{16}/(.*) /roundcubemail/$1 [PT] // Alias /roundcubemail /var/www/roundcubemail/ // Note: Use assets_path to not prevent the browser from caching assets $config['use_secure_urls'] = false; // Allows to define separate server/path for image/js/css files // Warning: If the domain is different cross-domain access to some // resources need to be allowed // Sample: // // Header set Access-Control-Allow-Origin "*" // $config['assets_path'] = ''; // While assets_path is for the browser, assets_dir informs // PHP code about the location of asset files in filesystem $config['assets_dir'] = ''; // Options passed when creating Guzzle HTTP client, used to fetch remote content // For example: // [ // 'timeout' => 10, // 'proxy' => 'tcp://localhost:8125', // ] $config['http_client'] = []; // List of supported subject prefixes for a message reply // This list is used to clean the subject when replying or sorting messages $config['subject_reply_prefixes'] = ['Re:']; // List of supported subject prefixes for a message forward // This list is used to clean the subject when forwarding or sorting messages $config['subject_forward_prefixes'] = ['Fwd:', 'Fw:']; // Prefix to use in subject when replying to a message $config['response_prefix'] = 'Re:'; // Prefix to use in subject when forwarding a message $config['forward_prefix'] = 'Fwd:'; // ---------------------------------- // PLUGINS // ---------------------------------- // List of active plugins (in plugins/ directory) $config['plugins'] = []; // ---------------------------------- // USER INTERFACE // ---------------------------------- // default messages sort column. Use empty value for default server's sorting, // or 'arrival', 'date', 'subject', 'from', 'to', 'fromto', 'size', 'cc' $config['message_sort_col'] = ''; // default messages sort order $config['message_sort_order'] = 'DESC'; // These cols are shown in the message list. Available cols are: // subject, from, to, fromto, cc, replyto, date, size, status, flag, attachment, priority $config['list_cols'] = ['subject', 'status', 'fromto', 'date', 'size', 'flag', 'attachment']; // the default locale setting (leave empty for auto-detection) // RFC1766 formatted language name like en_US, de_DE, de_CH, fr_FR, pt_BR $config['language'] = null; // use this format for date display (date or strftime format) $config['date_format'] = 'Y-m-d'; // give this choice of date formats to the user to select from // Note: do not use ambiguous formats like m/d/Y $config['date_formats'] = ['Y-m-d', 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y']; // use this format for time display (date or strftime format) $config['time_format'] = 'H:i'; // give this choice of time formats to the user to select from $config['time_formats'] = ['G:i', 'H:i', 'g:i a', 'h:i A']; // use this format for short date display (derived from date_format and time_format) $config['date_short'] = 'D H:i'; // use this format for detailed date/time formatting (derived from date_format and time_format) $config['date_long'] = 'Y-m-d H:i'; // store draft message is this mailbox // leave blank if draft messages should not be stored // NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP) $config['drafts_mbox'] = 'Drafts'; // store spam messages in this mailbox // NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP) $config['junk_mbox'] = 'Junk'; // store sent message is this mailbox // leave blank if sent messages should not be stored // NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP) $config['sent_mbox'] = 'Sent'; // move messages to this folder when deleting them // leave blank if they should be deleted directly // NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP) $config['trash_mbox'] = 'Trash'; // automatically create the above listed default folders on user login $config['create_default_folders'] = false; // protect the default folders from renames, deletes, and subscription changes $config['protect_default_folders'] = true; // Disable localization of the default folder names listed above $config['show_real_foldernames'] = false; // if in your system 0 quota means no limit set this option to true $config['quota_zero_as_unlimited'] = false; // Make use of the built-in spell checker. $config['enable_spellcheck'] = false; // Enables spellchecker exceptions dictionary. // Setting it to 'shared' will make the dictionary shared by all users. $config['spellcheck_dictionary'] = false; // Set the spell checking engine. Possible values: // - 'googie' - the default (also used for connecting to Nox Spell Server, see 'spellcheck_uri' setting) // - 'pspell' - requires the PHP Pspell module and aspell installed // - 'enchant' - requires the PHP Enchant module // - 'atd' - install your own After the Deadline server or check with the people at http://www.afterthedeadline.com before using their API // Since Google shut down their public spell checking service, the default settings // connect to http://spell.roundcube.net which is a hosted service provided by Roundcube. // You can connect to any other googie-compliant service by setting 'spellcheck_uri' accordingly. $config['spellcheck_engine'] = 'googie'; // For locally installed Nox Spell Server or After the Deadline services, // please specify the URI to call it. // Get Nox Spell Server from http://orangoo.com/labs/?page_id=72 or // the After the Deadline package from http://www.afterthedeadline.com. // Leave empty to use the public API of service.afterthedeadline.com $config['spellcheck_uri'] = ''; // These languages can be selected for spell checking. // Configure as a PHP style hash array: ['en'=>'English', 'de'=>'Deutsch']; // Leave empty for default set of available language. $config['spellcheck_languages'] = null; // Makes that words with all letters capitalized will be ignored (e.g. GOOGLE) $config['spellcheck_ignore_caps'] = false; // Makes that words with numbers will be ignored (e.g. g00gle) $config['spellcheck_ignore_nums'] = false; // Makes that words with symbols will be ignored (e.g. g@@gle) $config['spellcheck_ignore_syms'] = false; // Number of lines at the end of a message considered to contain the signature. // Increase this value if signatures are not properly detected and colored $config['sig_max_lines'] = 15; // don't let users set pagesize to more than this value if set $config['max_pagesize'] = 200; // Minimal value of user's 'refresh_interval' setting (in seconds) $config['min_refresh_interval'] = 60; // Specifies for how many seconds the Undo button will be available // after object delete action. Currently used with supporting address book sources. // Setting it to 0, disables the feature. $config['undo_timeout'] = 0; // A static list of canned responses which are immutable for the user $config['compose_responses_static'] = [ // ['name' => 'Canned Response 1', 'text' => 'Static Response One'], // ['name' => 'Canned Response 2', 'text' => 'Static Response Two'], ]; // List of HKP key servers for PGP public key lookups in Enigma/Mailvelope // Note: Lookup is client-side, so the server must support Cross-Origin Resource Sharing $config['keyservers'] = ['keys.openpgp.org']; // Enables use of the Main Keyring in Mailvelope? If disabled, a per-site keyring // will be used. This is set to false for backwards compatibility. $config['mailvelope_main_keyring'] = false; // Mailvelope RSA bit size for newly generated keys, either 2048 or 4096. // It maybe desirable to use 2048 for sites with many mobile users. $config['mailvelope_keysize'] = 4096; // ---------------------------------- // ADDRESSBOOK SETTINGS // ---------------------------------- // This indicates which type of address book to use. Possible choices: // 'sql' - built-in sql addressbook enabled (default), // '' - built-in sql addressbook disabled. // Still LDAP or plugin-added addressbooks will be available. // BC Note: The value can actually be anything except 'sql', it does not matter. $config['address_book_type'] = 'sql'; // In order to enable public ldap search, configure an array like the Verisign // example further below. if you would like to test, simply uncomment the example. // Array key must contain only safe characters, ie. a-zA-Z0-9_ $config['ldap_public'] = []; // If you are going to use LDAP for individual address books, you will need to // set 'user_specific' to true and use the variables to generate the appropriate DNs to access it. // // The recommended directory structure for LDAP is to store all the address book entries // under the users main entry, e.g.: // // o=root // ou=people // uid=user@domain // mail=contact@contactdomain // // So the base_dn would be uid=%fu,ou=people,o=root // The bind_dn would be the same as based_dn or some super user login. /* * example config for Verisign directory * $config['ldap_public']['Verisign'] = [ 'name' => 'Verisign.com', // Replacement variables supported in host names: // %h - user's IMAP hostname // %n - hostname ($_SERVER['SERVER_NAME']) // %t - hostname without the first part // %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part) // %z - IMAP domain (IMAP hostname without the first part) // For example %n = mail.domain.tld, %t = domain.tld // Note: Host can also be a full URI e.g. ldaps://hostname.local:636 (for SSL) 'hosts' => array('directory.verisign.com'), 'port' => 389, 'use_tls' => false, 'ldap_version' => 3, // using LDAPv3 'network_timeout' => 10, // The timeout (in seconds) for connect + bind attempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x 'user_specific' => false, // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login. // When 'user_specific' is enabled following variables can be used in base_dn/bind_dn config: // %fu - The full username provided, assumes the username is an email // address, uses the username_domain value if not an email address. // %u - The username prior to the '@'. // %d - The domain name after the '@'. // %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com" // %dn - DN found by ldap search when search_filter/search_base_dn are used 'base_dn' => '', 'bind_dn' => '', 'bind_pass' => '', // It's possible to bind for an individual address book // The login name is used to search for the DN to bind with 'search_base_dn' => '', 'search_filter' => '', // e.g. '(&(objectClass=posixAccount)(uid=%u))' // DN and password to bind as before searching for bind DN, if anonymous search is not allowed 'search_bind_dn' => '', 'search_bind_pw' => '', // Base DN and filter used for resolving the user's domain root DN which feeds the %dc variables // Leave empty to skip this lookup and derive the root DN from the username domain 'domain_base_dn' => '', 'domain_filter' => '', // Optional map of replacement strings => attributes used when binding for an individual address book 'search_bind_attrib' => [], // e.g. ['%udc' => 'ou'] // Default for %dn variable if search doesn't return DN value 'search_dn_default' => '', // Optional authentication identifier to be used as SASL authorization proxy // bind_dn need to be empty 'auth_cid' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5 'auth_method' => '', // Indicates if the addressbook shall be hidden from the list. // With this option enabled you can still search/view contacts. 'hidden' => false, // Indicates if the addressbook shall not list contacts but only allows searching. 'searchonly' => false, // Indicates if we can write to the LDAP directory or not. // If writable is true then these fields need to be populated: // LDAP_Object_Classes, required_fields, LDAP_rdn 'writable' => false, // To create a new contact these are the object classes to specify // (or any other classes you wish to use). 'LDAP_Object_Classes' => ['top', 'inetOrgPerson'], // The RDN field that is used for new entries, this field needs // to be one of the search_fields, the base of base_dn is appended // to the RDN to insert into the LDAP directory. 'LDAP_rdn' => 'cn', // The required fields needed to build a new contact as required by // the object classes (can include additional fields not required by the object classes). 'required_fields' => ['cn', 'sn', 'mail'], 'search_fields' => ['mail', 'cn'], // fields to search in // mapping of contact fields to directory attributes // 1. for every attribute one can specify the number of values (limit) allowed. // default is 1, a wildcard * means unlimited // 2. another possible parameter is separator character for composite fields // 3. it's possible to define field format for write operations, e.g. for date fields // example: 'birthday:date[YmdHis\\Z]' 'fieldmap' => [ // Roundcube => LDAP:limit 'name' => 'cn', 'surname' => 'sn', 'firstname' => 'givenName', 'jobtitle' => 'title', 'email' => 'mail:*', 'phone:home' => 'homePhone', 'phone:work' => 'telephoneNumber', 'phone:mobile' => 'mobile', 'phone:pager' => 'pager', 'phone:workfax' => 'facsimileTelephoneNumber', 'street' => 'street', 'zipcode' => 'postalCode', 'region' => 'st', 'locality' => 'l', // if you country is a complex object, you need to configure 'sub_fields' below 'country' => 'c', 'organization' => 'o', 'department' => 'ou', 'jobtitle' => 'title', 'notes' => 'description', 'photo' => 'jpegPhoto', // these currently don't work: // 'manager' => 'manager', // 'assistant' => 'secretary', ], // Map of contact sub-objects (attribute name => objectClass(es)), e.g. 'c' => 'country' 'sub_fields' => [], // Generate values for the following LDAP attributes automatically when creating a new record 'autovalues' => [ // 'uid' => 'md5(microtime())', // You may specify PHP code snippets which are then eval'ed // 'mail' => '{givenname}.{sn}@mydomain.com', // or composite strings with placeholders for existing attributes ], 'sort' => 'cn', // The field to sort the listing by. 'scope' => 'sub', // search mode: sub|base|list 'filter' => '(objectClass=inetOrgPerson)', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act 'fuzzy_search' => true, // server allows wildcard search 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it) 'vlv_search' => false, // Use Virtual List View functions for autocompletion searches (if server supports it) 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting 'config_root_dn' => 'cn=config', // Root DN to search config entries (e.g. vlv indexes) 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit. 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit. 'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups 'dereference' => 0, // Sets the LDAP_OPT_DEREF option. One of: LDAP_DEREF_NEVER, LDAP_DEREF_SEARCHING, LDAP_DEREF_FINDING, LDAP_DEREF_ALWAYS // Used where addressbook contains aliases to objects elsewhere in the LDAP tree. // definition for contact groups (uncomment if no groups are supported) // for the groups base_dn, the user replacements %fu, %u, %d and %dc work as for base_dn (see above) // if the groups base_dn is empty, the contact base_dn is used for the groups as well // -> in this case, assure that groups and contacts are separated due to the concerning filters! 'groups' => [ 'base_dn' => '', 'scope' => 'sub', // Search mode: sub|base|list 'filter' => '(objectClass=groupOfNames)', 'object_classes' => ['top', 'groupOfNames'], // Object classes to be assigned to new groups 'member_attr' => 'member', // Name of the default member attribute, e.g. uniqueMember 'name_attr' => 'cn', // Attribute to be used as group name 'email_attr' => 'mail', // Group email address attribute (e.g. for mailing lists) 'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members 'vlv' => false, // Use VLV controls to list groups 'class_member_attr' => [ // Mapping of group object class to member attribute used in these objects 'groupofnames' => 'member', 'groupofuniquenames' => 'uniquemember' ], ], // this configuration replaces the regular groups listing in the directory tree with // a hard-coded list of groups, each listing entries with the configured base DN and filter. // if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups' 'group_filters' => [ 'departments' => [ 'name' => 'Company Departments', 'scope' => 'list', 'base_dn' => 'ou=Groups,dc=mydomain,dc=com', 'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))', 'name_attr' => 'cn', ], 'customers' => [ 'name' => 'Customers', 'scope' => 'sub', 'base_dn' => 'ou=Customers,dc=mydomain,dc=com', 'filter' => '(objectClass=inetOrgPerson)', 'name_attr' => 'sn', ], ], ]; */ // An ordered array of the ids of the addressbooks that should be searched // when populating address autocomplete fields server-side. ex: ['sql','Verisign']; $config['autocomplete_addressbooks'] = ['sql']; // The minimum number of characters required to be typed in an autocomplete field // before address books will be searched. Most useful for LDAP directories that // may need to do lengthy results building given overly-broad searches $config['autocomplete_min_length'] = 1; // Number of parallel autocomplete requests. // If there's more than one address book, n parallel (async) requests will be created, // where each request will search in one address book. By default (0), all address // books are searched in one request. $config['autocomplete_threads'] = 0; // Max. number of entries in autocomplete popup. Default: 15. $config['autocomplete_max'] = 15; // show address fields in this order // available placeholders: {street}, {locality}, {zipcode}, {country}, {region} $config['address_template'] = '{street}
{locality} {zipcode}
{country} {region}'; // Matching mode for addressbook search (including autocompletion) // 0 - partial (*abc*), default // 1 - strict (abc) // 2 - prefix (abc*) // Note: For LDAP sources fuzzy_search must be enabled to use 'partial' or 'prefix' mode $config['addressbook_search_mode'] = 0; // List of fields used on contacts list and for autocompletion searches // Warning: These are field names not LDAP attributes (see 'fieldmap' setting)! $config['contactlist_fields'] = ['name', 'firstname', 'surname', 'email']; // Template of contact entry on the autocompletion list. // You can use contact fields as: name, email, organization, department, etc. // See program/actions/contacts/index.php for a list $config['contact_search_name'] = '{name} <{email}>'; // Contact mode. If your contacts are mostly business, switch it to 'business'. // This will prioritize form fields related to 'work' (instead of 'home'). // Default: 'private'. $config['contact_form_mode'] = 'private'; // The addressbook source to store automatically collected recipients in. // Default: true (the built-in "Collected recipients" addressbook, source id = '1') // Note: It can be set to any writeable addressbook, e.g. 'sql' $config['collected_recipients'] = true; // The addressbook source to store trusted senders in. // Default: true (the built-in "Trusted senders" addressbook, source id = '2') // Note: It can be set to any writeable addressbook, e.g. 'sql' $config['collected_senders'] = true; // ---------------------------------- // USER PREFERENCES // ---------------------------------- // Use this charset as fallback for message decoding $config['default_charset'] = 'ISO-8859-1'; // Skin name: folder from skins/ $config['skin'] = 'elastic'; // Limit skins available for the user. // Note: When not empty, it should include the default skin set in 'skin' option. $config['skins_allowed'] = []; // Enables using standard browser windows (that can be handled as tabs) // instead of popup windows $config['standard_windows'] = false; // show up to X items in messages list view $config['mail_pagesize'] = 50; // show up to X items in contacts list view $config['addressbook_pagesize'] = 50; // sort contacts by this col (preferably either one of name, firstname, surname) $config['addressbook_sort_col'] = 'surname'; // The way how contact names are displayed in the list. // 0: prefix firstname middlename surname suffix (only if display name is not set) // 1: firstname middlename surname // 2: surname firstname middlename // 3: surname, firstname middlename $config['addressbook_name_listing'] = 0; // use this timezone to display date/time // valid timezone identifiers are listed here: php.net/manual/en/timezones.php // 'auto' will use the browser's timezone settings $config['timezone'] = 'auto'; // prefer displaying HTML messages $config['prefer_html'] = true; // Display remote resources (inline images, styles) in HTML messages. Default: 0. // 0 - Never, always ask // 1 - Allow from my contacts (all writeable addressbooks + collected senders and recipients) // 2 - Always allow // 3 - Allow from trusted senders only $config['show_images'] = 0; // open messages in new window $config['message_extwin'] = false; // open message compose form in new window $config['compose_extwin'] = false; // compose html formatted messages by default // 0 - never, // 1 - always, // 2 - on reply to HTML message, // 3 - on forward or reply to HTML message // 4 - always, except when replying to plain text message $config['htmleditor'] = 0; // save copies of compose messages in the browser's local storage // for recovery in case of browser crashes and session timeout. $config['compose_save_localstorage'] = true; // show pretty dates as standard $config['prettydate'] = true; // save compose message every 300 seconds (5min) $config['draft_autosave'] = 300; // Interface layout. Default: 'widescreen'. // 'widescreen' - three columns // 'desktop' - two columns, preview on bottom // 'list' - two columns, no preview $config['layout'] = 'widescreen'; // Mark as read when viewing a message (delay in seconds) // Set to -1 if messages should not be marked as read $config['mail_read_time'] = 0; // Clear Trash on logout $config['logout_purge'] = false; // Compact INBOX on logout $config['logout_expunge'] = false; // Display attached images below the message body $config['inline_images'] = true; // Encoding of long/non-ascii attachment names: // 0 - Full RFC 2231 compatible // 1 - RFC 2047 for 'name' and RFC 2231 for 'filename' parameter (Thunderbird's default) // 2 - Full 2047 compatible $config['mime_param_folding'] = 1; // Set true if deleted messages should not be displayed // This will make the application run slower $config['skip_deleted'] = false; // Set true to Mark deleted messages as read as well as deleted // False means that a message's read status is not affected by marking it as deleted $config['read_when_deleted'] = true; // Set to true to never delete messages immediately // Use 'Purge' to remove messages marked as deleted $config['flag_for_deletion'] = false; // Default interval for auto-refresh requests (in seconds) // These are requests for system state updates e.g. checking for new messages, etc. // Setting it to 0 disables the feature. $config['refresh_interval'] = 60; // If true all folders will be checked for recent messages $config['check_all_folders'] = false; // If true, after message/contact delete/move, the next message/contact will be displayed $config['display_next'] = true; // Default messages listing mode. One of 'threads' or 'list'. $config['default_list_mode'] = 'list'; // 0 - Do not expand threads // 1 - Expand all threads automatically // 2 - Expand only threads with unread messages $config['autoexpand_threads'] = 0; // When replying: // -1 - don't cite the original message // 0 - place cursor below the original message // 1 - place cursor above original message (top posting) // 2 - place cursor above original message (top posting), but do not indent the quote $config['reply_mode'] = 0; // When replying strip original signature from message $config['strip_existing_sig'] = true; // Show signature: // 0 - Never // 1 - Always // 2 - New messages only // 3 - Forwards and Replies only $config['show_sig'] = 1; // By default the signature is placed depending on cursor position (reply_mode). // Sometimes it might be convenient to start the reply on top but keep // the signature below the quoted text (sig_below = true). $config['sig_below'] = false; // Enables adding of standard separator to the signature $config['sig_separator'] = true; // Use MIME encoding (quoted-printable) for 8bit characters in message body $config['force_7bit'] = false; // Default fields configuration for mail search. // The array can contain a per-folder list of header fields which should be considered when searching // The entry with key '*' stands for all folders which do not have a specific list set. // Supported fields: subject, from, to, cc, bcc, body, text. // Please note that folder names should to be in sync with $config['*_mbox'] options $config['search_mods'] = null; // Example: ['*' => ['subject'=>1, 'from'=>1], 'Sent' => ['subject'=>1, 'to'=>1]]; // Defaults of the addressbook search field configuration. $config['addressbook_search_mods'] = null; // Example: ['name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1]; // Directly delete messages in Junk instead of moving to Trash $config['delete_junk'] = false; // Behavior if a received message requests a message delivery notification (read receipt) // 0 = ask the user, // 1 = send automatically, // 2 = ignore (never send or ask) // 3 = send automatically if sender is in my contacts, otherwise ask the user // 4 = send automatically if sender is in my contacts, otherwise ignore // 5 = send automatically if sender is a trusted sender, otherwise ask the user // 6 = send automatically if sender is a trusted sender, otherwise ignore $config['mdn_requests'] = 0; // Return receipt checkbox default state $config['mdn_default'] = 0; // Delivery Status Notification checkbox default state // Note: This can be used only if smtp_server is non-empty $config['dsn_default'] = 0; // Place replies in the folder of the message being replied to $config['reply_same_folder'] = false; // Sets default mode of Forward feature to "forward as attachment" $config['forward_attachment'] = false; // Defines address book (internal index) to which new contacts will be added // By default it is the first writeable addressbook. // Note: Use '0' for built-in address book. $config['default_addressbook'] = null; // Enables spell checking before sending a message. $config['spellcheck_before_send'] = false; // Skip alternative email addresses in autocompletion (show one address per contact) $config['autocomplete_single'] = false; // Default font for composed HTML message. // Supported values: Andale Mono, Arial, Arial Black, Book Antiqua, Courier New, // Georgia, Helvetica, Impact, Tahoma, Terminal, Times New Roman, Trebuchet MS, Verdana $config['default_font'] = 'Verdana'; // Default font size for composed HTML message. // Supported sizes: 8pt, 10pt, 12pt, 14pt, 18pt, 24pt, 36pt $config['default_font_size'] = '10pt'; // Enables display of email address with name instead of a name (and address in title) $config['message_show_email'] = false; // Default behavior of Reply-All button: // 0 - Reply-All always // 1 - Reply-List if mailing list is detected $config['reply_all_mode'] = 0; diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 2848659af..ab0603fdd 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -1,4181 +1,4189 @@ | | Author: Ryo Chijiiwa | +-----------------------------------------------------------------------+ */ /** * PHP based wrapper class to connect to an IMAP server * * @package Framework * @subpackage Storage */ class rcube_imap_generic { public $error; public $errornum; public $result; public $resultcode; public $selected; public $data = []; public $flags = [ 'SEEN' => '\\Seen', 'DELETED' => '\\Deleted', 'ANSWERED' => '\\Answered', 'DRAFT' => '\\Draft', 'FLAGGED' => '\\Flagged', 'FORWARDED' => '$Forwarded', 'MDNSENT' => '$MDNSent', '*' => '\\*', ]; protected $fp; protected $host; protected $user; protected $cmd_tag; protected $cmd_num = 0; protected $resourceid; protected $extensions_enabled; protected $prefs = []; protected $logged = false; protected $capability = []; protected $capability_read = false; protected $debug = false; protected $debug_handler = false; const ERROR_OK = 0; const ERROR_NO = -1; const ERROR_BAD = -2; const ERROR_BYE = -3; const ERROR_UNKNOWN = -4; const ERROR_COMMAND = -5; const ERROR_READONLY = -6; const COMMAND_NORESPONSE = 1; const COMMAND_CAPABILITY = 2; const COMMAND_LASTLINE = 4; const COMMAND_ANONYMIZED = 8; const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n /** * Send simple (one line) command to the connection stream * * @param string $string Command string * @param bool $endln True if CRLF need to be added at the end of command * @param bool $anonymized Don't write the given data to log but a placeholder * * @param int Number of bytes sent, False on error */ protected function putLine($string, $endln = true, $anonymized = false) { if (!$this->fp) { return false; } if ($this->debug) { // anonymize the sent command for logging $cut = $endln ? 2 : 0; if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) { $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut); } else if ($anonymized) { $log = sprintf('****** [%d]', strlen($string) - $cut); } else { $log = rtrim($string); } $this->debug('C: ' . $log); } if ($endln) { $string .= "\r\n"; } $res = fwrite($this->fp, $string); if ($res === false) { $this->closeSocket(); } return $res; } /** * Send command to the connection stream with Command Continuation * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) and LITERAL- (RFC7888) support. * * @param string $string Command string * @param bool $endln True if CRLF need to be added at the end of command * @param bool $anonymized Don't write the given data to log but a placeholder * * @return int|bool Number of bytes sent, False on error */ protected function putLineC($string, $endln = true, $anonymized = false) { if (!$this->fp) { return false; } if ($endln) { $string .= "\r\n"; } $res = 0; if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) { for ($i = 0, $cnt = count($parts); $i < $cnt; $i++) { if ($i + 1 < $cnt && preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) { // LITERAL+/LITERAL- support $literal_plus = false; if ( !empty($this->prefs['literal+']) || (!empty($this->prefs['literal-']) && $matches[1] <= 4096) ) { $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]); $literal_plus = true; } $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized); if ($bytes === false) { return false; } $res += $bytes; // don't wait if server supports LITERAL+ capability if (!$literal_plus) { $line = $this->readLine(1000); // handle error in command if (!isset($line[0]) || $line[0] != '+') { return false; } } $i++; } else { $bytes = $this->putLine($parts[$i], false, $anonymized); if ($bytes === false) { return false; } $res += $bytes; } } } return $res; } /** * Reads line from the connection stream * * @param int $size Buffer size * * @return string Line of text response */ protected function readLine($size = 1024) { $line = ''; if (!$size) { $size = 1024; } do { if ($this->eof()) { return $line ?: null; } $buffer = fgets($this->fp, $size); if ($buffer === false) { $this->closeSocket(); break; } if ($this->debug) { $this->debug('S: '. rtrim($buffer)); } $line .= $buffer; } while (substr($buffer, -1) != "\n"); return $line; } /** * Reads a line of data from the connection stream including all * string continuation literals. * * @param int $size Buffer size * * @return string Line of text response */ protected function readFullLine($size = 1024) { $line = $this->readLine($size); // include all string literals untile the real end of "line" while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) { $bytes = $m[1]; $out = ''; while (strlen($out) < $bytes) { $out = $this->readBytes($bytes); if ($out === null) { break; } $line .= $out; } $line .= $this->readLine($size); } return $line; } /** * Reads more data from the connection stream when provided * data contain string literal * * @param string $line Response text * @param bool $escape Enables escaping * * @return string Line of text response */ protected function multLine($line, $escape = false) { $line = rtrim($line); if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { $out = ''; $str = substr($line, 0, -strlen($m[0])); $bytes = $m[1]; while (strlen($out) < $bytes) { $line = $this->readBytes($bytes); if ($line === null) { break; } $out .= $line; } $line = $str . ($escape ? $this->escape($out) : $out); } return $line; } /** * Reads specified number of bytes from the connection stream * * @param int $bytes Number of bytes to get * * @return string Response text */ protected function readBytes($bytes) { $data = ''; $len = 0; while ($len < $bytes && !$this->eof()) { $d = fread($this->fp, $bytes-$len); if ($this->debug) { $this->debug('S: '. $d); } $data .= $d; $data_len = strlen($data); if ($len == $data_len) { break; // nothing was read -> exit to avoid apache lockups } $len = $data_len; } return $data; } /** * Reads complete response to the IMAP command * * @param array $untagged Will be filled with untagged response lines * * @return string Response text */ protected function readReply(&$untagged = null) { while (true) { $line = trim($this->readLine(1024)); // store untagged response lines if (isset($line[0]) && $line[0] == '*') { $untagged[] = $line; } else { break; } } if ($untagged) { $untagged = implode("\n", $untagged); } return $line; } /** * Response parser. * * @param string $string Response text * @param string $err_prefix Error message prefix * * @return int Response status */ protected function parseResult($string, $err_prefix = '') { if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) { $res = strtoupper($matches[1]); $str = trim($matches[2]); if ($res == 'OK') { $this->errornum = self::ERROR_OK; } else if ($res == 'NO') { $this->errornum = self::ERROR_NO; } else if ($res == 'BAD') { $this->errornum = self::ERROR_BAD; } else if ($res == 'BYE') { $this->closeSocket(); $this->errornum = self::ERROR_BYE; } if ($str) { $str = trim($str); // get response string and code (RFC5530) if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) { $this->resultcode = strtoupper($m[1]); $str = trim(substr($str, strlen($m[1]) + 2)); } else { $this->resultcode = null; // parse response for [APPENDUID 1204196876 3456] if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) { $this->data['APPENDUID'] = $m[1]; } // parse response for [COPYUID 1204196876 3456:3457 123:124] else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) { $this->data['COPYUID'] = [$m[1], $m[2]]; } } $this->result = $str; if ($this->errornum != self::ERROR_OK) { $this->error = $err_prefix ? $err_prefix.$str : $str; } } return $this->errornum; } return self::ERROR_UNKNOWN; } /** * Checks connection stream state. * * @return bool True if connection is closed */ protected function eof() { if (!is_resource($this->fp)) { return true; } // If a connection opened by fsockopen() wasn't closed // by the server, feof() will hang. $start = microtime(true); if (feof($this->fp) || ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout'])) ) { $this->closeSocket(); return true; } return false; } /** * Closes connection stream. */ protected function closeSocket() { if ($this->fp) { fclose($this->fp); $this->fp = null; } } /** * Error code/message setter. */ protected function setError($code, $msg = '') { $this->errornum = $code; $this->error = $msg; return $code; } /** * Checks response status. * Checks if command response line starts with specified prefix (or * BYE/BAD) * * @param string $string Response text * @param string $match Prefix to match with (case-sensitive) * @param bool $error Enables BYE/BAD checking * @param bool $nonempty Enables empty response checking * * @return bool True any check is true or connection is closed. */ protected function startsWith($string, $match, $error = false, $nonempty = false) { if (!$this->fp) { return true; } if (strncmp($string, $match, strlen($match)) == 0) { return true; } if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) { if (strtoupper($m[1]) == 'BYE') { $this->closeSocket(); } return true; } if ($nonempty && !strlen($string)) { return true; } return false; } /** * Capabilities checker */ protected function hasCapability($name) { if (empty($this->capability) || empty($name)) { return false; } if (in_array($name, $this->capability)) { return true; } else if (strpos($name, '=')) { return false; } $result = []; foreach ($this->capability as $cap) { $entry = explode('=', $cap); if ($entry[0] == $name) { $result[] = $entry[1]; } } return $result ?: false; } /** * Capabilities checker * * @param string $name Capability name * * @return mixed Capability values array for key=value pairs, true/false for others */ public function getCapability($name) { $result = $this->hasCapability($name); if (!empty($result)) { return $result; } else if ($this->capability_read) { return false; } // get capabilities (only once) because initial // optional CAPABILITY response may differ $result = $this->execute('CAPABILITY'); if ($result[0] == self::ERROR_OK) { $this->parseCapability($result[1]); } $this->capability_read = true; return $this->hasCapability($name); } /** * Clears detected server capabilities */ public function clearCapability() { $this->capability = []; $this->capability_read = false; } /** * DIGEST-MD5/CRAM-MD5/PLAIN Authentication * * @param string $user Username * @param string $pass Password * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5) * * @return resource Connection resource on success, error code on error */ protected function authenticate($user, $pass, $type = 'PLAIN') { if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') { if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) { return $this->setError(self::ERROR_BYE, "The Auth_SASL package is required for DIGEST-MD5 authentication"); } $this->putLine($this->nextTag() . " AUTHENTICATE $type"); $line = trim($this->readReply()); if ($line[0] == '+') { $challenge = substr($line, 2); } else { return $this->parseResult($line); } if ($type == 'CRAM-MD5') { // RFC2195: CRAM-MD5 $ipad = ''; $opad = ''; $xor = function($str1, $str2) { $result = ''; $size = strlen($str1); for ($i=0; $i<$size; $i++) { $result .= chr(ord($str1[$i]) ^ ord($str2[$i])); } return $result; }; // initialize ipad, opad for ($i=0; $i<64; $i++) { $ipad .= chr(0x36); $opad .= chr(0x5C); } // pad $pass so it's 64 bytes $pass = str_pad($pass, 64, chr(0)); // generate hash $hash = md5($xor($pass, $opad) . pack("H*", md5($xor($pass, $ipad) . base64_decode($challenge)))); $reply = base64_encode($user . ' ' . $hash); // send result $this->putLine($reply, true, true); } else { // RFC2831: DIGEST-MD5 // proxy authorization if (!empty($this->prefs['auth_cid'])) { $authc = $this->prefs['auth_cid']; $pass = $this->prefs['auth_pw']; } else { $authc = $user; $user = ''; } $auth_sasl = new Auth_SASL; $auth_sasl = $auth_sasl->factory('digestmd5'); $reply = base64_encode($auth_sasl->getResponse($authc, $pass, base64_decode($challenge), $this->host, 'imap', $user)); // send result $this->putLine($reply, true, true); $line = trim($this->readReply()); if ($line[0] != '+') { return $this->parseResult($line); } // check response $challenge = substr($line, 2); $challenge = base64_decode($challenge); if (strpos($challenge, 'rspauth=') === false) { return $this->setError(self::ERROR_BAD, "Unexpected response from server to DIGEST-MD5 response"); } $this->putLine(''); } $line = $this->readReply(); $result = $this->parseResult($line); } else if ($type == 'GSSAPI') { if (!extension_loaded('krb5')) { return $this->setError(self::ERROR_BYE, "The krb5 extension is required for GSSAPI authentication"); } if (empty($this->prefs['gssapi_cn'])) { return $this->setError(self::ERROR_BYE, "The gssapi_cn parameter is required for GSSAPI authentication"); } if (empty($this->prefs['gssapi_context'])) { return $this->setError(self::ERROR_BYE, "The gssapi_context parameter is required for GSSAPI authentication"); } putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']); try { $ccache = new KRB5CCache(); $ccache->open($this->prefs['gssapi_cn']); $gssapicontext = new GSSAPIContext(); $gssapicontext->acquireCredentials($ccache); $token = ''; $success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token); $token = base64_encode($token); } catch (Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); } $this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token); $line = trim($this->readReply()); if ($line[0] != '+') { return $this->parseResult($line); } try { $itoken = base64_decode(substr($line, 2)); if (!$gssapicontext->unwrap($itoken, $itoken)) { throw new Exception("GSSAPI SASL input token unwrap failed"); } if (strlen($itoken) < 4) { throw new Exception("GSSAPI SASL input token invalid"); } // Integrity/encryption layers are not supported. The first bit // indicates that the server supports "no security layers". // 0x00 should not occur, but support broken implementations. $server_layers = ord($itoken[0]); if ($server_layers && ($server_layers & 0x1) != 0x1) { throw new Exception("Server requires GSSAPI SASL integrity/encryption"); } // Construct output token. 0x01 in the first octet = SASL layer "none", // zero in the following three octets = no data follows. // See https://github.com/cyrusimap/cyrus-sasl/blob/e41cfb986c1b1935770de554872247453fdbb079/plugins/gssapi.c#L1284 if (!$gssapicontext->wrap(pack("CCCC", 0x1, 0, 0, 0), $otoken, true)) { throw new Exception("GSSAPI SASL output token wrap failed"); } } catch (Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed"); } $this->putLine(base64_encode($otoken)); $line = $this->readReply(); $result = $this->parseResult($line); } else if ($type == 'PLAIN') { // proxy authorization if (!empty($this->prefs['auth_cid'])) { $authc = $this->prefs['auth_cid']; $pass = $this->prefs['auth_pw']; } else { $authc = $user; $user = ''; } $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass); // RFC 4959 (SASL-IR): save one round trip if ($this->getCapability('SASL-IR')) { list($result, $line) = $this->execute("AUTHENTICATE PLAIN", [$reply], self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); } else { $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN"); $line = trim($this->readReply()); if ($line[0] != '+') { return $this->parseResult($line); } // send result, get reply and process it $this->putLine($reply, true, true); $line = $this->readReply(); $result = $this->parseResult($line); } } else if ($type == 'LOGIN') { $this->putLine($this->nextTag() . " AUTHENTICATE LOGIN"); $line = trim($this->readReply()); if ($line[0] != '+') { return $this->parseResult($line); } $this->putLine(base64_encode($user), true, true); $line = trim($this->readReply()); if ($line[0] != '+') { return $this->parseResult($line); } // send result, get reply and process it $this->putLine(base64_encode($pass), true, true); $line = $this->readReply(); $result = $this->parseResult($line); } else if ($type == 'XOAUTH2') { $auth = base64_encode("user=$user\1auth=$pass\1\1"); $this->putLine($this->nextTag() . " AUTHENTICATE XOAUTH2 $auth", true, true); $line = trim($this->readReply()); if ($line[0] == '+') { // send empty line $this->putLine('', true, true); $line = $this->readReply(); } $result = $this->parseResult($line); } else { $line = 'not supported'; $result = self::ERROR_UNKNOWN; } if ($result === self::ERROR_OK) { // optional CAPABILITY response if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { $this->parseCapability($matches[1], true); } return $this->fp; } return $this->setError($result, "AUTHENTICATE $type: $line"); } /** * LOGIN Authentication * * @param string $user Username * @param string $pass Password * * @return resource Connection resource on success, error code on error */ protected function login($user, $password) { // Prevent from sending credentials in plain text when connection is not secure if ($this->getCapability('LOGINDISABLED')) { return $this->setError(self::ERROR_BAD, "Login disabled by IMAP server"); } list($code, $response) = $this->execute('LOGIN', [$this->escape($user), $this->escape($password)], self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); // re-set capabilities list if untagged CAPABILITY response provided if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) { $this->parseCapability($matches[1], true); } if ($code == self::ERROR_OK) { return $this->fp; } return $code; } /** * Detects hierarchy delimiter * * @return string The delimiter */ public function getHierarchyDelimiter() { if (!empty($this->prefs['delimiter'])) { return $this->prefs['delimiter']; } // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8) list($code, $response) = $this->execute('LIST', [$this->escape(''), $this->escape('')]); if ($code == self::ERROR_OK) { $args = $this->tokenizeResponse($response, 4); $delimiter = $args[3]; if (strlen($delimiter) > 0) { return ($this->prefs['delimiter'] = $delimiter); } } } /** * NAMESPACE handler (RFC 2342) * * @return array Namespace data hash (personal, other, shared) */ public function getNamespace() { if (array_key_exists('namespace', $this->prefs)) { return $this->prefs['namespace']; } if (!$this->getCapability('NAMESPACE')) { return self::ERROR_BAD; } list($code, $response) = $this->execute('NAMESPACE'); if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) { $response = substr($response, 11); $data = $this->tokenizeResponse($response); } if (!isset($data) || !is_array($data)) { return $code; } $this->prefs['namespace'] = [ 'personal' => $data[0], 'other' => $data[1], 'shared' => $data[2], ]; return $this->prefs['namespace']; } /** * Connects to IMAP server and authenticates. * * @param string $host Server hostname or IP * @param string $user User name * @param string $password Password * @param array $options Connection and class options * * @return bool True on success, False on failure */ public function connect($host, $user, $password, $options = []) { // configure $this->set_prefs($options); $this->host = $host; $this->user = $user; $this->logged = false; $this->selected = null; // check input if (empty($host)) { $this->setError(self::ERROR_BAD, "Empty host"); return false; } if (empty($user)) { $this->setError(self::ERROR_NO, "Empty user"); return false; } if (empty($password) && empty($options['gssapi_cn'])) { $this->setError(self::ERROR_NO, "Empty password"); return false; } // Connect if (!$this->_connect($host)) { return false; } // Send pre authentication ID info (#7860) if (!empty($this->prefs['preauth_ident']) && $this->getCapability('ID')) { $this->data['ID'] = $this->id($this->prefs['preauth_ident']); } $auth_method = $this->prefs['auth_type']; $auth_methods = []; $result = null; // check for supported auth methods if (!$auth_method || $auth_method == 'CHECK') { if ($auth_caps = $this->getCapability('AUTH')) { $auth_methods = $auth_caps; } // Use best (for security) supported authentication method $all_methods = ['DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN']; if (!empty($this->prefs['gssapi_cn'])) { array_unshift($all_methods, 'GSSAPI'); } foreach ($all_methods as $auth_method) { if (in_array($auth_method, $auth_methods)) { break; } } // Prefer LOGIN over AUTHENTICATE LOGIN for performance reasons if ($auth_method == 'LOGIN' && !$this->getCapability('LOGINDISABLED')) { $auth_method = 'IMAP'; } } // pre-login capabilities can be not complete $this->capability_read = false; // Authenticate switch ($auth_method) { case 'CRAM_MD5': $auth_method = 'CRAM-MD5'; case 'CRAM-MD5': case 'DIGEST-MD5': case 'GSSAPI': case 'PLAIN': case 'LOGIN': case 'XOAUTH2': $result = $this->authenticate($user, $password, $auth_method); break; case 'IMAP': $result = $this->login($user, $password); break; default: $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method"); } // Connected and authenticated if (is_resource($result)) { if (!empty($this->prefs['force_caps'])) { $this->clearCapability(); } $this->logged = true; // Send ID info after authentication to ensure reliable result (#7517) if (!empty($this->prefs['ident']) && $this->getCapability('ID')) { $this->data['ID'] = $this->id($this->prefs['ident']); } return true; } $this->closeConnection(); return false; } /** * Connects to IMAP server. * * @param string $host Server hostname or IP * * @return bool True on success, False on failure */ protected function _connect($host) { // initialize connection $this->error = ''; $this->errornum = self::ERROR_OK; if (empty($this->prefs['port'])) { $this->prefs['port'] = 143; } // check for SSL if (!empty($this->prefs['ssl_mode']) && $this->prefs['ssl_mode'] != 'tls') { $host = $this->prefs['ssl_mode'] . '://' . $host; } if (empty($this->prefs['timeout']) || $this->prefs['timeout'] < 0) { $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); } if ($this->debug) { // set connection identifier for debug output $this->resourceid = strtoupper(substr(md5(microtime() . $host . $this->user), 0, 4)); $_host = ($this->prefs['ssl_mode'] == 'tls' ? 'tls://' : '') . $host . ':' . $this->prefs['port']; $this->debug("Connecting to $_host..."); } if (!empty($this->prefs['socket_options'])) { $options = array_intersect_key($this->prefs['socket_options'], ['ssl' => 1]); $context = stream_context_create($options); $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr, $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context); } else { $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); } if (!$this->fp) { $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr ?: "Unknown reason")); return false; } + // insert proxy protocol header, if enabled + if (!empty($this->prefs['socket_options'])) { + $proxy_protocol_header = rcube_utils::proxy_protocol_header($this->prefs['socket_options'], $this->fp); + if (strlen($proxy_protocol_header) > 0) { + fwrite($this->fp, $proxy_protocol_header); + } + } + if ($this->prefs['timeout'] > 0) { stream_set_timeout($this->fp, $this->prefs['timeout']); } $line = trim(fgets($this->fp, 8192)); if ($this->debug && $line) { $this->debug('S: '. $line); } // Connected to wrong port or connection error? if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { if ($line) $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); else $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); $this->setError(self::ERROR_BAD, $error); $this->closeConnection(); return false; } $this->data['GREETING'] = trim(preg_replace('/\[[^\]]+\]\s*/', '', $line)); // RFC3501 [7.1] optional CAPABILITY response if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { $this->parseCapability($matches[1], true); } // TLS connection if (isset($this->prefs['ssl_mode']) && $this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { $res = $this->execute('STARTTLS'); if (empty($res) || $res[0] != self::ERROR_OK) { $this->closeConnection(); return false; } if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) { $crypto_method = $this->prefs['socket_options']['ssl']['crypto_method']; } else { // There is no flag to enable all TLS methods. Net_SMTP // handles enabling TLS similarly. $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; } if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) { $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); $this->closeConnection(); return false; } // Now we're secure, capabilities need to be reread $this->clearCapability(); } return true; } /** * Initializes environment */ protected function set_prefs($prefs) { // set preferences if (is_array($prefs)) { $this->prefs = $prefs; } // set auth method if (!empty($this->prefs['auth_type'])) { $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']); } else { $this->prefs['auth_type'] = 'CHECK'; } // disabled capabilities if (!empty($this->prefs['disabled_caps'])) { $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']); } // additional message flags if (!empty($this->prefs['message_flags'])) { $this->flags = array_merge($this->flags, $this->prefs['message_flags']); unset($this->prefs['message_flags']); } } /** * Checks connection status * * @return bool True if connection is active and user is logged in, False otherwise. */ public function connected() { return $this->fp && $this->logged; } /** * Closes connection with logout. */ public function closeConnection() { if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) { $this->readReply(); } $this->closeSocket(); } /** * Executes SELECT command (if mailbox is already not in selected state) * * @param string $mailbox Mailbox name * @param array $qresync_data QRESYNC data (RFC5162) * * @return bool True on success, false on error */ public function select($mailbox, $qresync_data = null) { if (!strlen($mailbox)) { return false; } if ($this->selected === $mailbox) { return true; } $params = [$this->escape($mailbox)]; // QRESYNC data items // 0. the last known UIDVALIDITY, // 1. the last known modification sequence, // 2. the optional set of known UIDs, and // 3. an optional parenthesized list of known sequence ranges and their // corresponding UIDs. if (!empty($qresync_data)) { if (!empty($qresync_data[2])) { $qresync_data[2] = self::compressMessageSet($qresync_data[2]); } $params[] = ['QRESYNC', $qresync_data]; } list($code, $response) = $this->execute('SELECT', $params); if ($code == self::ERROR_OK) { $this->clear_mailbox_cache(); $response = explode("\r\n", $response); foreach ($response as $line) { if (preg_match('/^\* OK \[/i', $line)) { $pos = strcspn($line, ' ]', 6); $token = strtoupper(substr($line, 6, $pos)); $pos += 7; switch ($token) { case 'UIDNEXT': case 'UIDVALIDITY': case 'UNSEEN': if ($len = strspn($line, '0123456789', $pos)) { $this->data[$token] = (int) substr($line, $pos, $len); } break; case 'HIGHESTMODSEQ': if ($len = strspn($line, '0123456789', $pos)) { $this->data[$token] = (string) substr($line, $pos, $len); } break; case 'NOMODSEQ': $this->data[$token] = true; break; case 'PERMANENTFLAGS': $start = strpos($line, '(', $pos); $end = strrpos($line, ')'); if ($start && $end) { $flags = substr($line, $start + 1, $end - $start - 1); $this->data[$token] = explode(' ', $flags); } break; } } else if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) { $token = strtoupper($match[2]); switch ($token) { case 'EXISTS': case 'RECENT': $this->data[$token] = (int) $match[1]; break; case 'FETCH': // QRESYNC FETCH response (RFC5162) $line = substr($line, strlen($match[0])); $fetch_data = $this->tokenizeResponse($line, 1); $data = ['id' => $match[1]]; for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) { $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1]; } $this->data['QRESYNC'][$data['uid']] = $data; break; } } // QRESYNC VANISHED response (RFC5162) else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { $line = substr($line, strlen($match[0])); $v_data = $this->tokenizeResponse($line, 1); $this->data['VANISHED'] = $v_data; } } $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY'; $this->selected = $mailbox; return true; } return false; } /** * Executes STATUS command * * @param string $mailbox Mailbox name * @param array $items Additional requested item names. By default * MESSAGES and UNSEEN are requested. Other defined * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT * * @return array Status item-value hash * @since 0.5-beta */ public function status($mailbox, $items = []) { if (!strlen($mailbox)) { return false; } if (!in_array('MESSAGES', $items)) { $items[] = 'MESSAGES'; } if (!in_array('UNSEEN', $items)) { $items[] = 'UNSEEN'; } list($code, $response) = $this->execute('STATUS', [$this->escape($mailbox), '(' . implode(' ', $items) . ')'], 0, '/^\* STATUS /i'); if ($code == self::ERROR_OK && $response) { $result = []; $response = substr($response, 9); // remove prefix "* STATUS " list($mbox, $items) = $this->tokenizeResponse($response, 2); // Fix for #1487859. Some buggy server returns not quoted // folder name with spaces. Let's try to handle this situation if (!is_array($items) && ($pos = strpos($response, '(')) !== false) { $response = substr($response, $pos); $items = $this->tokenizeResponse($response, 1); } if (!is_array($items)) { return $result; } for ($i=0, $len=count($items); $i<$len; $i += 2) { $result[$items[$i]] = $items[$i+1]; } $this->data['STATUS:'.$mailbox] = $result; return $result; } return false; } /** * Executes EXPUNGE command * * @param string $mailbox Mailbox name * @param string|array $messages Message UIDs to expunge * * @return bool True on success, False on error */ public function expunge($mailbox, $messages = null) { if (!$this->select($mailbox)) { return false; } if (empty($this->data['READ-WRITE'])) { $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } // Clear internal status cache $this->clear_status_cache($mailbox); if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) { $messages = self::compressMessageSet($messages); $result = $this->execute('UID EXPUNGE', [$messages], self::COMMAND_NORESPONSE); } else { $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); } if ($result == self::ERROR_OK) { $this->selected = null; // state has changed, need to reselect return true; } return false; } /** * Executes CLOSE command * * @return bool True on success, False on error * @since 0.5 */ public function close() { $result = $this->execute('CLOSE', null, self::COMMAND_NORESPONSE); if ($result == self::ERROR_OK) { $this->selected = null; return true; } return false; } /** * Folder subscription (SUBSCRIBE) * * @param string $mailbox Mailbox name * * @return bool True on success, False on error */ public function subscribe($mailbox) { $result = $this->execute('SUBSCRIBE', [$this->escape($mailbox)], self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Folder unsubscription (UNSUBSCRIBE) * * @param string $mailbox Mailbox name * * @return bool True on success, False on error */ public function unsubscribe($mailbox) { $result = $this->execute('UNSUBSCRIBE', [$this->escape($mailbox)], self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Folder creation (CREATE) * * @param string $mailbox Mailbox name * @param array $types Optional folder types (RFC 6154) * * @return bool True on success, False on error */ public function createFolder($mailbox, $types = null) { $args = [$this->escape($mailbox)]; // RFC 6154: CREATE-SPECIAL-USE if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) { $args[] = '(USE (' . implode(' ', $types) . '))'; } $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Folder renaming (RENAME) * * @param string $mailbox Mailbox name * * @return bool True on success, False on error */ public function renameFolder($from, $to) { $result = $this->execute('RENAME', [$this->escape($from), $this->escape($to)], self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Executes DELETE command * * @param string $mailbox Mailbox name * * @return bool True on success, False on error */ public function deleteFolder($mailbox) { $result = $this->execute('DELETE', [$this->escape($mailbox)], self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Removes all messages in a folder * * @param string $mailbox Mailbox name * * @return bool True on success, False on error */ public function clearFolder($mailbox) { if ($this->countMessages($mailbox) > 0) { $res = $this->flag($mailbox, '1:*', 'DELETED'); } else { return true; } if (!empty($res)) { if ($this->selected === $mailbox) { $res = $this->close(); } else { $res = $this->expunge($mailbox); } return $res; } return false; } /** * Returns list of mailboxes * * @param string $ref Reference name * @param string $mailbox Mailbox name * @param array $return_opts (see self::_listMailboxes) * @param array $select_opts (see self::_listMailboxes) * * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response * is requested, False on error. */ public function listMailboxes($ref, $mailbox, $return_opts = [], $select_opts = []) { return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts); } /** * Returns list of subscribed mailboxes * * @param string $ref Reference name * @param string $mailbox Mailbox name * @param array $return_opts (see self::_listMailboxes) * * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response * is requested, False on error. */ public function listSubscribed($ref, $mailbox, $return_opts = []) { return $this->_listMailboxes($ref, $mailbox, true, $return_opts, null); } /** * IMAP LIST/LSUB command * * @param string $ref Reference name * @param string $mailbox Mailbox name * @param bool $subscribed Enables returning subscribed mailboxes only * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED) * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN, * MYRIGHTS, SUBSCRIBED, CHILDREN * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE, * SPECIAL-USE (RFC6154) * * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response * is requested, False on error. */ protected function _listMailboxes($ref, $mailbox, $subscribed = false, $return_opts = [], $select_opts = []) { if (!strlen($mailbox)) { $mailbox = '*'; } $lstatus = false; $args = []; $rets = []; if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) { $select_opts = (array) $select_opts; $args[] = '(' . implode(' ', $select_opts) . ')'; } $args[] = $this->escape($ref); $args[] = $this->escape($mailbox); if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) { $ext_opts = ['SUBSCRIBED', 'CHILDREN']; $rets = array_intersect($return_opts, $ext_opts); $return_opts = array_diff($return_opts, $rets); } if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) { $lstatus = true; $status_opts = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN', 'SIZE']; $opts = array_diff($return_opts, $status_opts); $status_opts = array_diff($return_opts, $opts); if (!empty($status_opts)) { $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')'; } if (!empty($opts)) { $rets = array_merge($rets, $opts); } } if (!empty($rets)) { $args[] = 'RETURN (' . implode(' ', $rets) . ')'; } list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args); if ($code == self::ERROR_OK) { $folders = []; $last = 0; $pos = 0; $response .= "\r\n"; while ($pos = strpos($response, "\r\n", $pos+1)) { // literal string, not real end-of-command-line if ($response[$pos-1] == '}') { continue; } $line = substr($response, $last, $pos - $last); $last = $pos + 2; if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) { continue; } $cmd = strtoupper($m[1]); $line = substr($line, strlen($m[0])); // * LIST () if ($cmd == 'LIST' || $cmd == 'LSUB') { list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3); // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879) if ($delim) { $mailbox = rtrim($mailbox, $delim); } // Add to result array if (!$lstatus) { $folders[] = $mailbox; } else { $folders[$mailbox] = []; } // store folder options if ($cmd == 'LIST') { // Add to options array if (empty($this->data['LIST'][$mailbox])) { $this->data['LIST'][$mailbox] = $opts; } else if (!empty($opts)) { $this->data['LIST'][$mailbox] = array_unique(array_merge( $this->data['LIST'][$mailbox], $opts)); } } } else if ($lstatus) { // * STATUS () if ($cmd == 'STATUS') { list($mailbox, $status) = $this->tokenizeResponse($line, 2); for ($i=0, $len=count($status); $i<$len; $i += 2) { list($name, $value) = $this->tokenizeResponse($status, 2); $folders[$mailbox][$name] = $value; } } // * MYRIGHTS else if ($cmd == 'MYRIGHTS') { list($mailbox, $acl) = $this->tokenizeResponse($line, 2); $folders[$mailbox]['MYRIGHTS'] = $acl; } } } return $folders; } return false; } /** * Returns count of all messages in a folder * * @param string $mailbox Mailbox name * * @return int Number of messages, False on error */ public function countMessages($mailbox) { if ($this->selected === $mailbox && isset($this->data['EXISTS'])) { return $this->data['EXISTS']; } // Check internal cache if (!empty($this->data['STATUS:'.$mailbox])) { $cache = $this->data['STATUS:'.$mailbox]; if (isset($cache['MESSAGES'])) { return (int) $cache['MESSAGES']; } } // Try STATUS (should be faster than SELECT) $counts = $this->status($mailbox); if (is_array($counts)) { return (int) $counts['MESSAGES']; } return false; } /** * Returns count of messages with \Recent flag in a folder * * @param string $mailbox Mailbox name * * @return int Number of messages, False on error */ public function countRecent($mailbox) { if ($this->selected === $mailbox && isset($this->data['RECENT'])) { return $this->data['RECENT']; } // Check internal cache $cache = $this->data['STATUS:'.$mailbox]; if (!empty($cache) && isset($cache['RECENT'])) { return (int) $cache['RECENT']; } // Try STATUS (should be faster than SELECT) $counts = $this->status($mailbox, ['RECENT']); if (is_array($counts)) { return (int) $counts['RECENT']; } return false; } /** * Returns count of messages without \Seen flag in a specified folder * * @param string $mailbox Mailbox name * * @return int Number of messages, False on error */ public function countUnseen($mailbox) { // Check internal cache if (!empty($this->data['STATUS:'.$mailbox])) { $cache = $this->data['STATUS:'.$mailbox]; if (isset($cache['UNSEEN'])) { return (int) $cache['UNSEEN']; } } // Try STATUS (should be faster than SELECT+SEARCH) $counts = $this->status($mailbox); if (is_array($counts)) { return (int) $counts['UNSEEN']; } // Invoke SEARCH as a fallback $index = $this->search($mailbox, 'ALL UNSEEN', false, ['COUNT']); if (!$index->is_error()) { return $index->count(); } return false; } /** * Executes ID command (RFC2971) * * @param array $items Client identification information key/value hash * * @return array|false Server identification information key/value hash, False on error * @since 0.6 */ public function id($items = []) { if (is_array($items) && !empty($items)) { foreach ($items as $key => $value) { $args[] = $this->escape($key, true); $args[] = $this->escape($value, true); } } list($code, $response) = $this->execute('ID', [!empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)], 0, '/^\* ID /i' ); if ($code == self::ERROR_OK && $response) { $response = substr($response, 5); // remove prefix "* ID " $items = $this->tokenizeResponse($response, 1); $result = []; if (is_array($items)) { for ($i=0, $len=count($items); $i<$len; $i += 2) { $result[$items[$i]] = $items[$i+1]; } } return $result; } return false; } /** * Executes ENABLE command (RFC5161) * * @param mixed $extension Extension name to enable (or array of names) * * @return array|bool List of enabled extensions, False on error * @since 0.6 */ public function enable($extension) { if (empty($extension)) { return false; } if (!$this->hasCapability('ENABLE')) { return false; } if (!is_array($extension)) { $extension = [$extension]; } if (!empty($this->extensions_enabled)) { // check if all extensions are already enabled $diff = array_diff($extension, $this->extensions_enabled); if (empty($diff)) { return $extension; } // Make sure the mailbox isn't selected, before enabling extension(s) if ($this->selected !== null) { $this->close(); } } list($code, $response) = $this->execute('ENABLE', $extension, 0, '/^\* ENABLED /i'); if ($code == self::ERROR_OK && $response) { $response = substr($response, 10); // remove prefix "* ENABLED " $result = (array) $this->tokenizeResponse($response); $this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result)); return $this->extensions_enabled; } return false; } /** * Executes SORT command * * @param string $mailbox Mailbox name * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) * @param string $criteria Searching criteria * @param bool $return_uid Enables UID SORT usage * @param string $encoding Character set * * @return rcube_result_index Response data */ public function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII') { $old_sel = $this->selected; $supported = ['ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO']; $field = strtoupper($field); if ($field == 'INTERNALDATE') { $field = 'ARRIVAL'; } if (!in_array($field, $supported)) { return new rcube_result_index($mailbox); } if (!$this->select($mailbox)) { return new rcube_result_index($mailbox); } // return empty result when folder is empty and we're just after SELECT if ($old_sel != $mailbox && empty($this->data['EXISTS'])) { return new rcube_result_index($mailbox, '* SORT'); } // RFC 5957: SORT=DISPLAY if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) { $field = 'DISPLAY' . $field; } $encoding = $encoding ? trim($encoding) : 'US-ASCII'; $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL'; list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT', ["($field)", $encoding, $criteria]); if ($code != self::ERROR_OK) { $response = null; } return new rcube_result_index($mailbox, $response); } /** * Executes THREAD command * * @param string $mailbox Mailbox name * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS) * @param string $criteria Searching criteria * @param bool $return_uid Enables UIDs in result instead of sequence numbers * @param string $encoding Character set * * @return rcube_result_thread Thread data */ public function thread($mailbox, $algorithm = 'REFERENCES', $criteria = '', $return_uid = false, $encoding = 'US-ASCII') { $old_sel = $this->selected; if (!$this->select($mailbox)) { return new rcube_result_thread($mailbox); } // return empty result when folder is empty and we're just after SELECT if ($old_sel != $mailbox && !$this->data['EXISTS']) { return new rcube_result_thread($mailbox, '* THREAD'); } $encoding = $encoding ? trim($encoding) : 'US-ASCII'; $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES'; $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL'; list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD', [$algorithm, $encoding, $criteria]); if ($code != self::ERROR_OK) { $response = null; } return new rcube_result_thread($mailbox, $response); } /** * Executes SEARCH command * * @param string $mailbox Mailbox name * @param string $criteria Searching criteria * @param bool $return_uid Enable UID in result instead of sequence ID * @param array $items Return items (MIN, MAX, COUNT, ALL) * * @return rcube_result_index Result data */ public function search($mailbox, $criteria, $return_uid = false, $items = []) { $old_sel = $this->selected; if (!$this->select($mailbox)) { return new rcube_result_index($mailbox); } // return empty result when folder is empty and we're just after SELECT if ($old_sel != $mailbox && !$this->data['EXISTS']) { return new rcube_result_index($mailbox, '* SEARCH'); } // If ESEARCH is supported always use ALL // but not when items are specified or using simple id2uid search if (empty($items) && preg_match('/[^0-9]/', $criteria)) { $items = ['ALL']; } $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); $criteria = trim($criteria); $params = ''; // RFC4731: ESEARCH if (!empty($items) && $esearch) { $params .= 'RETURN (' . implode(' ', $items) . ')'; } if (!empty($criteria)) { $params .= ($params ? ' ' : '') . $criteria; } else { $params .= 'ALL'; } list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', [$params]); if ($code != self::ERROR_OK) { $response = null; } return new rcube_result_index($mailbox, $response); } /** * Simulates SORT command by using FETCH and sorting. * * @param string $mailbox Mailbox name * @param string|array $message_set Searching criteria (list of messages to return) * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) * @param bool $skip_deleted Makes that DELETED messages will be skipped * @param bool $uidfetch Enables UID FETCH usage * @param bool $return_uid Enables returning UIDs instead of IDs * * @return rcube_result_index Response data */ public function index($mailbox, $message_set, $index_field = '', $skip_deleted = true, $uidfetch = false, $return_uid = false) { $msg_index = $this->fetchHeaderIndex($mailbox, $message_set, $index_field, $skip_deleted, $uidfetch, $return_uid); if (!empty($msg_index)) { asort($msg_index); // ASC $msg_index = array_keys($msg_index); $msg_index = '* SEARCH ' . implode(' ', $msg_index); } else { $msg_index = is_array($msg_index) ? '* SEARCH' : null; } return new rcube_result_index($mailbox, $msg_index); } /** * Fetches specified header/data value for a set of messages. * * @param string $mailbox Mailbox name * @param string|array $message_set Searching criteria (list of messages to return) * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) * @param bool $skip_deleted Makes that DELETED messages will be skipped * @param bool $uidfetch Enables UID FETCH usage * @param bool $return_uid Enables returning UIDs instead of IDs * * @return array|bool List of header values or False on failure */ public function fetchHeaderIndex($mailbox, $message_set, $index_field = '', $skip_deleted = true, $uidfetch = false, $return_uid = false) { // Validate input if (is_array($message_set)) { if (!($message_set = $this->compressMessageSet($message_set))) { return false; } } else if (empty($message_set)) { return false; } else if (strpos($message_set, ':')) { list($from_idx, $to_idx) = explode(':', $message_set); if ($to_idx != '*' && (int) $from_idx > (int) $to_idx) { return false; } } $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field); $supported = [ 'DATE' => 1, 'INTERNALDATE' => 4, 'ARRIVAL' => 4, 'FROM' => 1, 'REPLY-TO' => 1, 'SENDER' => 1, 'TO' => 1, 'CC' => 1, 'SUBJECT' => 1, 'UID' => 2, 'SIZE' => 2, 'SEEN' => 3, 'RECENT' => 3, 'DELETED' => 3, ]; if (empty($supported[$index_field])) { return false; } $mode = $supported[$index_field]; // Select the mailbox if (!$this->select($mailbox)) { return false; } // build FETCH command string $key = $this->nextTag(); $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; $fields = []; if ($return_uid) { $fields[] = 'UID'; } if ($skip_deleted) { $fields[] = 'FLAGS'; } if ($mode == 1) { if ($index_field == 'DATE') { $fields[] = 'INTERNALDATE'; } $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]"; } else if ($mode == 2) { if ($index_field == 'SIZE') { $fields[] = 'RFC822.SIZE'; } else if (!$return_uid || $index_field != 'UID') { $fields[] = $index_field; } } else if ($mode == 3 && !$skip_deleted) { $fields[] = 'FLAGS'; } else if ($mode == 4) { $fields[] = 'INTERNALDATE'; } $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")"; if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); return false; } $result = []; do { $line = rtrim($this->readLine(200)); $line = $this->multLine($line); if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { $id = $m[1]; $flags = null; if ($return_uid) { if (preg_match('/UID ([0-9]+)/', $line, $matches)) { $id = (int) $matches[1]; } else { continue; } } if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { $flags = explode(' ', strtoupper($matches[1])); if (in_array('\\DELETED', $flags)) { continue; } } if ($mode == 1 && $index_field == 'DATE') { if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) { $value = preg_replace(['/^"*[a-z]+:/i'], '', $matches[1]); $value = trim($value); $result[$id] = rcube_utils::strtotime($value); } // non-existent/empty Date: header, use INTERNALDATE if (empty($result[$id])) { if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { $result[$id] = rcube_utils::strtotime($matches[1]); } else { $result[$id] = 0; } } } else if ($mode == 1) { if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) { $value = preg_replace(['/^"*[a-z]+:/i', '/\s+$/sm'], ['', ''], $matches[2]); $result[$id] = trim($value); } else { $result[$id] = ''; } } else if ($mode == 2) { if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { $result[$id] = trim($matches[1]); } else { $result[$id] = 0; } } else if ($mode == 3) { if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { $flags = explode(' ', $matches[1]); } $result[$id] = in_array("\\".$index_field, (array) $flags) ? 1 : 0; } else if ($mode == 4) { if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { $result[$id] = rcube_utils::strtotime($matches[1]); } else { $result[$id] = 0; } } } } while (!$this->startsWith($line, $key, true, true)); return $result; } /** * Returns message sequence identifier * * @param string $mailbox Mailbox name * @param int $uid Message unique identifier (UID) * * @return int Message sequence identifier */ public function UID2ID($mailbox, $uid) { if ($uid > 0) { $index = $this->search($mailbox, "UID $uid"); if ($index->count() == 1) { $arr = $index->get(); return (int) $arr[0]; } } } /** * Returns message unique identifier (UID) * * @param string $mailbox Mailbox name * @param int $uid Message sequence identifier * * @return int Message unique identifier */ public function ID2UID($mailbox, $id) { if (empty($id) || $id < 0) { return null; } if (!$this->select($mailbox)) { return null; } if (!empty($this->data['UID-MAP'][$id])) { return $this->data['UID-MAP'][$id]; } if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) { return null; } $index = $this->search($mailbox, $id, true); if ($index->count() == 1) { $arr = $index->get(); return $this->data['UID-MAP'][$id] = (int) $arr[0]; } } /** * Sets flag of the message(s) * * @param string $mailbox Mailbox name * @param string|array $messages Message UID(s) * @param string $flag Flag name * * @return bool True on success, False on failure */ public function flag($mailbox, $messages, $flag) { return $this->modFlag($mailbox, $messages, $flag, '+'); } /** * Unsets flag of the message(s) * * @param string $mailbox Mailbox name * @param string|array $messages Message UID(s) * @param string $flag Flag name * * @return bool True on success, False on failure */ public function unflag($mailbox, $messages, $flag) { return $this->modFlag($mailbox, $messages, $flag, '-'); } /** * Changes flag of the message(s) * * @param string $mailbox Mailbox name * @param string|array $messages Message UID(s) * @param string $flag Flag name * @param string $mod Modifier [+|-]. Default: "+". * * @return bool True on success, False on failure */ protected function modFlag($mailbox, $messages, $flag, $mod = '+') { if (!$flag) { return false; } if (!$this->select($mailbox)) { return false; } if (empty($this->data['READ-WRITE'])) { $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } if (!empty($this->flags[strtoupper($flag)])) { $flag = $this->flags[strtoupper($flag)]; } // if PERMANENTFLAGS is not specified all flags are allowed if (!empty($this->data['PERMANENTFLAGS']) && !in_array($flag, (array) $this->data['PERMANENTFLAGS']) && !in_array('\\*', (array) $this->data['PERMANENTFLAGS']) ) { return false; } // Clear internal status cache if ($flag == 'SEEN') { unset($this->data['STATUS:'.$mailbox]['UNSEEN']); } if ($mod != '+' && $mod != '-') { $mod = '+'; } $result = $this->execute('UID STORE', [$this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } /** * Copies message(s) from one folder to another * * @param string|array $messages Message UID(s) * @param string $from Mailbox name * @param string $to Destination mailbox name * * @return bool True on success, False on failure */ public function copy($messages, $from, $to) { // Clear last COPYUID data unset($this->data['COPYUID']); if (!$this->select($from)) { return false; } // Clear internal status cache unset($this->data['STATUS:'.$to]); $result = $this->execute('UID COPY', [$this->compressMessageSet($messages), $this->escape($to)], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } /** * Moves message(s) from one folder to another. * * @param string|array $messages Message UID(s) * @param string $from Mailbox name * @param string $to Destination mailbox name * * @return bool True on success, False on failure */ public function move($messages, $from, $to) { if (!$this->select($from)) { return false; } if (empty($this->data['READ-WRITE'])) { $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } // use MOVE command (RFC 6851) if ($this->hasCapability('MOVE')) { // Clear last COPYUID data unset($this->data['COPYUID']); // Clear internal status cache unset($this->data['STATUS:'.$to]); $this->clear_status_cache($from); $result = $this->execute('UID MOVE', [$this->compressMessageSet($messages), $this->escape($to)], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE $result = $this->copy($messages, $from, $to); if ($result) { // Clear internal status cache unset($this->data['STATUS:'.$from]); $result = $this->flag($from, $messages, 'DELETED'); if ($messages == '*') { // CLOSE+SELECT should be faster than EXPUNGE $this->close(); } else { $this->expunge($from, $messages); } } return $result; } /** * FETCH command (RFC3501) * * @param string $mailbox Mailbox name * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) * @param bool $is_uid True if $message_set contains UIDs * @param array $query_items FETCH command data items * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query * * @return array List of rcube_message_header elements, False on error * @since 0.6 */ public function fetch($mailbox, $message_set, $is_uid = false, $query_items = [], $mod_seq = null, $vanished = false) { if (!$this->select($mailbox)) { return false; } $message_set = $this->compressMessageSet($message_set); $result = []; $key = $this->nextTag(); $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; $request = "$key $cmd $message_set (" . implode(' ', $query_items) . ")"; if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) { $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")"; } if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); return false; } do { $line = $this->readFullLine(4096); if (!$line) { break; } // Sample reply line: // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen) // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...) // BODY[HEADER.FIELDS ... if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { $id = intval($m[1]); $result[$id] = new rcube_message_header; $result[$id]->id = $id; $result[$id]->subject = ''; $result[$id]->messageID = 'mid:' . $id; $headers = null; $lines = []; $line = substr($line, strlen($m[0]) + 2); $ln = 0; // Tokenize response and assign to object properties while (($tokens = $this->tokenizeResponse($line, 2)) && count($tokens) == 2) { list($name, $value) = $tokens; if ($name == 'UID') { $result[$id]->uid = intval($value); } else if ($name == 'RFC822.SIZE') { $result[$id]->size = intval($value); } else if ($name == 'RFC822.TEXT') { $result[$id]->body = $value; } else if ($name == 'INTERNALDATE') { $result[$id]->internaldate = $value; $result[$id]->date = $value; $result[$id]->timestamp = rcube_utils::strtotime($value); } else if ($name == 'FLAGS') { if (!empty($value)) { foreach ((array)$value as $flag) { $flag = str_replace(['$', "\\"], '', $flag); $flag = strtoupper($flag); $result[$id]->flags[$flag] = true; } } } else if ($name == 'MODSEQ') { $result[$id]->modseq = $value[0]; } else if ($name == 'ENVELOPE') { $result[$id]->envelope = $value; } else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) { if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) { $value = [$value]; } $result[$id]->bodystructure = $value; } else if ($name == 'RFC822') { $result[$id]->body = $value; } else if (stripos($name, 'BODY[') === 0) { $name = str_replace(']', '', substr($name, 5)); if ($name == 'HEADER.FIELDS') { // skip ']' after headers list $this->tokenizeResponse($line, 1); $headers = $this->tokenizeResponse($line, 1); } else if (strlen($name)) { $result[$id]->bodypart[$name] = $value; } else { $result[$id]->body = $value; } } } // create array with header field:data if (!empty($headers)) { $headers = explode("\n", trim($headers)); foreach ($headers as $resln) { if (ord($resln[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); } else { $lines[++$ln] = trim($resln); } } foreach ($lines as $str) { list($field, $string) = explode(':', $str, 2); $field = strtolower($field); $string = preg_replace('/\n[\t\s]*/', ' ', trim($string)); switch ($field) { case 'date'; $string = substr($string, 0, 128); $result[$id]->date = $string; $result[$id]->timestamp = rcube_utils::strtotime($string); break; case 'to': $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string); break; case 'from': case 'subject': $string = substr($string, 0, 2048); case 'cc': case 'bcc': case 'references': $result[$id]->{$field} = $string; break; case 'reply-to': $result[$id]->replyto = $string; break; case 'content-transfer-encoding': $result[$id]->encoding = substr($string, 0, 32); break; case 'content-type': $ctype_parts = preg_split('/[; ]+/', $string); $result[$id]->ctype = strtolower(array_first($ctype_parts)); if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) { $result[$id]->charset = $regs[1]; } break; case 'in-reply-to': $result[$id]->in_reply_to = str_replace(["\n", '<', '>'], '', $string); break; case 'disposition-notification-to': case 'x-confirm-reading-to': $result[$id]->mdn_to = substr($string, 0, 2048); break; case 'message-id': $result[$id]->messageID = substr($string, 0, 2048); break; case 'x-priority': if (preg_match('/^(\d+)/', $string, $matches)) { $result[$id]->priority = intval($matches[1]); } break; default: if (strlen($field) < 3) { break; } if (!empty($result[$id]->others[$field])) { $string = array_merge((array) $result[$id]->others[$field], (array) $string); } $result[$id]->others[$field] = $string; } } } } // VANISHED response (QRESYNC RFC5162) // Sample: * VANISHED (EARLIER) 300:310,405,411 else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { $line = substr($line, strlen($match[0])); $v_data = $this->tokenizeResponse($line, 1); $this->data['VANISHED'] = $v_data; } } while (!$this->startsWith($line, $key, true)); return $result; } /** * Returns message(s) data (flags, headers, etc.) * * @param string $mailbox Mailbox name * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) * @param bool $is_uid True if $message_set contains UIDs * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result * @param array $add_headers List of additional headers * * @return bool|array List of rcube_message_header elements, False on error */ public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = []) { $query_items = ['UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE']; $headers = ['DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY']; if (!empty($add_headers)) { $add_headers = array_map('strtoupper', $add_headers); $headers = array_unique(array_merge($headers, $add_headers)); } if ($bodystr) { $query_items[] = 'BODYSTRUCTURE'; } $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]'; return $this->fetch($mailbox, $message_set, $is_uid, $query_items); } /** * Returns message data (flags, headers, etc.) * * @param string $mailbox Mailbox name * @param int $id Message sequence identifier or UID * @param bool $is_uid True if $id is an UID * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result * @param array $add_headers List of additional headers * * @return bool|rcube_message_header Message data, False on error */ public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = []) { $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); if (is_array($a)) { return array_first($a); } return false; } /** * Sort messages by specified header field * * @param array $messages Array of rcube_message_header objects * @param string $field Name of the property to sort by * @param string $order Sorting order (ASC|DESC) * * @return array Sorted input array */ public static function sortHeaders($messages, $field, $order = 'ASC') { $field = empty($field) ? 'uid' : strtolower($field); $order = empty($order) ? 'ASC' : strtoupper($order); $index = []; reset($messages); // Create an index foreach ($messages as $key => $headers) { switch ($field) { case 'arrival': $field = 'internaldate'; // no-break case 'date': case 'internaldate': case 'timestamp': $value = rcube_utils::strtotime($headers->$field); if (!$value && $field != 'timestamp') { $value = $headers->timestamp; } break; default: // @TODO: decode header value, convert to UTF-8 $value = $headers->$field; if (is_string($value)) { $value = str_replace('"', '', $value); if ($field == 'subject') { $value = rcube_utils::remove_subject_prefix($value); } } } $index[$key] = $value; } $sort_order = $order == 'ASC' ? SORT_ASC : SORT_DESC; $sort_flags = SORT_STRING | SORT_FLAG_CASE; if (in_array($field, ['arrival', 'date', 'internaldate', 'timestamp'])) { $sort_flags = SORT_NUMERIC; } array_multisort($index, $sort_order, $sort_flags, $messages); return $messages; } /** * Fetch MIME headers of specified message parts * * @param string $mailbox Mailbox name * @param int $uid Message UID * @param array $parts Message part identifiers * @param bool $mime Use MIME instead of HEADER * * @return array|bool Array containing headers string for each specified body * False on failure. */ public function fetchMIMEHeaders($mailbox, $uid, $parts, $mime = true) { if (!$this->select($mailbox)) { return false; } $result = false; $parts = (array) $parts; $key = $this->nextTag(); $peeks = []; $type = $mime ? 'MIME' : 'HEADER'; // format request foreach ($parts as $part) { $peeks[] = "BODY.PEEK[$part.$type]"; } $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')'; // send request if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Failed to send UID FETCH command"); return false; } do { $line = $this->readLine(1024); if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) { $line = ltrim(substr($line, strlen($m[0]))); while (preg_match('/^\s*BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) { $line = substr($line, strlen($matches[0])); $result[$matches[1]] = trim($this->multLine($line)); $line = $this->readLine(1024); } } } while (!$this->startsWith($line, $key, true)); return $result; } /** * Fetches message part header */ public function fetchPartHeader($mailbox, $id, $is_uid = false, $part = null) { $part = empty($part) ? 'HEADER' : $part.'.MIME'; return $this->handlePartBody($mailbox, $id, $is_uid, $part); } /** * Fetches body of the specified message part */ public function handlePartBody($mailbox, $id, $is_uid = false, $part = '', $encoding = null, $print = null, $file = null, $formatted = false, $max_bytes = 0) { if (!$this->select($mailbox)) { return false; } $binary = true; $initiated = false; do { if (!$initiated) { switch ($encoding) { case 'base64': $mode = 1; break; case 'quoted-printable': $mode = 2; break; case 'x-uuencode': case 'x-uue': case 'uue': case 'uuencode': $mode = 3; break; default: $mode = 0; } // Use BINARY extension when possible (and safe) $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); $fetch_mode = $binary ? 'BINARY' : 'BODY'; $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; // format request $key = $this->nextTag(); $cmd = ($is_uid ? 'UID ' : '') . 'FETCH'; $request = "$key $cmd $id ($fetch_mode.PEEK[$part]$partial)"; $result = false; $found = false; $initiated = true; // send request if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); return false; } if ($binary) { // WARNING: Use $formatted argument with care, this may break binary data stream $mode = -1; } } $line = trim($this->readLine(1024)); if (!$line) { break; } // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request if ($binary && !$found && preg_match('/^' . $key . ' NO \[(UNKNOWN-CTE|PARSE)\]/i', $line)) { $binary = $initiated = false; continue; } // skip irrelevant untagged responses (we have a result already) if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { continue; } $line = $m[2]; // handle one line response if ($line[0] == '(' && substr($line, -1) == ')') { // tokenize content inside brackets // the content can be e.g.: (UID 9844 BODY[2.4] NIL) $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line)); for ($i=0; $i 0) { $line = $this->readLine(8192); if ($line === null) { break; } $len = strlen($line); if ($len > $bytes) { $line = substr($line, 0, $bytes); $len = strlen($line); } $bytes -= $len; // BASE64 if ($mode == 1) { $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); // create chunks with proper length for base64 decoding $line = $prev.$line; $length = strlen($line); if ($length % 4) { $length = floor($length / 4) * 4; $prev = substr($line, $length); $line = substr($line, 0, $length); } else { $prev = ''; } $line = base64_decode($line); } // QUOTED-PRINTABLE else if ($mode == 2) { $line = rtrim($line, "\t\r\0\x0B"); $line = quoted_printable_decode($line); } // UUENCODE else if ($mode == 3) { $line = rtrim($line, "\t\r\n\0\x0B"); if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) { continue; } $line = convert_uudecode($line); } // default else if ($formatted) { $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; } if ($file) { if (fwrite($file, $line) === false) { break; } } else if ($print) { echo $line; } else { $result .= $line; } } } } while (!$this->startsWith($line, $key, true) || !$initiated); if ($result !== false) { if ($file) { return fwrite($file, $result); } else if ($print) { echo $result; return true; } return $result; } return false; } /** * Handler for IMAP APPEND command * * @param string $mailbox Mailbox name * @param string|array $message The message source string or array (of strings and file pointers) * @param array $flags Message flags * @param string $date Message internal date * @param bool $binary Enable BINARY append (RFC3516) * * @return string|bool On success APPENDUID response (if available) or True, False on failure */ public function append($mailbox, &$message, $flags = [], $date = null, $binary = false) { unset($this->data['APPENDUID']); if ($mailbox === null || $mailbox === '') { return false; } $binary = $binary && $this->getCapability('BINARY'); $literal_plus = !$binary && !empty($this->prefs['literal+']); $len = 0; $msg = is_array($message) ? $message : [&$message]; $chunk_size = 512000; for ($i=0, $cnt=count($msg); $i<$cnt; $i++) { if (is_resource($msg[$i])) { $stat = fstat($msg[$i]); if ($stat === false) { return false; } $len += $stat['size']; } else { if (!$binary) { $msg[$i] = str_replace("\r", '', $msg[$i]); $msg[$i] = str_replace("\n", "\r\n", $msg[$i]); } $len += strlen($msg[$i]); } } if (!$len) { return false; } // build APPEND command $key = $this->nextTag(); $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; if (!empty($date)) { $request .= ' ' . $this->escape($date); } $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; // send APPEND command if (!$this->putLine($request)) { $this->setError(self::ERROR_COMMAND, "Failed to send APPEND command"); return false; } // Do not wait when LITERAL+ is supported if (!$literal_plus) { $line = $this->readReply(); if ($line[0] != '+') { $this->parseResult($line, 'APPEND: '); return false; } } foreach ($msg as $msg_part) { // file pointer if (is_resource($msg_part)) { rewind($msg_part); while (!feof($msg_part) && $this->fp) { $buffer = fread($msg_part, $chunk_size); $this->putLine($buffer, false); } fclose($msg_part); } // string else { $size = strlen($msg_part); // Break up the data by sending one chunk (up to 512k) at a time. // This approach reduces our peak memory usage for ($offset = 0; $offset < $size; $offset += $chunk_size) { $chunk = substr($msg_part, $offset, $chunk_size); if (!$this->putLine($chunk, false)) { return false; } } } } if (!$this->putLine('')) { // \r\n return false; } do { $line = $this->readLine(); } while (!$this->startsWith($line, $key, true, true)); // Clear internal status cache unset($this->data['STATUS:'.$mailbox]); if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) { return false; } if (!empty($this->data['APPENDUID'])) { return $this->data['APPENDUID']; } return true; } /** * Handler for IMAP APPEND command. * * @param string $mailbox Mailbox name * @param string $path Path to the file with message body * @param string $headers Message headers * @param array $flags Message flags * @param string $date Message internal date * @param bool $binary Enable BINARY append (RFC3516) * * @return string|bool On success APPENDUID response (if available) or True, False on failure */ public function appendFromFile($mailbox, $path, $headers = null, $flags = [], $date = null, $binary = false) { // open message file if (file_exists(realpath($path))) { $fp = fopen($path, 'r'); } if (empty($fp)) { $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading"); return false; } $message = []; if ($headers) { $message[] = trim($headers, "\r\n") . "\r\n\r\n"; } $message[] = $fp; return $this->append($mailbox, $message, $flags, $date, $binary); } /** * Returns QUOTA information * * @param string $mailbox Mailbox name * * @return array Quota information */ public function getQuota($mailbox = null) { if ($mailbox === null || $mailbox === '') { $mailbox = 'INBOX'; } // a0001 GETQUOTAROOT INBOX // * QUOTAROOT INBOX user/sample // * QUOTA user/sample (STORAGE 654 9765) // a0001 OK Completed list($code, $response) = $this->execute('GETQUOTAROOT', [$this->escape($mailbox)], 0, '/^\* QUOTA /i'); $result = false; $min_free = PHP_INT_MAX; $all = []; if ($code == self::ERROR_OK) { foreach (explode("\n", $response) as $line) { $tokens = $this->tokenizeResponse($line, 3); $quota_root = isset($tokens[2]) ? $tokens[2] : null; $quotas = $this->tokenizeResponse($line, 1); if (empty($quotas)) { continue; } foreach (array_chunk($quotas, 3) as $quota) { list($type, $used, $total) = $quota; $type = strtolower($type); if ($type && $total) { $all[$quota_root][$type]['used'] = intval($used); $all[$quota_root][$type]['total'] = intval($total); } } if (empty($all[$quota_root]['storage'])) { continue; } $used = $all[$quota_root]['storage']['used']; $total = $all[$quota_root]['storage']['total']; $free = $total - $used; // calculate lowest available space from all storage quotas if ($free < $min_free) { $min_free = $free; $result['used'] = $used; $result['total'] = $total; $result['percent'] = min(100, round(($used/max(1,$total))*100)); $result['free'] = 100 - $result['percent']; } } } if (!empty($result)) { $result['all'] = $all; } return $result; } /** * Send the SETACL command (RFC4314) * * @param string $mailbox Mailbox name * @param string $user User name * @param mixed $acl ACL string or array * * @return bool True on success, False on failure * * @since 0.5-beta */ public function setACL($mailbox, $user, $acl) { if (is_array($acl)) { $acl = implode('', $acl); } $result = $this->execute('SETACL', [$this->escape($mailbox), $this->escape($user), strtolower($acl)], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } /** * Send the DELETEACL command (RFC4314) * * @param string $mailbox Mailbox name * @param string $user User name * * @return bool True on success, False on failure * * @since 0.5-beta */ public function deleteACL($mailbox, $user) { $result = $this->execute('DELETEACL', [$this->escape($mailbox), $this->escape($user)], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } /** * Send the GETACL command (RFC4314) * * @param string $mailbox Mailbox name * * @return array User-rights array on success, NULL on error * @since 0.5-beta */ public function getACL($mailbox) { list($code, $response) = $this->execute('GETACL', [$this->escape($mailbox)], 0, '/^\* ACL /i'); if ($code == self::ERROR_OK && $response) { // Parse server response (remove "* ACL ") $response = substr($response, 6); $ret = $this->tokenizeResponse($response); $mbox = array_shift($ret); $size = count($ret); // Create user-rights hash array // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1 // so we could return only standard rights defined in RFC4314, // excluding 'c' and 'd' defined in RFC2086. if ($size % 2 == 0) { for ($i=0; $i<$size; $i++) { $ret[$ret[$i]] = str_split($ret[++$i]); unset($ret[$i-1]); unset($ret[$i]); } return $ret; } $this->setError(self::ERROR_COMMAND, "Incomplete ACL response"); } } /** * Send the LISTRIGHTS command (RFC4314) * * @param string $mailbox Mailbox name * @param string $user User name * * @return array List of user rights * @since 0.5-beta */ public function listRights($mailbox, $user) { list($code, $response) = $this->execute('LISTRIGHTS', [$this->escape($mailbox), $this->escape($user)], 0, '/^\* LISTRIGHTS /i'); if ($code == self::ERROR_OK && $response) { // Parse server response (remove "* LISTRIGHTS ") $response = substr($response, 13); $ret_mbox = $this->tokenizeResponse($response, 1); $ret_user = $this->tokenizeResponse($response, 1); $granted = $this->tokenizeResponse($response, 1); $optional = trim($response); return [ 'granted' => str_split($granted), 'optional' => explode(' ', $optional), ]; } } /** * Send the MYRIGHTS command (RFC4314) * * @param string $mailbox Mailbox name * * @return array MYRIGHTS response on success, NULL on error * @since 0.5-beta */ public function myRights($mailbox) { list($code, $response) = $this->execute('MYRIGHTS', [$this->escape($mailbox)], 0, '/^\* MYRIGHTS /i'); if ($code == self::ERROR_OK && $response) { // Parse server response (remove "* MYRIGHTS ") $response = substr($response, 11); $ret_mbox = $this->tokenizeResponse($response, 1); $rights = $this->tokenizeResponse($response, 1); return str_split($rights); } } /** * Send the SETMETADATA command (RFC5464) * * @param string $mailbox Mailbox name * @param array $entries Entry-value array (use NULL value as NIL) * * @return bool True on success, False on failure * @since 0.5-beta */ public function setMetadata($mailbox, $entries) { if (!is_array($entries) || empty($entries)) { $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); return false; } foreach ($entries as $name => $value) { $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true); } $entries = implode(' ', $entries); $result = $this->execute('SETMETADATA', [$this->escape($mailbox), '(' . $entries . ')'], self::COMMAND_NORESPONSE ); return $result == self::ERROR_OK; } /** * Send the SETMETADATA command with NIL values (RFC5464) * * @param string $mailbox Mailbox name * @param array $entries Entry names array * * @return bool True on success, False on failure * * @since 0.5-beta */ public function deleteMetadata($mailbox, $entries) { if (!is_array($entries) && !empty($entries)) { $entries = explode(' ', $entries); } if (empty($entries)) { $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); return false; } $data = []; foreach ($entries as $entry) { $data[$entry] = null; } return $this->setMetadata($mailbox, $data); } /** * Send the GETMETADATA command (RFC5464) * * @param string $mailbox Mailbox name * @param array $entries Entries * @param array $options Command options (with MAXSIZE and DEPTH keys) * * @return array GETMETADATA result on success, NULL on error * * @since 0.5-beta */ public function getMetadata($mailbox, $entries, $options = []) { if (!is_array($entries)) { $entries = [$entries]; } // create entries string foreach ($entries as $idx => $name) { $entries[$idx] = $this->escape($name); } $optlist = ''; $entlist = '(' . implode(' ', $entries) . ')'; // create options string if (is_array($options)) { $options = array_change_key_case($options, CASE_UPPER); $opts = []; if (!empty($options['MAXSIZE'])) { $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']); } if (!empty($options['DEPTH'])) { $opts[] = 'DEPTH '.intval($options['DEPTH']); } if ($opts) { $optlist = '(' . implode(' ', $opts) . ')'; } } $optlist .= ($optlist ? ' ' : '') . $entlist; list($code, $response) = $this->execute('GETMETADATA', [$this->escape($mailbox), $optlist]); if ($code == self::ERROR_OK) { $result = []; $data = $this->tokenizeResponse($response); // The METADATA response can contain multiple entries in a single // response or multiple responses for each entry or group of entries for ($i = 0, $size = count($data); $i < $size; $i++) { if ($data[$i] === '*' && $data[++$i] === 'METADATA' && is_string($mbox = $data[++$i]) && is_array($data[++$i]) ) { for ($x = 0, $size2 = count($data[$i]); $x < $size2; $x += 2) { if ($data[$i][$x+1] !== null) { $result[$mbox][$data[$i][$x]] = $data[$i][$x+1]; } } } } return $result; } } /** * Send the SETANNOTATION command (draft-daboo-imap-annotatemore) * * @param string $mailbox Mailbox name * @param array $data Data array where each item is an array with * three elements: entry name, attribute name, value * * @return bool True on success, False on failure * @since 0.5-beta */ public function setAnnotation($mailbox, $data) { if (!is_array($data) || empty($data)) { $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); return false; } foreach ($data as $entry) { // ANNOTATEMORE drafts before version 08 require quoted parameters $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), $this->escape($entry[1], true), $this->escape($entry[2], true)); } $entries = implode(' ', $entries); $result = $this->execute('SETANNOTATION', [$this->escape($mailbox), $entries], self::COMMAND_NORESPONSE); return $result == self::ERROR_OK; } /** * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore) * * @param string $mailbox Mailbox name * @param array $data Data array where each item is an array with * two elements: entry name and attribute name * * @return bool True on success, False on failure * * @since 0.5-beta */ public function deleteAnnotation($mailbox, $data) { if (!is_array($data) || empty($data)) { $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); return false; } return $this->setAnnotation($mailbox, $data); } /** * Send the GETANNOTATION command (draft-daboo-imap-annotatemore) * * @param string $mailbox Mailbox name * @param array $entries Entries names * @param array $attribs Attribs names * * @return array Annotations result on success, NULL on error * * @since 0.5-beta */ public function getAnnotation($mailbox, $entries, $attribs) { if (!is_array($entries)) { $entries = [$entries]; } // create entries string // ANNOTATEMORE drafts before version 08 require quoted parameters foreach ($entries as $idx => $name) { $entries[$idx] = $this->escape($name, true); } $entries = '(' . implode(' ', $entries) . ')'; if (!is_array($attribs)) { $attribs = [$attribs]; } // create attributes string foreach ($attribs as $idx => $name) { $attribs[$idx] = $this->escape($name, true); } $attribs = '(' . implode(' ', $attribs) . ')'; list($code, $response) = $this->execute('GETANNOTATION', [$this->escape($mailbox), $entries, $attribs]); if ($code == self::ERROR_OK) { $result = []; $data = $this->tokenizeResponse($response); $last_entry = null; // Here we returns only data compatible with METADATA result format if (!empty($data) && ($size = count($data))) { for ($i=0; $i<$size; $i++) { $entry = $data[$i]; if (isset($mbox) && is_array($entry)) { $attribs = $entry; $entry = $last_entry; } else if ($entry == '*') { if ($data[$i+1] == 'ANNOTATION') { $mbox = $data[$i+2]; unset($data[$i]); // "*" unset($data[++$i]); // "ANNOTATION" unset($data[++$i]); // Mailbox } // get rid of other untagged responses else { unset($mbox); unset($data[$i]); } continue; } else if (isset($mbox)) { $attribs = $data[++$i]; } else { unset($data[$i]); continue; } if (!empty($attribs)) { for ($x=0, $len=count($attribs); $x<$len;) { $attr = $attribs[$x++]; $value = $attribs[$x++]; if ($attr == 'value.priv' && $value !== null) { $result[$mbox]['/private' . $entry] = $value; } else if ($attr == 'value.shared' && $value !== null) { $result[$mbox]['/shared' . $entry] = $value; } } } $last_entry = $entry; unset($data[$i]); } } return $result; } } /** * Returns BODYSTRUCTURE for the specified message. * * @param string $mailbox Folder name * @param int $id Message sequence number or UID * @param bool $is_uid True if $id is an UID * * @return array|bool Body structure array or False on error. * @since 0.6 */ public function getStructure($mailbox, $id, $is_uid = false) { $result = $this->fetch($mailbox, $id, $is_uid, ['BODYSTRUCTURE']); if (is_array($result) && !empty($result)) { $result = array_first($result); return $result->bodystructure; } return false; } /** * Returns data of a message part according to specified structure. * * @param array $structure Message structure (getStructure() result) * @param string $part Message part identifier * * @return array Part data as hash array (type, encoding, charset, size) */ public static function getStructurePartData($structure, $part) { $part_a = self::getStructurePartArray($structure, $part); $data = []; if (empty($part_a)) { return $data; } // content-type if (is_array($part_a[0])) { $data['type'] = 'multipart'; } else { $data['type'] = strtolower($part_a[0]); $data['subtype'] = strtolower($part_a[1]); $data['encoding'] = strtolower($part_a[5]); // charset if (is_array($part_a[2])) { foreach ($part_a[2] as $key => $val) { if (strcasecmp($val, 'charset') == 0) { $data['charset'] = $part_a[2][$key+1]; break; } } } } // size $data['size'] = intval($part_a[6]); return $data; } public static function getStructurePartArray($a, $part) { if (!is_array($a)) { return false; } if (empty($part)) { return $a; } $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : ''; if (strcasecmp($ctype, 'message/rfc822') == 0) { $a = $a[8]; } if (strpos($part, '.') > 0) { $orig_part = $part; $pos = strpos($part, '.'); $rest = substr($orig_part, $pos+1); $part = substr($orig_part, 0, $pos); return self::getStructurePartArray($a[$part-1], $rest); } else if ($part > 0) { return is_array($a[$part-1]) ? $a[$part-1] : $a; } } /** * Creates next command identifier (tag) * * @return string Command identifier * @since 0.5-beta */ public function nextTag() { $this->cmd_num++; $this->cmd_tag = sprintf('A%04d', $this->cmd_num); return $this->cmd_tag; } /** * Sends IMAP command and parses result * * @param string $command IMAP command * @param array $arguments Command arguments * @param int $options Execution options * @param string $filter Line filter (regexp) * * @return mixed Response code or list of response code and data * @since 0.5-beta */ public function execute($command, $arguments = [], $options = 0, $filter = null) { $tag = $this->nextTag(); $query = $tag . ' ' . $command; $noresp = ($options & self::COMMAND_NORESPONSE); $response = $noresp ? null : ''; if (!empty($arguments)) { foreach ($arguments as $arg) { $query .= ' ' . self::r_implode($arg); } } // Send command if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) { preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches); $cmd = $matches[1] ?: 'UNKNOWN'; $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command"); return $noresp ? self::ERROR_COMMAND : [self::ERROR_COMMAND, '']; } // Parse response do { $line = $this->readFullLine(4096); if ($response !== null) { if (!$filter || preg_match($filter, $line)) { $response .= $line; } } // parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851) if ($line && $command == 'UID MOVE') { if (preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) { $this->data['COPYUID'] = [$m[1], $m[2]]; } } } while (!$this->startsWith($line, $tag . ' ', true, true)); $code = $this->parseResult($line, $command . ': '); // Remove last line from response if ($response) { if (!$filter) { $line_len = min(strlen($response), strlen($line)); $response = substr($response, 0, -$line_len); } $response = rtrim($response, "\r\n"); } // optional CAPABILITY response if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches) ) { $this->parseCapability($matches[1], true); } // return last line only (without command tag, result and response code) if ($line && ($options & self::COMMAND_LASTLINE)) { $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line)); } return $noresp ? $code : [$code, $response]; } /** * Splits IMAP response into string tokens * * @param string &$str The IMAP's server response * @param int $num Number of tokens to return * * @return mixed Tokens array or string if $num=1 * @since 0.5-beta */ public static function tokenizeResponse(&$str, $num=0) { $result = []; while (!$num || count($result) < $num) { // remove spaces from the beginning of the string $str = ltrim($str); // empty string if ($str === '' || $str === null) { break; } switch ($str[0]) { // String literal case '{': if (($epos = strpos($str, "}\r\n", 1)) == false) { // error } if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) { // error } $result[] = $bytes ? substr($str, $epos + 3, $bytes) : ''; $str = substr($str, $epos + 3 + $bytes); break; // Quoted string case '"': $len = strlen($str); for ($pos=1; $pos<$len; $pos++) { if ($str[$pos] == '"') { break; } if ($str[$pos] == "\\") { if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { $pos++; } } } // we need to strip slashes for a quoted string $result[] = stripslashes(substr($str, 1, $pos - 1)); $str = substr($str, $pos + 1); break; // Parenthesized list case '(': $str = substr($str, 1); $result[] = self::tokenizeResponse($str); break; case ')': $str = substr($str, 1); return $result; // String atom, number, astring, NIL, *, % default: // excluded chars: SP, CTL, ), DEL // we do not exclude [ and ] (#1489223) if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) { $result[] = $m[1] == 'NIL' ? null : $m[1]; $str = substr($str, strlen($m[1])); } break; } } return $num == 1 ? (isset($result[0]) ? $result[0] : '') : $result; } /** * Joins IMAP command line elements (recursively) */ protected static function r_implode($element) { if (!is_array($element)) { return $element; } reset($element); $string = ''; foreach ($element as $value) { $string .= ' ' . self::r_implode($value); } return '(' . trim($string) . ')'; } /** * Converts message identifiers array into sequence-set syntax * * @param array $messages Message identifiers * @param bool $force Forces compression of any size * * @return string Compressed sequence-set */ public static function compressMessageSet($messages, $force = false) { // given a comma delimited list of independent mid's, // compresses by grouping sequences together if (!is_array($messages)) { // if less than 255 bytes long, let's not bother if (!$force && strlen($messages) < 255) { return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; } // see if it's already been compressed if (strpos($messages, ':') !== false) { return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages; } // separate, then sort $messages = explode(',', $messages); } sort($messages); $result = []; $start = $prev = $messages[0]; foreach ($messages as $id) { $incr = $id - $prev; if ($incr > 1) { // found a gap if ($start == $prev) { $result[] = $prev; // push single id } else { $result[] = $start . ':' . $prev; // push sequence as start_id:end_id } $start = $id; // start of new sequence } $prev = $id; } // handle the last sequence/id if ($start == $prev) { $result[] = $prev; } else { $result[] = $start.':'.$prev; } // return as comma separated string $result = implode(',', $result); return preg_match('/[^0-9:,*]/', $result) ? 'INVALID' : $result; } /** * Converts message sequence-set into array * * @param string $messages Message identifiers * * @return array List of message identifiers */ public static function uncompressMessageSet($messages) { if (empty($messages)) { return []; } $result = []; $messages = explode(',', $messages); foreach ($messages as $idx => $part) { $items = explode(':', $part); if (!empty($items[1]) && $items[1] > $items[0]) { $max = $items[1]; } else { $max = $items[0]; } for ($x = $items[0]; $x <= $max; $x++) { $result[] = (int) $x; } unset($messages[$idx]); } return $result; } /** * Clear internal status cache */ protected function clear_status_cache($mailbox) { unset($this->data['STATUS:' . $mailbox]); $keys = ['EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP']; foreach ($keys as $key) { unset($this->data[$key]); } } /** * Clear internal cache of the current mailbox */ protected function clear_mailbox_cache() { $this->clear_status_cache($this->selected); $keys = ['UIDNEXT', 'UIDVALIDITY', 'HIGHESTMODSEQ', 'NOMODSEQ', 'PERMANENTFLAGS', 'QRESYNC', 'VANISHED', 'READ-WRITE']; foreach ($keys as $key) { unset($this->data[$key]); } } /** * Converts flags array into string for inclusion in IMAP command * * @param array $flags Flags (see self::flags) * * @return string Space-separated list of flags */ protected function flagsToStr($flags) { foreach ((array) $flags as $idx => $flag) { if ($flag = $this->flags[strtoupper($flag)]) { $flags[$idx] = $flag; } } return implode(' ', (array) $flags); } /** * CAPABILITY response parser */ protected function parseCapability($str, $trusted=false) { $str = preg_replace('/^\* CAPABILITY /i', '', $str); $this->capability = explode(' ', strtoupper($str)); if (!empty($this->prefs['disabled_caps'])) { $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']); } if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) { $this->prefs['literal+'] = true; } else if (!isset($this->prefs['literal-']) && in_array('LITERAL-', $this->capability)) { $this->prefs['literal-'] = true; } if ($trusted) { $this->capability_read = true; } } /** * Escapes a string when it contains special characters (RFC3501) * * @param string $string IMAP string * @param bool $force_quotes Forces string quoting (for atoms) * * @return string String atom, quoted-string or string literal * @todo lists */ public static function escape($string, $force_quotes = false) { if ($string === null) { return 'NIL'; } if ($string === '') { return '""'; } // atom-string (only safe characters) if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) { return $string; } // quoted-string if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) { return '"' . addcslashes($string, '\\"') . '"'; } // literal-string return sprintf("{%d}\r\n%s", strlen($string), $string); } /** * Set the value of the debugging flag. * * @param bool $debug New value for the debugging flag. * @param callback $handler Logging handler function * * @since 0.5-stable */ public function setDebug($debug, $handler = null) { $this->debug = $debug; $this->debug_handler = $handler; } /** * Write the given debug text to the current debug output handler. * * @param string $message Debug message text. * * @since 0.5-stable */ protected function debug($message) { if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { $diff = $len - self::DEBUG_LINE_LENGTH; $message = substr($message, 0, self::DEBUG_LINE_LENGTH) . "... [truncated $diff bytes]"; } if ($this->resourceid) { $message = sprintf('[%s] %s', $this->resourceid, $message); } if ($this->debug_handler) { call_user_func_array($this->debug_handler, [$this, $message]); } else { echo "DEBUG: $message\n"; } } } diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index f384cc7b5..4396f8f42 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -1,1736 +1,1815 @@ | | Author: Aleksander Machniak | +-----------------------------------------------------------------------+ */ /** * Utility class providing common functions * * @package Framework * @subpackage Utils */ class rcube_utils { // define constants for input reading const INPUT_GET = 1; const INPUT_POST = 2; const INPUT_COOKIE = 4; const INPUT_GP = 3; // GET + POST const INPUT_GPC = 7; // GET + POST + COOKIE /** * A wrapper for PHP's explode() that does not throw a warning * when the separator does not exist in the string * * @param string $separator Separator string * @param string $string The string to explode * * @return array Exploded string. Still an array if there's no separator in the string */ public static function explode($separator, $string) { if (strpos($string, $separator) !== false) { return explode($separator, $string); } return [$string, null]; } /** * Helper method to set a cookie with the current path and host settings * * @param string $name Cookie name * @param string $value Cookie value * @param int $exp Expiration time * @param bool $http_only HTTP Only */ public static function setcookie($name, $value, $exp = 0, $http_only = true) { if (headers_sent()) { return; } $attrib = session_get_cookie_params(); $attrib['expires'] = $exp; $attrib['secure'] = $attrib['secure'] || self::https_check(); $attrib['httponly'] = $http_only; // session_get_cookie_params() return includes 'lifetime' but setcookie() does not use it, instead it uses 'expires' unset($attrib['lifetime']); if (version_compare(PHP_VERSION, '7.3.0', '>=')) { // An alternative signature for setcookie supporting an options array added in PHP 7.3.0 setcookie($name, $value, $attrib); } else { setcookie($name, $value, $attrib['expires'], $attrib['path'], $attrib['domain'], $attrib['secure'], $attrib['httponly']); } } /** * E-mail address validation. * * @param string $email Email address * @param bool $dns_check True to check dns * * @return bool True on success, False if address is invalid */ public static function check_email($email, $dns_check = true) { // Check for invalid (control) characters if (preg_match('/\p{Cc}/u', $email)) { return false; } // Check for length limit specified by RFC 5321 (#1486453) if (strlen($email) > 254) { return false; } $pos = strrpos($email, '@'); if (!$pos) { return false; } $domain_part = substr($email, $pos + 1); $local_part = substr($email, 0, $pos); // quoted-string, make sure all backslashes and quotes are // escaped if (substr($local_part, 0, 1) == '"') { $local_quoted = preg_replace('/\\\\(\\\\|\")/','', substr($local_part, 1, -1)); if (preg_match('/\\\\|"/', $local_quoted)) { return false; } } // dot-atom portion, make sure there's no prohibited characters else if (preg_match('/(^\.|\.\.|\.$)/', $local_part) || preg_match('/[\\ ",:;<>@]/', $local_part) ) { return false; } // Validate domain part if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) { return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address } else { // If not an IP address $domain_array = explode('.', $domain_part); // Not enough parts to be a valid domain if (count($domain_array) < 2) { return false; } foreach ($domain_array as $part) { if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) { return false; } } // last domain part (allow extended TLD) $last_part = array_pop($domain_array); if (strpos($last_part, 'xn--') !== 0 && (preg_match('/[^a-zA-Z0-9]/', $last_part) || preg_match('/^[0-9]+$/', $last_part)) ) { return false; } $rcube = rcube::get_instance(); if (!$dns_check || !function_exists('checkdnsrr') || !$rcube->config->get('email_dns_check')) { return true; } // Check DNS record(s) // Note: We can't use ANY (#6581) foreach (['A', 'MX', 'CNAME', 'AAAA'] as $type) { if (checkdnsrr($domain_part, $type)) { return true; } } } return false; } /** * Validates IPv4 or IPv6 address * * @param string $ip IP address in v4 or v6 format * * @return bool True if the address is valid */ public static function check_ip($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; } /** * Replacing specials characters to a specific encoding type * * @param string $str Input string * @param string $enctype Encoding type: text|html|xml|js|url * @param string $mode Replace mode for tags: show|remove|strict * @param bool $newlines Convert newlines * * @return string The quoted string */ public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true) { static $html_encode_arr = false; static $js_rep_table = false; static $xml_rep_table = false; if (!is_string($str)) { $str = strval($str); } // encode for HTML output if ($enctype == 'html') { if (!$html_encode_arr) { $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS); unset($html_encode_arr['?']); } $encode_arr = $html_encode_arr; if ($mode == 'remove') { $str = strip_tags($str); } else if ($mode != 'strict') { // don't replace quotes and html tags $ltpos = strpos($str, '<'); if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) { unset($encode_arr['"']); unset($encode_arr['<']); unset($encode_arr['>']); unset($encode_arr['&']); } } $out = strtr($str, $encode_arr); return $newlines ? nl2br($out) : $out; } // if the replace tables for XML and JS are not yet defined if ($js_rep_table === false) { $js_rep_table = $xml_rep_table = []; $xml_rep_table['&'] = '&'; // can be increased to support more charsets for ($c=160; $c<256; $c++) { $xml_rep_table[chr($c)] = "&#$c;"; } $xml_rep_table['"'] = '"'; $js_rep_table['"'] = '\\"'; $js_rep_table["'"] = "\\'"; $js_rep_table["\\"] = "\\\\"; // Unicode line and paragraph separators (#1486310) $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '
'; $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '
'; } // encode for javascript use if ($enctype == 'js') { return preg_replace(["/\r?\n/", "/\r/", '/<\\//'], ['\n', '\n', '<\\/'], strtr($str, $js_rep_table)); } // encode for plaintext if ($enctype == 'text') { return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str); } if ($enctype == 'url') { return rawurlencode($str); } // encode for XML if ($enctype == 'xml') { return strtr($str, $xml_rep_table); } // no encoding given -> return original string return $str; } /** * Read input value and make sure it is a string. * * @param string $fname Field name to read * @param int $source Source to get value from (see self::INPUT_*) * @param bool $allow_html Allow HTML tags in field value * @param string $charset Charset to convert into * * @return string Request parameter value * @see self::get_input_value() */ public static function get_input_string($fname, $source, $allow_html = false, $charset = null) { $value = self::get_input_value($fname, $source, $allow_html, $charset); return is_string($value) ? $value : ''; } /** * Read request parameter value and convert it for internal use * Performs stripslashes() and charset conversion if necessary * * @param string $fname Field name to read * @param int $source Source to get value from (see self::INPUT_*) * @param bool $allow_html Allow HTML tags in field value * @param string $charset Charset to convert into * * @return string|array|null Request parameter value or NULL if not set */ public static function get_input_value($fname, $source, $allow_html = false, $charset = null) { $value = null; if (($source & self::INPUT_GET) && isset($_GET[$fname])) { $value = $_GET[$fname]; } if (($source & self::INPUT_POST) && isset($_POST[$fname])) { $value = $_POST[$fname]; } if (($source & self::INPUT_COOKIE) && isset($_COOKIE[$fname])) { $value = $_COOKIE[$fname]; } return self::parse_input_value($value, $allow_html, $charset); } /** * Parse/validate input value. See self::get_input_value() * Performs stripslashes() and charset conversion if necessary * * @param string $value Input value * @param bool $allow_html Allow HTML tags in field value * @param string $charset Charset to convert into * * @return string Parsed value */ public static function parse_input_value($value, $allow_html = false, $charset = null) { if (empty($value)) { return $value; } if (is_array($value)) { foreach ($value as $idx => $val) { $value[$idx] = self::parse_input_value($val, $allow_html, $charset); } return $value; } // remove HTML tags if not allowed if (!$allow_html) { $value = strip_tags($value); } $rcube = rcube::get_instance(); $output_charset = is_object($rcube->output) ? $rcube->output->get_charset() : null; // remove invalid characters (#1488124) if ($output_charset == 'UTF-8') { $value = rcube_charset::clean($value); } // convert to internal charset if ($charset && $output_charset) { $value = rcube_charset::convert($value, $output_charset, $charset); } return $value; } /** * Convert array of request parameters (prefixed with _) * to a regular array with non-prefixed keys. * * @param int $mode Source to get value from (GPC) * @param string $ignore PCRE expression to skip parameters by name * @param bool $allow_html Allow HTML tags in field value * * @return array Hash array with all request parameters */ public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false) { $out = []; $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST); foreach (array_keys($src) as $key) { $fname = $key[0] == '_' ? substr($key, 1) : $key; if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) { $out[$fname] = self::get_input_value($key, $mode, $allow_html); } } return $out; } /** * Convert the given string into a valid HTML identifier * Same functionality as done in app.js with rcube_webmail.html_identifier() * * @param string $str String input * @param bool $encode Use base64 encoding * * @param string Valid HTML identifier */ public static function html_identifier($str, $encode = false) { if ($encode) { return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); } return asciiwords($str, true, '_'); } /** * Replace all css definitions with #container [def] * and remove css-inlined scripting, make position style safe * * @param string $source CSS source code * @param string $container_id Container ID to use as prefix * @param bool $allow_remote Allow remote content * @param string $prefix Prefix to be added to id/class identifier * * @return string Modified CSS source */ public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '') { $source = self::xss_entity_decode($source); // No @import allowed // TODO: We should just remove it, not invalidate the whole content if (stripos($source, '@import') !== false) { return '/* evil! */'; } // Incomplete style expression if (strpos($source, '{') === false) { return '/* invalid! */'; } // To prevent from a double-escaping tricks we consider a script with // any escape sequences (after de-escaping them above) an evil script. // This probably catches many valid scripts, but we\'re on the safe side. if (preg_match('/\\\[0-9a-fA-F]{2}/', $source)) { return '/* evil! */'; } // remove html comments $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source); $url_callback = static function ($url) use ($allow_remote) { if (strpos($url, 'data:image') === 0) { return $url; } if ($allow_remote && preg_match('|^https?://[a-z0-9/._+-]+$|i', $url)) { return $url; } }; $last_pos = 0; $replacements = new rcube_string_replacer(); // cut out all contents between { and } while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) { $nested = strpos($source, '{', $pos+1); if ($nested && $nested < $pos2) { // when dealing with nested blocks (e.g. @media), take the inner one $pos = $nested; } $length = $pos2 - $pos - 1; $styles = substr($source, $pos+1, $length); $styles = self::sanitize_css_block($styles, $url_callback); $key = $replacements->add(strlen($styles) ? " {$styles} " : ''); $repl = $replacements->get_replacement($key); $source = substr_replace($source, $repl, $pos+1, $length); $last_pos = $pos2 - ($length - strlen($repl)); } // add #container to each tag selector and prefix to id/class identifiers if ($container_id || $prefix) { // Exclude rcube_string_replacer pattern matches, this is needed // for cases like @media { body { position: fixed; } } (#5811) $excl = '(?!' . substr($replacements->pattern, 1, -1) . ')'; $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)(' . $excl . ':?[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im'; $callback = function($matches) use ($container_id, $prefix) { $replace = $matches[2]; if (stripos($replace, ':root') === 0) { $replace = substr($replace, 5); } if ($prefix) { $replace = str_replace(['.', '#'], [".$prefix", "#$prefix"], $replace); } if ($container_id) { $replace = "#$container_id " . $replace; } // Remove redundant spaces (for simpler testing) $replace = preg_replace('/\s+/', ' ', $replace); return str_replace($matches[2], $replace, $matches[0]); }; $source = preg_replace_callback($regexp, $callback, $source); } // replace body definition because we also stripped off the tag if ($container_id) { $regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i'; $source = preg_replace($regexp, "#$container_id", $source); } // put block contents back in $source = $replacements->resolve($source); return $source; } /** * Parse and sanitize single CSS block * * @param string $styles CSS styles block * @param ?callable $url_callback URL validator callback * * @return string */ public static function sanitize_css_block($styles, $url_callback = null) { $output = []; // check every css rule in the style block... foreach (self::parse_css_block($styles) as $rule) { $property = $rule[0]; $value = $rule[1]; if ($property == 'page') { // Remove 'page' attributes (#7604) continue; } elseif ($property == 'position' && strcasecmp($value, 'fixed') === 0) { // Convert position:fixed to position:absolute (#5264) $value = 'absolute'; } elseif (preg_match('/expression|image-set/i', $value)) { continue; } else { $value = ''; foreach (self::explode_css_property_block($rule[1]) as $val) { if ($url_callback && preg_match('/^url\s*\(/i', $val)) { if (preg_match('/^url\s*\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) { if ($url = $url_callback($match[1])) { $value .= ' url(' . $url . ')'; } } } else { // whitelist ? $value .= ' ' . $val; // #1488535: Fix size units, so width:800 would be changed to width:800px if ($val && preg_match('/^(left|right|top|bottom|width|height)/i', $property) && preg_match('/^[0-9]+$/', $val) ) { $value .= 'px'; } } } } if (strlen($value)) { $output[] = $property . ': ' . trim($value); } } return count($output) > 0 ? implode('; ', $output) . ';' : ''; } /** * Explode css style. Property names will be lower-cased and trimmed. * Values will be trimmed. Invalid entries will be skipped. * * @param string $style CSS style * * @return array List of CSS rule pairs, e.g. [['color', 'red'], ['top', '0']] */ public static function parse_css_block($style) { $pos = 0; // first remove comments while (($pos = strpos($style, '/*', $pos)) !== false) { $end = strpos($style, '*/', $pos+2); if ($end === false) { $style = substr($style, 0, $pos); } else { $style = substr_replace($style, '', $pos, $end - $pos + 2); } } // Replace new lines with spaces $style = preg_replace('/[\r\n]+/', ' ', $style); $style = trim($style); $length = strlen($style); $result = []; $pos = 0; while ($pos < $length && ($colon_pos = strpos($style, ':', $pos))) { // Property name $name = strtolower(trim(substr($style, $pos, $colon_pos - $pos))); // get the property value $q = $s = false; for ($i = $colon_pos + 1; $i < $length; $i++) { if (($style[$i] == "\"" || $style[$i] == "'") && ($i == 0 || $style[$i-1] != "\\")) { if ($q == $style[$i]) { $q = false; } else if ($q === false) { $q = $style[$i]; } } else if ($style[$i] == "(" && !$q && ($i == 0 || $style[$i-1] != "\\")) { $q = "("; } else if ($style[$i] == ")" && $q == "(" && $style[$i-1] != "\\") { $q = false; } if ($q === false && (($s = $style[$i] == ';') || $i == $length - 1)) { break; } } $value_length = $i - $colon_pos - ($s ? 1 : 0); $value = trim(substr($style, $colon_pos + 1, $value_length)); if (strlen($name) && !preg_match('/[^a-z-]/', $name) && strlen($value) && $value !== ';') { $result[] = [$name, $value]; } $pos = $i + 1; } return $result; } /** * Explode css style value * * @param string $style CSS style * * @return array List of CSS values */ public static function explode_css_property_block($style) { $style = preg_replace('/\s+/', ' ', $style); $result = []; $strlen = strlen($style); $q = false; // explode value for ($p = $i = 0; $i < $strlen; $i++) { if (($style[$i] == '"' || $style[$i] == "'") && ($i == 0 || $style[$i - 1] != '\\')) { if ($q == $style[$i]) { $q = false; } elseif (!$q) { $q = $style[$i]; } } if (!$q && $style[$i] == ' ' && ($i == 0 || !preg_match('/[,\(]/', $style[$i - 1]))) { $result[] = substr($style, $p, $i - $p); $p = $i + 1; } } $result[] = (string) substr($style, $p); return $result; } /** * Generate CSS classes from mimetype and filename extension * * @param string $mimetype Mimetype * @param string $filename Filename * * @return string CSS classes separated by space */ public static function file2class($mimetype, $filename) { $mimetype = strtolower($mimetype); $filename = strtolower($filename); list($primary, $secondary) = rcube_utils::explode('/', $mimetype); $classes = [$primary ?: 'unknown']; if (!empty($secondary)) { $classes[] = $secondary; } if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) { if (!in_array($m[1], $classes)) { $classes[] = $m[1]; } } return implode(' ', $classes); } /** * Decode escaped entities used by known XSS exploits. * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples * * @param string $content CSS content to decode * * @return string Decoded string */ public static function xss_entity_decode($content) { $callback = function($matches) { return chr(hexdec($matches[1])); }; $out = html_entity_decode(html_entity_decode($content)); $out = trim(preg_replace('/(^$)/', '', trim($out))); $out = preg_replace_callback('/\\\([0-9a-f]{2,6})\s*/i', $callback, $out); $out = preg_replace('/\\\([^0-9a-f])/i', '\\1', $out); $out = preg_replace('#/\*.*\*/#Ums', '', $out); $out = strip_tags($out); return $out; } /** * Check if we can process not exceeding memory_limit * * @param int $need Required amount of memory * * @return bool True if memory won't be exceeded, False otherwise */ public static function mem_check($need) { $mem_limit = parse_bytes(ini_get('memory_limit')); $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true; } /** * Check if working in SSL mode * * @param int $port HTTPS port number * @param bool $use_https Enables 'use_https' option checking * * @return bool True in SSL mode, False otherwise */ public static function https_check($port = null, $use_https = true) { if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') { return true; } if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https' && in_array($_SERVER['REMOTE_ADDR'], (array) rcube::get_instance()->config->get('proxy_whitelist', [])) ) { return true; } if ($port && isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == $port) { return true; } if ($use_https && rcube::get_instance()->config->get('use_https')) { return true; } return false; } /** * Replaces hostname variables. * * @param string $name Hostname * @param string $host Optional IMAP hostname * * @return string Hostname */ public static function parse_host($name, $host = '') { if (!is_string($name)) { return $name; } // %n - host $n = self::server_name(); // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld // If %n=domain.tld then %t=domain.tld as well (remains valid) $t = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $n); // %d - domain name without first part (up to domain.tld) $d = preg_replace('/^[^.]+\.(?![^.]+$)/', '', self::server_name('HTTP_HOST')); // %h - IMAP host $h = !empty($_SESSION['storage_host']) ? $_SESSION['storage_host'] : $host; // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld // If %h=domain.tld then %z=domain.tld as well (remains valid) $z = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $h); // %s - domain name after the '@' from e-mail address provided at login screen. // Returns FALSE if an invalid email is provided $s = ''; if (strpos($name, '%s') !== false) { $user_email = self::idn_to_ascii(self::get_input_value('_user', self::INPUT_POST)); $matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s); if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) { return false; } $s = $s[2]; } return str_replace(['%n', '%t', '%d', '%h', '%z', '%s'], [$n, $t, $d, $h, $z, $s], $name); } /** * Returns the server name after checking it against trusted hostname patterns. * * Returns 'localhost' and logs a warning when the hostname is not trusted. * * @param string $type The $_SERVER key, e.g. 'HTTP_HOST', Default: 'SERVER_NAME'. * @param bool $strip_port Strip port from the host name * * @return string Server name */ public static function server_name($type = null, $strip_port = true) { if (!$type) { $type = 'SERVER_NAME'; } $name = isset($_SERVER[$type]) ? $_SERVER[$type] : null; $rcube = rcube::get_instance(); $patterns = (array) $rcube->config->get('trusted_host_patterns'); if (!empty($name)) { if ($strip_port) { $name = preg_replace('/:\d+$/', '', $name); } if (empty($patterns)) { return $name; } foreach ($patterns as $pattern) { // the pattern might be a regular expression or just a host/domain name if (preg_match('/[^a-zA-Z0-9.:-]/', $pattern)) { if (preg_match("/$pattern/", $name)) { return $name; } } else if (strtolower($name) === strtolower($pattern)) { return $name; } } $rcube->raise_error([ 'file' => __FILE__, 'line' => __LINE__, 'message' => "Specified host is not trusted. Using 'localhost'." ] , true, false ); } return 'localhost'; } /** * Returns remote IP address and forwarded addresses if found * * @return string Remote IP address(es) */ public static function remote_ip() { $address = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; // append the NGINX X-Real-IP header, if set if (!empty($_SERVER['HTTP_X_REAL_IP']) && $_SERVER['HTTP_X_REAL_IP'] != $address) { $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP']; } // append the X-Forwarded-For header, if set if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR']; } if (!empty($remote_ip)) { $address .= ' (' . implode(',', $remote_ip) . ')'; } return $address; } /** * Returns the real remote IP address * * @return string Remote IP address */ public static function remote_addr() { // Check if any of the headers are set first to improve performance if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) { $proxy_whitelist = (array) rcube::get_instance()->config->get('proxy_whitelist', []); if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) { if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) { $forwarded_ip = trim($forwarded_ip); if (!in_array($forwarded_ip, $proxy_whitelist)) { return $forwarded_ip; } } } if (!empty($_SERVER['HTTP_X_REAL_IP'])) { return $_SERVER['HTTP_X_REAL_IP']; } } } if (!empty($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; } return ''; } /** * Read a specific HTTP request header. * * @param string $name Header name * * @return string|null Header value or null if not available */ public static function request_header($name) { if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); $key = strtoupper($name); } else { $headers = $_SERVER; $key = 'HTTP_' . strtoupper(strtr($name, '-', '_')); } if (!empty($headers)) { $headers = array_change_key_case($headers, CASE_UPPER); return isset($headers[$key]) ? $headers[$key] : null; } } /** * Explode quoted string * * @param string $delimiter Delimiter expression string for preg_match() * @param string $string Input string * * @return array String items */ public static function explode_quoted_string($delimiter, $string) { $result = []; $strlen = strlen($string); for ($q=$p=$i=0; $i < $strlen; $i++) { if ($string[$i] == "\"" && (!isset($string[$i-1]) || $string[$i-1] != "\\")) { $q = $q ? false : true; } else if (!$q && preg_match("/$delimiter/", $string[$i])) { $result[] = substr($string, $p, $i - $p); $p = $i + 1; } } $result[] = (string) substr($string, $p); return $result; } /** * Improved equivalent to strtotime() * * @param string $date Date string * @param DateTimeZone $timezone Timezone to use for DateTime object * * @return int Unix timestamp */ public static function strtotime($date, $timezone = null) { $date = self::clean_datestr($date); $tzname = $timezone ? ' ' . $timezone->getName() : ''; // unix timestamp if (is_numeric($date)) { return (int) $date; } // It can be very slow when provided string is not a date and very long if (strlen($date) > 128) { $date = substr($date, 0, 128); } // if date parsing fails, we have a date in non-rfc format. // remove token from the end and try again while (($ts = @strtotime($date . $tzname)) === false || $ts < 0) { if (($pos = strrpos($date, ' ')) === false) { break; } $date = rtrim(substr($date, 0, $pos)); } return (int) $ts; } /** * Date parsing function that turns the given value into a DateTime object * * @param string $date Date string * @param DateTimeZone $timezone Timezone to use for DateTime object * * @return DateTime|false DateTime object or False on failure */ public static function anytodatetime($date, $timezone = null) { if ($date instanceof DateTime) { return $date; } $dt = false; $date = self::clean_datestr($date); // try to parse string with DateTime first if (!empty($date)) { try { $_date = preg_match('/^[0-9]+$/', $date) ? "@$date" : $date; $dt = $timezone ? new DateTime($_date, $timezone) : new DateTime($_date); } catch (Exception $e) { // ignore } } // try our advanced strtotime() method if (!$dt && ($timestamp = self::strtotime($date, $timezone))) { try { $dt = new DateTime("@".$timestamp); if ($timezone) { $dt->setTimezone($timezone); } } catch (Exception $e) { // ignore } } return $dt; } /** * Clean up date string for strtotime() input * * @param string $date Date string * * @return string Date string */ public static function clean_datestr($date) { $date = trim($date); // check for MS Outlook vCard date format YYYYMMDD if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) { return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3])); } // Clean malformed data $date = preg_replace( [ '/\(.*\)/', // remove RFC comments '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal '/[^a-z0-9\x20\x09:\/\.+-]/i', // remove any invalid characters '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names ], [ '', '\\1', '', '', ], $date ); $date = trim($date); // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) { $mdy = $m[2] > 12 && $m[1] <= 12; $day = $mdy ? $m[2] : $m[1]; $month = $mdy ? $m[1] : $m[2]; $date = sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, isset($m[4]) ? $m[4]: ' 00:00:00'); } // I've found that YYYY.MM.DD is recognized wrong, so here's a fix else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) { $date = sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], isset($m[4]) ? $m[4]: ' 00:00:00'); } return $date; } /** * Turns the given date-only string in defined format into YYYY-MM-DD format. * * Supported formats: 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y' * * @param string $date Date string * @param string $format Input date format * * @return string Date string in YYYY-MM-DD format, or the original string * if format is not supported */ public static function format_datestr($date, $format) { $format_items = preg_split('/[.-\/\\\\]/', $format); $date_items = preg_split('/[.-\/\\\\]/', $date); $iso_format = '%04d-%02d-%02d'; if (count($format_items) == 3 && count($date_items) == 3) { if ($format_items[0] == 'Y') { $date = sprintf($iso_format, $date_items[0], $date_items[1], $date_items[2]); } else if (strpos('dj', $format_items[0]) !== false) { $date = sprintf($iso_format, $date_items[2], $date_items[1], $date_items[0]); } else if (strpos('mn', $format_items[0]) !== false) { $date = sprintf($iso_format, $date_items[2], $date_items[0], $date_items[1]); } } return $date; } /** * Wrapper for idn_to_ascii with support for e-mail address. * * Warning: Domain names may be lowercase'd. * Warning: An empty string may be returned on invalid domain. * * @param string $str Decoded e-mail address * * @return string Encoded e-mail address */ public static function idn_to_ascii($str) { return self::idn_convert($str, true); } /** * Wrapper for idn_to_utf8 with support for e-mail address * * @param string $str Decoded e-mail address * * @return string Encoded e-mail address */ public static function idn_to_utf8($str) { return self::idn_convert($str, false); } /** * Convert a string to ascii or utf8 (using IDNA standard) * * @param string $input Decoded e-mail address * @param boolean $is_utf Convert by idn_to_ascii if true and idn_to_utf8 if false * * @return string Encoded e-mail address */ public static function idn_convert($input, $is_utf = false) { if ($at = strpos($input, '@')) { $user = substr($input, 0, $at); $domain = substr($input, $at + 1); } else { $user = ''; $domain = $input; } // Note that in PHP 7.2/7.3 calling idn_to_* functions with default arguments // throws a warning, so we have to set the variant explicitly (#6075) $variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : null; $options = 0; // Because php-intl extension lowercases domains and return false // on invalid input (#6224), we skip conversion when not needed if ($is_utf) { if (preg_match('/[^\x20-\x7E]/', $domain)) { $options = defined('IDNA_NONTRANSITIONAL_TO_ASCII') ? IDNA_NONTRANSITIONAL_TO_ASCII : 0; $domain = idn_to_ascii($domain, $options, $variant); } } else if (preg_match('/(^|\.)xn--/i', $domain)) { $options = defined('IDNA_NONTRANSITIONAL_TO_UNICODE') ? IDNA_NONTRANSITIONAL_TO_UNICODE : 0; $domain = idn_to_utf8($domain, $options, $variant); } if ($domain === false) { return ''; } return $at ? $user . '@' . $domain : $domain; } /** * Split the given string into word tokens * * @param string $str Input to tokenize * @param int $minlen Minimum length of a single token * * @return array List of tokens */ public static function tokenize_string($str, $minlen = 2) { $expr = ['/[\s;,"\'\/+-]+/ui', '/(\d)[-.\s]+(\d)/u']; $repl = [' ', '\\1\\2']; if ($minlen > 1) { $minlen--; $expr[] = "/(^|\s+)\w{1,$minlen}(\s+|$)/u"; $repl[] = ' '; } return array_filter(explode(" ", preg_replace($expr, $repl, $str))); } /** * Normalize the given string for fulltext search. * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended * * @param string $str Input string (UTF-8) * @param bool $as_array True to return list of words as array * @param int $minlen Minimum length of tokens * * @return string|array Normalized string or a list of normalized tokens */ public static function normalize_string($str, $as_array = false, $minlen = 2) { // replace 4-byte unicode characters with '?' character, // these are not supported in default utf-8 charset on mysql, // the chance we'd need them in searching is very low $str = preg_replace('/(' . '\xF0[\x90-\xBF][\x80-\xBF]{2}' . '|[\xF1-\xF3][\x80-\xBF]{3}' . '|\xF4[\x80-\x8F][\x80-\xBF]{2}' . ')/', '?', $str); // split by words $arr = self::tokenize_string($str, $minlen); // detect character set if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-1'), 'ISO-8859-1', 'UTF-8') == $str) { // ISO-8859-1 (or ASCII) preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys); preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values); $mapping = array_combine($keys[0], $values[0]); $mapping = array_merge($mapping, ['ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u']); } else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) { // ISO-8859-2 preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys); preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values); $mapping = array_combine($keys[0], $values[0]); $mapping = array_merge($mapping, ['ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u']); } foreach ($arr as $i => $part) { $part = mb_strtolower($part); if (!empty($mapping)) { $part = strtr($part, $mapping); } $arr[$i] = $part; } return $as_array ? $arr : implode(' ', $arr); } /** * Compare two strings for matching words (order not relevant) * * @param string $haystack Haystack * @param string $needle Needle * * @return bool True if match, False otherwise */ public static function words_match($haystack, $needle) { $a_needle = self::tokenize_string($needle, 1); $_haystack = implode(' ', self::tokenize_string($haystack, 1)); $valid = strlen($_haystack) > 0; $hits = 0; foreach ($a_needle as $w) { if ($valid) { if (stripos($_haystack, $w) !== false) { $hits++; } } else if (stripos($haystack, $w) !== false) { $hits++; } } return $hits >= count($a_needle); } /** * Parse commandline arguments into a hash array * * @param array $aliases Argument alias names * * @return array Argument values hash */ public static function get_opt($aliases = []) { $args = []; $bool = []; // find boolean (no value) options foreach ($aliases as $key => $alias) { if ($pos = strpos($alias, ':')) { $aliases[$key] = substr($alias, 0, $pos); $bool[] = $key; $bool[] = $aliases[$key]; } } for ($i=1; $i < count($_SERVER['argv']); $i++) { $arg = $_SERVER['argv'][$i]; $value = true; $key = null; if ($arg[0] == '-') { $key = preg_replace('/^-+/', '', $arg); $sp = strpos($arg, '='); if ($sp > 0) { $key = substr($key, 0, $sp - 2); $value = substr($arg, $sp+1); } else if (in_array($key, $bool)) { $value = true; } else if ( isset($_SERVER['argv'][$i + 1]) && strlen($_SERVER['argv'][$i + 1]) && $_SERVER['argv'][$i + 1][0] != '-' ) { $value = $_SERVER['argv'][++$i]; } $args[$key] = is_string($value) ? preg_replace(['/^["\']/', '/["\']$/'], '', $value) : $value; } else { $args[] = $arg; } if (!empty($aliases[$key])) { $alias = $aliases[$key]; $args[$alias] = $args[$key]; } } return $args; } /** * Safe password prompt for command line * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/ * * @param string $prompt Prompt text * * @return string Password */ public static function prompt_silent($prompt = "Password:") { if (preg_match('/^win/i', PHP_OS)) { $vbscript = sys_get_temp_dir() . 'prompt_password.vbs'; $vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))'; file_put_contents($vbscript, $vbcontent); $command = "cscript //nologo " . escapeshellarg($vbscript); $password = rtrim(shell_exec($command)); unlink($vbscript); return $password; } $command = "/usr/bin/env bash -c 'echo OK'"; if (rtrim(shell_exec($command)) !== 'OK') { echo $prompt; $pass = trim(fgets(STDIN)); echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n"; return $pass; } $command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'"; $password = rtrim(shell_exec($command)); echo "\n"; return $password; } /** * Find out if the string content means true or false * * @param string $str Input value * * @return bool Boolean value */ public static function get_boolean($str) { $str = strtolower($str); return !in_array($str, ['false', '0', 'no', 'off', 'nein', ''], true); } /** * OS-dependent absolute path detection * * @param string $path File path * * @return bool True if the path is absolute, False otherwise */ public static function is_absolute_path($path) { if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path); } return isset($path[0]) && $path[0] == '/'; } /** * Resolve relative URL * * @param string $url Relative URL * * @return string Absolute URL */ public static function resolve_url($url) { // prepend protocol://hostname:port if (!preg_match('|^https?://|', $url)) { $schema = 'http'; $default_port = 80; if (self::https_check()) { $schema = 'https'; $default_port = 443; } $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null; $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : null; $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $host); if ($port != $default_port && $port != 80) { $prefix .= ':' . $port; } $url = $prefix . ($url[0] == '/' ? '' : '/') . $url; } return $url; } /** * Generate a random string * * @param int $length String length * @param bool $raw Return RAW data instead of ascii * * @return string The generated random string */ public static function random_bytes($length, $raw = false) { $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; $tabsize = strlen($hextab); // Use PHP7 true random generator if ($raw && function_exists('random_bytes')) { return random_bytes($length); } if (!$raw && function_exists('random_int')) { $result = ''; while ($length-- > 0) { $result .= $hextab[random_int(0, $tabsize - 1)]; } return $result; } $random = openssl_random_pseudo_bytes($length); if ($random === false && $length > 0) { throw new Exception("Failed to get random bytes"); } if (!$raw) { for ($x = 0; $x < $length; $x++) { $random[$x] = $hextab[ord($random[$x]) % $tabsize]; } } return $random; } /** * Convert binary data into readable form (containing a-zA-Z0-9 characters) * * @param string $input Binary input * * @return string Readable output (Base62) * @deprecated since 1.3.1 */ public static function bin2ascii($input) { $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; $result = ''; for ($x = 0; $x < strlen($input); $x++) { $result .= $hextab[ord($input[$x]) % 62]; } return $result; } /** * Format current date according to specified format. * This method supports microseconds (u). * * @param string $format Date format (default: 'd-M-Y H:i:s O') * * @return string Formatted date */ public static function date_format($format = null) { if (empty($format)) { $format = 'd-M-Y H:i:s O'; } if (strpos($format, 'u') !== false) { $dt = number_format(microtime(true), 6, '.', ''); try { $date = date_create_from_format('U.u', $dt); $date->setTimeZone(new DateTimeZone(date_default_timezone_get())); return $date->format($format); } catch (Exception $e) { // ignore, fallback to date() } } return date($format); } /** * Parses socket options and returns options for specified hostname. * * @param array &$options Configured socket options * @param string $host Hostname */ public static function parse_socket_options(&$options, $host = null) { if (empty($host) || empty($options)) { return; } // get rid of schema and port from the hostname $host_url = parse_url($host); if (isset($host_url['host'])) { $host = $host_url['host']; } // find per-host options if ($host && array_key_exists($host, $options)) { $options = $options[$host]; } } /** * Get maximum upload size * * @return int Maximum size in bytes */ public static function max_upload_size() { // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } return $max_filesize; } /** * Detect and log last PREG operation error * * @param array $error Error data (line, file, code, message) * @param bool $terminate Stop script execution * * @return bool True on error, False otherwise */ public static function preg_error($error = [], $terminate = false) { if (($preg_error = preg_last_error()) != PREG_NO_ERROR) { $errstr = "PCRE Error: $preg_error."; if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) { $errstr .= " Consider raising pcre.backtrack_limit!"; } if ($preg_error == PREG_RECURSION_LIMIT_ERROR) { $errstr .= " Consider raising pcre.recursion_limit!"; } $error = array_merge(['code' => 620, 'line' => __LINE__, 'file' => __FILE__], $error); if (!empty($error['message'])) { $error['message'] .= ' ' . $errstr; } else { $error['message'] = $errstr; } rcube::raise_error($error, true, $terminate); return true; } return false; } /** * Generate a temporary file path in the Roundcube temp directory * * @param string $file_name String identifier for the type of temp file * @param bool $unique Generate unique file names based on $file_name * @param bool $create Create the temp file or not * * @return string temporary file path */ public static function temp_filename($file_name, $unique = true, $create = true) { $temp_dir = rcube::get_instance()->config->get('temp_dir'); // Fall back to system temp dir if configured dir is not writable if (!is_writable($temp_dir)) { $temp_dir = sys_get_temp_dir(); } // On Windows tempnam() uses only the first three characters of prefix so use uniqid() and manually add the prefix // Full prefix is required for garbage collection to recognise the file $temp_file = $unique ? str_replace('.', '', uniqid($file_name, true)) : $file_name; $temp_path = unslashify($temp_dir) . '/' . RCUBE_TEMP_FILE_PREFIX . $temp_file; // Sanity check for unique file name if ($unique && file_exists($temp_path)) { return self::temp_filename($file_name, $unique, $create); } // Create the file to prevent possible race condition like tempnam() does if ($create) { touch($temp_path); } return $temp_path; } /** * Clean the subject from reply and forward prefix * * @param string $subject Subject to clean * @param string $mode Mode of cleaning : reply, forward or both * * @return string Cleaned subject */ public static function remove_subject_prefix($subject, $mode = 'both') { $config = rcmail::get_instance()->config; // Clean subject prefix for reply, forward or both if ($mode == 'both') { $reply_prefixes = $config->get('subject_reply_prefixes', ['Re:']); $forward_prefixes = $config->get('subject_forward_prefixes', ['Fwd:', 'Fw:']); $prefixes = array_merge($reply_prefixes, $forward_prefixes); } else if ($mode == 'reply') { $prefixes = $config->get('subject_reply_prefixes', ['Re:']); // replace (was: ...) (#1489375) $subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject); } else if ($mode == 'forward') { $prefixes = $config->get('subject_forward_prefixes', ['Fwd:', 'Fw:']); } // replace Re:, Re[x]:, Re-x (#1490497) $pieces = array_map(function($prefix) { $prefix = strtolower(str_replace(':', '', $prefix)); return "$prefix:|$prefix\[\d\]:|$prefix-\d:"; }, $prefixes); $pattern = '/^('.implode('|', $pieces).')\s*/i'; do { $subject = preg_replace($pattern, '', $subject, -1, $count); } while ($count); return trim($subject); } + + /** + * When proxy_protocol is configured for a connection type, + * generate the HAproxy style PROXY protocol header for injection + * into the TCP stream. + * http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt + * + * PROXY protocol headers must be sent before any other data is sent on the TCP socket. + * + * @param array $conn_options preferences array which may contain proxy_protocol (generally {driver}_conn_options) + * + * @return string proxy protocol header data, if enabled, otherwise empty string + */ + public static function proxy_protocol_header($conn_options = null) + { + if ($conn_options === null) + { + return ""; + } + // verify that proxy_protocol option is present + if (is_array($conn_options) && array_key_exists('proxy_protocol', $conn_options)) { + if (is_array($conn_options['proxy_protocol'])) { + $proxy_protocol_version = $conn_options['proxy_protocol']['version']; + $proxy_protocol_options = $conn_options['proxy_protocol']; + } + else { + $proxy_protocol_version = $conn_options['proxy_protocol']; + $proxy_protocol_options = null; + } + + $proxy_protocol_remote_addr = (array_key_exists('remote_addr', $proxy_protocol_options) ? $proxy_protocol_options['remote_addr'] : $_SERVER['REMOTE_ADDR'] ); + $proxy_protocol_remote_port = (array_key_exists('remote_port', $proxy_protocol_options) ? $proxy_protocol_options['remote_port'] : $_SERVER['REMOTE_PORT'] ); + $proxy_protocol_local_addr = (array_key_exists('local_addr' ,$proxy_protocol_options) ? $proxy_protocol_options['local_addr'] : $_SERVER['SERVER_ADDR'] ); + $proxy_protocol_local_port = (array_key_exists('local_port', $proxy_protocol_options) ? $proxy_protocol_options['local_port'] : $_SERVER['SERVER_PORT'] ); + $proxy_protocol_ip_version = (strpos($proxy_protocol_remote_addr, ":") === false ? 4 : 6); + + if ($proxy_protocol_version === 1) { + // text based PROXY protocol + + // PROXY protocol does not support dual IPv6+IPv4 type addresses, e.g. ::127.0.0.1 + if ($proxy_protocol_ip_version === 6 && strpos($proxy_protocol_remote_addr, ".") !== false) { + $proxy_protocol_remote_addr = inet_ntop(inet_pton($proxy_protocol_remote_addr)); + } + if ($proxy_protocol_ip_version === 6 && strpos($proxy_protocol_local_addr, ".") !== false) { + $proxy_protocol_local_addr = inet_ntop(inet_pton($proxy_protocol_local_addr)); + } + + $proxy_protocol_text = "PROXY " . // protocol header + ($proxy_protocol_ip_version === 6 ? "TCP6 " : "TCP4 ") . // IP version type + $proxy_protocol_remote_addr . + " " . + $proxy_protocol_local_addr . + " " . + $proxy_protocol_remote_port . + " " . + $proxy_protocol_local_port . + "\r\n"; + return $proxy_protocol_text; + } + else if ($proxy_protocol_version === 2) { + // binary PROXY protocol + $proxy_protocol_bin = pack("H*", "0D0A0D0A000D0A515549540A" . // protocol header + "21" . // protocol version and command + ($proxy_protocol_ip_version === 6 ? "2" : "1") . // IP version type + "1"); // TCP + $proxy_protocol_addr = inet_pton($proxy_protocol_remote_addr) . + inet_pton($proxy_protocol_local_addr) . + pack("n", $proxy_protocol_remote_port) . + pack("n", $proxy_protocol_local_port); + $proxy_protocol_bin .= pack("n", strlen($proxy_protocol_addr)) . $proxy_protocol_addr; + return $proxy_protocol_bin; + } + else { + // unknown proxy protocol version + return ""; + } + } + return ""; + } }