[Web] Adjust mailbox format

[Web] Include IMAP lib for future use
[Web] Fix default exception handler
[Web] Fix sync job edit forms
[Web] Other minor fixes
master
andryyy 2018-11-12 10:03:50 +01:00
parent f9bfac4d27
commit d82c2bfdb7
90 changed files with 5671 additions and 56 deletions

View File

@ -734,13 +734,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
$active = intval($_data['active']);
$quota_b = ($quota_m * 1048576);
$maildir = $domain . "/" . $local_part . "/";
$mailbox_attrs = json_encode(
array(
'force_pw_update' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'])),
'tls_enforce_in' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'])),
'tls_enforce_out' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'])),
'sogo_access' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']))
'sogo_access' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'])),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'])
)
);
if (!is_valid_domain_name($domain)) {
@ -875,13 +875,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `attributes`, `active`)
VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `active`)
VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
$stmt->execute(array(
':username' => $username,
':password_hashed' => $password_hashed,
':name' => $name,
':maildir' => $maildir,
':quota_b' => $quota_b,
':local_part' => $local_part,
':domain' => $domain,
@ -1004,8 +1003,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`)
VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :active, :multiple_bookings, :kind)");
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`)
VALUES (:name, 'RESOURCE', :description, 0, :local_part, :domain, :active, :multiple_bookings, :kind)");
$stmt->execute(array(
':name' => $name,
':description' => $description,
@ -2847,7 +2846,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`active` AS `active_int`,
CASE `mailbox`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
`mailbox`.`domain`,
`mailbox`.`maildir`,
`mailbox`.`local_part`,
`mailbox`.`quota`,
`quota2`.`bytes`,
`attributes`,
@ -2878,7 +2877,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['active'] = $row['active'];
$mailboxdata['active_int'] = $row['active_int'];
$mailboxdata['domain'] = $row['domain'];
$mailboxdata['maildir'] = $row['maildir'];
$mailboxdata['local_part'] = $row['local_part'];
$mailboxdata['quota'] = $row['quota'];
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
$mailboxdata['quota_used'] = intval($row['bytes']);
@ -3193,7 +3192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
'msg' => 'Could not move mail storage to garbage collector: ' . $maildir_gc['msg']
);
}
$stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
@ -3369,14 +3368,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
$maildir = mailbox('get', 'mailbox_details', $username)['maildir'];
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
if ($maildir_gc['type'] != 'success') {
$mailbox_details = mailbox('get', 'mailbox_details', $username);
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
if ($maildir_gc['type'] != 'success') {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
);
}
}
else {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty'
);
}
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
$db_version = "22102018_1502";
$db_version = "03112018_1117";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@ -78,7 +78,6 @@ function init_db_schema() {
// TODO -> use TEXT and check if SOGo login breaks on empty aliases
"aliases" => "TEXT NOT NULL",
"ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''",
"home" => "VARCHAR(255)",
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
"multiple_bookings" => "INT NOT NULL DEFAULT -1"
),
@ -230,7 +229,8 @@ function init_db_schema() {
"username" => "VARCHAR(255) NOT NULL",
"password" => "VARCHAR(255) NOT NULL",
"name" => "VARCHAR(255)",
"maildir" => "VARCHAR(255) NOT NULL",
// mailbox_path_prefix is followed by domain/local_part/
"mailbox_path_prefix" => "VARCHAR(150) DEFAULT '/var/vmail/'",
"quota" => "BIGINT(20) NOT NULL DEFAULT '102400'",
"local_part" => "VARCHAR(255) NOT NULL",
"domain" => "VARCHAR(255) NOT NULL",
@ -962,16 +962,26 @@ DELIMITER ;';
// Insert new DB schema version
$stmt = $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');");
// Migrate tls_enforce_* options and add force_pw_update attribute
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` IS NULL;");
// Migrate attributes
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR NULL;");
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_EXTRACT(`attributes`, '$.force_pw_update') IS NULL;");
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_EXTRACT(`attributes`, '$.sogo_access') IS NULL;");
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_EXTRACT(`attributes`, '$.mailbox_format') IS NULL;");
foreach($tls_options as $tls_user => $tls_options) {
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in),
`attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out)
WHERE `username` = :username");
$stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user));
}
// Set tls_enforce_* if still missing (due to deleted attrs for example)
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', \"1\") WHERE JSON_EXTRACT(`attributes`, '$.tls_enforce_out') IS NULL;");
$stmt = $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', \"1\") WHERE JSON_EXTRACT(`attributes`, '$.tls_enforce_in') IS NULL;");
// Fix ACL
$stmt = $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);");
$stmt = $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);");
// Fix domain_admins
$stmt = $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';");
if (php_sapi_name() == "cli") {
echo "DB initialization completed" . PHP_EOL;
} else {
@ -981,11 +991,6 @@ DELIMITER ;';
'msg' => 'db_init_complete'
);
}
// Fix ACL
$stmt = $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);");
$stmt = $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);");
// Fix domain_admins
$stmt = $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';");
}
catch (PDOException $e) {
if (php_sapi_name() == "cli") {
@ -1001,25 +1006,27 @@ DELIMITER ;';
}
if (php_sapi_name() == "cli") {
include '/web/inc/vars.inc.php';
$now = new DateTime();
$mins = $now->getOffset() / 60;
$sgn = ($mins < 0 ? -1 : 1);
$mins = abs($mins);
$hrs = floor($mins / 60);
$mins -= $hrs * 60;
$offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
// $now = new DateTime();
// $mins = $now->getOffset() / 60;
// $sgn = ($mins < 0 ? -1 : 1);
// $mins = abs($mins);
// $hrs = floor($mins / 60);
// $mins -= $hrs * 60;
// $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
//PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
];
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';");
$res = $stmt->fetch(PDO::FETCH_ASSOC);
if (intval($res['OK_C']) === 2) {
$stmt = $pdo->query("REPLACE INTO _sogo_static_view SELECT * from sogo_view");
// Be more precise when replacing into _sogo_static_view, col orders may change
$stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings`)
SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings` from sogo_view");
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
echo "Fixed _sogo_static_view" . PHP_EOL;
}

View File

@ -4,6 +4,7 @@
"yubico/u2flib-server": "^1.0",
"phpmailer/phpmailer": "^5.2",
"php-mime-mail-parser/php-mime-mail-parser": "^2.9",
"soundasleep/html2text": "^0.5.0"
"soundasleep/html2text": "^0.5.0",
"ddeboer/imap": "^1.5"
}
}

View File

@ -4,8 +4,67 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3edeec2e3fa875d4f9d5e7f22a8179be",
"content-hash": "baad410246ce54c06f9bbc7761e02a76",
"packages": [
{
"name": "ddeboer/imap",
"version": "1.5.5",
"source": {
"type": "git",
"url": "https://github.com/ddeboer/imap.git",
"reference": "acf56f54375babb27a245338a13f4e8246975268"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ddeboer/imap/zipball/acf56f54375babb27a245338a13f4e8246975268",
"reference": "acf56f54375babb27a245338a13f4e8246975268",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"ext-imap": "*",
"ext-mbstring": "*",
"php": "^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.10",
"phpstan/phpstan": "^0.9.1",
"phpstan/phpstan-phpunit": "^0.9.3",
"phpunit/phpunit": "^6.5 || ^7.0",
"zendframework/zend-mail": "^2.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Ddeboer\\Imap\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David de Boer",
"email": "david@ddeboer.nl"
},
{
"name": "Community contributors",
"homepage": "https://github.com/ddeboer/imap/graphs/contributors"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com"
}
],
"description": "Object-oriented IMAP for PHP",
"keywords": [
"email",
"imap",
"mail"
],
"time": "2018-08-21T07:30:59+00:00"
},
{
"name": "php-mime-mail-parser/php-mime-mail-parser",
"version": "2.11.1",

View File

@ -279,7 +279,7 @@ class ClassLoader
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
@ -377,7 +377,7 @@ class ClassLoader
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath.'\\';
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {

View File

@ -9,4 +9,5 @@ return array(
'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'),
'PhpMimeMailParser\\' => array($vendorDir . '/php-mime-mail-parser/php-mime-mail-parser/src'),
'Html2Text\\' => array($vendorDir . '/soundasleep/html2text/src'),
'Ddeboer\\Imap\\' => array($vendorDir . '/ddeboer/imap/src'),
);

View File

@ -19,6 +19,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array (
'Html2Text\\' => 10,
),
'D' =>
array (
'Ddeboer\\Imap\\' => 13,
),
);
public static $prefixDirsPsr4 = array (
@ -34,6 +38,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array (
0 => __DIR__ . '/..' . '/soundasleep/html2text/src',
),
'Ddeboer\\Imap\\' =>
array (
0 => __DIR__ . '/..' . '/ddeboer/imap/src',
),
);
public static $classMap = array (

View File

@ -1,4 +1,65 @@
[
{
"name": "ddeboer/imap",
"version": "1.5.5",
"version_normalized": "1.5.5.0",
"source": {
"type": "git",
"url": "https://github.com/ddeboer/imap.git",
"reference": "acf56f54375babb27a245338a13f4e8246975268"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ddeboer/imap/zipball/acf56f54375babb27a245338a13f4e8246975268",
"reference": "acf56f54375babb27a245338a13f4e8246975268",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"ext-imap": "*",
"ext-mbstring": "*",
"php": "^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.10",
"phpstan/phpstan": "^0.9.1",
"phpstan/phpstan-phpunit": "^0.9.3",
"phpunit/phpunit": "^6.5 || ^7.0",
"zendframework/zend-mail": "^2.8"
},
"time": "2018-08-21T07:30:59+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Ddeboer\\Imap\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David de Boer",
"email": "david@ddeboer.nl"
},
{
"name": "Community contributors",
"homepage": "https://github.com/ddeboer/imap/graphs/contributors"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com"
}
],
"description": "Object-oriented IMAP for PHP",
"keywords": [
"email",
"imap",
"mail"
]
},
{
"name": "php-mime-mail-parser/php-mime-mail-parser",
"version": "2.11.1",

View File

@ -0,0 +1,486 @@
# Change Log
## [1.5.5](https://github.com/ddeboer/imap/tree/1.5.5) (2018-08-21)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.5.4...1.5.5)
**Fixed bugs:**
- Plain text attachments are not identified as Attachment parts [\#341](https://github.com/ddeboer/imap/issues/341)
- Handle plain/text attachments without Content-Type header [\#367](https://github.com/ddeboer/imap/pull/367) ([Slamdunk](https://github.com/Slamdunk))
## [1.5.4](https://github.com/ddeboer/imap/tree/1.5.4) (2018-08-19)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.5.3...1.5.4)
**Fixed bugs:**
- Very long filename, result of getFilename\(\) = NULL? [\#365](https://github.com/ddeboer/imap/issues/365)
- Support RFC2231 attachment filenames [\#366](https://github.com/ddeboer/imap/pull/366) ([Slamdunk](https://github.com/Slamdunk))
## [1.5.3](https://github.com/ddeboer/imap/tree/1.5.3) (2018-07-20)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.5.2...1.5.3)
**Fixed bugs:**
- Dates: handle UT timezone [\#361](https://github.com/ddeboer/imap/pull/361) ([Slamdunk](https://github.com/Slamdunk))
## [1.5.2](https://github.com/ddeboer/imap/tree/1.5.2) (2018-07-10)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.5.1...1.5.2)
**Fixed bugs:**
- Fails to load Message Headers [\#358](https://github.com/ddeboer/imap/issues/358)
- Handle invalid headers [\#359](https://github.com/ddeboer/imap/pull/359) ([Slamdunk](https://github.com/Slamdunk))
## [1.5.1](https://github.com/ddeboer/imap/tree/1.5.1) (2018-05-04)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.5.0...1.5.1)
**Fixed bugs:**
- getContent\(\) method returns wrong content part [\#342](https://github.com/ddeboer/imap/issues/342)
- Fix handle of attachment messages with attachments [\#343](https://github.com/ddeboer/imap/pull/343) ([Slamdunk](https://github.com/Slamdunk))
## [1.5.0](https://github.com/ddeboer/imap/tree/1.5.0) (2018-03-26)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.4.1...1.5.0)
**Implemented enhancements:**
- ImapResource: cache last opened mailbox [\#328](https://github.com/ddeboer/imap/pull/328) ([Slamdunk](https://github.com/Slamdunk))
## [1.4.1](https://github.com/ddeboer/imap/tree/1.4.1) (2018-03-22)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.4.0...1.4.1)
**Fixed bugs:**
- Return value of Ddeboer\\Imap\\Message\\AbstractPart::getDecodedContent\(\) must be of the type string, boolean returned [\#284](https://github.com/ddeboer/imap/issues/284)
- base64\_decode may return false in PHP \< 7.1 [\#324](https://github.com/ddeboer/imap/pull/324) ([Slamdunk](https://github.com/Slamdunk))
**Merged pull requests:**
- Add entry in README about Mailbox::addMessage [\#325](https://github.com/ddeboer/imap/pull/325) ([soywod](https://github.com/soywod))
## [1.4.0](https://github.com/ddeboer/imap/tree/1.4.0) (2018-03-19)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.3.1...1.4.0)
**Implemented enhancements:**
- Lazy load Message [\#320](https://github.com/ddeboer/imap/pull/320) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- Invalid argument supplied for foreach\(\) in Parameters.php line 52 [\#317](https://github.com/ddeboer/imap/issues/317)
- Message "11964" does not exist: imap\_fetchstructure\(\): Bad message number [\#310](https://github.com/ddeboer/imap/issues/310)
- imap\_mime\_header\_decode may return false [\#322](https://github.com/ddeboer/imap/pull/322) ([Slamdunk](https://github.com/Slamdunk))
## [1.3.1](https://github.com/ddeboer/imap/tree/1.3.1) (2018-03-09)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.3.0...1.3.1)
**Implemented enhancements:**
- Allow empty port [\#312](https://github.com/ddeboer/imap/pull/312) ([Slamdunk](https://github.com/Slamdunk))
**Closed issues:**
- getServerString\(\) with no port [\#311](https://github.com/ddeboer/imap/issues/311)
## [1.3.0](https://github.com/ddeboer/imap/tree/1.3.0) (2018-02-28)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.2.3...1.3.0)
**Implemented enhancements:**
- Implement bulk-move [\#306](https://github.com/ddeboer/imap/pull/306) ([particleflux](https://github.com/particleflux))
**Closed issues:**
- feature: Bulk move [\#305](https://github.com/ddeboer/imap/issues/305)
**Merged pull requests:**
- README.md: add `Unknown search criterion: OR` note [\#304](https://github.com/ddeboer/imap/pull/304) ([Slamdunk](https://github.com/Slamdunk))
## [1.2.3](https://github.com/ddeboer/imap/tree/1.2.3) (2018-02-09)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.2.2...1.2.3)
**Fixed bugs:**
- $part-\>type can be 9 [\#301](https://github.com/ddeboer/imap/issues/301)
- AbstractPart::isAttachment\(\) handle unknown part type [\#302](https://github.com/ddeboer/imap/pull/302) ([Slamdunk](https://github.com/Slamdunk))
**Merged pull requests:**
- README.md: code-coverage has higher priority than Scrutinizer [\#300](https://github.com/ddeboer/imap/pull/300) ([Slamdunk](https://github.com/Slamdunk))
## [1.2.2](https://github.com/ddeboer/imap/tree/1.2.2) (2018-02-05)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.2.1...1.2.2)
**Implemented enhancements:**
- Allow PHPUnit ^7.0 [\#296](https://github.com/ddeboer/imap/pull/296) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- Attachment-\>getFilename return null [\#297](https://github.com/ddeboer/imap/issues/297)
- Don't handle multiplart as an attachment [\#298](https://github.com/ddeboer/imap/pull/298) ([Slamdunk](https://github.com/Slamdunk))
## [1.2.1](https://github.com/ddeboer/imap/tree/1.2.1) (2018-01-29)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.2.0...1.2.1)
**Implemented enhancements:**
- Introduce strict comparison [\#289](https://github.com/ddeboer/imap/pull/289) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- Invalid Date header found: "Thur, 04 Jan 2018 06:44:23 +0400" [\#293](https://github.com/ddeboer/imap/issues/293)
- MessageIterator::current\(\) fails when there are no messages [\#288](https://github.com/ddeboer/imap/issues/288)
- Remove weekday while parsing date header [\#294](https://github.com/ddeboer/imap/pull/294) ([Slamdunk](https://github.com/Slamdunk))
- MessageIterator: forbid raw calls [\#290](https://github.com/ddeboer/imap/pull/290) ([Slamdunk](https://github.com/Slamdunk))
## [1.2.0](https://github.com/ddeboer/imap/tree/1.2.0) (2018-01-15)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.1.2...1.2.0)
**Implemented enhancements:**
- Make imap\_append\(\) optional arguments reachable [\#280](https://github.com/ddeboer/imap/pull/280) ([Slamdunk](https://github.com/Slamdunk))
- PHPStan: introduce static analysis [\#276](https://github.com/ddeboer/imap/pull/276) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- getAttachments\(\) problem when mixin inline and attachment [\#281](https://github.com/ddeboer/imap/issues/281)
- UnexpectedEncodingException: Cannot decode "5" [\#278](https://github.com/ddeboer/imap/issues/278)
- Handle correctly multiple nested attachments [\#283](https://github.com/ddeboer/imap/pull/283) ([Slamdunk](https://github.com/Slamdunk))
- Manageable UnexpectedEncodingException [\#282](https://github.com/ddeboer/imap/pull/282) ([Slamdunk](https://github.com/Slamdunk))
**Closed issues:**
- Appending mail with options [\#279](https://github.com/ddeboer/imap/issues/279)
## [1.1.2](https://github.com/ddeboer/imap/tree/1.1.2) (2017-12-12)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.1.1...1.1.2)
**Fixed bugs:**
- Unsupported charset "134": mb\_convert\_encoding\(\): Illegal character encoding specified [\#270](https://github.com/ddeboer/imap/issues/270)
- Support Microsoft charset values [\#271](https://github.com/ddeboer/imap/pull/271) ([Slamdunk](https://github.com/Slamdunk))
## [1.1.1](https://github.com/ddeboer/imap/tree/1.1.1) (2017-11-10)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.1.0...1.1.1)
**Implemented enhancements:**
- Transcoder: expand charset aliases list [\#267](https://github.com/ddeboer/imap/pull/267) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- Charset aliases: fix to lowercase search [\#266](https://github.com/ddeboer/imap/pull/266) ([Slamdunk](https://github.com/Slamdunk))
**Merged pull requests:**
- README.md: add timeout note [\#263](https://github.com/ddeboer/imap/pull/263) ([Slamdunk](https://github.com/Slamdunk))
## [1.1.0](https://github.com/ddeboer/imap/tree/1.1.0) (2017-11-06)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.8...1.1.0)
**Implemented enhancements:**
- Headers: no catchable exception [\#246](https://github.com/ddeboer/imap/issues/246)
- imap\_thread [\#113](https://github.com/ddeboer/imap/issues/113)
- Deprecate MessageInterface::maskAsSeen\(\) in favour of MessageInterface::markAsSeen\(\) [\#255](https://github.com/ddeboer/imap/pull/255) ([Slamdunk](https://github.com/Slamdunk))
- Lazy load structured Headers [\#250](https://github.com/ddeboer/imap/pull/250) ([Slamdunk](https://github.com/Slamdunk))
- Implement imap\_thread [\#249](https://github.com/ddeboer/imap/pull/249) ([Slamdunk](https://github.com/Slamdunk))
- Require ext-iconv [\#248](https://github.com/ddeboer/imap/pull/248) ([Slamdunk](https://github.com/Slamdunk))
- Message Part: expose $partNumber [\#244](https://github.com/ddeboer/imap/pull/244) ([wujku](https://github.com/wujku))
- Add Mockability helpers and documentation [\#236](https://github.com/ddeboer/imap/pull/236) ([Slamdunk](https://github.com/Slamdunk))
- Add missing interface change for \#225 [\#233](https://github.com/ddeboer/imap/pull/233) ([Slamdunk](https://github.com/Slamdunk))
- Connection: check if the connection is still active with `imap\_ping` [\#232](https://github.com/ddeboer/imap/pull/232) ([wujku](https://github.com/wujku))
- Message: add `References` and `In-Reply-To` headers shortcuts [\#230](https://github.com/ddeboer/imap/pull/230) ([wujku](https://github.com/wujku))
- Added bulk set / clear flags functionality for mailbox messages [\#225](https://github.com/ddeboer/imap/pull/225) ([wujku](https://github.com/wujku))
**Merged pull requests:**
- make docs more obvious [\#252](https://github.com/ddeboer/imap/pull/252) ([lgg](https://github.com/lgg))
- README.md: add Table of Contents with Travis checker [\#234](https://github.com/ddeboer/imap/pull/234) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.8](https://github.com/ddeboer/imap/tree/1.0.8) (2017-10-27)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.7...1.0.8)
**Fixed bugs:**
- \[TypeError\] Return value of Ddeboer\Imap\Message\AbstractMessage::getId\(\) must be of the type string, null returned [\#253](https://github.com/ddeboer/imap/issues/253)
- BasicMessageInterface::getId\(\) can be null [\#254](https://github.com/ddeboer/imap/pull/254) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.7](https://github.com/ddeboer/imap/tree/1.0.7) (2017-10-16)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.6...1.0.7)
**Fixed bugs:**
- Problem with a IMAP resource stream [\#245](https://github.com/ddeboer/imap/issues/245)
- IMAP resource must be checked at every call for mailbox context [\#247](https://github.com/ddeboer/imap/pull/247) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.6](https://github.com/ddeboer/imap/tree/1.0.6) (2017-10-12)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.5...1.0.6)
**Fixed bugs:**
- \[TypeError\] Return value of AbstractMessage::getFrom\(\) must be an instance of EmailAddress, null returned [\#241](https://github.com/ddeboer/imap/issues/241)
- Message: Date header can be absent [\#243](https://github.com/ddeboer/imap/pull/243) ([Slamdunk](https://github.com/Slamdunk))
- Message: From header can be absent [\#242](https://github.com/ddeboer/imap/pull/242) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.5](https://github.com/ddeboer/imap/tree/1.0.5) (2017-10-12)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.4...1.0.5)
**Fixed bugs:**
- Use set\_error\_handler with late exception [\#240](https://github.com/ddeboer/imap/pull/240) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.4](https://github.com/ddeboer/imap/tree/1.0.4) (2017-10-11)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.3...1.0.4)
**Implemented enhancements:**
- Avoid \(set|restor\)\_error\_handler [\#239](https://github.com/ddeboer/imap/pull/239) ([Slamdunk](https://github.com/Slamdunk))
**Fixed bugs:**
- Current Transcoder class does not support all charsets. [\#237](https://github.com/ddeboer/imap/issues/237)
- Relay also iconv during decoding [\#238](https://github.com/ddeboer/imap/pull/238) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.3](https://github.com/ddeboer/imap/tree/1.0.3) (2017-10-11)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.2...1.0.3)
**Fixed bugs:**
- Attachment::getFilename\(\) may be null on inline-att, widen return type [\#235](https://github.com/ddeboer/imap/pull/235) ([wujku](https://github.com/wujku))
## [1.0.2](https://github.com/ddeboer/imap/tree/1.0.2) (2017-10-06)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.1...1.0.2)
**Fixed bugs:**
- Issue with saving XML attachments [\#228](https://github.com/ddeboer/imap/issues/228)
- Do not charset-decode attachments [\#231](https://github.com/ddeboer/imap/pull/231) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.1](https://github.com/ddeboer/imap/tree/1.0.1) (2017-10-05)
[Full Changelog](https://github.com/ddeboer/imap/compare/1.0.0...1.0.1)
**Fixed bugs:**
- Error with attachment charset [\#226](https://github.com/ddeboer/imap/issues/226)
- If charset is not specified defaults to "us-ascii" [\#227](https://github.com/ddeboer/imap/pull/227) ([Slamdunk](https://github.com/Slamdunk))
## [1.0.0](https://github.com/ddeboer/imap/tree/1.0.0) (2017-10-04)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.5.2...1.0.0)
**Implemented enhancements:**
- Need getAll for headers [\#200](https://github.com/ddeboer/imap/issues/200)
- Tests: implement @covers to avoid false positive on code-coverage [\#188](https://github.com/ddeboer/imap/issues/188)
- Remove commented code [\#174](https://github.com/ddeboer/imap/issues/174)
- Regex in SearchExpressions [\#157](https://github.com/ddeboer/imap/issues/157)
- How do I get unread messages count? [\#98](https://github.com/ddeboer/imap/issues/98)
- Add mocking ability through Interfaces [\#221](https://github.com/ddeboer/imap/pull/221) ([Slamdunk](https://github.com/Slamdunk))
- Wrap imap resource to periodically check its status [\#220](https://github.com/ddeboer/imap/pull/220) ([Slamdunk](https://github.com/Slamdunk))
- Add more coding-standard rules [\#218](https://github.com/ddeboer/imap/pull/218) ([Slamdunk](https://github.com/Slamdunk))
- Always keep unseen: remove keepUnseen, add markAsSeen [\#217](https://github.com/ddeboer/imap/pull/217) ([Slamdunk](https://github.com/Slamdunk))
- Embedded messages: refactor \#106 [\#216](https://github.com/ddeboer/imap/pull/216) ([Slamdunk](https://github.com/Slamdunk))
- Headers now extends \ArrayIterator [\#215](https://github.com/ddeboer/imap/pull/215) ([Slamdunk](https://github.com/Slamdunk))
- Implement imap\_mail\_copy [\#214](https://github.com/ddeboer/imap/pull/214) ([Slamdunk](https://github.com/Slamdunk))
- Imap sort [\#213](https://github.com/ddeboer/imap/pull/213) ([Slamdunk](https://github.com/Slamdunk))
- Increased code-coverage [\#211](https://github.com/ddeboer/imap/pull/211) ([Slamdunk](https://github.com/Slamdunk))
- Update to PHPUnit ^6.2 [\#209](https://github.com/ddeboer/imap/pull/209) ([Slamdunk](https://github.com/Slamdunk))
- Use specific exceptions to ease user catches [\#208](https://github.com/ddeboer/imap/pull/208) ([Slamdunk](https://github.com/Slamdunk))
- Wrap Exception on invalid Date header [\#205](https://github.com/ddeboer/imap/pull/205) ([Slamdunk](https://github.com/Slamdunk))
- Add tests for \#144 set flags functionalities [\#203](https://github.com/ddeboer/imap/pull/203) ([Slamdunk](https://github.com/Slamdunk))
- Add imap\_fetchheader\(\) functionality to get raw headers [\#202](https://github.com/ddeboer/imap/pull/202) ([Slamdunk](https://github.com/Slamdunk))
- Parse all email type headers [\#199](https://github.com/ddeboer/imap/pull/199) ([Slamdunk](https://github.com/Slamdunk))
- Test search conditions [\#198](https://github.com/ddeboer/imap/pull/198) ([Slamdunk](https://github.com/Slamdunk))
- Mailbox: get status [\#192](https://github.com/ddeboer/imap/pull/192) ([Slamdunk](https://github.com/Slamdunk))
- SearchExpression is a Search\ConditionInterface [\#191](https://github.com/ddeboer/imap/pull/191) ([Slamdunk](https://github.com/Slamdunk))
- SearchCondition: \_\_toString\(\) -\> toString\(\) [\#187](https://github.com/ddeboer/imap/pull/187) ([Slamdunk](https://github.com/Slamdunk))
- Retain imap\_getmailboxes\(\) results [\#184](https://github.com/ddeboer/imap/pull/184) ([Slamdunk](https://github.com/Slamdunk))
- Add type hints and return types [\#183](https://github.com/ddeboer/imap/pull/183) ([Slamdunk](https://github.com/Slamdunk))
- Exception: increase verbosity with imap\_alerts\(\) and imap\_errors\(\) [\#182](https://github.com/ddeboer/imap/pull/182) ([Slamdunk](https://github.com/Slamdunk))
- Add coding-standards [\#181](https://github.com/ddeboer/imap/pull/181) ([Slamdunk](https://github.com/Slamdunk))
- Travis: re-enable code-coverage on scrutinizer [\#177](https://github.com/ddeboer/imap/pull/177) ([Slamdunk](https://github.com/Slamdunk))
- Add .gitattributes to remove from releases unneded files [\#173](https://github.com/ddeboer/imap/pull/173) ([Slamdunk](https://github.com/Slamdunk))
- Travis: use local Dovecot installation [\#170](https://github.com/ddeboer/imap/pull/170) ([Slamdunk](https://github.com/Slamdunk))
- Need all Headers in string format [\#149](https://github.com/ddeboer/imap/pull/149) ([FlashWS](https://github.com/FlashWS))
- Get raw mail [\#146](https://github.com/ddeboer/imap/pull/146) ([styxit](https://github.com/styxit))
- add getBcc\(\), Set, Clear Flag\(\Seen, \Answered, \Flagged, \Deleted, and \Draft\), getHeadersRaw\(\) [\#144](https://github.com/ddeboer/imap/pull/144) ([trungpv93](https://github.com/trungpv93))
**Fixed bugs:**
- Search\Condition needs charset escaping/indication [\#190](https://github.com/ddeboer/imap/issues/190)
- imap\_utf7\_\(encode|decode\) -\> mb\_convert\_encoding [\#185](https://github.com/ddeboer/imap/issues/185)
- España [\#176](https://github.com/ddeboer/imap/issues/176)
- getHeaders\(\) decode broke information [\#171](https://github.com/ddeboer/imap/issues/171)
- Date format for date search condition [\#168](https://github.com/ddeboer/imap/issues/168)
- Error when trying fetch messages from container [\#167](https://github.com/ddeboer/imap/issues/167)
- Attachment encoding error [\#158](https://github.com/ddeboer/imap/issues/158)
- getFilename\(\) is empty and no attachment, even when there is an attachment. [\#142](https://github.com/ddeboer/imap/issues/142)
- Encoding issues [\#136](https://github.com/ddeboer/imap/issues/136)
- URGENT: The timezone could not be found in the database [\#135](https://github.com/ddeboer/imap/issues/135)
- Incorrect transcoding of text attachments [\#132](https://github.com/ddeboer/imap/issues/132)
- Undefined offset [\#123](https://github.com/ddeboer/imap/issues/123)
- ICS file not supported as attachment [\#120](https://github.com/ddeboer/imap/issues/120)
- Should iconv be a requirement? [\#115](https://github.com/ddeboer/imap/issues/115)
- KeepUnseen doen't work [\#92](https://github.com/ddeboer/imap/issues/92)
- PHP Fatal error Failed to parse time string in ddeboer/imap/src/Message.php [\#89](https://github.com/ddeboer/imap/issues/89)
- encoding issue [\#85](https://github.com/ddeboer/imap/issues/85)
- keepUnseen not working correctly with Hotmail [\#84](https://github.com/ddeboer/imap/issues/84)
- Iconv Exception [\#78](https://github.com/ddeboer/imap/issues/78)
- $message-\>getAttachments\(\) doesn't recognize some attachments [\#74](https://github.com/ddeboer/imap/issues/74)
- Message::move\(\) doesn't work. [\#73](https://github.com/ddeboer/imap/issues/73)
- Message\Part: part number must distinguish original message [\#223](https://github.com/ddeboer/imap/pull/223) ([Slamdunk](https://github.com/Slamdunk))
- Recursive Embedded email body bug [\#222](https://github.com/ddeboer/imap/pull/222) ([Slamdunk](https://github.com/Slamdunk))
- Exclude HTML from allowed attachment subtype [\#212](https://github.com/ddeboer/imap/pull/212) ([Slamdunk](https://github.com/Slamdunk))
- Fix imap\_mail\_move behaviour and test it [\#207](https://github.com/ddeboer/imap/pull/207) ([Slamdunk](https://github.com/Slamdunk))
- Undefined encoding: throw exception [\#197](https://github.com/ddeboer/imap/pull/197) ([Slamdunk](https://github.com/Slamdunk))
- Message charset: mb\_convert\_encoding + aliases [\#196](https://github.com/ddeboer/imap/pull/196) ([Slamdunk](https://github.com/Slamdunk))
- Mailbox: only UTF-8 names [\#193](https://github.com/ddeboer/imap/pull/193) ([Slamdunk](https://github.com/Slamdunk))
- Search\Date\AbstractDate: fix format to RFC-3501 [\#189](https://github.com/ddeboer/imap/pull/189) ([Slamdunk](https://github.com/Slamdunk))
- Travis: fix failing tests [\#172](https://github.com/ddeboer/imap/pull/172) ([Slamdunk](https://github.com/Slamdunk))
- Return body of single-part HTML message as HTML, not text [\#101](https://github.com/ddeboer/imap/pull/101) ([joker806](https://github.com/joker806))
- Implement "undisclosed recipients" addresses [\#86](https://github.com/ddeboer/imap/pull/86) ([darit](https://github.com/darit))
**Closed issues:**
- Potential memory issue with attachments [\#195](https://github.com/ddeboer/imap/issues/195)
- Explain Message::delete [\#175](https://github.com/ddeboer/imap/issues/175)
- Get raw message [\#161](https://github.com/ddeboer/imap/issues/161)
- Composer install problem [\#160](https://github.com/ddeboer/imap/issues/160)
- Transcoder not exist [\#154](https://github.com/ddeboer/imap/issues/154)
- The library doesn't support using sort by [\#151](https://github.com/ddeboer/imap/issues/151)
- Office 365 - Array to string conversion error [\#131](https://github.com/ddeboer/imap/issues/131)
- Is there a method to turn a seen message into an "unseen" one ? [\#130](https://github.com/ddeboer/imap/issues/130)
- Create mailbox [\#126](https://github.com/ddeboer/imap/issues/126)
- Move and Delete Message not working [\#112](https://github.com/ddeboer/imap/issues/112)
- Problem on production server [\#111](https://github.com/ddeboer/imap/issues/111)
- Authentication failed for a Gmail account [\#109](https://github.com/ddeboer/imap/issues/109)
- A method to run IMAP commands? [\#83](https://github.com/ddeboer/imap/issues/83)
**Merged pull requests:**
- Update README.md to latest develop changes [\#224](https://github.com/ddeboer/imap/pull/224) ([Slamdunk](https://github.com/Slamdunk))
- Add Filippo Tessarotto as an author of the package [\#219](https://github.com/ddeboer/imap/pull/219) ([Slamdunk](https://github.com/Slamdunk))
- README.md: call Connection::expunge after move and delete [\#210](https://github.com/ddeboer/imap/pull/210) ([Slamdunk](https://github.com/Slamdunk))
- Remove misleading Mailbox::expunge\(\) [\#206](https://github.com/ddeboer/imap/pull/206) ([Slamdunk](https://github.com/Slamdunk))
- Add CHANGELOG.md [\#194](https://github.com/ddeboer/imap/pull/194) ([Slamdunk](https://github.com/Slamdunk))
- README.md updates [\#178](https://github.com/ddeboer/imap/pull/178) ([Slamdunk](https://github.com/Slamdunk))
## [0.5.2](https://github.com/ddeboer/imap/tree/0.5.2) (2015-12-03)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.5.1...0.5.2)
**Closed issues:**
- $message-\>getAttachments\(\) returns null if message has no attachments [\#80](https://github.com/ddeboer/imap/issues/80)
- Email objects visibility [\#76](https://github.com/ddeboer/imap/issues/76)
**Merged pull requests:**
- Fixed the keepUnseen method [\#95](https://github.com/ddeboer/imap/pull/95) ([aeyoll](https://github.com/aeyoll))
- Mark Mailbox as countable, fix doc comments [\#91](https://github.com/ddeboer/imap/pull/91) ([krzysiekpiasecki](https://github.com/krzysiekpiasecki))
- Message::getAttachments confirm to signature [\#82](https://github.com/ddeboer/imap/pull/82) ([boekkooi](https://github.com/boekkooi))
- Added hasMailbox to Connection [\#81](https://github.com/ddeboer/imap/pull/81) ([boekkooi](https://github.com/boekkooi))
- Make sure imap connection are reopened [\#79](https://github.com/ddeboer/imap/pull/79) ([joserobleda](https://github.com/joserobleda))
## [0.5.1](https://github.com/ddeboer/imap/tree/0.5.1) (2015-02-01)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.5.0...0.5.1)
**Closed issues:**
- imap\_open error [\#72](https://github.com/ddeboer/imap/issues/72)
- $message-\>getAttachments\(\) does not return anything, even though a message has at least one attachment [\#71](https://github.com/ddeboer/imap/issues/71)
- Prepare docs for 1.0 [\#69](https://github.com/ddeboer/imap/issues/69)
- "date" header is not reliable [\#63](https://github.com/ddeboer/imap/issues/63)
- File Attachments don't show up [\#55](https://github.com/ddeboer/imap/issues/55)
**Merged pull requests:**
- Add support for attachments without content disposition [\#70](https://github.com/ddeboer/imap/pull/70) ([ddeboer](https://github.com/ddeboer))
## [0.5.0](https://github.com/ddeboer/imap/tree/0.5.0) (2015-01-24)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.4.0...0.5.0)
**Closed issues:**
- Use utf8\_encode\(\) function to encode content [\#66](https://github.com/ddeboer/imap/issues/66)
- Please add function order by date [\#59](https://github.com/ddeboer/imap/issues/59)
- mb\_convert\_encoding breaks code [\#57](https://github.com/ddeboer/imap/issues/57)
- How get I getMessages but newest first ... [\#11](https://github.com/ddeboer/imap/issues/11)
## [0.4.0](https://github.com/ddeboer/imap/tree/0.4.0) (2015-01-04)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.3.1...0.4.0)
**Closed issues:**
- Please add 6th parameter to imap\_open call [\#62](https://github.com/ddeboer/imap/issues/62)
- Should Message::delete\(\) use the Message UID? [\#46](https://github.com/ddeboer/imap/issues/46)
- mb\_convert\_encoding\(\): Illegal character encoding specified [\#35](https://github.com/ddeboer/imap/issues/35)
- Deleting a message isn't working [\#30](https://github.com/ddeboer/imap/issues/30)
- imap\_header doesn't work with message uid [\#26](https://github.com/ddeboer/imap/issues/26)
**Merged pull requests:**
- Added basic requirement [\#61](https://github.com/ddeboer/imap/pull/61) ([nikoskip](https://github.com/nikoskip))
- FIX: PHP error: "Cannot declare class Ddeboer\Imap\Search\Text\Text ..." [\#58](https://github.com/ddeboer/imap/pull/58) ([racztiborzoltan](https://github.com/racztiborzoltan))
- Message::delete sets the FT\_UID flag. Fixes \#30 Fixes \#46 [\#54](https://github.com/ddeboer/imap/pull/54) ([ctalbot](https://github.com/ctalbot))
- Allow binary-encoded part content [\#48](https://github.com/ddeboer/imap/pull/48) ([joker806](https://github.com/joker806))
- Fix CS [\#47](https://github.com/ddeboer/imap/pull/47) ([xelan](https://github.com/xelan))
- fixed typo [\#45](https://github.com/ddeboer/imap/pull/45) ([xelan](https://github.com/xelan))
## [0.3.1](https://github.com/ddeboer/imap/tree/0.3.1) (2014-08-11)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.3.0...0.3.1)
**Merged pull requests:**
- \imap\_header dosen't work with UID [\#44](https://github.com/ddeboer/imap/pull/44) ([ysramirez](https://github.com/ysramirez))
## [0.3.0](https://github.com/ddeboer/imap/tree/0.3.0) (2014-08-10)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.2...0.3.0)
**Closed issues:**
- please remove useless wiki [\#42](https://github.com/ddeboer/imap/issues/42)
- Travis tests allways fail? [\#40](https://github.com/ddeboer/imap/issues/40)
- Garbled e-mail body encoding [\#27](https://github.com/ddeboer/imap/issues/27)
- Improve docs [\#25](https://github.com/ddeboer/imap/issues/25)
- "undisclosed-recipients" throws error [\#23](https://github.com/ddeboer/imap/issues/23)
**Merged pull requests:**
- correct minor typo [\#43](https://github.com/ddeboer/imap/pull/43) ([cordoval](https://github.com/cordoval))
- Utf-8 encode body content. [\#39](https://github.com/ddeboer/imap/pull/39) ([cmoralesweb](https://github.com/cmoralesweb))
- Fix regex parsing the date header \(allowing multiple brackets\) [\#38](https://github.com/ddeboer/imap/pull/38) ([joker806](https://github.com/joker806))
- Allow empty connection flags [\#34](https://github.com/ddeboer/imap/pull/34) ([joker806](https://github.com/joker806))
- Fixed typo [\#32](https://github.com/ddeboer/imap/pull/32) ([abhinavkumar940](https://github.com/abhinavkumar940))
## [0.2](https://github.com/ddeboer/imap/tree/0.2) (2013-11-24)
[Full Changelog](https://github.com/ddeboer/imap/compare/0.1...0.2)
## [0.1](https://github.com/ddeboer/imap/tree/0.1) (2013-11-22)
**Closed issues:**
- Prevent setting SEEN flag [\#20](https://github.com/ddeboer/imap/issues/20)
- Add tests [\#18](https://github.com/ddeboer/imap/issues/18)
- delete messages [\#9](https://github.com/ddeboer/imap/issues/9)
- README is missing basic usage [\#7](https://github.com/ddeboer/imap/issues/7)
- Subject and other texts are decoded incorrectly [\#3](https://github.com/ddeboer/imap/issues/3)
**Merged pull requests:**
- also fetch inline attachments [\#24](https://github.com/ddeboer/imap/pull/24) ([kaiserlos](https://github.com/kaiserlos))
- since leading slash is always needed [\#22](https://github.com/ddeboer/imap/pull/22) ([huglester](https://github.com/huglester))
- Added missed createMailbox\($name\) function [\#19](https://github.com/ddeboer/imap/pull/19) ([burci](https://github.com/burci))
- Added move and delete function to message + expunge function [\#17](https://github.com/ddeboer/imap/pull/17) ([burci](https://github.com/burci))
- Clean up some unused variable [\#16](https://github.com/ddeboer/imap/pull/16) ([burci](https://github.com/burci))
- Fixed mailbox encoding [\#15](https://github.com/ddeboer/imap/pull/15) ([burci](https://github.com/burci))
- Create new mailbox [\#14](https://github.com/ddeboer/imap/pull/14) ([burci](https://github.com/burci))
- Fixed bug in getDecodedContent with 'format=flowed' email [\#13](https://github.com/ddeboer/imap/pull/13) ([burci](https://github.com/burci))
- Fixed date parsing for some imap servers [\#12](https://github.com/ddeboer/imap/pull/12) ([thelfensdrfer](https://github.com/thelfensdrfer))
- Add support for more complex search expressions. [\#10](https://github.com/ddeboer/imap/pull/10) ([jamesiarmes](https://github.com/jamesiarmes))
- Allow user to change server connection flags [\#6](https://github.com/ddeboer/imap/pull/6) ([mvar](https://github.com/mvar))
- Improvements in EmailAddress class [\#4](https://github.com/ddeboer/imap/pull/4) ([mvar](https://github.com/mvar))
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*

View File

@ -0,0 +1,19 @@
Copyright (C) 2013 David de Boer <david@ddeboer.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,347 @@
# IMAP library
[![Build Status](https://travis-ci.org/ddeboer/imap.svg?branch=master)](https://travis-ci.org/ddeboer/imap)
[![Code Coverage](https://scrutinizer-ci.com/g/ddeboer/imap/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/ddeboer/imap/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ddeboer/imap/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ddeboer/imap/?branch=master)
[![Latest Stable Version](https://poser.pugx.org/ddeboer/imap/v/stable.svg)](https://packagist.org/packages/ddeboer/imap)
[![Total Downloads](https://poser.pugx.org/ddeboer/imap/downloads.png)](https://packagist.org/packages/ddeboer/imap)
A PHP 7.0+ library to read and process e-mails over IMAP.
This library requires [IMAP](https://secure.php.net/manual/en/book.imap.php),
[iconv](https://secure.php.net/manual/en/book.iconv.php) and
[Multibyte String](https://secure.php.net/manual/en/book.mbstring.php) extensions installed.
## Table of Contents
1. [Installation](#installation)
1. [Usage](#usage)
1. [Connect and Authenticate](#connect-and-authenticate)
1. [Mailboxes](#mailboxes)
1. [Messages](#messages)
1. [Searching for Messages](#searching-for-messages)
1. [Unknown search criterion: OR](#unknown-search-criterion-or)
1. [Message Properties and Operations](#message-properties-and-operations)
1. [Message Attachments](#message-attachments)
1. [Embedded Messages](#embedded-messages)
1. [Timeouts](#timeouts)
1. [Mock the library](#mock-the-library)
1. [Running the Tests](#running-the-tests)
## Installation
The recommended way to install the IMAP library is through [Composer](https://getcomposer.org):
```bash
$ composer require ddeboer/imap
```
This command requires you to have Composer installed globally, as explained
in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.
## Usage
### Connect and Authenticate
```php
use Ddeboer\Imap\Server;
$server = new Server('imap.gmail.com');
// $connection is instance of \Ddeboer\Imap\Connection
$connection = $server->authenticate('my_username', 'my_password');
```
You can specify port, [flags and parameters](https://secure.php.net/manual/en/function.imap-open.php)
to the server:
```php
$server = new Server(
$hostname, // required
$port, // defaults to '993'
$flags, // defaults to '/imap/ssl/validate-cert'
$parameters
);
```
### Mailboxes
Retrieve mailboxes (also known as mail folders) from the mail server and iterate
over them:
```php
$mailboxes = $connection->getMailboxes();
foreach ($mailboxes as $mailbox) {
// Skip container-only mailboxes
// @see https://secure.php.net/manual/en/function.imap-getmailboxes.php
if ($mailbox->getAttributes() & \LATT_NOSELECT) {
continue;
}
// $mailbox is instance of \Ddeboer\Imap\Mailbox
printf('Mailbox "%s" has %s messages', $mailbox->getName(), $mailbox->count());
}
```
Or retrieve a specific mailbox:
```php
$mailbox = $connection->getMailbox('INBOX');
```
Delete a mailbox:
```php
$connection->deleteMailbox($mailbox);
```
You can bulk set, or clear, any [flag](https://secure.php.net/manual/en/function.imap-setflag-full.php) of mailbox messages (by UIDs):
```php
$mailbox->setFlag('\\Seen \\Flagged', ['1:5', '7', '9']);
$mailbox->setFlag('\\Seen', '1,3,5,6:8');
$mailbox->clearFlag('\\Flagged', '1,3');
```
**WARNING** You must retrieve new Message instances in case of bulk modify flags to refresh the single Messages flags.
### Messages
Retrieve messages (e-mails) from a mailbox and iterate over them:
```php
$messages = $mailbox->getMessages();
foreach ($messages as $message) {
// $message is instance of \Ddeboer\Imap\Message
}
```
To insert a new message (that just has been sent) into the Sent mailbox and flag it as seen:
```php
$mailbox = $connection->getMailbox('Sent');
$mailbox->addMessage($messageMIME, '\\Seen');
```
Note that the message should be a string at MIME format (as described in the [RFC2045](https://tools.ietf.org/html/rfc2045)).
#### Searching for Messages
```php
use Ddeboer\Imap\SearchExpression;
use Ddeboer\Imap\Search\Email\To;
use Ddeboer\Imap\Search\Text\Body;
$search = new SearchExpression();
$search->addCondition(new To('me@here.com'));
$search->addCondition(new Body('contents'));
$messages = $mailbox->getMessages($search);
```
**WARNING** We are currently unable to have both spaces _and_ double-quotes
escaped together. Only spaces are currently escaped correctly.
You can use `Ddeboer\Imap\Search\RawExpression` to write the complete search
condition by yourself.
Messages can also be retrieved sorted as per [imap_sort](https://secure.php.net/manual/en/function.imap-sort.php)
function:
```php
$today = new DateTimeImmutable();
$lastMonth = $today->sub(new DateInterval('P30D'));
$messages = $mailbox->getMessages(
new Ddeboer\Imap\Search\Date\Since($lastMonth),
\SORTDATE, // Sort criteria
true // Descending order
);
```
#### Unknown search criterion: OR
Note that PHP imap library relies on the `c-client` library available at https://www.washington.edu/imap/
which doesn't fully support some IMAP4 search criteria like `OR`. If you want those unsupported criteria,
you need to manually patch the latest version (`imap-2007f` of 23-Jul-2011 at the time of this commit)
and recompile PHP onto your patched `c-client` library.
By the way most of the common search criteria are available and functioning, browse them in `./src/Search`.
References:
1. https://stackoverflow.com/questions/36356715/imap-search-unknown-search-criterion-or
1. imap-2007f.tar.gz: `./src/c-client/mail.c` and `./docs/internal.txt`
#### Message Properties and Operations
Get message number and unique [message id](https://en.wikipedia.org/wiki/Message-ID)
in the form <...>:
```php
$message->getNumber();
$message->getId();
```
Get other message properties:
```php
$message->getSubject();
$message->getFrom(); // Message\EmailAddress
$message->getTo(); // array of Message\EmailAddress
$message->getDate(); // DateTimeImmutable
$message->isAnswered();
$message->isDeleted();
$message->isDraft();
$message->isSeen();
```
Get message headers as a [\Ddeboer\Imap\Message\Headers](/src/Ddeboer/Imap/Message/Headers.php) object:
```php
$message->getHeaders();
```
Get message body as HTML or plain text:
```php
$message->getBodyHtml(); // Content of text/html part, if present
$message->getBodyText(); // Content of text/plain part, if present
```
Reading the message body keeps the message as unseen.
If you want to mark the message as seen:
```php
$message->markAsSeen();
```
Or you can set, or clear, any [flag](https://secure.php.net/manual/en/function.imap-setflag-full.php):
```php
$message->setFlag('\\Seen \\Flagged');
$message->clearFlag('\\Flagged');
```
Move a message to another mailbox:
```php
$mailbox = $connection->getMailbox('another-mailbox');
$message->move($mailbox);
$connection->expunge();
```
Deleting messages:
```php
$mailbox->getMessage(1)->delete();
$mailbox->getMessage(2)->delete();
$connection->expunge();
```
### Message Attachments
Get message attachments (both inline and attached) and iterate over them:
```php
$attachments = $message->getAttachments();
foreach ($attachments as $attachment) {
// $attachment is instance of \Ddeboer\Imap\Message\Attachment
}
```
Download a message attachment to a local file:
```php
// getDecodedContent() decodes the attachments contents automatically:
file_put_contents(
'/my/local/dir/' . $attachment->getFilename(),
$attachment->getDecodedContent()
);
```
### Embedded Messages
Check if attachment is embedded message and get it:
```php
$attachments = $message->getAttachments();
foreach ($attachments as $attachment) {
if ($attachment->isEmbeddedMessage()) {
$embeddedMessage = $attachment->getEmbeddedMessage();
// $embeddedMessage is instance of \Ddeboer\Imap\Message\EmbeddedMessage
}
}
```
An EmbeddedMessage has the same API as a normal Message, apart from flags
and operations like copy, move or delete.
### Timeouts
The IMAP extension provides the [imap_timeout](https://secure.php.net/manual/en/function.imap-timeout.php)
function to adjust the timeout seconds for various operations.
However the extension's implementation doesn't link the functionality to a
specific context or connection, instead they are global. So in order to not
affect functionalities outside this library, we had to choose whether wrap
every `imap_*` call around an optional user-provided timeout or leave this
task to the user.
Because of the heterogeneous world of IMAP servers and the high complexity
burden cost for such a little gain of the former, we chose the latter.
## Mock the library
Mockability is granted by interfaces present for each API.
Dig into [MockabilityTest](tests/MockabilityTest.php) for an example of a
mocked workflow.
## Running the Tests
This library is functionally tested on [Travis CI](https://travis-ci.org/ddeboer/imap)
against a local Dovecot server.
If you have your own IMAP (test) account, you can run the tests locally by
providing your IMAP credentials:
```bash
$ composer install
$ IMAP_SERVER_NAME="my.imap.server.com" IMAP_SERVER_PORT="60993" IMAP_USERNAME="johndoe" IMAP_PASSWORD="p4ssword" vendor/bin/phpunit
```
You can also copy `phpunit.xml.dist` file to a custom `phpunit.xml` and put
these environment variables in it:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="./vendor/autoload.php"
colors="true"
verbose="true"
>
<testsuites>
<testsuite name="ddeboer/imap">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<php>
<env name="IMAP_SERVER_NAME" value="my.imap.server.com" />
<env name="IMAP_SERVER_PORT" value="60993" />
<env name="IMAP_USERNAME" value="johndoe" />
<env name="IMAP_PASSWORD" value="p4ssword" />
</php>
</phpunit>
```
**WARNING** Tests create new mailboxes without removing them.

View File

@ -0,0 +1,47 @@
{
"name": "ddeboer/imap",
"description": "Object-oriented IMAP for PHP",
"keywords": [
"email",
"mail",
"imap"
],
"license": "MIT",
"authors": [
{
"name": "David de Boer",
"email": "david@ddeboer.nl"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com"
},
{
"name": "Community contributors",
"homepage": "https://github.com/ddeboer/imap/graphs/contributors"
}
],
"require": {
"php": "^7.0",
"ext-iconv": "*",
"ext-imap": "*",
"ext-mbstring": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.10",
"phpstan/phpstan": "^0.9.1",
"phpstan/phpstan-phpunit": "^0.9.3",
"phpunit/phpunit": "^6.5 || ^7.0",
"zendframework/zend-mail": "^2.8"
},
"autoload": {
"psr-4": {
"Ddeboer\\Imap\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Ddeboer\\Imap\\Tests\\": "tests/"
}
}
}

View File

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\CreateMailboxException;
use Ddeboer\Imap\Exception\DeleteMailboxException;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\MailboxDoesNotExistException;
/**
* A connection to an IMAP server that is authenticated for a user.
*/
final class Connection implements ConnectionInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* @var string
*/
private $server;
/**
* @var null|array
*/
private $mailboxes;
/**
* @var null|array
*/
private $mailboxNames;
/**
* Constructor.
*
* @param ImapResourceInterface $resource
* @param string $server
*
* @throws \InvalidArgumentException
*/
public function __construct(ImapResourceInterface $resource, string $server)
{
$this->resource = $resource;
$this->server = $server;
}
/**
* Get IMAP resource.
*
* @return ImapResourceInterface
*/
public function getResource(): ImapResourceInterface
{
return $this->resource;
}
/**
* Delete all messages marked for deletion.
*
* @return bool
*/
public function expunge(): bool
{
return \imap_expunge($this->resource->getStream());
}
/**
* Close connection.
*
* @param int $flag
*
* @return bool
*/
public function close(int $flag = 0): bool
{
return \imap_close($this->resource->getStream(), $flag);
}
/**
* Get a list of mailboxes (also known as folders).
*
* @return MailboxInterface[]
*/
public function getMailboxes(): array
{
$this->initMailboxNames();
if (null === $this->mailboxes) {
$this->mailboxes = [];
foreach ($this->mailboxNames as $mailboxName => $mailboxInfo) {
$this->mailboxes[$mailboxName] = $this->getMailbox($mailboxName);
}
}
return $this->mailboxes;
}
/**
* Check that a mailbox with the given name exists.
*
* @param string $name Mailbox name
*
* @return bool
*/
public function hasMailbox(string $name): bool
{
$this->initMailboxNames();
return isset($this->mailboxNames[$name]);
}
/**
* Get a mailbox by its name.
*
* @param string $name Mailbox name
*
* @throws MailboxDoesNotExistException If mailbox does not exist
*
* @return MailboxInterface
*/
public function getMailbox(string $name): MailboxInterface
{
if (false === $this->hasMailbox($name)) {
throw new MailboxDoesNotExistException(\sprintf('Mailbox name "%s" does not exist', $name));
}
return new Mailbox($this->resource, $name, $this->mailboxNames[$name]);
}
/**
* Count number of messages not in any mailbox.
*
* @return int
*/
public function count()
{
return \imap_num_msg($this->resource->getStream());
}
/**
* Check if the connection is still active.
*
* @throws InvalidResourceException If connection was closed
*
* @return bool
*/
public function ping(): bool
{
return \imap_ping($this->resource->getStream());
}
/**
* Create mailbox.
*
* @param string $name
*
* @throws CreateMailboxException
*
* @return MailboxInterface
*/
public function createMailbox(string $name): MailboxInterface
{
if (false === \imap_createmailbox($this->resource->getStream(), $this->server . \mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'))) {
throw new CreateMailboxException(\sprintf('Can not create "%s" mailbox at "%s"', $name, $this->server));
}
$this->mailboxNames = $this->mailboxes = null;
$this->resource->clearLastMailboxUsedCache();
return $this->getMailbox($name);
}
/**
* Create mailbox.
*
* @param MailboxInterface $mailbox
*
* @throws DeleteMailboxException
*/
public function deleteMailbox(MailboxInterface $mailbox)
{
if (false === \imap_deletemailbox($this->resource->getStream(), $mailbox->getFullEncodedName())) {
throw new DeleteMailboxException(\sprintf('Mailbox "%s" could not be deleted', $mailbox->getName()));
}
$this->mailboxes = $this->mailboxNames = null;
$this->resource->clearLastMailboxUsedCache();
}
/**
* Get mailbox names.
*/
private function initMailboxNames()
{
if (null !== $this->mailboxNames) {
return;
}
$this->mailboxNames = [];
$mailboxesInfo = \imap_getmailboxes($this->resource->getStream(), $this->server, '*');
foreach ($mailboxesInfo as $mailboxInfo) {
$name = \mb_convert_encoding(\str_replace($this->server, '', $mailboxInfo->name), 'UTF-8', 'UTF7-IMAP');
$this->mailboxNames[$name] = $mailboxInfo;
}
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* A connection to an IMAP server that is authenticated for a user.
*/
interface ConnectionInterface extends \Countable
{
/**
* Get IMAP resource.
*
* @return ImapResourceInterface
*/
public function getResource(): ImapResourceInterface;
/**
* Delete all messages marked for deletion.
*
* @return bool
*/
public function expunge(): bool;
/**
* Close connection.
*
* @param int $flag
*
* @return bool
*/
public function close(int $flag = 0): bool;
/**
* Check if the connection is still active.
*
* @return bool
*/
public function ping(): bool;
/**
* Get a list of mailboxes (also known as folders).
*
* @return MailboxInterface[]
*/
public function getMailboxes(): array;
/**
* Check that a mailbox with the given name exists.
*
* @param string $name Mailbox name
*
* @return bool
*/
public function hasMailbox(string $name): bool;
/**
* Get a mailbox by its name.
*
* @param string $name Mailbox name
*
* @return MailboxInterface
*/
public function getMailbox(string $name): MailboxInterface;
/**
* Create mailbox.
*
* @param string $name
*
* @return MailboxInterface
*/
public function createMailbox(string $name): MailboxInterface;
/**
* Create mailbox.
*
* @param MailboxInterface $mailbox
*/
public function deleteMailbox(MailboxInterface $mailbox);
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
abstract class AbstractException extends \RuntimeException
{
/**
* @var array
*/
private static $errorLabels = [
\E_ERROR => 'E_ERROR',
\E_WARNING => 'E_WARNING',
\E_PARSE => 'E_PARSE',
\E_NOTICE => 'E_NOTICE',
\E_CORE_ERROR => 'E_CORE_ERROR',
\E_CORE_WARNING => 'E_CORE_WARNING',
\E_COMPILE_ERROR => 'E_COMPILE_ERROR',
\E_COMPILE_WARNING => 'E_COMPILE_WARNING',
\E_USER_ERROR => 'E_USER_ERROR',
\E_USER_WARNING => 'E_USER_WARNING',
\E_USER_NOTICE => 'E_USER_NOTICE',
\E_STRICT => 'E_STRICT',
\E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
\E_DEPRECATED => 'E_DEPRECATED',
\E_USER_DEPRECATED => 'E_USER_DEPRECATED',
];
/**
* @param string $message The exception message
* @param int $code The exception code
* @param \Throwable $previous The previous exception
*/
final public function __construct(string $message, int $code = 0, \Throwable $previous = null)
{
$errorType = '';
if (\is_int($code) && isset(self::$errorLabels[$code])) {
$errorType = \sprintf('[%s] ', self::$errorLabels[$code]);
}
$joinString = "\n- ";
$alerts = \imap_alerts();
$errors = \imap_errors();
$completeMessage = \sprintf(
"%s%s\nimap_alerts (%s):%s\nimap_errors (%s):%s",
$errorType,
$message,
$alerts ? \count($alerts) : 0,
$alerts ? $joinString . \implode($joinString, $alerts) : '',
$errors ? \count($errors) : 0,
$errors ? $joinString . \implode($joinString, $errors) : ''
);
parent::__construct($completeMessage, $code, $previous);
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class AuthenticationFailedException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class CreateMailboxException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class DeleteMailboxException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidDateHeaderException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidHeadersException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidResourceException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class InvalidSearchCriteriaException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MailboxDoesNotExistException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageCopyException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageDeleteException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageDoesNotExistException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageMoveException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class MessageStructureException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class NotEmbeddedMessageException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class OutOfBoundsException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class ReopenMailboxException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class UnexpectedEncodingException extends AbstractException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Exception;
final class UnsupportedCharsetException extends AbstractException
{
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\ReopenMailboxException;
/**
* An imap resource stream.
*/
final class ImapResource implements ImapResourceInterface
{
/**
* @var resource
*/
private $resource;
/**
* @var null|MailboxInterface
*/
private $mailbox;
/**
* @var null|string
*/
private static $lastMailboxUsedCache;
/**
* Constructor.
*
* @param resource $resource
*/
public function __construct($resource, MailboxInterface $mailbox = null)
{
$this->resource = $resource;
$this->mailbox = $mailbox;
}
/**
* Get IMAP resource stream.
*
* @throws InvalidResourceException
*
* @return resource
*/
public function getStream()
{
if (false === \is_resource($this->resource) || 'imap' !== \get_resource_type($this->resource)) {
throw new InvalidResourceException('Supplied resource is not a valid imap resource');
}
$this->initMailbox();
return $this->resource;
}
/**
* Clear last mailbox used cache.
*/
public function clearLastMailboxUsedCache()
{
self::$lastMailboxUsedCache = null;
}
/**
* If connection is not currently in this mailbox, switch it to this mailbox.
*/
private function initMailbox()
{
if (null === $this->mailbox || $this->isMailboxOpen()) {
return;
}
\imap_reopen($this->resource, $this->mailbox->getFullEncodedName());
if ($this->isMailboxOpen()) {
return;
}
throw new ReopenMailboxException(\sprintf('Cannot reopen mailbox "%s"', $this->mailbox->getName()));
}
/**
* Check whether the current mailbox is open.
*
* @return bool
*/
private function isMailboxOpen(): bool
{
$currentMailboxName = $this->mailbox->getFullEncodedName();
if ($currentMailboxName === self::$lastMailboxUsedCache) {
return true;
}
self::$lastMailboxUsedCache = null;
$check = \imap_check($this->resource);
$return = false !== $check && $check->Mailbox === $currentMailboxName;
if (true === $return) {
self::$lastMailboxUsedCache = $currentMailboxName;
}
return $return;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
interface ImapResourceInterface
{
/**
* Get IMAP resource stream.
*
* @return resource
*/
public function getStream();
/**
* Clear last mailbox used cache.
*/
public function clearLastMailboxUsedCache();
}

View File

@ -0,0 +1,294 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use DateTimeInterface;
use Ddeboer\Imap\Exception\InvalidSearchCriteriaException;
use Ddeboer\Imap\Exception\MessageCopyException;
use Ddeboer\Imap\Exception\MessageMoveException;
use Ddeboer\Imap\Search\ConditionInterface;
use Ddeboer\Imap\Search\LogicalOperator\All;
/**
* An IMAP mailbox (commonly referred to as a 'folder').
*/
final class Mailbox implements MailboxInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* @var string
*/
private $name;
/**
* @var \stdClass
*/
private $info;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param string $name Mailbox decoded name
* @param \stdClass $info Mailbox info
*/
public function __construct(ImapResourceInterface $resource, string $name, \stdClass $info)
{
$this->resource = new ImapResource($resource->getStream(), $this);
$this->name = $name;
$this->info = $info;
}
/**
* Get mailbox decoded name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Get mailbox encoded path.
*
* @return string
*/
public function getEncodedName(): string
{
return \preg_replace('/^{.+}/', '', $this->info->name);
}
/**
* Get mailbox encoded full name.
*
* @return string
*/
public function getFullEncodedName(): string
{
return $this->info->name;
}
/**
* Get mailbox attributes.
*
* @return int
*/
public function getAttributes(): int
{
return $this->info->attributes;
}
/**
* Get mailbox delimiter.
*
* @return string
*/
public function getDelimiter(): string
{
return $this->info->delimiter;
}
/**
* Get number of messages in this mailbox.
*
* @return int
*/
public function count()
{
return \imap_num_msg($this->resource->getStream());
}
/**
* Get Mailbox status.
*
* @param null|int $flags
*
* @return \stdClass
*/
public function getStatus(int $flags = null): \stdClass
{
return \imap_status($this->resource->getStream(), $this->getFullEncodedName(), $flags ?? \SA_ALL);
}
/**
* Bulk Set Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function setFlag(string $flag, $numbers): bool
{
return \imap_setflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
/**
* Bulk Clear Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function clearFlag(string $flag, $numbers): bool
{
return \imap_clearflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
}
/**
* Get message ids.
*
* @param ConditionInterface $search Search expression (optional)
*
* @return MessageIteratorInterface
*/
public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false): MessageIteratorInterface
{
if (null === $search) {
$search = new All();
}
$query = $search->toString();
// We need to clear the stack to know whether imap_last_error()
// is related to this imap_search
\imap_errors();
if (null !== $sortCriteria) {
$messageNumbers = \imap_sort($this->resource->getStream(), $sortCriteria, $descending ? 1 : 0, \SE_UID, $query);
} else {
$messageNumbers = \imap_search($this->resource->getStream(), $query, \SE_UID);
}
if (false === $messageNumbers) {
if (false !== \imap_last_error()) {
throw new InvalidSearchCriteriaException(\sprintf('Invalid search criteria [%s]', $query));
}
// imap_search can also return false
$messageNumbers = [];
}
return new MessageIterator($this->resource, $messageNumbers);
}
/**
* Get a message by message number.
*
* @param int $number Message number
*
* @return MessageInterface
*/
public function getMessage(int $number): MessageInterface
{
return new Message($this->resource, $number);
}
/**
* Get messages in this mailbox.
*
* @return MessageIteratorInterface
*/
public function getIterator(): MessageIteratorInterface
{
return $this->getMessages();
}
/**
* Add a message to the mailbox.
*
* @param string $message
* @param null|string $options
* @param null|DateTimeInterface $internalDate
*
* @return bool
*/
public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool
{
$arguments = [
$this->resource->getStream(),
$this->getFullEncodedName(),
$message,
];
if (null !== $options) {
$arguments[] = $options;
if (null !== $internalDate) {
$arguments[] = $internalDate->format('d-M-Y H:i:s O');
}
}
return \imap_append(...$arguments);
}
/**
* Returns a tree of threaded message for the current Mailbox.
*
* @return array
*/
public function getThread(): array
{
\set_error_handler(function () {});
$tree = \imap_thread($this->resource->getStream());
\restore_error_handler();
return false !== $tree ? $tree : [];
}
/**
* Bulk move messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to move the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageMoveException
*/
public function move($numbers, MailboxInterface $mailbox)
{
if (!\imap_mail_move($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageMoveException(\sprintf('Messages cannot be moved to "%s"', $mailbox->getName()));
}
}
/**
* Bulk copy messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to copy the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageCopyException
*/
public function copy($numbers, MailboxInterface $mailbox)
{
if (!\imap_mail_copy($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageCopyException(\sprintf('Messages cannot be copied to "%s"', $mailbox->getName()));
}
}
/**
* Prepare message ids for the use with bulk functions.
*
* @param array|MessageIterator|string $messageIds Message numbers
*
* @return string
*/
private function prepareMessageIds($messageIds): string
{
if ($messageIds instanceof MessageIterator) {
$messageIds = $messageIds->getArrayCopy();
}
if (\is_array($messageIds)) {
$messageIds = \implode(',', $messageIds);
}
return (string) $messageIds;
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use DateTimeInterface;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* An IMAP mailbox (commonly referred to as a 'folder').
*/
interface MailboxInterface extends \Countable, \IteratorAggregate
{
/**
* Get mailbox decoded name.
*
* @return string
*/
public function getName(): string;
/**
* Get mailbox encoded path.
*
* @return string
*/
public function getEncodedName(): string;
/**
* Get mailbox encoded full name.
*
* @return string
*/
public function getFullEncodedName(): string;
/**
* Get mailbox attributes.
*
* @return int
*/
public function getAttributes(): int;
/**
* Get mailbox delimiter.
*
* @return string
*/
public function getDelimiter(): string;
/**
* Get Mailbox status.
*
* @param null|int $flags
*
* @return \stdClass
*/
public function getStatus(int $flags = null): \stdClass;
/**
* Bulk Set Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function setFlag(string $flag, $numbers): bool;
/**
* Bulk Clear Flag for Messages.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
* @param array|MessageIterator|string $numbers Message numbers
*
* @return bool
*/
public function clearFlag(string $flag, $numbers): bool;
/**
* Get message ids.
*
* @param ConditionInterface $search Search expression (optional)
*
* @return MessageIteratorInterface
*/
public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false): MessageIteratorInterface;
/**
* Get a message by message number.
*
* @param int $number Message number
*
* @return MessageInterface
*/
public function getMessage(int $number): MessageInterface;
/**
* Get messages in this mailbox.
*
* @return MessageIteratorInterface
*/
public function getIterator(): MessageIteratorInterface;
/**
* Add a message to the mailbox.
*
* @param string $message
* @param null|string $options
* @param null|DateTimeInterface $internalDate
*
* @return bool
*/
public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool;
/**
* Returns a tree of threaded message for the current Mailbox.
*
* @return array
*/
public function getThread(): array;
/**
* Bulk move messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to move the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageMoveException
*/
public function move($numbers, self $mailbox);
/**
* Bulk copy messages.
*
* @param array|MessageIterator|string $numbers Message numbers
* @param MailboxInterface $mailbox Destination Mailbox to copy the messages to
*
* @throws \Ddeboer\Imap\Exception\MessageCopyException
*/
public function copy($numbers, self $mailbox);
}

View File

@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\InvalidHeadersException;
use Ddeboer\Imap\Exception\MessageCopyException;
use Ddeboer\Imap\Exception\MessageDeleteException;
use Ddeboer\Imap\Exception\MessageDoesNotExistException;
use Ddeboer\Imap\Exception\MessageMoveException;
use Ddeboer\Imap\Exception\MessageStructureException;
/**
* An IMAP message (e-mail).
*/
final class Message extends Message\AbstractMessage implements MessageInterface
{
/**
* @var bool
*/
private $messageNumberVerified = false;
/**
* @var bool
*/
private $structureLoaded = false;
/**
* @var null|Message\Headers
*/
private $headers;
/**
* @var null|string
*/
private $rawHeaders;
/**
* @var null|string
*/
private $rawMessage;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param int $messageNumber Message number
*/
public function __construct(ImapResourceInterface $resource, int $messageNumber)
{
parent::__construct($resource, $messageNumber, '1', new \stdClass());
}
/**
* Lazy load structure.
*/
protected function lazyLoadStructure()
{
if (true === $this->structureLoaded) {
return;
}
$this->structureLoaded = true;
$messageNumber = $this->getNumber();
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$structure = \imap_fetchstructure(
$this->resource->getStream(),
$messageNumber,
\FT_UID
);
\restore_error_handler();
if (!$structure instanceof \stdClass) {
throw new MessageStructureException(\sprintf(
'Message "%s" structure is empty: %s',
$messageNumber,
$errorMessage
), $errorNumber);
}
$this->setStructure($structure);
}
/**
* Ensure message exists.
*
* @param int $messageNumber
*/
protected function assertMessageExists(int $messageNumber)
{
if (true === $this->messageNumberVerified) {
return;
}
$this->messageNumberVerified = true;
$msgno = \imap_msgno($this->resource->getStream(), $messageNumber);
if (\is_numeric($msgno) && $msgno > 0) {
return;
}
throw new MessageDoesNotExistException(\sprintf(
'Message "%s" does not exist',
$messageNumber
));
}
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string
{
if (null === $this->rawHeaders) {
$this->rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID);
}
return $this->rawHeaders;
}
/**
* Get the raw message, including all headers, parts, etc. unencoded and unparsed.
*
* @return string the raw message
*/
public function getRawMessage(): string
{
if (null === $this->rawMessage) {
$this->rawMessage = $this->doGetContent('');
}
return $this->rawMessage;
}
/**
* Get message headers.
*
* @return Message\Headers
*/
public function getHeaders(): Message\Headers
{
if (null === $this->headers) {
// imap_headerinfo is much faster than imap_fetchheader
// imap_headerinfo returns only a subset of all mail headers,
// but it does include the message flags.
$headers = \imap_headerinfo($this->resource->getStream(), \imap_msgno($this->resource->getStream(), $this->getNumber()));
if (false === $headers) {
// @see https://github.com/ddeboer/imap/issues/358
throw new InvalidHeadersException(\sprintf('Message "%s" has invalid headers', $this->getNumber()));
}
$this->headers = new Message\Headers($headers);
}
return $this->headers;
}
/**
* Clearmessage headers.
*/
private function clearHeaders()
{
$this->headers = null;
}
/**
* Get message recent flag value (from headers).
*
* @return null|string
*/
public function isRecent()
{
return $this->getHeaders()->get('recent');
}
/**
* Get message unseen flag value (from headers).
*
* @return bool
*/
public function isUnseen(): bool
{
return 'U' === $this->getHeaders()->get('unseen');
}
/**
* Get message flagged flag value (from headers).
*
* @return bool
*/
public function isFlagged(): bool
{
return 'F' === $this->getHeaders()->get('flagged');
}
/**
* Get message answered flag value (from headers).
*
* @return bool
*/
public function isAnswered(): bool
{
return 'A' === $this->getHeaders()->get('answered');
}
/**
* Get message deleted flag value (from headers).
*
* @return bool
*/
public function isDeleted(): bool
{
return 'D' === $this->getHeaders()->get('deleted');
}
/**
* Get message draft flag value (from headers).
*
* @return bool
*/
public function isDraft(): bool
{
return 'X' === $this->getHeaders()->get('draft');
}
/**
* Has the message been marked as read?
*
* @return bool
*/
public function isSeen(): bool
{
return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
}
/**
* Mark message as seen.
*
* @return bool
*
* @deprecated since version 1.1, to be removed in 2.0
*/
public function maskAsSeen(): bool
{
\trigger_error(\sprintf('%s is deprecated and will be removed in 2.0. Use %s::markAsSeen instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED);
return $this->markAsSeen();
}
/**
* Mark message as seen.
*
* @return bool
*/
public function markAsSeen(): bool
{
return $this->setFlag('\\Seen');
}
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*
* @throws MessageCopyException
*/
public function copy(MailboxInterface $mailbox)
{
// 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
$this->clearHeaders();
if (!\imap_mail_copy($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageCopyException(\sprintf('Message "%s" cannot be copied to "%s"', $this->getNumber(), $mailbox->getName()));
}
}
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*
* @throws MessageMoveException
*/
public function move(MailboxInterface $mailbox)
{
// 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
$this->clearHeaders();
if (!\imap_mail_move($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
throw new MessageMoveException(\sprintf('Message "%s" cannot be moved to "%s"', $this->getNumber(), $mailbox->getName()));
}
}
/**
* Delete message.
*
* @throws MessageDeleteException
*/
public function delete()
{
// 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
$this->clearHeaders();
if (!\imap_delete($this->resource->getStream(), $this->getNumber(), \FT_UID)) {
throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber()));
}
}
/**
* Set Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function setFlag(string $flag): bool
{
$result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
$this->clearHeaders();
return $result;
}
/**
* Clear Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function clearFlag(string $flag): bool
{
$result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
$this->clearHeaders();
return $result;
}
}

View File

@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\InvalidDateHeaderException;
abstract class AbstractMessage extends AbstractPart
{
/**
* @var null|array
*/
private $attachments;
/**
* Get message headers.
*
* @return Headers
*/
abstract public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*
* @return null|string
*/
final public function getId()
{
return $this->getHeaders()->get('message_id');
}
/**
* Get message sender (from headers).
*
* @return null|EmailAddress
*/
final public function getFrom()
{
$from = $this->getHeaders()->get('from');
return null !== $from ? $this->decodeEmailAddress($from[0]) : null;
}
/**
* Get To recipients.
*
* @return EmailAddress[] Empty array in case message has no To: recipients
*/
final public function getTo(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('to') ?: []);
}
/**
* Get Cc recipients.
*
* @return EmailAddress[] Empty array in case message has no CC: recipients
*/
final public function getCc(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('cc') ?: []);
}
/**
* Get Bcc recipients.
*
* @return EmailAddress[] Empty array in case message has no BCC: recipients
*/
final public function getBcc(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('bcc') ?: []);
}
/**
* Get Reply-To recipients.
*
* @return EmailAddress[] Empty array in case message has no Reply-To: recipients
*/
final public function getReplyTo(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('reply_to') ?: []);
}
/**
* Get Sender.
*
* @return EmailAddress[] Empty array in case message has no Sender: recipients
*/
final public function getSender(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('sender') ?: []);
}
/**
* Get Return-Path.
*
* @return EmailAddress[] Empty array in case message has no Return-Path: recipients
*/
final public function getReturnPath(): array
{
return $this->decodeEmailAddresses($this->getHeaders()->get('return_path') ?: []);
}
/**
* Get date (from headers).
*
* @return null|\DateTimeImmutable
*/
final public function getDate()
{
$dateHeader = $this->getHeaders()->get('date');
if (null === $dateHeader) {
return null;
}
$alteredValue = $dateHeader;
$alteredValue = \str_replace(',', '', $alteredValue);
$alteredValue = \preg_replace('/^[a-zA-Z]+ ?/', '', $alteredValue);
$alteredValue = \preg_replace('/ +\(.*\)/', '', $alteredValue);
$alteredValue = \preg_replace('/\bUT\b/', 'UTC', $alteredValue);
if (0 === \preg_match('/\d\d:\d\d:\d\d.* [\+\-]\d\d:?\d\d/', $alteredValue)) {
$alteredValue .= ' +0000';
}
try {
$date = new \DateTimeImmutable($alteredValue);
} catch (\Throwable $ex) {
throw new InvalidDateHeaderException(\sprintf('Invalid Date header found: "%s"', $dateHeader), 0, $ex);
}
return $date;
}
/**
* Get message size (from headers).
*
* @return null|int|string
*/
final public function getSize()
{
return $this->getHeaders()->get('size');
}
/**
* Get message subject (from headers).
*
* @return null|string
*/
final public function getSubject()
{
return $this->getHeaders()->get('subject');
}
/**
* Get message In-Reply-To (from headers).
*
* @return array
*/
final public function getInReplyTo(): array
{
$inReplyTo = $this->getHeaders()->get('in_reply_to');
return null !== $inReplyTo ? \explode(' ', $inReplyTo) : [];
}
/**
* Get message References (from headers).
*
* @return array
*/
final public function getReferences(): array
{
$references = $this->getHeaders()->get('references');
return null !== $references ? \explode(' ', $references) : [];
}
/**
* Get body HTML.
*
* @return null|string
*/
final public function getBodyHtml()
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $part) {
if (self::SUBTYPE_HTML === $part->getSubtype()) {
return $part->getDecodedContent();
}
}
// If message has no parts and is HTML, return content of message itself.
if (self::SUBTYPE_HTML === $this->getSubtype()) {
return $this->getDecodedContent();
}
return null;
}
/**
* Get body text.
*
* @return null|string
*/
final public function getBodyText()
{
$iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $part) {
if (self::SUBTYPE_PLAIN === $part->getSubtype()) {
return $part->getDecodedContent();
}
}
// If message has no parts, return content of message itself.
if (self::SUBTYPE_PLAIN === $this->getSubtype()) {
return $this->getDecodedContent();
}
return null;
}
/**
* Get attachments (if any) linked to this e-mail.
*
* @return AttachmentInterface[]
*/
final public function getAttachments(): array
{
if (null === $this->attachments) {
static $gatherAttachments;
if (null === $gatherAttachments) {
$gatherAttachments = static function (PartInterface $part) use (&$gatherAttachments): array {
$attachments = [];
foreach ($part->getParts() as $childPart) {
if ($childPart instanceof Attachment) {
$attachments[] = $childPart;
}
if ($childPart->hasChildren()) {
$attachments = \array_merge($attachments, $gatherAttachments($childPart));
}
}
return $attachments;
};
}
$this->attachments = $gatherAttachments($this);
}
return $this->attachments;
}
/**
* Does this message have attachments?
*
* @return bool
*/
final public function hasAttachments(): bool
{
return \count($this->getAttachments()) > 0;
}
/**
* @param array $addresses Addesses
*
* @return array
*/
private function decodeEmailAddresses(array $addresses): array
{
$return = [];
foreach ($addresses as $address) {
if (isset($address->mailbox)) {
$return[] = $this->decodeEmailAddress($address);
}
}
return $return;
}
/**
* @param \stdClass $value
*
* @return EmailAddress
*/
private function decodeEmailAddress(\stdClass $value): EmailAddress
{
return new EmailAddress($value->mailbox, $value->host, $value->personal);
}
}

View File

@ -0,0 +1,578 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\UnexpectedEncodingException;
use Ddeboer\Imap\ImapResourceInterface;
use Ddeboer\Imap\Message;
/**
* A message part.
*/
abstract class AbstractPart implements PartInterface
{
/**
* @var ImapResourceInterface
*/
protected $resource;
/**
* @var bool
*/
private $structureParsed = false;
/**
* @var array
*/
private $parts = [];
/**
* @var string
*/
private $partNumber;
/**
* @var int
*/
private $messageNumber;
/**
* @var \stdClass
*/
private $structure;
/**
* @var Parameters
*/
private $parameters;
/**
* @var null|string
*/
private $type;
/**
* @var null|string
*/
private $subtype;
/**
* @var null|string
*/
private $encoding;
/**
* @var null|string
*/
private $disposition;
/**
* @var null|string
*/
private $bytes;
/**
* @var null|string
*/
private $lines;
/**
* @var null|string
*/
private $content;
/**
* @var null|string
*/
private $decodedContent;
/**
* @var int
*/
private $key = 0;
/**
* @var array
*/
private static $typesMap = [
\TYPETEXT => self::TYPE_TEXT,
\TYPEMULTIPART => self::TYPE_MULTIPART,
\TYPEMESSAGE => self::TYPE_MESSAGE,
\TYPEAPPLICATION => self::TYPE_APPLICATION,
\TYPEAUDIO => self::TYPE_AUDIO,
\TYPEIMAGE => self::TYPE_IMAGE,
\TYPEVIDEO => self::TYPE_VIDEO,
\TYPEMODEL => self::TYPE_MODEL,
\TYPEOTHER => self::TYPE_OTHER,
];
/**
* @var array
*/
private static $encodingsMap = [
\ENC7BIT => self::ENCODING_7BIT,
\ENC8BIT => self::ENCODING_8BIT,
\ENCBINARY => self::ENCODING_BINARY,
\ENCBASE64 => self::ENCODING_BASE64,
\ENCQUOTEDPRINTABLE => self::ENCODING_QUOTED_PRINTABLE,
];
/**
* @var array
*/
private static $attachmentKeys = [
'name' => true,
'filename' => true,
'name*' => true,
'filename*' => true,
];
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param int $messageNumber Message number
* @param string $partNumber Part number
* @param \stdClass $structure Part structure
*/
public function __construct(
ImapResourceInterface $resource,
int $messageNumber,
string $partNumber,
\stdClass $structure
) {
$this->resource = $resource;
$this->messageNumber = $messageNumber;
$this->partNumber = $partNumber;
$this->setStructure($structure);
}
/**
* Get message number (from headers).
*
* @return int
*/
final public function getNumber(): int
{
$this->assertMessageExists($this->messageNumber);
return $this->messageNumber;
}
/**
* Ensure message exists.
*
* @param int $messageNumber
*/
protected function assertMessageExists(int $messageNumber)
{
}
/**
* @param \stdClass $structure Part structure
*/
final protected function setStructure(\stdClass $structure)
{
$this->structure = $structure;
}
/**
* Part structure.
*
* @return \stdClass
*/
final public function getStructure(): \stdClass
{
$this->lazyLoadStructure();
return $this->structure;
}
/**
* Lazy load structure.
*/
protected function lazyLoadStructure()
{
}
/**
* Part parameters.
*
* @return Parameters
*/
final public function getParameters(): Parameters
{
$this->lazyParseStructure();
return $this->parameters;
}
/**
* Part charset.
*
* @return null|string
*/
final public function getCharset()
{
$this->lazyParseStructure();
return $this->parameters->get('charset') ?: null;
}
/**
* Part type.
*
* @return null|string
*/
final public function getType()
{
$this->lazyParseStructure();
return $this->type;
}
/**
* Part subtype.
*
* @return null|string
*/
final public function getSubtype()
{
$this->lazyParseStructure();
return $this->subtype;
}
/**
* Part encoding.
*
* @return null|string
*/
final public function getEncoding()
{
$this->lazyParseStructure();
return $this->encoding;
}
/**
* Part disposition.
*
* @return null|string
*/
final public function getDisposition()
{
$this->lazyParseStructure();
return $this->disposition;
}
/**
* Part bytes.
*
* @return null|string
*/
final public function getBytes()
{
$this->lazyParseStructure();
return $this->bytes;
}
/**
* Part lines.
*
* @return null|string
*/
final public function getLines()
{
$this->lazyParseStructure();
return $this->lines;
}
/**
* Get raw part content.
*
* @return string
*/
final public function getContent(): string
{
if (null === $this->content) {
$this->content = $this->doGetContent($this->getContentPartNumber());
}
return $this->content;
}
/**
* Get content part number.
*
* @return string
*/
protected function getContentPartNumber(): string
{
return $this->partNumber;
}
/**
* Get part number.
*
* @return string
*/
final public function getPartNumber(): string
{
return $this->partNumber;
}
/**
* Get decoded part content.
*
* @return string
*/
final public function getDecodedContent(): string
{
if (null === $this->decodedContent) {
if (self::ENCODING_UNKNOWN === $this->getEncoding()) {
throw new UnexpectedEncodingException('Cannot decode a content with an uknown encoding');
}
$content = $this->getContent();
if (self::ENCODING_BASE64 === $this->getEncoding()) {
$content = \base64_decode($content);
} elseif (self::ENCODING_QUOTED_PRINTABLE === $this->getEncoding()) {
$content = \quoted_printable_decode($content);
}
if (false === $content) {
throw new UnexpectedEncodingException('Cannot decode content');
}
// If this part is a text part, convert its charset to UTF-8.
// We don't want to decode an attachment's charset.
if (!$this instanceof Attachment && null !== $this->getCharset() && self::TYPE_TEXT === $this->getType()) {
$content = Transcoder::decode($content, $this->getCharset());
}
$this->decodedContent = $content;
}
return $this->decodedContent;
}
/**
* Get raw message content.
*
* @param string $partNumber
*
* @return string
*/
final protected function doGetContent(string $partNumber): string
{
return \imap_fetchbody(
$this->resource->getStream(),
$this->getNumber(),
$partNumber,
\FT_UID | \FT_PEEK
);
}
/**
* Get an array of all parts for this message.
*
* @return PartInterface[]
*/
final public function getParts(): array
{
$this->lazyParseStructure();
return $this->parts;
}
/**
* Get current child part.
*
* @return mixed
*/
final public function current()
{
$this->lazyParseStructure();
return $this->parts[$this->key];
}
/**
* Get current child part.
*
* @return mixed
*/
final public function getChildren()
{
return $this->current();
}
/**
* Get current child part.
*
* @return bool
*/
final public function hasChildren()
{
$this->lazyParseStructure();
return \count($this->parts) > 0;
}
/**
* Get current part key.
*
* @return int
*/
final public function key()
{
return $this->key;
}
/**
* Move to next part.
*
* @return int
*/
final public function next()
{
++$this->key;
}
/**
* Reset part key.
*
* @return int
*/
final public function rewind()
{
$this->key = 0;
}
/**
* Check if current part is a valid one.
*
* @return bool
*/
final public function valid()
{
$this->lazyParseStructure();
return isset($this->parts[$this->key]);
}
/**
* Parse part structure.
*/
private function lazyParseStructure()
{
if (true === $this->structureParsed) {
return;
}
$this->structureParsed = true;
$this->lazyLoadStructure();
$this->type = self::$typesMap[$this->structure->type] ?? self::TYPE_UNKNOWN;
// In our context, \ENCOTHER is as useful as an uknown encoding
$this->encoding = self::$encodingsMap[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
$this->subtype = $this->structure->subtype;
foreach (['disposition', 'bytes', 'description'] as $optional) {
if (isset($this->structure->{$optional})) {
$this->{$optional} = $this->structure->{$optional};
}
}
$this->parameters = new Parameters();
if ($this->structure->ifparameters) {
$this->parameters->add($this->structure->parameters);
}
if ($this->structure->ifdparameters) {
$this->parameters->add($this->structure->dparameters);
}
// When the message is not multipart and the body is the attachment content
// Prevents infinite recursion
if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
$this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
}
if (isset($this->structure->parts)) {
$parts = $this->structure->parts;
// https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
$parts = $parts[0]->parts;
}
foreach ($parts as $key => $partStructure) {
$partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
$partNumber .= (string) ($key + 1);
$newPartClass = self::isAttachment($partStructure)
? Attachment::class
: SimplePart::class
;
$this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
}
}
}
/**
* Check if the given part is an attachment.
*
* @param \stdClass $part
*
* @return bool
*/
private static function isAttachment(\stdClass $part): bool
{
if (isset(self::$typesMap[$part->type]) && self::TYPE_MULTIPART === self::$typesMap[$part->type]) {
return false;
}
// Attachment with correct Content-Disposition header
if ($part->ifdisposition) {
if ('attachment' === \strtolower($part->disposition)) {
return true;
}
if (
'inline' === \strtolower($part->disposition)
&& self::SUBTYPE_PLAIN !== \strtoupper($part->subtype)
&& self::SUBTYPE_HTML !== \strtoupper($part->subtype)
) {
return true;
}
}
// Attachment without Content-Disposition header
if ($part->ifparameters) {
foreach ($part->parameters as $parameter) {
if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
return true;
}
}
}
/*
if ($part->ifdparameters) {
foreach ($part->dparameters as $parameter) {
if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
return true;
}
}
}
*/
return false;
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\NotEmbeddedMessageException;
/**
* An e-mail attachment.
*/
final class Attachment extends AbstractPart implements AttachmentInterface
{
/**
* Get attachment filename.
*
* @return null|string
*/
public function getFilename()
{
return $this->getParameters()->get('filename')
?: $this->getParameters()->get('name');
}
/**
* Get attachment file size.
*
* @return int Number of bytes
*/
public function getSize()
{
return $this->getParameters()->get('size');
}
/**
* Is this attachment also an Embedded Message?
*
* @return bool
*/
public function isEmbeddedMessage(): bool
{
return self::TYPE_MESSAGE === $this->getType();
}
/**
* Return embedded message.
*
* @throws NotEmbeddedMessageException
*
* @return EmbeddedMessageInterface
*/
public function getEmbeddedMessage(): EmbeddedMessageInterface
{
if (!$this->isEmbeddedMessage()) {
throw new NotEmbeddedMessageException(\sprintf(
'Attachment "%s" in message "%s" is not embedded message',
$this->getPartNumber(),
$this->getNumber()
));
}
return new EmbeddedMessage($this->resource, $this->getNumber(), $this->getPartNumber(), $this->getStructure()->parts[0]);
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* An e-mail attachment.
*/
interface AttachmentInterface extends PartInterface
{
/**
* Get attachment filename.
*
* @return null|string
*/
public function getFilename();
/**
* Get attachment file size.
*
* @return int Number of bytes
*/
public function getSize();
/**
* Is this attachment also an Embedded Message?
*
* @return bool
*/
public function isEmbeddedMessage(): bool;
/**
* Return embedded message.
*
* @return EmbeddedMessageInterface
*/
public function getEmbeddedMessage(): EmbeddedMessageInterface;
}

View File

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
interface BasicMessageInterface extends PartInterface
{
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string;
/**
* Get the raw message, including all headers, parts, etc. unencoded and unparsed.
*
* @return string the raw message
*/
public function getRawMessage(): string;
/**
* Get message headers.
*
* @return Headers
*/
public function getHeaders(): Headers;
/**
* Get message id.
*
* A unique message id in the form <...>
*
* @return null|string
*/
public function getId();
/**
* Get message sender (from headers).
*
* @return null|EmailAddress
*/
public function getFrom();
/**
* Get To recipients.
*
* @return EmailAddress[] Empty array in case message has no To: recipients
*/
public function getTo(): array;
/**
* Get Cc recipients.
*
* @return EmailAddress[] Empty array in case message has no CC: recipients
*/
public function getCc(): array;
/**
* Get Bcc recipients.
*
* @return EmailAddress[] Empty array in case message has no BCC: recipients
*/
public function getBcc(): array;
/**
* Get Reply-To recipients.
*
* @return EmailAddress[] Empty array in case message has no Reply-To: recipients
*/
public function getReplyTo(): array;
/**
* Get Sender.
*
* @return EmailAddress[] Empty array in case message has no Sender: recipients
*/
public function getSender(): array;
/**
* Get Return-Path.
*
* @return EmailAddress[] Empty array in case message has no Return-Path: recipients
*/
public function getReturnPath(): array;
/**
* Get date (from headers).
*
* @return null|\DateTimeImmutable
*/
public function getDate();
/**
* Get message size (from headers).
*
* @return int
*/
public function getSize();
/**
* Get message subject (from headers).
*
* @return string
*/
public function getSubject();
/**
* Get message In-Reply-To (from headers).
*
* @return array
*/
public function getInReplyTo(): array;
/**
* Get message References (from headers).
*
* @return array
*/
public function getReferences(): array;
/**
* Get body HTML.
*
* @return string | null Null if message has no HTML message part
*/
public function getBodyHtml();
/**
* Get body text.
*
* @return string
*/
public function getBodyText();
/**
* Get attachments (if any) linked to this e-mail.
*
* @return AttachmentInterface[]
*/
public function getAttachments(): array;
/**
* Does this message have attachments?
*
* @return bool
*/
public function hasAttachments(): bool;
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* An e-mail address.
*/
final class EmailAddress
{
/**
* @var string
*/
private $mailbox;
/**
* @var null|string
*/
private $hostname;
/**
* @var null|string
*/
private $name;
/**
* @var null|string
*/
private $address;
/**
* @param string $mailbox
* @param null|string $hostname
* @param null|string $name
*/
public function __construct(string $mailbox, string $hostname = null, string $name = null)
{
$this->mailbox = $mailbox;
$this->hostname = $hostname;
$this->name = $name;
if (null !== $hostname) {
$this->address = $mailbox . '@' . $hostname;
}
}
/**
* @return null|string
*/
public function getAddress()
{
return $this->address;
}
/**
* Returns address with person name.
*
* @return string
*/
public function getFullAddress(): string
{
$address = \sprintf('%s@%s', $this->mailbox, $this->hostname);
if (null !== $this->name) {
$address = \sprintf('"%s" <%s>', \addcslashes($this->name, '"'), $address);
}
return $address;
}
/**
* @return string
*/
public function getMailbox(): string
{
return $this->mailbox;
}
/**
* @return null|string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* @return null|string
*/
public function getName()
{
return $this->name;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
final class EmbeddedMessage extends AbstractMessage implements EmbeddedMessageInterface
{
/**
* @var null|Headers
*/
private $headers;
/**
* @var null|string
*/
private $rawHeaders;
/**
* @var null|string
*/
private $rawMessage;
/**
* Get message headers.
*
* @return Headers
*/
public function getHeaders(): Headers
{
if (null === $this->headers) {
$this->headers = new Headers(\imap_rfc822_parse_headers($this->getRawHeaders()));
}
return $this->headers;
}
/**
* Get raw message headers.
*
* @return string
*/
public function getRawHeaders(): string
{
if (null === $this->rawHeaders) {
$rawHeaders = \explode("\r\n\r\n", $this->getRawMessage(), 2);
$this->rawHeaders = \current($rawHeaders);
}
return $this->rawHeaders;
}
/**
* Get the raw message, including all headers, parts, etc. unencoded and unparsed.
*
* @return string the raw message
*/
public function getRawMessage(): string
{
if (null === $this->rawMessage) {
$this->rawMessage = $this->doGetContent($this->getPartNumber());
}
return $this->rawMessage;
}
/**
* Get content part number.
*
* @return string
*/
protected function getContentPartNumber(): string
{
$partNumber = $this->getPartNumber();
if (0 === \count($this->getParts())) {
$partNumber .= '.1';
}
return $partNumber;
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
interface EmbeddedMessageInterface extends BasicMessageInterface
{
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* Collection of message headers.
*/
final class Headers extends Parameters
{
/**
* Constructor.
*
* @param \stdClass $headers
*/
public function __construct(\stdClass $headers)
{
parent::__construct();
// Store all headers as lowercase
$headers = \array_change_key_case((array) $headers);
foreach ($headers as $key => $value) {
$this[$key] = $this->parseHeader($key, $value);
}
}
/**
* Get header.
*
* @param string $key
*
* @return null|string
*/
public function get(string $key)
{
return parent::get(\strtolower($key));
}
/**
* Parse header.
*
* @param string $key
* @param mixed $value
*
* @return mixed
*/
private function parseHeader(string $key, $value)
{
switch ($key) {
case 'msgno':
return (int) $value;
case 'from':
case 'to':
case 'cc':
case 'bcc':
case 'reply_to':
case 'sender':
case 'return_path':
foreach ($value as $address) {
if (isset($address->mailbox)) {
$address->host = $address->host ?? null;
$address->personal = isset($address->personal) ? $this->decode($address->personal) : null;
}
}
return $value;
case 'date':
case 'subject':
return $this->decode($value);
}
return $value;
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
class Parameters extends \ArrayIterator
{
/**
* @var array
*/
private static $attachmentCustomKeys = [
'name*' => 'name',
'filename*' => 'filename',
];
/**
* @param array $parameters
*/
public function __construct(array $parameters = [])
{
parent::__construct();
$this->add($parameters);
}
/**
* @param array $parameters
*/
public function add(array $parameters = [])
{
foreach ($parameters as $parameter) {
$key = \strtolower($parameter->attribute);
if (isset(self::$attachmentCustomKeys[$key])) {
$key = self::$attachmentCustomKeys[$key];
}
$value = $this->decode($parameter->value);
$this[$key] = $value;
}
}
/**
* @param string $key
*
* @return mixed
*/
public function get(string $key)
{
return $this[$key] ?? null;
}
/**
* Decode value.
*
* @param string $value
*
* @return string
*/
final protected function decode(string $value): string
{
$parts = \imap_mime_header_decode($value);
if (!\is_array($parts)) {
return $value;
}
$decoded = '';
foreach ($parts as $part) {
$text = $part->text;
if ('default' !== $part->charset) {
$text = Transcoder::decode($text, $part->charset);
}
// RFC2231
if (1 === \preg_match('/^(?<encoding>[^\']+)\'[^\']*?\'(?<urltext>.+)$/', $text, $matches)) {
$hasInvalidChars = \preg_match('#[^%a-zA-Z0-9\-_\.\+]#', $matches['urltext']);
$hasEscapedChars = \preg_match('#%[a-zA-Z0-9]{2}#', $matches['urltext']);
if (!$hasInvalidChars && $hasEscapedChars) {
$text = Transcoder::decode(\urldecode($matches['urltext']), $matches['encoding']);
}
}
$decoded .= $text;
}
return $decoded;
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* A message part.
*/
interface PartInterface extends \RecursiveIterator
{
const TYPE_TEXT = 'text';
const TYPE_MULTIPART = 'multipart';
const TYPE_MESSAGE = 'message';
const TYPE_APPLICATION = 'application';
const TYPE_AUDIO = 'audio';
const TYPE_IMAGE = 'image';
const TYPE_VIDEO = 'video';
const TYPE_MODEL = 'model';
const TYPE_OTHER = 'other';
const TYPE_UNKNOWN = 'unknown';
const ENCODING_7BIT = '7bit';
const ENCODING_8BIT = '8bit';
const ENCODING_BINARY = 'binary';
const ENCODING_BASE64 = 'base64';
const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
const ENCODING_UNKNOWN = 'unknown';
const SUBTYPE_PLAIN = 'PLAIN';
const SUBTYPE_HTML = 'HTML';
/**
* Get message number (from headers).
*
* @return int
*/
public function getNumber(): int;
/**
* Part charset.
*
* @return string
*/
public function getCharset();
/**
* Part type.
*
* @return null|string
*/
public function getType();
/**
* Part subtype.
*
* @return null|string
*/
public function getSubtype();
/**
* Part encoding.
*
* @return null|string
*/
public function getEncoding();
/**
* Part disposition.
*
* @return null|string
*/
public function getDisposition();
/**
* Part bytes.
*
* @return null|string
*/
public function getBytes();
/**
* Part lines.
*
* @return null|string
*/
public function getLines();
/**
* Part parameters.
*
* @return Parameters
*/
public function getParameters(): Parameters;
/**
* Get raw part content.
*
* @return string
*/
public function getContent(): string;
/**
* Get decoded part content.
*
* @return string
*/
public function getDecodedContent(): string;
/**
* Part structure.
*
* @return \stdClass
*/
public function getStructure(): \stdClass;
/**
* Get part number.
*
* @return string
*/
public function getPartNumber(): string;
/**
* Get an array of all parts for this message.
*
* @return PartInterface[]
*/
public function getParts(): array;
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
/**
* A message part.
*/
final class SimplePart extends AbstractPart
{
}

View File

@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Message;
use Ddeboer\Imap\Exception\UnsupportedCharsetException;
final class Transcoder
{
/**
* @var array
*
* @see https://encoding.spec.whatwg.org/#encodings
* @see https://dxr.mozilla.org/mozilla-central/source/dom/encoding/labelsencodings.properties
* @see https://dxr.mozilla.org/mozilla1.9.1/source/intl/uconv/src/charsetalias.properties
* @see https://msdn.microsoft.com/en-us/library/cc194829.aspx
*/
private static $charsetAliases = [
'128' => 'Shift_JIS',
'129' => 'EUC-KR',
'134' => 'GB2312',
'136' => 'Big5',
'161' => 'windows-1253',
'162' => 'windows-1254',
'177' => 'windows-1255',
'178' => 'windows-1256',
'186' => 'windows-1257',
'204' => 'windows-1251',
'222' => 'windows-874',
'238' => 'windows-1250',
'5601' => 'EUC-KR',
'646' => 'us-ascii',
'850' => 'IBM850',
'852' => 'IBM852',
'855' => 'IBM855',
'857' => 'IBM857',
'862' => 'IBM862',
'864' => 'IBM864',
'864i' => 'IBM864i',
'866' => 'IBM866',
'ansi-1251' => 'windows-1251',
'ansi_x3.4-1968' => 'us-ascii',
'arabic' => 'ISO-8859-6',
'ascii' => 'us-ascii',
'asmo-708' => 'ISO-8859-6',
'big5-hkscs' => 'Big5',
'chinese' => 'GB2312',
'cn-big5' => 'Big5',
'cns11643' => 'x-euc-tw',
'cp-866' => 'IBM866',
'cp1250' => 'windows-1250',
'cp1251' => 'windows-1251',
'cp1252' => 'windows-1252',
'cp1253' => 'windows-1253',
'cp1254' => 'windows-1254',
'cp1255' => 'windows-1255',
'cp1256' => 'windows-1256',
'cp1257' => 'windows-1257',
'cp1258' => 'windows-1258',
'cp819' => 'ISO-8859-1',
'cp850' => 'IBM850',
'cp852' => 'IBM852',
'cp855' => 'IBM855',
'cp857' => 'IBM857',
'cp862' => 'IBM862',
'cp864' => 'IBM864',
'cp864i' => 'IBM864i',
'cp866' => 'IBM866',
'cp932' => 'Shift_JIS',
'csbig5' => 'Big5',
'cseucjpkdfmtjapanese' => 'EUC-JP',
'cseuckr' => 'EUC-KR',
'cseucpkdfmtjapanese' => 'EUC-JP',
'csgb2312' => 'GB2312',
'csibm850' => 'IBM850',
'csibm852' => 'IBM852',
'csibm855' => 'IBM855',
'csibm857' => 'IBM857',
'csibm862' => 'IBM862',
'csibm864' => 'IBM864',
'csibm864i' => 'IBM864i',
'csibm866' => 'IBM866',
'csiso103t618bit' => 'T.61-8bit',
'csiso111ecmacyrillic' => 'ISO-IR-111',
'csiso2022jp' => 'ISO-2022-JP',
'csiso2022jp2' => 'ISO-2022-JP',
'csiso2022kr' => 'ISO-2022-KR',
'csiso58gb231280' => 'GB2312',
'csiso88596e' => 'ISO-8859-6-E',
'csiso88596i' => 'ISO-8859-6-I',
'csiso88598e' => 'ISO-8859-8-E',
'csiso88598i' => 'ISO-8859-8-I',
'csisolatin1' => 'ISO-8859-1',
'csisolatin2' => 'ISO-8859-2',
'csisolatin3' => 'ISO-8859-3',
'csisolatin4' => 'ISO-8859-4',
'csisolatin5' => 'ISO-8859-9',
'csisolatin6' => 'ISO-8859-10',
'csisolatin9' => 'ISO-8859-15',
'csisolatinarabic' => 'ISO-8859-6',
'csisolatincyrillic' => 'ISO-8859-5',
'csisolatingreek' => 'ISO-8859-7',
'csisolatinhebrew' => 'ISO-8859-8',
'cskoi8r' => 'KOI8-R',
'csksc56011987' => 'EUC-KR',
'csmacintosh' => 'x-mac-roman',
'csshiftjis' => 'Shift_JIS',
'csueckr' => 'EUC-KR',
'csunicode' => 'UTF-16BE',
'csunicode11' => 'UTF-16BE',
'csunicode11utf7' => 'UTF-7',
'csunicodeascii' => 'UTF-16BE',
'csunicodelatin1' => 'UTF-16BE',
'csviqr' => 'VIQR',
'csviscii' => 'VISCII',
'cyrillic' => 'ISO-8859-5',
'dos-874' => 'windows-874',
'ecma-114' => 'ISO-8859-6',
'ecma-118' => 'ISO-8859-7',
'ecma-cyrillic' => 'ISO-IR-111',
'elot_928' => 'ISO-8859-7',
'gb_2312' => 'GB2312',
'gb_2312-80' => 'GB2312',
'gbk' => 'x-gbk',
'greek' => 'ISO-8859-7',
'greek8' => 'ISO-8859-7',
'hebrew' => 'ISO-8859-8',
'ibm-864' => 'IBM864',
'ibm-864i' => 'IBM864i',
'ibm819' => 'ISO-8859-1',
'ibm874' => 'windows-874',
'iso-10646' => 'UTF-16BE',
'iso-10646-j-1' => 'UTF-16BE',
'iso-10646-ucs-2' => 'UTF-16BE',
'iso-10646-ucs-4' => 'UTF-32BE',
'iso-10646-ucs-basic' => 'UTF-16BE',
'iso-10646-unicode-latin1' => 'UTF-16BE',
'iso-2022-cn-ext' => 'ISO-2022-CN',
'iso-2022-jp-2' => 'ISO-2022-JP',
'iso-8859-8i' => 'ISO-8859-8-I',
'iso-ir-100' => 'ISO-8859-1',
'iso-ir-101' => 'ISO-8859-2',
'iso-ir-103' => 'T.61-8bit',
'iso-ir-109' => 'ISO-8859-3',
'iso-ir-110' => 'ISO-8859-4',
'iso-ir-126' => 'ISO-8859-7',
'iso-ir-127' => 'ISO-8859-6',
'iso-ir-138' => 'ISO-8859-8',
'iso-ir-144' => 'ISO-8859-5',
'iso-ir-148' => 'ISO-8859-9',
'iso-ir-149' => 'EUC-KR',
'iso-ir-157' => 'ISO-8859-10',
'iso-ir-58' => 'GB2312',
'iso8859-1' => 'ISO-8859-1',
'iso8859-10' => 'ISO-8859-10',
'iso8859-11' => 'ISO-8859-11',
'iso8859-13' => 'ISO-8859-13',
'iso8859-14' => 'ISO-8859-14',
'iso8859-15' => 'ISO-8859-15',
'iso8859-2' => 'ISO-8859-2',
'iso8859-3' => 'ISO-8859-3',
'iso8859-4' => 'ISO-8859-4',
'iso8859-5' => 'ISO-8859-5',
'iso8859-6' => 'ISO-8859-6',
'iso8859-7' => 'ISO-8859-7',
'iso8859-8' => 'ISO-8859-8',
'iso8859-9' => 'ISO-8859-9',
'iso88591' => 'ISO-8859-1',
'iso885910' => 'ISO-8859-10',
'iso885911' => 'ISO-8859-11',
'iso885912' => 'ISO-8859-12',
'iso885913' => 'ISO-8859-13',
'iso885914' => 'ISO-8859-14',
'iso885915' => 'ISO-8859-15',
'iso88592' => 'ISO-8859-2',
'iso88593' => 'ISO-8859-3',
'iso88594' => 'ISO-8859-4',
'iso88595' => 'ISO-8859-5',
'iso88596' => 'ISO-8859-6',
'iso88597' => 'ISO-8859-7',
'iso88598' => 'ISO-8859-8',
'iso88599' => 'ISO-8859-9',
'iso_8859-1' => 'ISO-8859-1',
'iso_8859-15' => 'ISO-8859-15',
'iso_8859-1:1987' => 'ISO-8859-1',
'iso_8859-2' => 'ISO-8859-2',
'iso_8859-2:1987' => 'ISO-8859-2',
'iso_8859-3' => 'ISO-8859-3',
'iso_8859-3:1988' => 'ISO-8859-3',
'iso_8859-4' => 'ISO-8859-4',
'iso_8859-4:1988' => 'ISO-8859-4',
'iso_8859-5' => 'ISO-8859-5',
'iso_8859-5:1988' => 'ISO-8859-5',
'iso_8859-6' => 'ISO-8859-6',
'iso_8859-6:1987' => 'ISO-8859-6',
'iso_8859-7' => 'ISO-8859-7',
'iso_8859-7:1987' => 'ISO-8859-7',
'iso_8859-8' => 'ISO-8859-8',
'iso_8859-8:1988' => 'ISO-8859-8',
'iso_8859-9' => 'ISO-8859-9',
'iso_8859-9:1989' => 'ISO-8859-9',
'koi' => 'KOI8-R',
'koi8' => 'KOI8-R',
'koi8-ru' => 'KOI8-U',
'koi8_r' => 'KOI8-R',
'korean' => 'EUC-KR',
'ks_c_5601-1987' => 'EUC-KR',
'ks_c_5601-1989' => 'EUC-KR',
'ksc5601' => 'EUC-KR',
'ksc_5601' => 'EUC-KR',
'l1' => 'ISO-8859-1',
'l2' => 'ISO-8859-2',
'l3' => 'ISO-8859-3',
'l4' => 'ISO-8859-4',
'l5' => 'ISO-8859-9',
'l6' => 'ISO-8859-10',
'l9' => 'ISO-8859-15',
'latin1' => 'ISO-8859-1',
'latin2' => 'ISO-8859-2',
'latin3' => 'ISO-8859-3',
'latin4' => 'ISO-8859-4',
'latin5' => 'ISO-8859-9',
'latin6' => 'ISO-8859-10',
'logical' => 'ISO-8859-8-I',
'mac' => 'x-mac-roman',
'macintosh' => 'x-mac-roman',
'ms932' => 'Shift_JIS',
'ms_kanji' => 'Shift_JIS',
'shift-jis' => 'Shift_JIS',
'sjis' => 'Shift_JIS',
'sun_eu_greek' => 'ISO-8859-7',
't.61' => 'T.61-8bit',
'tis620' => 'TIS-620',
'unicode-1-1-utf-7' => 'UTF-7',
'unicode-1-1-utf-8' => 'UTF-8',
'unicode-2-0-utf-7' => 'UTF-7',
'visual' => 'ISO-8859-8',
'windows-31j' => 'Shift_JIS',
'windows-949' => 'EUC-KR',
'x-cp1250' => 'windows-1250',
'x-cp1251' => 'windows-1251',
'x-cp1252' => 'windows-1252',
'x-cp1253' => 'windows-1253',
'x-cp1254' => 'windows-1254',
'x-cp1255' => 'windows-1255',
'x-cp1256' => 'windows-1256',
'x-cp1257' => 'windows-1257',
'x-cp1258' => 'windows-1258',
'x-euc-jp' => 'EUC-JP',
'x-iso-10646-ucs-2-be' => 'UTF-16BE',
'x-iso-10646-ucs-2-le' => 'UTF-16LE',
'x-iso-10646-ucs-4-be' => 'UTF-32BE',
'x-iso-10646-ucs-4-le' => 'UTF-32LE',
'x-sjis' => 'Shift_JIS',
'x-unicode-2-0-utf-7' => 'UTF-7',
'x-x-big5' => 'Big5',
'zh_cn.euc' => 'GB2312',
'zh_tw-big5' => 'Big5',
'zh_tw-euc' => 'x-euc-tw',
];
/**
* Decode text to UTF-8.
*
* @param string $text Text to decode
* @param string $fromCharset Original charset
*
* @return string
*/
public static function decode(string $text, string $fromCharset): string
{
static $utf8Aliases = [
'unicode-1-1-utf-8' => true,
'utf8' => true,
'utf-8' => true,
'UTF8' => true,
'UTF-8' => true,
];
if (isset($utf8Aliases[$fromCharset])) {
return $text;
}
$originalFromCharset = $fromCharset;
$lowercaseFromCharset = \strtolower($fromCharset);
if (isset(self::$charsetAliases[$lowercaseFromCharset])) {
$fromCharset = self::$charsetAliases[$lowercaseFromCharset];
}
\set_error_handler(function () {});
$iconvDecodedText = \iconv($fromCharset, 'UTF-8', $text);
if (false === $iconvDecodedText) {
$iconvDecodedText = \iconv($originalFromCharset, 'UTF-8', $text);
}
\restore_error_handler();
if (false !== $iconvDecodedText) {
return $iconvDecodedText;
}
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$decodedText = \mb_convert_encoding($text, 'UTF-8', $fromCharset);
\restore_error_handler();
if (null !== $errorMessage) {
throw new UnsupportedCharsetException(\sprintf(
'Unsupported charset "%s"%s: %s',
$originalFromCharset,
($fromCharset !== $originalFromCharset) ? \sprintf(' (alias found: "%s")', $fromCharset) : '',
$errorMessage
), $errorNumber);
}
return $decodedText;
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* An IMAP message (e-mail).
*/
interface MessageInterface extends Message\BasicMessageInterface
{
/**
* Get raw part content.
*
* @return string
*/
public function getContent(): string;
/**
* Get message recent flag value (from headers).
*
* @return null|string
*/
public function isRecent();
/**
* Get message unseen flag value (from headers).
*
* @return bool
*/
public function isUnseen(): bool;
/**
* Get message flagged flag value (from headers).
*
* @return bool
*/
public function isFlagged(): bool;
/**
* Get message answered flag value (from headers).
*
* @return bool
*/
public function isAnswered(): bool;
/**
* Get message deleted flag value (from headers).
*
* @return bool
*/
public function isDeleted(): bool;
/**
* Get message draft flag value (from headers).
*
* @return bool
*/
public function isDraft(): bool;
/**
* Has the message been marked as read?
*
* @return bool
*/
public function isSeen(): bool;
/**
* Mark message as seen.
*
* @return bool
*
* @deprecated since version 1.1, to be removed in 2.0
*/
public function maskAsSeen(): bool;
/**
* Mark message as seen.
*
* @return bool
*/
public function markAsSeen(): bool;
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*/
public function copy(MailboxInterface $mailbox);
/**
* Move message to another mailbox.
*
* @param MailboxInterface $mailbox
*/
public function move(MailboxInterface $mailbox);
/**
* Delete message.
*/
public function delete();
/**
* Set Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function setFlag(string $flag): bool;
/**
* Clear Flag Message.
*
* @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
*
* @return bool
*/
public function clearFlag(string $flag): bool;
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
final class MessageIterator extends \ArrayIterator implements MessageIteratorInterface
{
/**
* @var ImapResourceInterface
*/
private $resource;
/**
* Constructor.
*
* @param ImapResourceInterface $resource IMAP resource
* @param array $messageNumbers Array of message numbers
*/
public function __construct(ImapResourceInterface $resource, array $messageNumbers)
{
$this->resource = $resource;
parent::__construct($messageNumbers);
}
/**
* Get current message.
*
* @return MessageInterface
*/
public function current(): MessageInterface
{
$current = parent::current();
if (!\is_int($current)) {
throw new Exception\OutOfBoundsException(\sprintf(
'The current value "%s" isn\'t an integer and doesn\'t represent a message;'
. ' try to cycle this "%s" with a native php function like foreach or with the method getArrayCopy(),'
. ' or check it by calling the methods valid().',
\is_object($current) ? \get_class($current) : \gettype($current),
static::class
));
}
return new Message($this->resource, $current);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
interface MessageIteratorInterface extends \Iterator
{
/**
* Get current message.
*
* @return MessageInterface
*/
public function current(): MessageInterface;
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
use DateTimeInterface;
/**
* Represents a date condition.
*/
abstract class AbstractDate implements ConditionInterface
{
/**
* Format for dates to be sent to the IMAP server.
*
* @var string
*/
private $dateFormat;
/**
* The date to be used for the condition.
*
* @var DateTimeInterface
*/
private $date;
/**
* Constructor.
*
* @param DateTimeInterface $date optional date for the condition
*/
public function __construct(DateTimeInterface $date, string $dateFormat = 'j-M-Y')
{
$this->date = $date;
$this->dateFormat = $dateFormat;
}
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->date->format($this->dateFormat));
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
abstract protected function getKeyword(): string;
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a text based condition. Text based conditions use a contains
* restriction.
*/
abstract class AbstractText implements ConditionInterface
{
/**
* Text to be used for the condition.
*
* @var string
*/
private $text;
/**
* Constructor.
*
* @param string $text optional text for the condition
*/
public function __construct(string $text)
{
$this->text = $text;
}
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
final public function toString(): string
{
return \sprintf('%s "%s"', $this->getKeyword(), $this->text);
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
abstract protected function getKeyword(): string;
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a condition that can be used in a search expression.
*/
interface ConditionInterface
{
/**
* Converts the condition to a string that can be sent to the IMAP server.
*
* @return string
*/
public function toString(): string;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date before condition. Messages must have a date before the
* specified date in order to match the condition.
*/
final class Before extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BEFORE';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date on condition. Messages must have a date matching the
* specified date in order to match the condition.
*/
final class On extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'ON';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Date;
use Ddeboer\Imap\Search\AbstractDate;
/**
* Represents a date after condition. Messages must have a date after the
* specified date in order to match the condition.
*/
final class Since extends AbstractDate
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'SINCE';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "Bcc" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class Bcc extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BCC';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "Cc" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class Cc extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'CC';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "From" email address condition. Messages must have been sent
* from the specified email address in order to match the condition.
*/
final class From extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'FROM';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Email;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a "To" email address condition. Messages must have been addressed
* to the specified recipient (along with any others) in order to match the
* condition.
*/
final class To extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'TO';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an ANSWERED flag condition. Messages must have the \\ANSWERED flag
* set in order to match the condition.
*/
final class Answered implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'ANSWERED';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a FLAGGED flag condition. Messages must have the \\FLAGGED flag
* (i.e. urgent or important) set in order to match the condition.
*/
final class Flagged implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'FLAGGED';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an RECENT flag condition. Messages must have the \\RECENT flag
* set in order to match the condition.
*/
final class Recent implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'RECENT';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an SEEN flag condition. Messages must have the \\SEEN flag
* set in order to match the condition.
*/
final class Seen implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'SEEN';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an UNANSWERED flag condition. Messages must not have the
* \\ANSWERED flag set in order to match the condition.
*/
final class Unanswered implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNANSWERED';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a UNFLAGGED flag condition. Messages must no have the \\FLAGGED
* flag (i.e. urgent or important) set in order to match the condition.
*/
final class Unflagged implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNFLAGGED';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Flag;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an UNSEEN flag condition. Messages must not have the \\SEEN flag
* set in order to match the condition.
*/
final class Unseen implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNSEEN';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\LogicalOperator;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an ALL operator. Messages must match all conditions following this
* operator in order to match the expression.
*/
final class All implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'ALL';
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\LogicalOperator;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an OR operator. Messages only need to match one of the conditions
* after this operator to match the expression.
*/
final class OrConditions implements ConditionInterface
{
/**
* The conditions that together represent the expression.
*
* @var array
*/
private $conditions = [];
public function __construct(array $conditions)
{
foreach ($conditions as $condition) {
$this->addCondition($condition);
}
}
/**
* Adds a new condition to the expression.
*
* @param ConditionInterface $condition the condition to be added
*/
private function addCondition(ConditionInterface $condition)
{
$this->conditions[] = $condition;
}
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
$conditions = \array_map(function (ConditionInterface $condition) {
return $condition->toString();
}, $this->conditions);
return \sprintf('( %s )', \implode(' OR ', $conditions));
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search;
/**
* Represents a raw expression.
*/
final class RawExpression implements ConditionInterface
{
/**
* Text to be used for the condition.
*
* @var string
*/
private $expression;
/**
* @param string $expression text for the condition
*/
public function __construct(string $expression)
{
$this->expression = $expression;
}
/**
* @return string
*/
public function toString(): string
{
return $this->expression;
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a DELETED condition. Messages must have been marked for deletion
* but not yet expunged in order to match the condition.
*/
final class Deleted implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'DELETED';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a NEW condition. Only new messages will match this condition.
*/
final class NewMessage implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'NEW';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents an OLD condition. Only old messages will match this condition.
*/
final class Old implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'OLD';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\State;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Represents a UNDELETED condition. Messages must not have been marked for
* deletion in order to match the condition.
*/
final class Undeleted implements ConditionInterface
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
public function toString(): string
{
return 'UNDELETED';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a body text contains condition. Messages must have a body
* containing the specified text in order to match the condition.
*/
final class Body extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'BODY';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a keyword text contains condition. Messages must have a keyword
* matching the specified text in order to match the condition.
*/
final class Keyword extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'KEYWORD';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a subject contains condition. Messages must have a subject
* containing the specified text in order to match the condition.
*/
final class Subject extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'SUBJECT';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a message text contains condition. Messages must contain the
* specified text in order to match the condition.
*/
final class Text extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'TEXT';
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Search\Text;
use Ddeboer\Imap\Search\AbstractText;
/**
* Represents a keyword text does not contain condition. Messages must not have
* a keyword matching the specified text in order to match the condition.
*/
final class Unkeyword extends AbstractText
{
/**
* Returns the keyword that the condition represents.
*
* @return string
*/
protected function getKeyword(): string
{
return 'UNKEYWORD';
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Search\ConditionInterface;
/**
* Defines a search expression that can be used to look up email messages.
*/
final class SearchExpression implements ConditionInterface
{
/**
* The conditions that together represent the expression.
*
* @var array
*/
private $conditions = [];
/**
* Adds a new condition to the expression.
*
* @param ConditionInterface $condition the condition to be added
*
* @return self
*/
public function addCondition(ConditionInterface $condition): self
{
$this->conditions[] = $condition;
return $this;
}
/**
* Converts the expression to a string that can be sent to the IMAP server.
*
* @return string
*/
public function toString(): string
{
$conditions = \array_map(function (ConditionInterface $condition) {
return $condition->toString();
}, $this->conditions);
return \implode(' ', $conditions);
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\AuthenticationFailedException;
/**
* An IMAP server.
*/
final class Server implements ServerInterface
{
/**
* @var string Internet domain name or bracketed IP address of server
*/
private $hostname;
/**
* @var string TCP port number
*/
private $port;
/**
* @var string Optional flags
*/
private $flags;
/**
* @var array
*/
private $parameters;
/**
* Constructor.
*
* @param string $hostname Internet domain name or bracketed IP address
* of server
* @param string $port TCP port number
* @param string $flags Optional flags
* @param array $parameters Connection parameters
*/
public function __construct(
string $hostname,
string $port = '993',
string $flags = '/imap/ssl/validate-cert',
array $parameters = []
) {
if (!\function_exists('imap_open')) {
throw new \RuntimeException('IMAP extension must be enabled');
}
$this->hostname = $hostname;
$this->port = $port;
$this->flags = $flags ? '/' . \ltrim($flags, '/') : '';
$this->parameters = $parameters;
}
/**
* Authenticate connection.
*
* @param string $username Username
* @param string $password Password
*
* @throws AuthenticationFailedException
*
* @return ConnectionInterface
*/
public function authenticate(string $username, string $password): ConnectionInterface
{
$errorMessage = null;
$errorNumber = 0;
\set_error_handler(function ($nr, $message) use (&$errorMessage, &$errorNumber) {
$errorMessage = $message;
$errorNumber = $nr;
});
$resource = \imap_open(
$this->getServerString(),
$username,
$password,
0,
1,
$this->parameters
);
\restore_error_handler();
if (false === $resource || null !== $errorMessage) {
throw new AuthenticationFailedException(\sprintf(
'Authentication failed for user "%s"%s',
$username,
null !== $errorMessage ? ': ' . $errorMessage : ''
), $errorNumber);
}
$check = \imap_check($resource);
$mailbox = $check->Mailbox;
$connection = \substr($mailbox, 0, \strpos($mailbox, '}') + 1);
// These are necessary to get rid of PHP throwing IMAP errors
\imap_errors();
\imap_alerts();
return new Connection(new ImapResource($resource), $connection);
}
/**
* Glues hostname, port and flags and returns result.
*
* @return string
*/
private function getServerString(): string
{
return \sprintf(
'{%s%s%s}',
$this->hostname,
'' !== $this->port ? ':' . $this->port : '',
$this->flags
);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap;
/**
* An IMAP server.
*/
interface ServerInterface
{
/**
* Authenticate connection.
*
* @param string $username Username
* @param string $password Password
*
* @return ConnectionInterface
*/
public function authenticate(string $username, string $password): ConnectionInterface;
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Ddeboer\Imap\Test;
use Ddeboer\Imap\MessageInterface;
use Ddeboer\Imap\MessageIteratorInterface;
/**
* A MessageIterator to be used in a mocked environment.
*/
final class RawMessageIterator extends \ArrayIterator implements MessageIteratorInterface
{
public function current(): MessageInterface
{
return parent::current();
}
}

View File

@ -27,20 +27,20 @@ $redis->connect('redis-mailcow', 6379);
// PDO
// Calculate offset
$now = new DateTime();
$mins = $now->getOffset() / 60;
$sgn = ($mins < 0 ? -1 : 1);
$mins = abs($mins);
$hrs = floor($mins / 60);
$mins -= $hrs * 60;
$offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
// $now = new DateTime();
// $mins = $now->getOffset() / 60;
// $sgn = ($mins < 0 ? -1 : 1);
// $mins = abs($mins);
// $hrs = floor($mins / 60);
// $mins -= $hrs * 60;
// $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
//PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
@ -60,8 +60,7 @@ if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
exit;
}
function pdo_exception_handler($e) {
print_r($e);
function exception_handler($e) {
if ($e instanceof PDOException) {
$_SESSION['return'][] = array(
'type' => 'danger',
@ -74,12 +73,12 @@ function pdo_exception_handler($e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => array('mysql_error', 'unknown error')
'msg' => 'An unknown error occured: ' . print_r($e, true)
);
return false;
}
}
set_exception_handler('pdo_exception_handler');
set_exception_handler('exception_handler');
// TODO: Move function
function get_remote_ip($anonymize = null) {
@ -109,6 +108,10 @@ function get_remote_ip($anonymize = null) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
// IMAP lib
// use Ddeboer\Imap\Server;
// $imap_server = new Server('dovecot', 143, '/imap/tls/novalidate-cert');
// Set language
if (!isset($_SESSION['mailcow_locale']) && !isset($_COOKIE['mailcow_locale'])) {
if ($DETECT_LANGUAGE && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
@ -153,6 +156,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
init_db_schema();
if (isset($_SESSION['mailcow_cc_role'])) {
// if ($_SESSION['mailcow_cc_role'] == 'user') {
// list($master_user, $master_passwd) = explode(':', file_get_contents('/etc/sogo/sieve.creds'));
// $imap_connection = $imap_server->authenticate($_SESSION['mailcow_cc_username'] . '*' . trim($master_user), trim($master_passwd));
// $master_user = $master_passwd = null;
// }
acl('to_session');
}
$UI_TEXTS = customize('get', 'ui_texts');

View File

@ -142,3 +142,6 @@ $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
// Force password change on next login (only allows login to mailcow UI)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
// Default mailbox format, should not be changed, keep the trailing ":"
$MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'mdbox:';

View File

@ -54,7 +54,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
<label class="control-label col-sm-2" for="mins_interval"><?=$lang['add']['mins_interval'];?></label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="3600" value="20" required>
<small class="help-block">10-3600</small>
<small class="help-block">1-3600</small>
</div>
</div>
<div class="form-group">
@ -77,44 +77,71 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
<small class="help-block">0-125000000</small>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="timeout1"><?=$lang['edit']['timeout1'];?></label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
<small class="help-block">1-32000</small>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="timeout2"><?=$lang['edit']['timeout2'];?></label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
<small class="help-block">1-32000</small>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="exclude"><?=$lang['add']['exclude'];?></label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" placeholder="--delete2folders --otheroption">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?></label>
<label><input type="checkbox" value="1" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?> (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="delete1"> <?=$lang['add']['delete1'];?></label>
<label><input type="checkbox" value="1" name="delete1"> <?=$lang['add']['delete1'];?> (--delete1)</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="delete2"> <?=$lang['add']['delete2'];?></label>
<label><input type="checkbox" value="1" name="delete2"> <?=$lang['add']['delete2'];?> (--delete2)</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="automap"> <?=$lang['add']['automap'];?></label>
<label><input type="checkbox" value="1" name="automap" checked> <?=$lang['add']['automap'];?> (--automap)</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?></label>
<label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?> (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label><input type="checkbox" value="1" name="subscribeall" checked> <?=$lang['add']['subscribeall'];?> (--subscribeall)</label>
</div>
</div>
</div>