Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117884485
rcube_imap_generic.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
140 KB
Referenced Files
None
Subscribers
None
rcube_imap_generic.php
View Options
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide alternative IMAP library that doesn't rely on the standard |
| C-Client based version. This allows to function regardless |
| of whether or not the PHP build it's running on has IMAP |
| functionality built-in. |
| |
| Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
+-----------------------------------------------------------------------+
*/
/**
* PHP based wrapper class to connect to an IMAP server
*/
class
rcube_imap_generic
{
public
$error
;
public
$errornum
;
public
$result
;
public
$resultcode
;
public
$selected
;
public
$data
=
[];
public
$flags
=
[
'SEEN'
=>
'
\S
een'
,
'DELETED'
=>
'
\D
eleted'
,
'ANSWERED'
=>
'
\A
nswered'
,
'DRAFT'
=>
'
\D
raft'
,
'FLAGGED'
=>
'
\F
lagged'
,
'FORWARDED'
=>
'$Forwarded'
,
'MDNSENT'
=>
'$MDNSent'
,
'*'
=>
'
\*
'
,
];
protected
$fp
;
protected
$host
;
protected
$user
;
protected
$cmd_tag
;
protected
$cmd_num
=
0
;
protected
$resourceid
;
protected
$extensions_enabled
=
[];
protected
$prefs
=
[];
protected
$logged
=
false
;
protected
$capability
=
[];
protected
$capability_read
=
false
;
protected
$debug
=
false
;
protected
$debug_handler
=
false
;
public
const
ERROR_OK
=
0
;
public
const
ERROR_NO
=
-
1
;
public
const
ERROR_BAD
=
-
2
;
public
const
ERROR_BYE
=
-
3
;
public
const
ERROR_UNKNOWN
=
-
4
;
public
const
ERROR_COMMAND
=
-
5
;
public
const
ERROR_READONLY
=
-
6
;
public
const
COMMAND_NORESPONSE
=
1
;
public
const
COMMAND_CAPABILITY
=
2
;
public
const
COMMAND_LASTLINE
=
4
;
public
const
COMMAND_ANONYMIZED
=
8
;
public
const
DEBUG_LINE_LENGTH
=
4098
;
// 4KB + 2B for \r\n
/**
* Send simple (one line) command to the connection stream
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|false Number of bytes sent, False on error
*/
protected
function
putLine
(
$string
,
$endln
=
true
,
$anonymized
=
false
)
{
if
(!
$this
->
fp
)
{
return
false
;
}
if
(
$this
->
debug
)
{
// anonymize the sent command for logging
$cut
=
$endln
?
2
:
0
;
if
(
$anonymized
&&
preg_match
(
'/^(A
\d
+ (?:[A-Z]+ )+)(.+)/'
,
$string
,
$m
))
{
$log
=
$m
[
1
]
.
sprintf
(
'****** [%d]'
,
strlen
(
$m
[
2
])
-
$cut
);
}
elseif
(
$anonymized
)
{
$log
=
sprintf
(
'****** [%d]'
,
strlen
(
$string
)
-
$cut
);
}
else
{
$log
=
rtrim
(
$string
);
}
$this
->
debug
(
'C: '
.
$log
);
}
if
(
$endln
)
{
$string
.=
"
\r\n
"
;
}
$res
=
fwrite
(
$this
->
fp
,
$string
);
if
(
$res
===
false
)
{
$this
->
closeSocket
();
}
return
$res
;
}
/**
* Send command to the connection stream with Command Continuation
* Requests (RFC3501 7.5) and LITERAL+ (RFC2088) and LITERAL- (RFC7888) support.
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|false Number of bytes sent, False on error
*/
protected
function
putLineC
(
$string
,
$endln
=
true
,
$anonymized
=
false
)
{
if
(!
$this
->
fp
)
{
return
false
;
}
if
(
$endln
)
{
$string
.=
"
\r\n
"
;
}
$res
=
0
;
if
(
$parts
=
preg_split
(
'/(
\{
[0-9]+
\}\r\n
)/m'
,
$string
,
-
1
,
\PREG_SPLIT_DELIM_CAPTURE
))
{
for
(
$i
=
0
,
$cnt
=
count
(
$parts
);
$i
<
$cnt
;
$i
++)
{
if
(
$i
+
1
<
$cnt
&&
preg_match
(
'/^
\{
([0-9]+)
\}\r\n
$/'
,
$parts
[
$i
+
1
],
$matches
))
{
// LITERAL+/LITERAL- support
$literal_plus
=
false
;
if
(
!
empty
(
$this
->
prefs
[
'literal+'
])
||
(!
empty
(
$this
->
prefs
[
'literal-'
])
&&
$matches
[
1
]
<=
4096
)
)
{
$parts
[
$i
+
1
]
=
sprintf
(
"{%d+}
\r\n
"
,
$matches
[
1
]);
$literal_plus
=
true
;
}
$bytes
=
$this
->
putLine
(
$parts
[
$i
]
.
$parts
[
$i
+
1
],
false
,
$anonymized
);
if
(
$bytes
===
false
)
{
return
false
;
}
$res
+=
$bytes
;
// don't wait if server supports LITERAL+ capability
if
(!
$literal_plus
)
{
$line
=
$this
->
readLine
(
1000
);
// handle error in command
if
(!
isset
(
$line
[
0
])
||
$line
[
0
]
!=
'+'
)
{
return
false
;
}
}
$i
++;
}
else
{
$bytes
=
$this
->
putLine
(
$parts
[
$i
],
false
,
$anonymized
);
if
(
$bytes
===
false
)
{
return
false
;
}
$res
+=
$bytes
;
}
}
}
return
$res
;
}
/**
* Reads line from the connection stream
*
* @param int $size Buffer size
*
* @return string Line of text response
*/
protected
function
readLine
(
$size
=
1024
)
{
$line
=
''
;
if
(!
$size
)
{
$size
=
1024
;
}
do
{
if
(
$this
->
eof
())
{
return
$line
;
}
$buffer
=
fgets
(
$this
->
fp
,
$size
);
if
(
$buffer
===
false
)
{
$this
->
closeSocket
();
break
;
}
if
(
$this
->
debug
)
{
$this
->
debug
(
'S: '
.
rtrim
(
$buffer
));
}
$line
.=
$buffer
;
}
while
(
substr
(
$buffer
,
-
1
)
!=
"
\n
"
);
return
$line
;
}
/**
* Reads a line of data from the connection stream including all
* string continuation literals.
*
* @param int $size Buffer size
*
* @return string Line of text response
*/
protected
function
readFullLine
(
$size
=
1024
)
{
$line
=
$this
->
readLine
(
$size
);
// include all string literals untile the real end of "line"
while
(
preg_match
(
'/
\{
([0-9]+)
\}\r\n
$/'
,
$line
,
$m
))
{
$bytes
=
$m
[
1
];
$out
=
''
;
while
(
strlen
(
$out
)
<
$bytes
)
{
$out
=
$this
->
readBytes
(
$bytes
);
if
(
$out
===
''
)
{
break
;
}
$line
.=
$out
;
}
$line
.=
$this
->
readLine
(
$size
);
}
return
$line
;
}
/**
* Reads more data from the connection stream when provided
* data contain string literal
*
* @param string $line Response text
* @param bool $escape Enables escaping
*
* @return string Line of text response
*/
protected
function
multLine
(
$line
,
$escape
=
false
)
{
$line
=
rtrim
(
$line
);
if
(
preg_match
(
'/
\{
([0-9]+)
\}
$/'
,
$line
,
$m
))
{
$out
=
''
;
$str
=
substr
(
$line
,
0
,
-
strlen
(
$m
[
0
]));
$bytes
=
$m
[
1
];
while
(
strlen
(
$out
)
<
$bytes
)
{
$line
=
$this
->
readBytes
(
$bytes
);
if
(
$line
===
''
)
{
break
;
}
$out
.=
$line
;
}
$line
=
$str
.
(
$escape
?
$this
->
escape
(
$out
)
:
$out
);
}
return
$line
;
}
/**
* Reads specified number of bytes from the connection stream
*
* @param int $bytes Number of bytes to get
*
* @return string Response text
*/
protected
function
readBytes
(
$bytes
)
{
$data
=
''
;
$len
=
0
;
while
(
$len
<
$bytes
&&
!
$this
->
eof
())
{
$d
=
fread
(
$this
->
fp
,
$bytes
-
$len
);
if
(
$this
->
debug
)
{
$this
->
debug
(
'S: '
.
$d
);
}
$data
.=
$d
;
$data_len
=
strlen
(
$data
);
if
(
$len
==
$data_len
)
{
break
;
// nothing was read -> exit to avoid apache lockups
}
$len
=
$data_len
;
}
return
$data
;
}
/**
* Reads complete response to the IMAP command
*
* @param string $untagged Will be filled with untagged response line(s)
*
* @return string Response text
*/
protected
function
readReply
(&
$untagged
=
null
)
{
$untagged
=
[];
// @phpstan-ignore-line
while
(
true
)
{
$line
=
trim
(
$this
->
readLine
(
1024
));
// store untagged response lines
if
(
isset
(
$line
[
0
])
&&
$line
[
0
]
==
'*'
)
{
$untagged
[]
=
$line
;
// @phpstan-ignore-line
}
else
{
break
;
}
}
$untagged
=
count
(
$untagged
)
>
0
?
implode
(
"
\n
"
,
$untagged
)
:
null
;
return
$line
;
}
/**
* Response parser.
*
* @param string $string Response text
* @param string $err_prefix Error message prefix
*
* @return int Response status
*/
protected
function
parseResult
(
$string
,
$err_prefix
=
''
)
{
if
(
preg_match
(
'/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i'
,
trim
(
$string
),
$matches
))
{
$res
=
strtoupper
(
$matches
[
1
]);
$str
=
trim
(
$matches
[
2
]);
if
(
$res
==
'OK'
)
{
$this
->
errornum
=
self
::
ERROR_OK
;
}
elseif
(
$res
==
'NO'
)
{
$this
->
errornum
=
self
::
ERROR_NO
;
}
elseif
(
$res
==
'BAD'
)
{
$this
->
errornum
=
self
::
ERROR_BAD
;
}
elseif
(
$res
==
'BYE'
)
{
$this
->
closeSocket
();
$this
->
errornum
=
self
::
ERROR_BYE
;
}
if
(
$str
)
{
$str
=
trim
(
$str
);
// get response string and code (RFC5530)
if
(
preg_match
(
'/^
\[
([a-z-]+)
\]
/i'
,
$str
,
$m
))
{
$this
->
resultcode
=
strtoupper
(
$m
[
1
]);
$str
=
trim
(
substr
(
$str
,
strlen
(
$m
[
1
])
+
2
));
}
else
{
$this
->
resultcode
=
null
;
// parse response for [APPENDUID 1204196876 3456]
if
(
preg_match
(
'/^
\[
APPENDUID [0-9]+ ([0-9]+)
\]
/i'
,
$str
,
$m
))
{
$this
->
data
[
'APPENDUID'
]
=
$m
[
1
];
}
// parse response for [COPYUID 1204196876 3456:3457 123:124]
elseif
(
preg_match
(
'/^
\[
COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)
\]
/i'
,
$str
,
$m
))
{
$this
->
data
[
'COPYUID'
]
=
[
$m
[
1
],
$m
[
2
]];
}
}
$this
->
result
=
$str
;
if
(
$this
->
errornum
!=
self
::
ERROR_OK
)
{
$this
->
error
=
$err_prefix
?
$err_prefix
.
$str
:
$str
;
}
}
return
$this
->
errornum
;
}
return
self
::
ERROR_UNKNOWN
;
}
/**
* Checks connection stream state.
*
* @return bool True if connection is closed
*/
protected
function
eof
()
{
if
(!
$this
->
fp
)
{
return
true
;
}
// If a connection opened by fsockopen() wasn't closed
// by the server, feof() will hang.
$start
=
microtime
(
true
);
if
(
feof
(
$this
->
fp
)
||
(
$this
->
prefs
[
'timeout'
]
&&
(
microtime
(
true
)
-
$start
>
$this
->
prefs
[
'timeout'
]))
)
{
$this
->
closeSocket
();
return
true
;
}
return
false
;
}
/**
* Closes connection stream.
*/
protected
function
closeSocket
()
{
if
(
$this
->
fp
)
{
fclose
(
$this
->
fp
);
$this
->
fp
=
null
;
}
}
/**
* Error code/message setter.
*/
protected
function
setError
(
$code
,
$msg
=
''
)
{
$this
->
errornum
=
$code
;
$this
->
error
=
$msg
;
return
$code
;
}
/**
* Checks response status.
* Checks if command response line starts with specified prefix (or * BYE/BAD)
*
* @param string $string Response text
* @param string $match Prefix to match with (case-sensitive)
* @param bool $error Enables BYE/BAD checking
* @param bool $nonempty Enables empty response checking
*
* @return bool true any check is true or connection is closed
*/
protected
function
startsWith
(
$string
,
$match
,
$error
=
false
,
$nonempty
=
false
)
{
if
(!
$this
->
fp
)
{
return
true
;
}
if
(
strncmp
(
$string
,
$match
,
strlen
(
$match
))
==
0
)
{
return
true
;
}
if
(
$error
&&
preg_match
(
'/^
\*
(BYE|BAD) /i'
,
$string
,
$m
))
{
if
(
strtoupper
(
$m
[
1
])
==
'BYE'
)
{
$this
->
closeSocket
();
}
return
true
;
}
if
(
$nonempty
&&
!
strlen
(
$string
))
{
return
true
;
}
return
false
;
}
/**
* Capabilities checker
*/
protected
function
hasCapability
(
$name
)
{
if
(
empty
(
$this
->
capability
)
||
empty
(
$name
))
{
return
false
;
}
if
(
in_array
(
$name
,
$this
->
capability
))
{
return
true
;
}
elseif
(
strpos
(
$name
,
'='
))
{
return
false
;
}
$result
=
[];
foreach
(
$this
->
capability
as
$cap
)
{
$entry
=
explode
(
'='
,
$cap
);
if
(
$entry
[
0
]
==
$name
)
{
$result
[]
=
$entry
[
1
];
}
}
return
$result
?:
false
;
}
/**
* Capabilities checker
*
* @param string $name Capability name
*
* @return mixed Capability values array for key=value pairs, true/false for others
*/
public
function
getCapability
(
$name
)
{
$result
=
$this
->
hasCapability
(
$name
);
if
(!
empty
(
$result
))
{
return
$result
;
}
elseif
(
$this
->
capability_read
)
{
return
false
;
}
// get capabilities (only once) because initial
// optional CAPABILITY response may differ
$result
=
$this
->
execute
(
'CAPABILITY'
);
if
(
$result
[
0
]
==
self
::
ERROR_OK
)
{
$this
->
parseCapability
(
$result
[
1
]);
}
$this
->
capability_read
=
true
;
return
$this
->
hasCapability
(
$name
);
}
/**
* Clears detected server capabilities
*/
public
function
clearCapability
()
{
$this
->
capability
=
[];
$this
->
capability_read
=
false
;
}
/**
* DIGEST-MD5/CRAM-MD5/PLAIN Authentication
*
* @param string $user Username
* @param string $pass Password
* @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
*
* @return resource|int Connection resource on success, error code on error
*/
protected
function
authenticate
(
$user
,
$pass
,
$type
=
'PLAIN'
)
{
if
(
$type
==
'CRAM-MD5'
||
$type
==
'DIGEST-MD5'
)
{
if
(
$type
==
'DIGEST-MD5'
&&
!
class_exists
(
'Auth_SASL'
))
{
return
$this
->
setError
(
self
::
ERROR_BYE
,
'The Auth_SASL package is required for DIGEST-MD5 authentication'
);
}
$this
->
putLine
(
$this
->
nextTag
()
.
" AUTHENTICATE {$type}"
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
==
'+'
)
{
$challenge
=
substr
(
$line
,
2
);
}
else
{
return
$this
->
parseResult
(
$line
);
}
if
(
$type
==
'CRAM-MD5'
)
{
// RFC2195: CRAM-MD5
$ipad
=
''
;
$opad
=
''
;
$xor
=
static
function
(
$str1
,
$str2
)
{
$result
=
''
;
$size
=
strlen
(
$str1
);
for
(
$i
=
0
;
$i
<
$size
;
$i
++)
{
$result
.=
chr
(
ord
(
$str1
[
$i
])
^
ord
(
$str2
[
$i
]));
}
return
$result
;
};
// initialize ipad, opad
for
(
$i
=
0
;
$i
<
64
;
$i
++)
{
$ipad
.=
chr
(
0x36
);
$opad
.=
chr
(
0x5C
);
}
// pad $pass so it's 64 bytes
$pass
=
str_pad
(
$pass
,
64
,
chr
(
0
));
// generate hash
$hash
=
md5
(
$xor
(
$pass
,
$opad
)
.
pack
(
'H*'
,
md5
(
$xor
(
$pass
,
$ipad
)
.
base64_decode
(
$challenge
))));
$reply
=
base64_encode
(
$user
.
' '
.
$hash
);
// send result
$this
->
putLine
(
$reply
,
true
,
true
);
}
else
{
// RFC2831: DIGEST-MD5
// proxy authorization
if
(!
empty
(
$this
->
prefs
[
'auth_cid'
]))
{
$authc
=
$this
->
prefs
[
'auth_cid'
];
$pass
=
$this
->
prefs
[
'auth_pw'
];
}
else
{
$authc
=
$user
;
$user
=
''
;
}
$auth_sasl
=
new
Auth_SASL
();
$auth_sasl
=
$auth_sasl
->
factory
(
'digestmd5'
);
$reply
=
base64_encode
(
$auth_sasl
->
getResponse
(
$authc
,
$pass
,
base64_decode
(
$challenge
),
$this
->
host
,
'imap'
,
$user
));
// send result
$this
->
putLine
(
$reply
,
true
,
true
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
!=
'+'
)
{
return
$this
->
parseResult
(
$line
);
}
// check response
$challenge
=
substr
(
$line
,
2
);
$challenge
=
base64_decode
(
$challenge
);
if
(
strpos
(
$challenge
,
'rspauth='
)
===
false
)
{
return
$this
->
setError
(
self
::
ERROR_BAD
,
'Unexpected response from server to DIGEST-MD5 response'
);
}
$this
->
putLine
(
''
);
}
$line
=
$this
->
readReply
();
$result
=
$this
->
parseResult
(
$line
);
}
elseif
(
$type
==
'GSSAPI'
)
{
if
(!
extension_loaded
(
'krb5'
))
{
return
$this
->
setError
(
self
::
ERROR_BYE
,
'The krb5 extension is required for GSSAPI authentication'
);
}
if
(
empty
(
$this
->
prefs
[
'gssapi_cn'
]))
{
return
$this
->
setError
(
self
::
ERROR_BYE
,
'The gssapi_cn parameter is required for GSSAPI authentication'
);
}
if
(
empty
(
$this
->
prefs
[
'gssapi_context'
]))
{
return
$this
->
setError
(
self
::
ERROR_BYE
,
'The gssapi_context parameter is required for GSSAPI authentication'
);
}
putenv
(
'KRB5CCNAME='
.
$this
->
prefs
[
'gssapi_cn'
]);
try
{
$ccache
=
new
KRB5CCache
();
$ccache
->
open
(
$this
->
prefs
[
'gssapi_cn'
]);
$gssapicontext
=
new
GSSAPIContext
();
$gssapicontext
->
acquireCredentials
(
$ccache
);
$token
=
''
;
$success
=
$gssapicontext
->
initSecContext
(
$this
->
prefs
[
'gssapi_context'
],
''
,
0
,
0
,
$token
);
$token
=
base64_encode
(
$token
);
}
catch
(
Exception
$e
)
{
trigger_error
(
$e
->
getMessage
(),
\E_USER_WARNING
);
return
$this
->
setError
(
self
::
ERROR_BYE
,
'GSSAPI authentication failed'
);
}
$this
->
putLine
(
$this
->
nextTag
()
.
' AUTHENTICATE GSSAPI '
.
$token
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
!=
'+'
)
{
return
$this
->
parseResult
(
$line
);
}
$itoken
=
base64_decode
(
substr
(
$line
,
2
));
$otoken
=
null
;
try
{
if
(!
$gssapicontext
->
unwrap
(
$itoken
,
$itoken
))
{
throw
new
Exception
(
'GSSAPI SASL input token unwrap failed'
);
}
if
(
strlen
(
$itoken
)
<
4
)
{
throw
new
Exception
(
'GSSAPI SASL input token invalid'
);
}
// Integrity/encryption layers are not supported. The first bit
// indicates that the server supports "no security layers".
// 0x00 should not occur, but support broken implementations.
$server_layers
=
ord
(
$itoken
[
0
]);
if
(
$server_layers
&&
(
$server_layers
&
0x1
)
!=
0x1
)
{
throw
new
Exception
(
'Server requires GSSAPI SASL integrity/encryption'
);
}
// Construct output token. 0x01 in the first octet = SASL layer "none",
// zero in the following three octets = no data follows.
// See https://github.com/cyrusimap/cyrus-sasl/blob/e41cfb986c1b1935770de554872247453fdbb079/plugins/gssapi.c#L1284
if
(!
$gssapicontext
->
wrap
(
pack
(
'CCCC'
,
0x1
,
0
,
0
,
0
),
$otoken
,
true
))
{
throw
new
Exception
(
'GSSAPI SASL output token wrap failed'
);
}
}
catch
(
Exception
$e
)
{
trigger_error
(
$e
->
getMessage
(),
\E_USER_WARNING
);
return
$this
->
setError
(
self
::
ERROR_BYE
,
'GSSAPI authentication failed'
);
}
$this
->
putLine
(
base64_encode
(
$otoken
));
$line
=
$this
->
readReply
();
$result
=
$this
->
parseResult
(
$line
);
}
elseif
(
$type
==
'PLAIN'
)
{
// proxy authorization
if
(!
empty
(
$this
->
prefs
[
'auth_cid'
]))
{
$authc
=
$this
->
prefs
[
'auth_cid'
];
$pass
=
$this
->
prefs
[
'auth_pw'
];
}
else
{
$authc
=
$user
;
$user
=
''
;
}
$reply
=
base64_encode
(
$user
.
chr
(
0
)
.
$authc
.
chr
(
0
)
.
$pass
);
// RFC 4959 (SASL-IR): save one round trip
if
(
$this
->
getCapability
(
'SASL-IR'
))
{
[
$result
,
$line
]
=
$this
->
execute
(
'AUTHENTICATE PLAIN'
,
[
$reply
],
self
::
COMMAND_LASTLINE
|
self
::
COMMAND_CAPABILITY
|
self
::
COMMAND_ANONYMIZED
);
}
else
{
$this
->
putLine
(
$this
->
nextTag
()
.
' AUTHENTICATE PLAIN'
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
!=
'+'
)
{
return
$this
->
parseResult
(
$line
);
}
// send result, get reply and process it
$this
->
putLine
(
$reply
,
true
,
true
);
$line
=
$this
->
readReply
();
$result
=
$this
->
parseResult
(
$line
);
}
}
elseif
(
$type
==
'LOGIN'
)
{
$this
->
putLine
(
$this
->
nextTag
()
.
' AUTHENTICATE LOGIN'
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
!=
'+'
)
{
return
$this
->
parseResult
(
$line
);
}
$this
->
putLine
(
base64_encode
(
$user
),
true
,
true
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
!=
'+'
)
{
return
$this
->
parseResult
(
$line
);
}
// send result, get reply and process it
$this
->
putLine
(
base64_encode
(
$pass
),
true
,
true
);
$line
=
$this
->
readReply
();
$result
=
$this
->
parseResult
(
$line
);
}
elseif
((
$type
==
'XOAUTH2'
)
||
(
$type
==
'OAUTHBEARER'
))
{
$auth
=
(
$type
==
'XOAUTH2'
)
?
base64_encode
(
"user={$user}
\1
auth={$pass}
\1\1
"
)
// XOAUTH: original extension, still widely used
:
base64_encode
(
"n,a={$user},
\1
auth={$pass}
\1\1
"
);
// OAUTHBEARER: official RFC 7628
$this
->
putLine
(
$this
->
nextTag
()
.
" AUTHENTICATE {$type} {$auth}"
,
true
,
true
);
$line
=
trim
(
$this
->
readReply
());
if
(
$line
[
0
]
==
'+'
)
{
// send empty line
$this
->
putLine
(
''
,
true
,
true
);
$line
=
$this
->
readReply
();
}
$result
=
$this
->
parseResult
(
$line
);
}
else
{
$line
=
'not supported'
;
$result
=
self
::
ERROR_UNKNOWN
;
}
if
(
$result
===
self
::
ERROR_OK
)
{
// optional CAPABILITY response
if
(
$line
&&
preg_match
(
'/
\[
CAPABILITY ([^]]+)
\]
/i'
,
$line
,
$matches
))
{
$this
->
parseCapability
(
$matches
[
1
],
true
);
}
return
$this
->
fp
;
}
return
$this
->
setError
(
$result
,
"AUTHENTICATE {$type}: {$line}"
);
}
/**
* LOGIN Authentication
*
* @param string $user Username
* @param string $password Password
*
* @return resource|int Connection resource on success, error code on error
*/
protected
function
login
(
$user
,
$password
)
{
// Prevent from sending credentials in plain text when connection is not secure
if
(
$this
->
getCapability
(
'LOGINDISABLED'
))
{
return
$this
->
setError
(
self
::
ERROR_BAD
,
'Login disabled by IMAP server'
);
}
[
$code
,
$response
]
=
$this
->
execute
(
'LOGIN'
,
[
$this
->
escape
(
$user
,
true
),
$this
->
escape
(
$password
,
true
)],
self
::
COMMAND_CAPABILITY
|
self
::
COMMAND_ANONYMIZED
);
// re-set capabilities list if untagged CAPABILITY response provided
if
(
preg_match
(
'/
\*
CAPABILITY (.+)/i'
,
$response
,
$matches
))
{
$this
->
parseCapability
(
$matches
[
1
],
true
);
}
if
(
$code
==
self
::
ERROR_OK
)
{
return
$this
->
fp
;
}
return
$code
;
}
/**
* Detects hierarchy delimiter
*
* @return ?string The delimiter
*/
public
function
getHierarchyDelimiter
()
{
if
(!
empty
(
$this
->
prefs
[
'delimiter'
]))
{
return
$this
->
prefs
[
'delimiter'
];
}
// try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
[
$code
,
$response
]
=
$this
->
execute
(
'LIST'
,
[
$this
->
escape
(
''
),
$this
->
escape
(
''
)]);
if
(
$code
==
self
::
ERROR_OK
)
{
$args
=
$this
->
tokenizeResponse
(
$response
,
4
);
$delimiter
=
$args
[
3
];
if
(
strlen
(
$delimiter
)
>
0
)
{
return
$this
->
prefs
[
'delimiter'
]
=
$delimiter
;
}
}
return
null
;
}
/**
* NAMESPACE handler (RFC 2342)
*
* @return array|false Namespace data hash (personal, other, shared)
*/
public
function
getNamespace
()
{
if
(
array_key_exists
(
'namespace'
,
$this
->
prefs
))
{
return
$this
->
prefs
[
'namespace'
];
}
if
(!
$this
->
getCapability
(
'NAMESPACE'
))
{
return
false
;
}
[
$code
,
$response
]
=
$this
->
execute
(
'NAMESPACE'
);
if
(
$code
==
self
::
ERROR_OK
&&
preg_match
(
'/^
\*
NAMESPACE /'
,
$response
))
{
$response
=
substr
(
$response
,
11
);
$data
=
$this
->
tokenizeResponse
(
$response
);
}
if
(!
isset
(
$data
)
||
!
is_array
(
$data
))
{
return
false
;
}
$this
->
prefs
[
'namespace'
]
=
[
'personal'
=>
$data
[
0
],
'other'
=>
$data
[
1
],
'shared'
=>
$data
[
2
],
];
return
$this
->
prefs
[
'namespace'
];
}
/**
* Connects to IMAP server and authenticates.
*
* @param string $host Server hostname or IP
* @param string $user User name
* @param string $password Password
* @param array $options Connection and class options
*
* @return bool True on success, False on failure
*/
public
function
connect
(
$host
,
$user
,
$password
,
$options
=
[])
{
// configure
$this
->
set_prefs
(
$options
);
$this
->
host
=
$host
;
$this
->
user
=
$user
;
$this
->
logged
=
false
;
$this
->
selected
=
null
;
// check input
if
(
empty
(
$host
))
{
$this
->
setError
(
self
::
ERROR_BAD
,
'Empty host'
);
return
false
;
}
if
(
empty
(
$user
))
{
$this
->
setError
(
self
::
ERROR_NO
,
'Empty user'
);
return
false
;
}
if
(
empty
(
$password
)
&&
empty
(
$options
[
'gssapi_cn'
]))
{
$this
->
setError
(
self
::
ERROR_NO
,
'Empty password'
);
return
false
;
}
// Connect
if
(!
$this
->
_connect
(
$host
))
{
return
false
;
}
// Send pre authentication ID info (#7860)
if
(!
empty
(
$this
->
prefs
[
'preauth_ident'
])
&&
$this
->
getCapability
(
'ID'
))
{
$this
->
data
[
'ID'
]
=
$this
->
id
(
$this
->
prefs
[
'preauth_ident'
]);
}
$auth_method
=
$this
->
prefs
[
'auth_type'
];
$auth_methods
=
[];
$result
=
null
;
// check for supported auth methods
if
(!
$auth_method
||
$auth_method
===
'CHECK'
||
$auth_method
===
'OAUTH'
)
{
if
(
$auth_caps
=
$this
->
getCapability
(
'AUTH'
))
{
$auth_methods
=
$auth_caps
;
}
// Use best (for security) supported authentication method
$all_methods
=
[
'DIGEST-MD5'
,
'CRAM-MD5'
,
'CRAM_MD5'
,
'PLAIN'
,
'LOGIN'
];
// special case of OAUTH, use the supported method
if
(
$auth_method
===
'OAUTH'
)
{
$all_methods
=
[
'OAUTHBEARER'
,
'XOAUTH2'
];
}
if
(!
empty
(
$this
->
prefs
[
'gssapi_cn'
]))
{
array_unshift
(
$all_methods
,
'GSSAPI'
);
}
foreach
(
$all_methods
as
$method
)
{
$auth_method
=
$method
;
if
(
in_array
(
$method
,
$auth_methods
))
{
break
;
}
}
// Prefer LOGIN over AUTHENTICATE LOGIN for performance reasons
if
(
$auth_method
==
'LOGIN'
&&
!
$this
->
getCapability
(
'LOGINDISABLED'
))
{
$auth_method
=
'IMAP'
;
}
}
// pre-login capabilities can be not complete
$this
->
capability_read
=
false
;
// Authenticate
switch
(
$auth_method
)
{
case
'CRAM_MD5'
:
$auth_method
=
'CRAM-MD5'
;
case
'CRAM-MD5'
:
case
'DIGEST-MD5'
:
case
'GSSAPI'
:
case
'PLAIN'
:
case
'LOGIN'
:
case
'XOAUTH2'
:
case
'OAUTHBEARER'
:
$result
=
$this
->
authenticate
(
$user
,
$password
,
$auth_method
);
break
;
case
'IMAP'
:
$result
=
$this
->
login
(
$user
,
$password
);
break
;
default
:
$this
->
setError
(
self
::
ERROR_BAD
,
"Configuration error. Unknown auth method: {$auth_method}"
);
}
// Connected and authenticated
if
(
is_resource
(
$result
))
{
if
(!
empty
(
$this
->
prefs
[
'force_caps'
]))
{
$this
->
clearCapability
();
}
$this
->
logged
=
true
;
// Send ID info after authentication to ensure reliable result (#7517)
if
(!
empty
(
$this
->
prefs
[
'ident'
])
&&
$this
->
getCapability
(
'ID'
))
{
$this
->
data
[
'ID'
]
=
$this
->
id
(
$this
->
prefs
[
'ident'
]);
}
return
true
;
}
$this
->
closeConnection
();
return
false
;
}
/**
* Connects to IMAP server.
*
* @param string $host Server hostname or IP
*
* @return bool True on success, False on failure
*/
protected
function
_connect
(
$host
)
{
// initialize connection
$this
->
error
=
''
;
$this
->
errornum
=
self
::
ERROR_OK
;
$port
=
empty
(
$this
->
prefs
[
'port'
])
?
143
:
$this
->
prefs
[
'port'
];
$ssl_mode
=
$this
->
prefs
[
'ssl_mode'
]
??
null
;
// check for SSL
if
(!
empty
(
$ssl_mode
)
&&
$ssl_mode
!=
'tls'
)
{
$host
=
$ssl_mode
.
'://'
.
$host
;
}
if
(
empty
(
$this
->
prefs
[
'timeout'
])
||
$this
->
prefs
[
'timeout'
]
<
0
)
{
$this
->
prefs
[
'timeout'
]
=
max
(
0
,
intval
(
ini_get
(
'default_socket_timeout'
)));
}
if
(
$this
->
debug
)
{
// set connection identifier for debug output
$this
->
resourceid
=
strtoupper
(
substr
(
md5
(
microtime
()
.
$host
.
$this
->
user
),
0
,
4
));
$_host
=
(
$ssl_mode
==
'tls'
?
'tls://'
:
''
)
.
$host
.
':'
.
$port
;
$this
->
debug
(
"Connecting to {$_host}..."
);
}
if
(!
empty
(
$this
->
prefs
[
'socket_options'
]))
{
$options
=
array_intersect_key
(
$this
->
prefs
[
'socket_options'
],
[
'ssl'
=>
1
,
'socket'
=>
1
]);
$context
=
stream_context_create
(
$options
);
$this
->
fp
=
stream_socket_client
(
$host
.
':'
.
$port
,
$errno
,
$errstr
,
$this
->
prefs
[
'timeout'
],
\STREAM_CLIENT_CONNECT
,
$context
);
}
else
{
$this
->
fp
=
@
fsockopen
(
$host
,
$port
,
$errno
,
$errstr
,
$this
->
prefs
[
'timeout'
]);
}
if
(!
$this
->
fp
)
{
$this
->
setError
(
self
::
ERROR_BAD
,
sprintf
(
'Could not connect to %s:%d: %s'
,
$host
,
$port
,
$errstr
?:
'Unknown reason'
));
return
false
;
}
// insert proxy protocol header, if enabled
if
(!
empty
(
$this
->
prefs
[
'socket_options'
]))
{
$proxy_protocol_header
=
rcube_utils
::
proxy_protocol_header
(
$this
->
prefs
[
'socket_options'
]);
if
(
strlen
(
$proxy_protocol_header
)
>
0
)
{
fwrite
(
$this
->
fp
,
$proxy_protocol_header
);
}
}
if
(
$this
->
prefs
[
'timeout'
]
>
0
)
{
stream_set_timeout
(
$this
->
fp
,
$this
->
prefs
[
'timeout'
]);
}
$line
=
trim
(
fgets
(
$this
->
fp
,
8192
));
if
(
$this
->
debug
&&
$line
)
{
$this
->
debug
(
'S: '
.
$line
);
}
// Connected to wrong port or connection error?
if
(!
preg_match
(
'/^
\*
(OK|PREAUTH)/i'
,
$line
))
{
if
(
$line
)
{
$error
=
sprintf
(
'Wrong startup greeting (%s:%d): %s'
,
$host
,
$port
,
$line
);
}
else
{
$error
=
sprintf
(
'Empty startup greeting (%s:%d)'
,
$host
,
$port
);
}
$this
->
setError
(
self
::
ERROR_BAD
,
$error
);
$this
->
closeConnection
();
return
false
;
}
$this
->
data
[
'GREETING'
]
=
trim
(
preg_replace
(
'/
\[
[^
\]
]+
\]\s
*/'
,
''
,
$line
));
// RFC3501 [7.1] optional CAPABILITY response
if
(
preg_match
(
'/
\[
CAPABILITY ([^]]+)
\]
/i'
,
$line
,
$matches
))
{
$this
->
parseCapability
(
$matches
[
1
],
true
);
}
// TLS connection
if
(
$ssl_mode
==
'tls'
&&
$this
->
getCapability
(
'STARTTLS'
))
{
$res
=
$this
->
execute
(
'STARTTLS'
);
if
(
empty
(
$res
)
||
$res
[
0
]
!=
self
::
ERROR_OK
)
{
$this
->
closeConnection
();
return
false
;
}
if
(
isset
(
$this
->
prefs
[
'socket_options'
][
'ssl'
][
'crypto_method'
]))
{
$crypto_method
=
$this
->
prefs
[
'socket_options'
][
'ssl'
][
'crypto_method'
];
}
else
{
// There is no flag to enable all TLS methods. Net_SMTP
// handles enabling TLS similarly.
$crypto_method
=
\STREAM_CRYPTO_METHOD_TLS_CLIENT
|
@
\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
|
@
\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
;
}
if
(!
stream_socket_enable_crypto
(
$this
->
fp
,
true
,
$crypto_method
))
{
$this
->
setError
(
self
::
ERROR_BAD
,
'Unable to negotiate TLS'
);
$this
->
closeConnection
();
return
false
;
}
// Now we're secure, capabilities need to be reread
$this
->
clearCapability
();
}
return
true
;
}
/**
* Initializes environment
*/
protected
function
set_prefs
(
$prefs
)
{
// set preferences
if
(
is_array
(
$prefs
))
{
$this
->
prefs
=
$prefs
;
}
// set auth method
if
(!
empty
(
$this
->
prefs
[
'auth_type'
]))
{
$this
->
prefs
[
'auth_type'
]
=
strtoupper
(
$this
->
prefs
[
'auth_type'
]);
}
else
{
$this
->
prefs
[
'auth_type'
]
=
'CHECK'
;
}
// disabled capabilities
if
(!
empty
(
$this
->
prefs
[
'disabled_caps'
]))
{
$this
->
prefs
[
'disabled_caps'
]
=
array_map
(
'strtoupper'
,
(
array
)
$this
->
prefs
[
'disabled_caps'
]);
}
// additional message flags
if
(!
empty
(
$this
->
prefs
[
'message_flags'
]))
{
$this
->
flags
=
array_merge
(
$this
->
flags
,
$this
->
prefs
[
'message_flags'
]);
unset
(
$this
->
prefs
[
'message_flags'
]);
}
}
/**
* Checks connection status
*
* @return bool true if connection is active and user is logged in, False otherwise
*/
public
function
connected
()
{
return
$this
->
fp
&&
$this
->
logged
;
}
/**
* Closes connection with logout.
*/
public
function
closeConnection
()
{
if
(
$this
->
logged
&&
$this
->
putLine
(
$this
->
nextTag
()
.
' LOGOUT'
))
{
$this
->
readReply
();
}
$this
->
closeSocket
();
$this
->
clearCapability
();
}
/**
* Get crrent user.
*
* @return string|null Current username set for the connection
*/
public
function
getUser
()
{
return
$this
->
user
;
}
/**
* Executes SELECT command (if mailbox is already not in selected state)
*
* @param string $mailbox Mailbox name
* @param array $qresync_data QRESYNC data (RFC5162)
*
* @return bool True on success, false on error
*/
public
function
select
(
$mailbox
,
$qresync_data
=
null
)
{
if
(!
strlen
(
$mailbox
))
{
return
false
;
}
if
(
$this
->
selected
===
$mailbox
)
{
return
true
;
}
$params
=
[
$this
->
escape
(
$mailbox
)];
// QRESYNC data items
// 0. the last known UIDVALIDITY,
// 1. the last known modification sequence,
// 2. the optional set of known UIDs, and
// 3. an optional parenthesized list of known sequence ranges and their
// corresponding UIDs.
if
(!
empty
(
$qresync_data
))
{
if
(!
empty
(
$qresync_data
[
2
]))
{
$qresync_data
[
2
]
=
self
::
compressMessageSet
(
$qresync_data
[
2
]);
}
$params
[]
=
[
'QRESYNC'
,
$qresync_data
];
}
[
$code
,
$response
]
=
$this
->
execute
(
'SELECT'
,
$params
);
if
(
$code
==
self
::
ERROR_OK
)
{
$this
->
clear_mailbox_cache
();
$response
=
explode
(
"
\r\n
"
,
$response
);
foreach
(
$response
as
$line
)
{
if
(
preg_match
(
'/^
\*
OK
\[
/i'
,
$line
))
{
$pos
=
strcspn
(
$line
,
' ]'
,
6
);
$token
=
strtoupper
(
substr
(
$line
,
6
,
$pos
));
$pos
+=
7
;
switch
(
$token
)
{
case
'UIDNEXT'
:
case
'UIDVALIDITY'
:
case
'UNSEEN'
:
if
(
$len
=
strspn
(
$line
,
'0123456789'
,
$pos
))
{
$this
->
data
[
$token
]
=
(
int
)
substr
(
$line
,
$pos
,
$len
);
}
break
;
case
'HIGHESTMODSEQ'
:
if
(
$len
=
strspn
(
$line
,
'0123456789'
,
$pos
))
{
$this
->
data
[
$token
]
=
(
string
)
substr
(
$line
,
$pos
,
$len
);
}
break
;
case
'NOMODSEQ'
:
$this
->
data
[
$token
]
=
true
;
break
;
case
'PERMANENTFLAGS'
:
$start
=
strpos
(
$line
,
'('
,
$pos
);
$end
=
strrpos
(
$line
,
')'
);
if
(
$start
&&
$end
)
{
$flags
=
substr
(
$line
,
$start
+
1
,
$end
-
$start
-
1
);
$this
->
data
[
$token
]
=
explode
(
' '
,
$flags
);
}
break
;
}
}
elseif
(
preg_match
(
'/^
\*
([0-9]+) (EXISTS|RECENT|FETCH)/i'
,
$line
,
$match
))
{
$token
=
strtoupper
(
$match
[
2
]);
switch
(
$token
)
{
case
'EXISTS'
:
case
'RECENT'
:
$this
->
data
[
$token
]
=
(
int
)
$match
[
1
];
break
;
case
'FETCH'
:
// QRESYNC FETCH response (RFC5162)
$line
=
substr
(
$line
,
strlen
(
$match
[
0
]));
$fetch_data
=
$this
->
tokenizeResponse
(
$line
,
1
);
$data
=
[
'id'
=>
$match
[
1
]];
for
(
$i
=
0
,
$size
=
count
(
$fetch_data
);
$i
<
$size
;
$i
+=
2
)
{
$data
[
strtolower
(
$fetch_data
[
$i
])]
=
$fetch_data
[
$i
+
1
];
}
$this
->
data
[
'QRESYNC'
][
$data
[
'uid'
]]
=
$data
;
break
;
}
}
// QRESYNC VANISHED response (RFC5162)
elseif
(
preg_match
(
'/^
\*
VANISHED [()EARLIER]*/i'
,
$line
,
$match
))
{
$line
=
substr
(
$line
,
strlen
(
$match
[
0
]));
$v_data
=
$this
->
tokenizeResponse
(
$line
,
1
);
$this
->
data
[
'VANISHED'
]
=
$v_data
;
}
}
$this
->
data
[
'READ-WRITE'
]
=
$this
->
resultcode
!=
'READ-ONLY'
;
$this
->
selected
=
$mailbox
;
return
true
;
}
return
false
;
}
/**
* Executes STATUS command
*
* @param string $mailbox Mailbox name
* @param array $items Additional requested item names. By default
* MESSAGES and UNSEEN are requested. Other defined
* in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
*
* @return array|false Status item-value hash, False on error
*
* @since 0.5-beta
*/
public
function
status
(
$mailbox
,
$items
=
[])
{
if
(!
strlen
(
$mailbox
))
{
return
false
;
}
if
(!
in_array
(
'MESSAGES'
,
$items
))
{
$items
[]
=
'MESSAGES'
;
}
if
(!
in_array
(
'UNSEEN'
,
$items
))
{
$items
[]
=
'UNSEEN'
;
}
[
$code
,
$response
]
=
$this
->
execute
(
'STATUS'
,
[
$this
->
escape
(
$mailbox
),
'('
.
implode
(
' '
,
$items
)
.
')'
],
0
,
'/^
\*
STATUS /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
$result
=
[];
$response
=
substr
(
$response
,
9
);
// remove prefix "* STATUS "
[
$mbox
,
$items
]
=
$this
->
tokenizeResponse
(
$response
,
2
);
// Fix for #1487859. Some buggy server returns not quoted
// folder name with spaces. Let's try to handle this situation
if
(!
is_array
(
$items
)
&&
(
$pos
=
strpos
(
$response
,
'('
))
!==
false
)
{
$response
=
substr
(
$response
,
$pos
);
$items
=
$this
->
tokenizeResponse
(
$response
,
1
);
}
if
(!
is_array
(
$items
))
{
return
$result
;
}
for
(
$i
=
0
,
$len
=
count
(
$items
);
$i
<
$len
;
$i
+=
2
)
{
$result
[
$items
[
$i
]]
=
$items
[
$i
+
1
];
}
$this
->
data
[
'STATUS:'
.
$mailbox
]
=
$result
;
return
$result
;
}
return
false
;
}
/**
* Executes EXPUNGE command
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UIDs to expunge
*
* @return bool True on success, False on error
*/
public
function
expunge
(
$mailbox
,
$messages
=
null
)
{
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
if
(
empty
(
$this
->
data
[
'READ-WRITE'
]))
{
$this
->
setError
(
self
::
ERROR_READONLY
,
'Mailbox is read-only'
);
return
false
;
}
// Clear internal status cache
$this
->
clear_status_cache
(
$mailbox
);
if
(!
empty
(
$messages
)
&&
$messages
!=
'*'
&&
$this
->
hasCapability
(
'UIDPLUS'
))
{
$messages
=
self
::
compressMessageSet
(
$messages
);
$result
=
$this
->
execute
(
'UID EXPUNGE'
,
[
$messages
],
self
::
COMMAND_NORESPONSE
);
}
else
{
$result
=
$this
->
execute
(
'EXPUNGE'
,
null
,
self
::
COMMAND_NORESPONSE
);
}
if
(
$result
==
self
::
ERROR_OK
)
{
$this
->
selected
=
null
;
// state has changed, need to reselect
return
true
;
}
return
false
;
}
/**
* Executes CLOSE command
*
* @return bool True on success, False on error
*
* @since 0.5
*/
public
function
close
()
{
$result
=
$this
->
execute
(
'CLOSE'
,
null
,
self
::
COMMAND_NORESPONSE
);
if
(
$result
==
self
::
ERROR_OK
)
{
$this
->
selected
=
null
;
return
true
;
}
return
false
;
}
/**
* Folder subscription (SUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
public
function
subscribe
(
$mailbox
)
{
$result
=
$this
->
execute
(
'SUBSCRIBE'
,
[
$this
->
escape
(
$mailbox
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Folder unsubscription (UNSUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
public
function
unsubscribe
(
$mailbox
)
{
$result
=
$this
->
execute
(
'UNSUBSCRIBE'
,
[
$this
->
escape
(
$mailbox
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Folder creation (CREATE)
*
* @param string $mailbox Mailbox name
* @param array $types Optional folder types (RFC 6154)
*
* @return bool True on success, False on error
*/
public
function
createFolder
(
$mailbox
,
$types
=
null
)
{
$args
=
[
$this
->
escape
(
$mailbox
)];
// RFC 6154: CREATE-SPECIAL-USE
if
(!
empty
(
$types
)
&&
$this
->
getCapability
(
'CREATE-SPECIAL-USE'
))
{
$args
[]
=
'(USE ('
.
implode
(
' '
,
$types
)
.
'))'
;
}
$result
=
$this
->
execute
(
'CREATE'
,
$args
,
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Folder renaming (RENAME)
*
* @param string $from Mailbox name
* @param string $to Mailbox name
*
* @return bool True on success, False on error
*/
public
function
renameFolder
(
$from
,
$to
)
{
$result
=
$this
->
execute
(
'RENAME'
,
[
$this
->
escape
(
$from
),
$this
->
escape
(
$to
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Executes DELETE command
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
public
function
deleteFolder
(
$mailbox
)
{
// Unselect the folder to prevent "BYE Fatal error: Mailbox has been (re)moved" on Cyrus IMAP
if
(
$this
->
selected
===
$mailbox
&&
$this
->
hasCapability
(
'UNSELECT'
))
{
$this
->
execute
(
'UNSELECT'
,
[],
self
::
COMMAND_NORESPONSE
);
}
$result
=
$this
->
execute
(
'DELETE'
,
[
$this
->
escape
(
$mailbox
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Removes all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
public
function
clearFolder
(
$mailbox
)
{
if
(
$this
->
countMessages
(
$mailbox
)
>
0
)
{
$res
=
$this
->
flag
(
$mailbox
,
'1:*'
,
'DELETED'
);
}
else
{
return
true
;
}
if
(!
empty
(
$res
))
{
if
(
$this
->
selected
===
$mailbox
)
{
$res
=
$this
->
close
();
}
else
{
$res
=
$this
->
expunge
(
$mailbox
);
}
return
$res
;
}
return
false
;
}
/**
* Returns list of mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param array $return_opts (see self::_listMailboxes)
* @param array $select_opts (see self::_listMailboxes)
*
* @return array|bool list of mailboxes or hash of options if STATUS/MYRIGHTS response
* is requested, False on error
*/
public
function
listMailboxes
(
$ref
,
$mailbox
,
$return_opts
=
[],
$select_opts
=
[])
{
return
$this
->
_listMailboxes
(
$ref
,
$mailbox
,
false
,
$return_opts
,
$select_opts
);
}
/**
* Returns list of subscribed mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param array $return_opts (see self::_listMailboxes)
*
* @return array|bool list of mailboxes or hash of options if STATUS/MYRIGHTS response
* is requested, False on error
*/
public
function
listSubscribed
(
$ref
,
$mailbox
,
$return_opts
=
[])
{
return
$this
->
_listMailboxes
(
$ref
,
$mailbox
,
true
,
$return_opts
,
null
);
}
/**
* IMAP LIST/LSUB command
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param bool $subscribed Enables returning subscribed mailboxes only
* @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
* Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
* MYRIGHTS, SUBSCRIBED, CHILDREN
* @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
* Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
* SPECIAL-USE (RFC6154)
*
* @return array|bool list of mailboxes or hash of options if STATUS/MYRIGHTS response
* is requested, False on error
*/
protected
function
_listMailboxes
(
$ref
,
$mailbox
,
$subscribed
=
false
,
$return_opts
=
[],
$select_opts
=
[])
{
if
(!
strlen
(
$mailbox
))
{
$mailbox
=
'*'
;
}
$lstatus
=
false
;
$args
=
[];
$rets
=
[];
if
(!
empty
(
$select_opts
)
&&
$this
->
getCapability
(
'LIST-EXTENDED'
))
{
$select_opts
=
(
array
)
$select_opts
;
$args
[]
=
'('
.
implode
(
' '
,
$select_opts
)
.
')'
;
}
$args
[]
=
$this
->
escape
(
$ref
);
$args
[]
=
$this
->
escape
(
$mailbox
);
if
(!
empty
(
$return_opts
)
&&
$this
->
getCapability
(
'LIST-EXTENDED'
))
{
$ext_opts
=
[
'SUBSCRIBED'
,
'CHILDREN'
];
$rets
=
array_intersect
(
$return_opts
,
$ext_opts
);
$return_opts
=
array_diff
(
$return_opts
,
$rets
);
}
if
(!
empty
(
$return_opts
)
&&
$this
->
getCapability
(
'LIST-STATUS'
))
{
$lstatus
=
true
;
$status_opts
=
[
'MESSAGES'
,
'RECENT'
,
'UIDNEXT'
,
'UIDVALIDITY'
,
'UNSEEN'
,
'SIZE'
];
$opts
=
array_diff
(
$return_opts
,
$status_opts
);
$status_opts
=
array_diff
(
$return_opts
,
$opts
);
if
(!
empty
(
$status_opts
))
{
$rets
[]
=
'STATUS ('
.
implode
(
' '
,
$status_opts
)
.
')'
;
}
if
(!
empty
(
$opts
))
{
$rets
=
array_merge
(
$rets
,
$opts
);
}
}
if
(!
empty
(
$rets
))
{
$args
[]
=
'RETURN ('
.
implode
(
' '
,
$rets
)
.
')'
;
}
[
$code
,
$response
]
=
$this
->
execute
(
$subscribed
?
'LSUB'
:
'LIST'
,
$args
);
if
(
$code
==
self
::
ERROR_OK
)
{
$folders
=
[];
$last
=
0
;
$pos
=
0
;
$response
.=
"
\r\n
"
;
while
(
$pos
=
strpos
(
$response
,
"
\r\n
"
,
$pos
+
1
))
{
// literal string, not real end-of-command-line
if
(
$response
[
$pos
-
1
]
==
'}'
)
{
continue
;
}
$line
=
substr
(
$response
,
$last
,
$pos
-
$last
);
$last
=
$pos
+
2
;
if
(!
preg_match
(
'/^
\*
(LIST|LSUB|STATUS|MYRIGHTS) /i'
,
$line
,
$m
))
{
continue
;
}
$cmd
=
strtoupper
(
$m
[
1
]);
$line
=
substr
(
$line
,
strlen
(
$m
[
0
]));
// * LIST (<options>) <delimiter> <mailbox>
if
(
$cmd
==
'LIST'
||
$cmd
==
'LSUB'
)
{
[
$opts
,
$delim
,
$mailbox
]
=
$this
->
tokenizeResponse
(
$line
,
3
);
// Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
if
(
$delim
)
{
$mailbox
=
rtrim
(
$mailbox
,
$delim
);
}
// Make it easier for the client to deal with INBOX folder
// by always returning the word with all capital letters
if
(
strlen
(
$mailbox
)
==
5
&&
(
$mailbox
[
0
]
==
'i'
||
$mailbox
[
0
]
==
'I'
)
&&
(
$mailbox
[
1
]
==
'n'
||
$mailbox
[
1
]
==
'N'
)
&&
(
$mailbox
[
2
]
==
'b'
||
$mailbox
[
2
]
==
'B'
)
&&
(
$mailbox
[
3
]
==
'o'
||
$mailbox
[
3
]
==
'O'
)
&&
(
$mailbox
[
4
]
==
'x'
||
$mailbox
[
4
]
==
'X'
)
)
{
$mailbox
=
'INBOX'
;
}
// Add to result array
if
(!
$lstatus
)
{
$folders
[]
=
$mailbox
;
}
else
{
$folders
[
$mailbox
]
=
[];
}
// store folder options
if
(
$cmd
==
'LIST'
)
{
// Add to options array
if
(
empty
(
$this
->
data
[
'LIST'
][
$mailbox
]))
{
$this
->
data
[
'LIST'
][
$mailbox
]
=
$opts
;
}
elseif
(!
empty
(
$opts
))
{
$this
->
data
[
'LIST'
][
$mailbox
]
=
array_unique
(
array_merge
(
$this
->
data
[
'LIST'
][
$mailbox
],
$opts
));
}
}
}
elseif
(
$lstatus
)
{
// * STATUS <mailbox> (<result>)
if
(
$cmd
==
'STATUS'
)
{
[
$mailbox
,
$status
]
=
$this
->
tokenizeResponse
(
$line
,
2
);
for
(
$i
=
0
,
$len
=
count
(
$status
);
$i
<
$len
;
$i
+=
2
)
{
[
$name
,
$value
]
=
$this
->
tokenizeResponse
(
$status
,
2
);
$folders
[
$mailbox
][
$name
]
=
$value
;
}
}
// * MYRIGHTS <mailbox> <acl>
elseif
(
$cmd
==
'MYRIGHTS'
)
{
[
$mailbox
,
$acl
]
=
$this
->
tokenizeResponse
(
$line
,
2
);
$folders
[
$mailbox
][
'MYRIGHTS'
]
=
$acl
;
}
}
}
return
$folders
;
}
return
false
;
}
/**
* Returns count of all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return int|false Number of messages, False on error
*/
public
function
countMessages
(
$mailbox
)
{
if
(
$this
->
selected
===
$mailbox
&&
isset
(
$this
->
data
[
'EXISTS'
]))
{
return
$this
->
data
[
'EXISTS'
];
}
// Check internal cache
if
(!
empty
(
$this
->
data
[
'STATUS:'
.
$mailbox
]))
{
$cache
=
$this
->
data
[
'STATUS:'
.
$mailbox
];
if
(
isset
(
$cache
[
'MESSAGES'
]))
{
return
(
int
)
$cache
[
'MESSAGES'
];
}
}
// Try STATUS (should be faster than SELECT)
$counts
=
$this
->
status
(
$mailbox
);
if
(
is_array
(
$counts
))
{
return
(
int
)
$counts
[
'MESSAGES'
];
}
return
false
;
}
/**
* Returns count of messages with \Recent flag in a folder
*
* @param string $mailbox Mailbox name
*
* @return int|false Number of messages, False on error
*/
public
function
countRecent
(
$mailbox
)
{
if
(
$this
->
selected
===
$mailbox
&&
isset
(
$this
->
data
[
'RECENT'
]))
{
return
$this
->
data
[
'RECENT'
];
}
// Check internal cache
$cache
=
$this
->
data
[
'STATUS:'
.
$mailbox
];
if
(!
empty
(
$cache
)
&&
isset
(
$cache
[
'RECENT'
]))
{
return
(
int
)
$cache
[
'RECENT'
];
}
// Try STATUS (should be faster than SELECT)
$counts
=
$this
->
status
(
$mailbox
,
[
'RECENT'
]);
if
(
is_array
(
$counts
))
{
return
(
int
)
$counts
[
'RECENT'
];
}
return
false
;
}
/**
* Returns count of messages without \Seen flag in a specified folder
*
* @param string $mailbox Mailbox name
*
* @return int|false Number of messages, False on error
*/
public
function
countUnseen
(
$mailbox
)
{
// Check internal cache
if
(!
empty
(
$this
->
data
[
'STATUS:'
.
$mailbox
]))
{
$cache
=
$this
->
data
[
'STATUS:'
.
$mailbox
];
if
(
isset
(
$cache
[
'UNSEEN'
]))
{
return
(
int
)
$cache
[
'UNSEEN'
];
}
}
// Try STATUS (should be faster than SELECT+SEARCH)
$counts
=
$this
->
status
(
$mailbox
);
if
(
is_array
(
$counts
))
{
return
(
int
)
$counts
[
'UNSEEN'
];
}
// Invoke SEARCH as a fallback
$index
=
$this
->
search
(
$mailbox
,
'ALL UNSEEN'
,
false
,
[
'COUNT'
]);
if
(!
$index
->
is_error
())
{
return
$index
->
count
();
}
return
false
;
}
/**
* Executes ID command (RFC2971)
*
* @param array $items Client identification information key/value hash
*
* @return array|false Server identification information key/value hash, False on error
*
* @since 0.6
*/
public
function
id
(
$items
=
[])
{
$args
=
[];
if
(!
empty
(
$items
))
{
foreach
(
$items
as
$key
=>
$value
)
{
$args
[]
=
$this
->
escape
(
$key
,
true
);
$args
[]
=
$this
->
escape
(
$value
,
true
);
}
}
[
$code
,
$response
]
=
$this
->
execute
(
'ID'
,
[!
empty
(
$args
)
?
'('
.
implode
(
' '
,
(
array
)
$args
)
.
')'
:
$this
->
escape
(
null
)],
0
,
'/^
\*
ID /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
$response
=
substr
(
$response
,
5
);
// remove prefix "* ID "
$items
=
$this
->
tokenizeResponse
(
$response
,
1
);
$result
=
[];
if
(
is_array
(
$items
))
{
for
(
$i
=
0
,
$len
=
count
(
$items
);
$i
<
$len
;
$i
+=
2
)
{
$result
[
$items
[
$i
]]
=
$items
[
$i
+
1
];
}
}
return
$result
;
}
return
false
;
}
/**
* Executes ENABLE command (RFC5161)
*
* @param mixed $extension Extension name to enable (or array of names)
*
* @return array|bool List of enabled extensions, False on error
*
* @since 0.6
*/
public
function
enable
(
$extension
)
{
if
(
empty
(
$extension
))
{
return
false
;
}
if
(!
$this
->
hasCapability
(
'ENABLE'
))
{
return
false
;
}
if
(!
is_array
(
$extension
))
{
$extension
=
[
$extension
];
}
if
(!
empty
(
$this
->
extensions_enabled
))
{
// check if all extensions are already enabled
$diff
=
array_diff
(
$extension
,
$this
->
extensions_enabled
);
if
(
empty
(
$diff
))
{
return
$extension
;
}
// Make sure the mailbox isn't selected, before enabling extension(s)
if
(
$this
->
selected
!==
null
)
{
$this
->
close
();
}
}
[
$code
,
$response
]
=
$this
->
execute
(
'ENABLE'
,
$extension
,
0
,
'/^
\*
ENABLED /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
$response
=
substr
(
$response
,
10
);
// remove prefix "* ENABLED "
$result
=
(
array
)
$this
->
tokenizeResponse
(
$response
);
$this
->
extensions_enabled
=
array_unique
(
array_merge
(
$this
->
extensions_enabled
,
$result
));
return
$this
->
extensions_enabled
;
}
return
false
;
}
/**
* Executes SORT command
*
* @param string $mailbox Mailbox name
* @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param string $criteria Searching criteria
* @param bool $return_uid Enables UID SORT usage
* @param string $encoding Character set
*
* @return rcube_result_index Response data
*/
public
function
sort
(
$mailbox
,
$field
=
'ARRIVAL'
,
$criteria
=
''
,
$return_uid
=
false
,
$encoding
=
'US-ASCII'
)
{
$old_sel
=
$this
->
selected
;
$supported
=
[
'ARRIVAL'
,
'CC'
,
'DATE'
,
'FROM'
,
'SIZE'
,
'SUBJECT'
,
'TO'
];
$field
=
strtoupper
(
$field
);
if
(
$field
==
'INTERNALDATE'
)
{
$field
=
'ARRIVAL'
;
}
if
(!
in_array
(
$field
,
$supported
))
{
return
new
rcube_result_index
(
$mailbox
);
}
if
(!
$this
->
select
(
$mailbox
))
{
return
new
rcube_result_index
(
$mailbox
);
}
// return empty result when folder is empty and we're just after SELECT
if
(
$old_sel
!=
$mailbox
&&
empty
(
$this
->
data
[
'EXISTS'
]))
{
return
new
rcube_result_index
(
$mailbox
,
'* SORT'
);
}
// RFC 5957: SORT=DISPLAY
if
((
$field
==
'FROM'
||
$field
==
'TO'
)
&&
$this
->
getCapability
(
'SORT=DISPLAY'
))
{
$field
=
'DISPLAY'
.
$field
;
}
$encoding
=
$encoding
?
trim
(
$encoding
)
:
'US-ASCII'
;
$criteria
=
$criteria
?
'ALL '
.
trim
(
$criteria
)
:
'ALL'
;
[
$code
,
$response
]
=
$this
->
execute
(
$return_uid
?
'UID SORT'
:
'SORT'
,
[
"({$field})"
,
$encoding
,
$criteria
]);
if
(
$code
!=
self
::
ERROR_OK
)
{
$response
=
null
;
}
return
new
rcube_result_index
(
$mailbox
,
$response
);
}
/**
* Executes THREAD command
*
* @param string $mailbox Mailbox name
* @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
* @param string $criteria Searching criteria
* @param bool $return_uid Enables UIDs in result instead of sequence numbers
* @param string $encoding Character set
*
* @return rcube_result_thread Thread data
*/
public
function
thread
(
$mailbox
,
$algorithm
=
'REFERENCES'
,
$criteria
=
''
,
$return_uid
=
false
,
$encoding
=
'US-ASCII'
)
{
$old_sel
=
$this
->
selected
;
if
(!
$this
->
select
(
$mailbox
))
{
return
new
rcube_result_thread
(
$mailbox
);
}
// return empty result when folder is empty and we're just after SELECT
if
(
$old_sel
!=
$mailbox
&&
!
$this
->
data
[
'EXISTS'
])
{
return
new
rcube_result_thread
(
$mailbox
,
'* THREAD'
);
}
$encoding
=
$encoding
?
trim
(
$encoding
)
:
'US-ASCII'
;
$algorithm
=
$algorithm
?
trim
(
$algorithm
)
:
'REFERENCES'
;
$criteria
=
$criteria
?
'ALL '
.
trim
(
$criteria
)
:
'ALL'
;
[
$code
,
$response
]
=
$this
->
execute
(
$return_uid
?
'UID THREAD'
:
'THREAD'
,
[
$algorithm
,
$encoding
,
$criteria
]);
if
(
$code
!=
self
::
ERROR_OK
)
{
$response
=
null
;
}
return
new
rcube_result_thread
(
$mailbox
,
$response
);
}
/**
* Executes SEARCH command
*
* @param string $mailbox Mailbox name
* @param string $criteria Searching criteria
* @param bool $return_uid Enable UID in result instead of sequence ID
* @param array $items Return items (MIN, MAX, COUNT, ALL)
*
* @return rcube_result_index Result data
*/
public
function
search
(
$mailbox
,
$criteria
,
$return_uid
=
false
,
$items
=
[])
{
$old_sel
=
$this
->
selected
;
if
(!
$this
->
select
(
$mailbox
))
{
return
new
rcube_result_index
(
$mailbox
);
}
// return empty result when folder is empty and we're just after SELECT
if
(
$old_sel
!=
$mailbox
&&
!
$this
->
data
[
'EXISTS'
])
{
return
new
rcube_result_index
(
$mailbox
,
'* SEARCH'
);
}
// If ESEARCH is supported always use ALL
// but not when items are specified or using simple id2uid search
if
(
empty
(
$items
)
&&
preg_match
(
'/[^0-9]/'
,
$criteria
))
{
$items
=
[
'ALL'
];
}
$esearch
=
empty
(
$items
)
?
false
:
$this
->
getCapability
(
'ESEARCH'
);
$criteria
=
trim
(
$criteria
);
$params
=
''
;
// RFC4731: ESEARCH
if
(!
empty
(
$items
)
&&
$esearch
)
{
$params
.=
'RETURN ('
.
implode
(
' '
,
$items
)
.
')'
;
}
if
(!
empty
(
$criteria
))
{
$params
.=
(
$params
?
' '
:
''
)
.
$criteria
;
}
else
{
$params
.=
'ALL'
;
}
[
$code
,
$response
]
=
$this
->
execute
(
$return_uid
?
'UID SEARCH'
:
'SEARCH'
,
[
$params
]);
if
(
$code
!=
self
::
ERROR_OK
)
{
$response
=
null
;
}
return
new
rcube_result_index
(
$mailbox
,
$response
);
}
/**
* Simulates SORT command by using FETCH and sorting.
*
* @param string $mailbox Mailbox name
* @param string|array $message_set Searching criteria (list of messages to return)
* @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param bool $skip_deleted Makes that DELETED messages will be skipped
* @param bool $uidfetch Enables UID FETCH usage
* @param bool $return_uid Enables returning UIDs instead of IDs
*
* @return rcube_result_index Response data
*/
public
function
index
(
$mailbox
,
$message_set
,
$index_field
=
''
,
$skip_deleted
=
true
,
$uidfetch
=
false
,
$return_uid
=
false
)
{
$msg_index
=
$this
->
fetchHeaderIndex
(
$mailbox
,
$message_set
,
$index_field
,
$skip_deleted
,
$uidfetch
,
$return_uid
);
if
(!
empty
(
$msg_index
))
{
asort
(
$msg_index
);
// ASC
$msg_index
=
array_keys
(
$msg_index
);
$msg_index
=
'* SEARCH '
.
implode
(
' '
,
$msg_index
);
}
else
{
$msg_index
=
is_array
(
$msg_index
)
?
'* SEARCH'
:
null
;
}
return
new
rcube_result_index
(
$mailbox
,
$msg_index
);
}
/**
* Fetches specified header/data value for a set of messages.
*
* @param string $mailbox Mailbox name
* @param string|array $message_set Searching criteria (list of messages to return)
* @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param bool $skip_deleted Makes that DELETED messages will be skipped
* @param bool $uidfetch Enables UID FETCH usage
* @param bool $return_uid Enables returning UIDs instead of IDs
*
* @return array|bool List of header values or False on failure
*/
public
function
fetchHeaderIndex
(
$mailbox
,
$message_set
,
$index_field
=
''
,
$skip_deleted
=
true
,
$uidfetch
=
false
,
$return_uid
=
false
)
{
// Validate input
if
(
is_array
(
$message_set
))
{
$message_set
=
$this
->
compressMessageSet
(
$message_set
);
if
(
$message_set
===
''
||
$message_set
===
'INVALID'
)
{
return
false
;
}
}
elseif
(
empty
(
$message_set
))
{
return
false
;
}
elseif
(
strpos
(
$message_set
,
':'
))
{
[
$from_idx
,
$to_idx
]
=
explode
(
':'
,
$message_set
);
if
(
$to_idx
!=
'*'
&&
(
int
)
$from_idx
>
(
int
)
$to_idx
)
{
return
false
;
}
}
$index_field
=
empty
(
$index_field
)
?
'DATE'
:
strtoupper
(
$index_field
);
$supported
=
[
'DATE'
=>
1
,
'INTERNALDATE'
=>
4
,
'ARRIVAL'
=>
4
,
'FROM'
=>
1
,
'REPLY-TO'
=>
1
,
'SENDER'
=>
1
,
'TO'
=>
1
,
'CC'
=>
1
,
'SUBJECT'
=>
1
,
'UID'
=>
2
,
'SIZE'
=>
2
,
'SEEN'
=>
3
,
'RECENT'
=>
3
,
'DELETED'
=>
3
,
];
if
(
empty
(
$supported
[
$index_field
]))
{
return
false
;
}
$mode
=
$supported
[
$index_field
];
// Select the mailbox
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
// build FETCH command string
$key
=
$this
->
nextTag
();
$cmd
=
$uidfetch
?
'UID FETCH'
:
'FETCH'
;
$fields
=
[];
if
(
$return_uid
)
{
$fields
[]
=
'UID'
;
}
if
(
$skip_deleted
)
{
$fields
[]
=
'FLAGS'
;
}
if
(
$mode
==
1
)
{
if
(
$index_field
==
'DATE'
)
{
$fields
[]
=
'INTERNALDATE'
;
}
$fields
[]
=
"BODY.PEEK[HEADER.FIELDS ({$index_field})]"
;
}
elseif
(
$mode
==
2
)
{
if
(
$index_field
==
'SIZE'
)
{
$fields
[]
=
'RFC822.SIZE'
;
}
elseif
(!
$return_uid
||
$index_field
!=
'UID'
)
{
$fields
[]
=
$index_field
;
}
}
elseif
(
$mode
==
3
&&
!
$skip_deleted
)
{
$fields
[]
=
'FLAGS'
;
}
elseif
(
$mode
==
4
)
{
$fields
[]
=
'INTERNALDATE'
;
}
$request
=
"{$key} {$cmd} {$message_set} ("
.
implode
(
' '
,
$fields
)
.
')'
;
if
(!
$this
->
putLine
(
$request
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
"Failed to send {$cmd} command"
);
return
false
;
}
$result
=
[];
do
{
$line
=
rtrim
(
$this
->
readLine
(
200
));
$line
=
$this
->
multLine
(
$line
);
if
(
preg_match
(
'/^
\*
([0-9]+) FETCH/'
,
$line
,
$m
))
{
$id
=
$m
[
1
];
$flags
=
null
;
if
(
$return_uid
)
{
if
(
preg_match
(
'/UID ([0-9]+)/'
,
$line
,
$matches
))
{
$id
=
(
int
)
$matches
[
1
];
}
else
{
continue
;
}
}
if
(
$skip_deleted
&&
preg_match
(
'/FLAGS
\(
([^)]+)
\)
/'
,
$line
,
$matches
))
{
$flags
=
explode
(
' '
,
strtoupper
(
$matches
[
1
]));
if
(
in_array
(
'
\D
ELETED'
,
$flags
))
{
continue
;
}
}
if
(
$mode
==
1
&&
$index_field
==
'DATE'
)
{
if
(
preg_match
(
'/BODY
\[
HEADER
\.
FIELDS
\(
"*DATE"*
\)\]
(.*)/'
,
$line
,
$matches
))
{
$value
=
preg_replace
([
'/^"*[a-z]+:/i'
],
''
,
$matches
[
1
]);
$value
=
trim
(
$value
);
$result
[
$id
]
=
rcube_utils
::
strtotime
(
$value
);
}
// non-existent/empty Date: header, use INTERNALDATE
if
(
empty
(
$result
[
$id
]))
{
if
(
preg_match
(
'/INTERNALDATE "([^"]+)"/'
,
$line
,
$matches
))
{
$result
[
$id
]
=
rcube_utils
::
strtotime
(
$matches
[
1
]);
}
else
{
$result
[
$id
]
=
0
;
}
}
}
elseif
(
$mode
==
1
)
{
if
(
preg_match
(
'/BODY
\[
HEADER
\.
FIELDS
\(
"?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?
\)\]
(.*)/'
,
$line
,
$matches
))
{
$value
=
preg_replace
([
'/^"*[a-z]+:/i'
,
'/
\s
+$/sm'
],
[
''
,
''
],
$matches
[
2
]);
$result
[
$id
]
=
trim
(
$value
);
}
else
{
$result
[
$id
]
=
''
;
}
}
elseif
(
$mode
==
2
)
{
if
(
preg_match
(
'/'
.
$index_field
.
' ([0-9]+)/'
,
$line
,
$matches
))
{
$result
[
$id
]
=
trim
(
$matches
[
1
]);
}
else
{
$result
[
$id
]
=
0
;
}
}
elseif
(
$mode
==
3
)
{
if
(!
$flags
&&
preg_match
(
'/FLAGS
\(
([^)]+)
\)
/'
,
$line
,
$matches
))
{
$flags
=
explode
(
' '
,
$matches
[
1
]);
}
$result
[
$id
]
=
in_array
(
'
\\
'
.
$index_field
,
(
array
)
$flags
)
?
1
:
0
;
}
elseif
(
$mode
==
4
)
{
if
(
preg_match
(
'/INTERNALDATE "([^"]+)"/'
,
$line
,
$matches
))
{
$result
[
$id
]
=
rcube_utils
::
strtotime
(
$matches
[
1
]);
}
else
{
$result
[
$id
]
=
0
;
}
}
}
}
while
(!
$this
->
startsWith
(
$line
,
$key
,
true
,
true
));
return
$result
;
}
/**
* Returns message sequence identifier
*
* @param string $mailbox Mailbox name
* @param int $uid Message unique identifier (UID)
*
* @return ?int Message sequence identifier
*/
public
function
UID2ID
(
$mailbox
,
$uid
)
{
if
(
$uid
>
0
)
{
$index
=
$this
->
search
(
$mailbox
,
"UID {$uid}"
);
if
(
$index
->
count
()
==
1
)
{
$arr
=
$index
->
get
();
return
(
int
)
$arr
[
0
];
}
}
return
null
;
}
/**
* Returns message unique identifier (UID)
*
* @param string $mailbox Mailbox name
* @param int $id Message sequence identifier
*
* @return ?int Message unique identifier
*/
public
function
ID2UID
(
$mailbox
,
$id
)
{
if
(
empty
(
$id
)
||
$id
<
0
)
{
return
null
;
}
if
(!
$this
->
select
(
$mailbox
))
{
return
null
;
}
if
(!
empty
(
$this
->
data
[
'UID-MAP'
][
$id
]))
{
return
$this
->
data
[
'UID-MAP'
][
$id
];
}
if
(
isset
(
$this
->
data
[
'EXISTS'
])
&&
$id
>
$this
->
data
[
'EXISTS'
])
{
return
null
;
}
$index
=
$this
->
search
(
$mailbox
,
$id
,
true
);
if
(
$index
->
count
()
==
1
)
{
$arr
=
$index
->
get
();
return
$this
->
data
[
'UID-MAP'
][
$id
]
=
(
int
)
$arr
[
0
];
}
return
null
;
}
/**
* Sets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
public
function
flag
(
$mailbox
,
$messages
,
$flag
)
{
return
$this
->
modFlag
(
$mailbox
,
$messages
,
$flag
,
'+'
);
}
/**
* Unsets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
public
function
unflag
(
$mailbox
,
$messages
,
$flag
)
{
return
$this
->
modFlag
(
$mailbox
,
$messages
,
$flag
,
'-'
);
}
/**
* Changes flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
* @param string $mod Modifier [+|-]. Default: "+".
*
* @return bool True on success, False on failure
*/
protected
function
modFlag
(
$mailbox
,
$messages
,
$flag
,
$mod
=
'+'
)
{
if
(!
$flag
)
{
return
false
;
}
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
if
(
empty
(
$this
->
data
[
'READ-WRITE'
]))
{
$this
->
setError
(
self
::
ERROR_READONLY
,
'Mailbox is read-only'
);
return
false
;
}
if
(!
empty
(
$this
->
flags
[
strtoupper
(
$flag
)]))
{
$flag
=
$this
->
flags
[
strtoupper
(
$flag
)];
}
// if PERMANENTFLAGS is not specified all flags are allowed
if
(!
empty
(
$this
->
data
[
'PERMANENTFLAGS'
])
&&
!
in_array
(
$flag
,
(
array
)
$this
->
data
[
'PERMANENTFLAGS'
])
&&
!
in_array
(
'
\*
'
,
(
array
)
$this
->
data
[
'PERMANENTFLAGS'
])
)
{
return
false
;
}
// Clear internal status cache
if
(
$flag
==
'SEEN'
)
{
unset
(
$this
->
data
[
'STATUS:'
.
$mailbox
][
'UNSEEN'
]);
}
if
(
$mod
!=
'+'
&&
$mod
!=
'-'
)
{
$mod
=
'+'
;
}
$result
=
$this
->
execute
(
'UID STORE'
,
[
$this
->
compressMessageSet
(
$messages
),
$mod
.
'FLAGS.SILENT'
,
"({$flag})"
],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Copies message(s) from one folder to another
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
public
function
copy
(
$messages
,
$from
,
$to
)
{
// Clear last COPYUID data
unset
(
$this
->
data
[
'COPYUID'
]);
if
(!
$this
->
select
(
$from
))
{
return
false
;
}
// Clear internal status cache
unset
(
$this
->
data
[
'STATUS:'
.
$to
]);
$result
=
$this
->
execute
(
'UID COPY'
,
[
$this
->
compressMessageSet
(
$messages
),
$this
->
escape
(
$to
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Moves message(s) from one folder to another.
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
public
function
move
(
$messages
,
$from
,
$to
)
{
if
(!
$this
->
select
(
$from
))
{
return
false
;
}
if
(
empty
(
$this
->
data
[
'READ-WRITE'
]))
{
$this
->
setError
(
self
::
ERROR_READONLY
,
'Mailbox is read-only'
);
return
false
;
}
// use MOVE command (RFC 6851)
if
(
$this
->
hasCapability
(
'MOVE'
))
{
// Clear last COPYUID data
unset
(
$this
->
data
[
'COPYUID'
]);
// Clear internal status cache
unset
(
$this
->
data
[
'STATUS:'
.
$to
]);
$this
->
clear_status_cache
(
$from
);
$result
=
$this
->
execute
(
'UID MOVE'
,
[
$this
->
compressMessageSet
(
$messages
),
$this
->
escape
(
$to
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
// use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
$result
=
$this
->
copy
(
$messages
,
$from
,
$to
);
if
(
$result
)
{
// Clear internal status cache
unset
(
$this
->
data
[
'STATUS:'
.
$from
]);
$result
=
$this
->
flag
(
$from
,
$messages
,
'DELETED'
);
if
(
$messages
==
'*'
)
{
// CLOSE+SELECT should be faster than EXPUNGE
$this
->
close
();
}
else
{
$this
->
expunge
(
$from
,
$messages
);
}
}
return
$result
;
}
/**
* FETCH command (RFC3501)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param array $query_items FETCH command data items
* @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
* @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
*
* @return array|false List of rcube_message_header elements, False on error
*
* @since 0.6
*/
public
function
fetch
(
$mailbox
,
$message_set
,
$is_uid
=
false
,
$query_items
=
[],
$mod_seq
=
null
,
$vanished
=
false
)
{
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
$message_set
=
$this
->
compressMessageSet
(
$message_set
);
$result
=
[];
$key
=
$this
->
nextTag
();
$cmd
=
(
$is_uid
?
'UID '
:
''
)
.
'FETCH'
;
$request
=
"{$key} {$cmd} {$message_set} ("
.
implode
(
' '
,
$query_items
)
.
')'
;
if
(
$mod_seq
!==
null
&&
$this
->
hasCapability
(
'CONDSTORE'
))
{
$request
.=
" (CHANGEDSINCE {$mod_seq}"
.
(
$vanished
?
' VANISHED'
:
''
)
.
')'
;
}
if
(!
$this
->
putLine
(
$request
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
"Failed to send {$cmd} command"
);
return
false
;
}
do
{
$line
=
$this
->
readFullLine
(
4096
);
if
(!
$line
)
{
break
;
}
// Sample reply line:
// * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
// INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
// BODY[HEADER.FIELDS ...
if
(
preg_match
(
'/^
\*
([0-9]+) FETCH/'
,
$line
,
$m
))
{
$id
=
intval
(
$m
[
1
]);
$result
[
$id
]
=
new
rcube_message_header
();
$result
[
$id
]->
id
=
$id
;
$result
[
$id
]->
subject
=
''
;
$result
[
$id
]->
messageID
=
'mid:'
.
$id
;
$result
[
$id
]->
folder
=
$mailbox
;
$headers
=
null
;
$line
=
substr
(
$line
,
strlen
(
$m
[
0
])
+
2
);
// Tokenize response and assign to object properties
while
((
$tokens
=
$this
->
tokenizeResponse
(
$line
,
2
))
&&
count
(
$tokens
)
==
2
)
{
[
$name
,
$value
]
=
$tokens
;
if
(
$name
==
'UID'
)
{
$result
[
$id
]->
uid
=
intval
(
$value
);
}
elseif
(
$name
==
'RFC822.SIZE'
)
{
$result
[
$id
]->
size
=
intval
(
$value
);
}
elseif
(
$name
==
'RFC822.TEXT'
)
{
$result
[
$id
]->
body
=
$value
;
}
elseif
(
$name
==
'INTERNALDATE'
)
{
$result
[
$id
]->
internaldate
=
$value
;
$result
[
$id
]->
date
=
$value
;
$result
[
$id
]->
timestamp
=
rcube_utils
::
strtotime
(
$value
);
}
elseif
(
$name
==
'FLAGS'
)
{
if
(!
empty
(
$value
))
{
foreach
((
array
)
$value
as
$flag
)
{
$flag
=
str_replace
([
'$'
,
'
\\
'
],
''
,
$flag
);
$flag
=
strtoupper
(
$flag
);
$result
[
$id
]->
flags
[
$flag
]
=
true
;
}
}
}
elseif
(
$name
==
'ANNOTATION'
)
{
$result
[
$id
]->
annotations
=
[];
if
(!
empty
(
$value
)
&&
is_array
(
$value
))
{
$n
=
0
;
while
(!
empty
(
$value
[
$n
])
&&
is_string
(
$value
[
$n
]))
{
$name
=
$value
[
$n
++];
$list
=
$value
[
$n
++];
$result
[
$id
]->
annotations
[
$name
]
=
[];
$c
=
0
;
while
(!
empty
(
$list
[
$c
])
&&
is_string
(
$list
[
$c
]))
{
$result
[
$id
]->
annotations
[
$name
][
$list
[
$c
++]]
=
$list
[
$c
++];
}
}
}
}
elseif
(
$name
==
'MODSEQ'
)
{
$result
[
$id
]->
modseq
=
$value
[
0
];
}
elseif
(
$name
==
'ENVELOPE'
)
{
$result
[
$id
]->
envelope
=
$value
;
}
elseif
(
$name
==
'BODYSTRUCTURE'
||
(
$name
==
'BODY'
&&
count
(
$value
)
>
2
))
{
if
(!
is_array
(
$value
[
0
])
&&
(
strtolower
(
$value
[
0
])
==
'message'
&&
strtolower
(
$value
[
1
])
==
'rfc822'
))
{
$value
=
[
$value
];
}
$result
[
$id
]->
bodystructure
=
$value
;
}
elseif
(
$name
==
'RFC822'
)
{
$result
[
$id
]->
body
=
$value
;
}
elseif
(
stripos
(
$name
,
'BODY['
)
===
0
)
{
$name
=
str_replace
(
']'
,
''
,
substr
(
$name
,
5
));
if
(
$name
==
'HEADER.FIELDS'
)
{
// skip ']' after headers list
$this
->
tokenizeResponse
(
$line
,
1
);
$headers
=
$this
->
tokenizeResponse
(
$line
,
1
);
}
elseif
(
strlen
(
$name
))
{
$result
[
$id
]->
bodypart
[
$name
]
=
$value
;
}
else
{
$result
[
$id
]->
body
=
$value
;
}
}
}
// create array with header field:data
if
(!
empty
(
$headers
))
{
$headers
=
explode
(
"
\n
"
,
trim
(
$headers
));
$lines
=
[];
$ln
=
0
;
foreach
(
$headers
as
$resln
)
{
if
(!
isset
(
$resln
[
0
])
||
ord
(
$resln
[
0
])
<=
32
)
{
$lines
[
$ln
]
=
(
$lines
[
$ln
]
??
''
)
.
(
empty
(
$lines
[
$ln
])
?
''
:
"
\n
"
)
.
trim
(
$resln
);
}
else
{
$lines
[++
$ln
]
=
trim
(
$resln
);
}
}
foreach
(
$lines
as
$str
)
{
if
(
strpos
(
$str
,
':'
)
===
false
)
{
continue
;
}
[
$field
,
$string
]
=
explode
(
':'
,
$str
,
2
);
$field
=
strtolower
(
$field
);
$string
=
preg_replace
(
'/
\n
[
\t\s
]*/'
,
' '
,
trim
(
$string
));
switch
(
$field
)
{
case
'date'
:
$string
=
substr
(
$string
,
0
,
128
);
$result
[
$id
]->
date
=
$string
;
$result
[
$id
]->
timestamp
=
rcube_utils
::
strtotime
(
$string
);
break
;
case
'to'
:
$result
[
$id
]->
to
=
preg_replace
(
'/undisclosed-recipients:[;,]*/'
,
''
,
$string
);
break
;
case
'from'
:
case
'subject'
:
$string
=
substr
(
$string
,
0
,
2048
);
case
'cc'
:
case
'bcc'
:
case
'references'
:
$result
[
$id
]->{
$field
}
=
$string
;
break
;
case
'reply-to'
:
$result
[
$id
]->
replyto
=
$string
;
break
;
case
'content-transfer-encoding'
:
$result
[
$id
]->
encoding
=
substr
(
$string
,
0
,
32
);
break
;
case
'content-type'
:
$ctype_parts
=
preg_split
(
'/[; ]+/'
,
$string
);
$result
[
$id
]->
ctype
=
strtolower
(
array_first
(
$ctype_parts
));
if
(
preg_match
(
'/charset
\s
*=
\s
*"?([a-z0-9
\-\.\_
]+)"?/i'
,
$string
,
$regs
))
{
$result
[
$id
]->
charset
=
$regs
[
1
];
}
break
;
case
'in-reply-to'
:
$result
[
$id
]->
in_reply_to
=
str_replace
([
"
\n
"
,
'<'
,
'>'
],
''
,
$string
);
break
;
case
'disposition-notification-to'
:
case
'x-confirm-reading-to'
:
$result
[
$id
]->
mdn_to
=
substr
(
$string
,
0
,
2048
);
break
;
case
'message-id'
:
$result
[
$id
]->
messageID
=
substr
(
$string
,
0
,
2048
);
break
;
case
'x-priority'
:
if
(
preg_match
(
'/^(
\d
+)/'
,
$string
,
$matches
))
{
$result
[
$id
]->
priority
=
intval
(
$matches
[
1
]);
}
break
;
default
:
if
(
strlen
(
$field
)
<
3
)
{
break
;
}
if
(!
empty
(
$result
[
$id
]->
others
[
$field
]))
{
$string
=
array_merge
((
array
)
$result
[
$id
]->
others
[
$field
],
(
array
)
$string
);
}
$result
[
$id
]->
others
[
$field
]
=
$string
;
}
}
}
}
// VANISHED response (QRESYNC RFC5162)
// Sample: * VANISHED (EARLIER) 300:310,405,411
elseif
(
preg_match
(
'/^
\*
VANISHED [()EARLIER]*/i'
,
$line
,
$match
))
{
$line
=
substr
(
$line
,
strlen
(
$match
[
0
]));
$v_data
=
$this
->
tokenizeResponse
(
$line
,
1
);
$this
->
data
[
'VANISHED'
]
=
$v_data
;
}
}
while
(!
$this
->
startsWith
(
$line
,
$key
,
true
));
if
(
$this
->
parseResult
(
$line
,
'FETCH: '
)
!=
self
::
ERROR_OK
)
{
return
false
;
}
return
$result
;
}
/**
* Returns message(s) data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers to fetch
* @param array $query_items List of additional items to fetch
*
* @return bool|array List of rcube_message_header elements, False on error
*/
public
function
fetchHeaders
(
$mailbox
,
$message_set
,
$is_uid
=
false
,
$bodystr
=
false
,
$add_headers
=
[],
$query_items
=
[])
{
$query_items
=
array_unique
(
array_merge
(
$query_items
,
[
'UID'
,
'RFC822.SIZE'
,
'FLAGS'
,
'INTERNALDATE'
]));
$headers
=
[
'DATE'
,
'FROM'
,
'TO'
,
'SUBJECT'
,
'CONTENT-TYPE'
,
'CC'
,
'REPLY-TO'
,
'LIST-POST'
,
'DISPOSITION-NOTIFICATION-TO'
,
'X-PRIORITY'
];
if
(!
empty
(
$add_headers
))
{
$add_headers
=
array_map
(
'strtoupper'
,
$add_headers
);
$headers
=
array_unique
(
array_merge
(
$headers
,
$add_headers
));
}
if
(
$bodystr
)
{
$query_items
[]
=
'BODYSTRUCTURE'
;
}
$query_items
[]
=
'BODY.PEEK[HEADER.FIELDS ('
.
implode
(
' '
,
$headers
)
.
')]'
;
return
$this
->
fetch
(
$mailbox
,
$message_set
,
$is_uid
,
$query_items
);
}
/**
* Returns message data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param int $id Message sequence identifier or UID
* @param bool $is_uid True if $id is an UID
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers
* @param array $query_items List of additional items to fetch
*
* @return bool|rcube_message_header Message data, False on error
*/
public
function
fetchHeader
(
$mailbox
,
$id
,
$is_uid
=
false
,
$bodystr
=
false
,
$add_headers
=
[],
$query_items
=
[])
{
$a
=
$this
->
fetchHeaders
(
$mailbox
,
$id
,
$is_uid
,
$bodystr
,
$add_headers
,
$query_items
);
if
(
is_array
(
$a
))
{
return
array_first
(
$a
);
}
return
false
;
}
/**
* Sort messages by specified header field
*
* @param array $messages Array of rcube_message_header objects
* @param string $field Name of the property to sort by
* @param string $order Sorting order (ASC|DESC)
*
* @return array Sorted input array
*/
public
static
function
sortHeaders
(
$messages
,
$field
,
$order
=
'ASC'
)
{
$field
=
empty
(
$field
)
?
'uid'
:
strtolower
(
$field
);
$order
=
empty
(
$order
)
?
'ASC'
:
strtoupper
(
$order
);
$index
=
[];
reset
(
$messages
);
// Create an index
foreach
(
$messages
as
$key
=>
$headers
)
{
switch
(
$field
)
{
case
'arrival'
:
$field
=
'internaldate'
;
// no-break
case
'date'
:
case
'internaldate'
:
case
'timestamp'
:
$value
=
rcube_utils
::
strtotime
(
$headers
->{
$field
});
if
(!
$value
&&
$field
!=
'timestamp'
)
{
$value
=
$headers
->
timestamp
;
}
break
;
default
:
// @TODO: decode header value, convert to UTF-8
$value
=
$headers
->{
$field
};
if
(
is_string
(
$value
))
{
$value
=
str_replace
(
'"'
,
''
,
$value
);
if
(
$field
==
'subject'
)
{
$value
=
rcube_utils
::
remove_subject_prefix
(
$value
);
}
}
}
$index
[
$key
]
=
$value
;
}
$sort_order
=
$order
==
'ASC'
?
\SORT_ASC
:
\SORT_DESC
;
$sort_flags
=
\SORT_STRING
|
\SORT_FLAG_CASE
;
if
(
in_array
(
$field
,
[
'arrival'
,
'date'
,
'internaldate'
,
'timestamp'
,
'size'
,
'uid'
,
'id'
]))
{
$sort_flags
=
\SORT_NUMERIC
;
}
array_multisort
(
$index
,
$sort_order
,
$sort_flags
,
$messages
);
return
$messages
;
}
/**
* Fetch MIME headers of specified message parts
*
* @param string $mailbox Mailbox name
* @param int $uid Message UID
* @param array $parts Message part identifiers
* @param bool $mime Use MIME instead of HEADER
*
* @return array|bool array containing headers string for each specified body
* False on failure
*/
public
function
fetchMIMEHeaders
(
$mailbox
,
$uid
,
$parts
,
$mime
=
true
)
{
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
$parts
=
(
array
)
$parts
;
$key
=
$this
->
nextTag
();
$peeks
=
[];
$type
=
$mime
?
'MIME'
:
'HEADER'
;
// format request
foreach
(
$parts
as
$part
)
{
$peeks
[]
=
"BODY.PEEK[{$part}.{$type}]"
;
}
$request
=
"{$key} UID FETCH {$uid} ("
.
implode
(
' '
,
$peeks
)
.
')'
;
// send request
if
(!
$this
->
putLine
(
$request
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Failed to send UID FETCH command'
);
return
false
;
}
$result
=
[];
do
{
$line
=
$this
->
readLine
(
1024
);
if
(
preg_match
(
'/^
\*
[0-9]+ FETCH [0-9UID( ]+/'
,
$line
,
$m
))
{
$line
=
ltrim
(
substr
(
$line
,
strlen
(
$m
[
0
])));
while
(
preg_match
(
'/^
\s
*BODY
\[
([0-9
\.
]+)
\.
'
.
$type
.
'
\]
/'
,
$line
,
$matches
))
{
$line
=
substr
(
$line
,
strlen
(
$matches
[
0
]));
$result
[
$matches
[
1
]]
=
trim
(
$this
->
multLine
(
$line
));
$line
=
$this
->
readLine
(
1024
);
}
}
}
while
(!
$this
->
startsWith
(
$line
,
$key
,
true
));
return
$result
;
}
/**
* Fetches message part header
*/
public
function
fetchPartHeader
(
$mailbox
,
$id
,
$is_uid
=
false
,
$part
=
null
)
{
$part
=
empty
(
$part
)
?
'HEADER'
:
$part
.
'.MIME'
;
return
$this
->
handlePartBody
(
$mailbox
,
$id
,
$is_uid
,
$part
);
}
/**
* Fetches body of the specified message part
*/
public
function
handlePartBody
(
$mailbox
,
$id
,
$is_uid
=
false
,
$part
=
''
,
$encoding
=
null
,
$print
=
null
,
$file
=
null
,
$formatted
=
false
,
$max_bytes
=
0
)
{
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
$binary
=
true
;
$initiated
=
false
;
$result
=
false
;
$found
=
false
;
$mode
=
0
;
do
{
if
(!
$initiated
)
{
switch
(
$encoding
)
{
case
'base64'
:
$mode
=
1
;
break
;
case
'quoted-printable'
:
$mode
=
2
;
break
;
case
'x-uuencode'
:
case
'x-uue'
:
case
'uue'
:
case
'uuencode'
:
$mode
=
3
;
break
;
default
:
$mode
=
$formatted
?
4
:
0
;
}
// Use BINARY extension when possible (and safe)
$binary
=
$binary
&&
$mode
&&
preg_match
(
'/^[0-9.]+$/'
,
(
string
)
$part
)
&&
$this
->
hasCapability
(
'BINARY'
);
$fetch_mode
=
$binary
?
'BINARY'
:
'BODY'
;
$partial
=
$max_bytes
?
sprintf
(
'<0.%d>'
,
$max_bytes
)
:
''
;
// format request
$key
=
$this
->
nextTag
();
$cmd
=
(
$is_uid
?
'UID '
:
''
)
.
'FETCH'
;
$request
=
"{$key} {$cmd} {$id} ({$fetch_mode}.PEEK[{$part}]{$partial})"
;
$result
=
false
;
$found
=
false
;
$initiated
=
true
;
// send request
if
(!
$this
->
putLine
(
$request
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
"Failed to send {$cmd} command"
);
return
false
;
}
if
(
$binary
)
{
// WARNING: Use $formatted argument with care, this may break binary data stream
$mode
=
-
1
;
}
}
$line
=
trim
(
$this
->
readLine
(
1024
));
if
(!
$line
)
{
break
;
}
// handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
// @phpstan-ignore-next-line
if
(
$binary
&&
!
$found
&&
preg_match
(
'/^'
.
$key
.
' NO
\[
(UNKNOWN-CTE|PARSE)
\]
/i'
,
$line
))
{
$binary
=
$initiated
=
false
;
continue
;
}
// skip irrelevant untagged responses (we have a result already)
if
(
$found
||
!
preg_match
(
'/^
\*
([0-9]+) FETCH (.*)$/'
,
$line
,
$m
))
{
continue
;
}
$line
=
$m
[
2
];
// handle one line response
if
(
$line
[
0
]
==
'('
&&
substr
(
$line
,
-
1
)
==
')'
)
{
// tokenize content inside brackets
// the content can be e.g.: (UID 9844 BODY[2.4] NIL)
$line
=
preg_replace
(
'/(^
\(
|
\)
$)/'
,
''
,
$line
);
$tokens
=
$this
->
tokenizeResponse
(
$line
);
for
(
$i
=
0
;
$i
<
count
(
$tokens
);
$i
+=
2
)
{
if
(
preg_match
(
'/^(BODY|BINARY)/i'
,
$tokens
[
$i
]))
{
$result
=
$tokens
[
$i
+
1
];
$found
=
true
;
break
;
}
}
// Cyrus IMAP does not return a NO-response on error, but we can detect it
// and fallback to a non-binary fetch (#9097)
if
(
$binary
&&
!
$found
)
{
$binary
=
$initiated
=
false
;
$line
=
trim
(
$this
->
readLine
(
1024
));
// the OK response line
continue
;
}
if
(
$result
!==
false
)
{
$result
=
$this
->
decodeContent
(
$result
,
$mode
,
true
);
}
}
// response with string literal
elseif
(
preg_match
(
'/
\{
([0-9]+)
\}
$/'
,
$line
,
$m
))
{
$bytes
=
(
int
)
$m
[
1
];
$prev
=
''
;
$found
=
true
;
$chunkSize
=
1024
*
1024
;
// empty body
if
(!
$bytes
)
{
$result
=
''
;
}
// An optimal path for a case when we need the body as-is in a string
elseif
(!
$mode
&&
!
$file
&&
!
$print
)
{
$result
=
$this
->
readBytes
(
$bytes
);
}
else
{
$result
=
''
;
while
(
$bytes
>
0
)
{
$chunk
=
$this
->
readBytes
(
$bytes
>
$chunkSize
?
$chunkSize
:
$bytes
);
if
(
$chunk
===
''
)
{
break
;
}
$len
=
strlen
(
$chunk
);
if
(
$len
>
$bytes
)
{
$chunk
=
substr
(
$chunk
,
0
,
$bytes
);
$len
=
strlen
(
$chunk
);
}
$bytes
-=
$len
;
$chunk
=
$this
->
decodeContent
(
$chunk
,
$mode
,
$bytes
<=
0
,
$prev
);
if
(
$file
)
{
if
((
$result
=
fwrite
(
$file
,
$chunk
))
===
false
)
{
break
;
}
}
elseif
(
$print
)
{
echo
$chunk
;
}
else
{
$result
.=
$chunk
;
}
}
}
}
}
while
(!
$initiated
||
!
$this
->
startsWith
(
$line
,
$key
,
true
));
// @phpstan-ignore-line
if
(
$result
!==
false
)
{
if
(
$file
)
{
return
is_string
(
$result
)
?
fwrite
(
$file
,
$result
)
!==
false
:
true
;
}
if
(
$print
)
{
echo
$result
;
return
true
;
}
return
$result
;
}
return
false
;
}
/**
* Decodes a chunk of a message part content from a FETCH response.
*
* @param string $chunk Content
* @param int $mode Encoding mode
* @param bool $is_last Whether it is a last chunk of data
* @param string $prev Extra content from the previous chunk
*
* @return string Encoded string
*/
protected
static
function
decodeContent
(
$chunk
,
$mode
,
$is_last
=
false
,
&
$prev
=
''
)
{
// BASE64
if
(
$mode
==
1
)
{
$chunk
=
$prev
.
preg_replace
(
'|[^a-zA-Z0-9+=/]|'
,
''
,
$chunk
);
// create chunks with proper length for base64 decoding
$length
=
strlen
(
$chunk
);
if
(
$length
%
4
)
{
$length
=
floor
(
$length
/
4
)
*
4
;
$prev
=
substr
(
$chunk
,
$length
);
$chunk
=
substr
(
$chunk
,
0
,
$length
);
}
else
{
$prev
=
''
;
}
// There might be multiple base64 blocks in a single message part,
// we have to pass them separately to base64_decode() (#9290)
$result
=
''
;
foreach
(
preg_split
(
'|=+|'
,
$chunk
,
-
1
,
\PREG_SPLIT_NO_EMPTY
)
as
$_chunk
)
{
$result
.=
base64_decode
(
$_chunk
);
}
return
$result
;
}
// QUOTED-PRINTABLE
if
(
$mode
==
2
)
{
if
(!
self
::
decodeContentChunk
(
$chunk
,
$prev
,
$is_last
))
{
return
''
;
}
$chunk
=
preg_replace
(
'/[
\t\r\0\x
0B]+
\n
/'
,
"
\n
"
,
$chunk
);
return
quoted_printable_decode
(
$chunk
);
}
// X-UUENCODE
if
(
$mode
==
3
)
{
if
(!
self
::
decodeContentChunk
(
$chunk
,
$prev
,
$is_last
))
{
return
''
;
}
$chunk
=
preg_replace
(
[
'/
\r
?
\n
/'
,
'/(^|
\n
)end$/'
,
'/^begin
\s
+[0-7]{3,4}
\s
+[^
\n
]+
\n
/'
],
[
"
\n
"
,
''
,
''
],
$chunk
);
if
(!
strlen
(
$chunk
))
{
return
''
;
}
return
convert_uudecode
(
$chunk
);
}
// Plain text formatted
// TODO: Formatting should be handled outside of this class
if
(
$mode
==
4
)
{
if
(!
self
::
decodeContentChunk
(
$chunk
,
$prev
,
$is_last
))
{
return
''
;
}
if
(
$is_last
)
{
$chunk
=
rtrim
(
$chunk
,
"
\t\r\n\0\x
0B"
);
}
return
preg_replace
(
'/[
\t\r\0\x
0B]+
\n
/'
,
"
\n
"
,
$chunk
);
}
return
$chunk
;
}
/**
* A helper for a new-line aware parsing. See self::decodeContent().
*/
private
static
function
decodeContentChunk
(&
$chunk
,
&
$prev
,
$is_last
)
{
$chunk
=
$prev
.
$chunk
;
$prev
=
''
;
if
(!
$is_last
)
{
if
((
$pos
=
strrpos
(
$chunk
,
"
\n
"
))
!==
false
)
{
$prev
=
substr
(
$chunk
,
$pos
+
1
);
$chunk
=
substr
(
$chunk
,
0
,
$pos
+
1
);
}
else
{
$prev
=
$chunk
;
return
false
;
}
}
return
true
;
}
/**
* Handler for IMAP APPEND command
*
* @param string $mailbox Mailbox name
* @param string|array $message The message source string or array (of strings and file pointers)
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
public
function
append
(
$mailbox
,
&
$message
,
$flags
=
[],
$date
=
null
,
$binary
=
false
)
{
unset
(
$this
->
data
[
'APPENDUID'
]);
// @phpstan-ignore-next-line
if
(
$mailbox
===
null
||
$mailbox
===
''
)
{
return
false
;
}
$binary
=
$binary
&&
$this
->
getCapability
(
'BINARY'
);
$literal_plus
=
!
$binary
&&
!
empty
(
$this
->
prefs
[
'literal+'
]);
$len
=
0
;
$msg
=
is_array
(
$message
)
?
$message
:
[&
$message
];
$chunk_size
=
512000
;
for
(
$i
=
0
,
$cnt
=
count
(
$msg
);
$i
<
$cnt
;
$i
++)
{
if
(
is_resource
(
$msg
[
$i
]))
{
$stat
=
fstat
(
$msg
[
$i
]);
if
(
$stat
===
false
)
{
return
false
;
}
$len
+=
$stat
[
'size'
];
}
else
{
if
(!
$binary
)
{
$msg
[
$i
]
=
str_replace
(
"
\r
"
,
''
,
$msg
[
$i
]);
$msg
[
$i
]
=
str_replace
(
"
\n
"
,
"
\r\n
"
,
$msg
[
$i
]);
}
$len
+=
strlen
(
$msg
[
$i
]);
}
}
if
(!
$len
)
{
return
false
;
}
// build APPEND command
$key
=
$this
->
nextTag
();
$request
=
"{$key} APPEND "
.
$this
->
escape
(
$mailbox
)
.
' ('
.
$this
->
flagsToStr
(
$flags
)
.
')'
;
if
(!
empty
(
$date
))
{
$request
.=
' '
.
$this
->
escape
(
$date
);
}
$request
.=
' '
.
(
$binary
?
'~'
:
''
)
.
'{'
.
$len
.
(
$literal_plus
?
'+'
:
''
)
.
'}'
;
// send APPEND command
if
(!
$this
->
putLine
(
$request
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Failed to send APPEND command'
);
return
false
;
}
// Do not wait when LITERAL+ is supported
if
(!
$literal_plus
)
{
$line
=
$this
->
readReply
();
if
(
$line
[
0
]
!=
'+'
)
{
$this
->
parseResult
(
$line
,
'APPEND: '
);
return
false
;
}
}
foreach
(
$msg
as
$msg_part
)
{
// file pointer
if
(
is_resource
(
$msg_part
))
{
rewind
(
$msg_part
);
while
(!
feof
(
$msg_part
)
&&
$this
->
fp
)
{
$buffer
=
fread
(
$msg_part
,
$chunk_size
);
$this
->
putLine
(
$buffer
,
false
);
}
fclose
(
$msg_part
);
}
// string
else
{
$size
=
strlen
(
$msg_part
);
// Break up the data by sending one chunk (up to 512k) at a time.
// This approach reduces our peak memory usage
for
(
$offset
=
0
;
$offset
<
$size
;
$offset
+=
$chunk_size
)
{
$chunk
=
substr
(
$msg_part
,
$offset
,
$chunk_size
);
if
(!
$this
->
putLine
(
$chunk
,
false
))
{
return
false
;
}
}
}
}
if
(!
$this
->
putLine
(
''
))
{
// \r\n
return
false
;
}
do
{
$line
=
$this
->
readLine
();
}
while
(!
$this
->
startsWith
(
$line
,
$key
,
true
,
true
));
// Clear internal status cache
unset
(
$this
->
data
[
'STATUS:'
.
$mailbox
]);
if
(
$this
->
parseResult
(
$line
,
'APPEND: '
)
!=
self
::
ERROR_OK
)
{
return
false
;
}
if
(!
empty
(
$this
->
data
[
'APPENDUID'
]))
{
return
$this
->
data
[
'APPENDUID'
];
}
return
true
;
}
/**
* Handler for IMAP APPEND command.
*
* @param string $mailbox Mailbox name
* @param string $path Path to the file with message body
* @param string $headers Message headers
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
public
function
appendFromFile
(
$mailbox
,
$path
,
$headers
=
null
,
$flags
=
[],
$date
=
null
,
$binary
=
false
)
{
// open message file
if
(
file_exists
(
realpath
(
$path
)))
{
$fp
=
fopen
(
$path
,
'r'
);
}
if
(
empty
(
$fp
))
{
$this
->
setError
(
self
::
ERROR_UNKNOWN
,
"Couldn't open {$path} for reading"
);
return
false
;
}
$message
=
[];
if
(
$headers
)
{
$message
[]
=
trim
(
$headers
,
"
\r\n
"
)
.
"
\r\n\r\n
"
;
}
$message
[]
=
$fp
;
return
$this
->
append
(
$mailbox
,
$message
,
$flags
,
$date
,
$binary
);
}
/**
* Returns QUOTA information
*
* @param string $mailbox Mailbox name
*
* @return array|false Quota information, False on error
*/
public
function
getQuota
(
$mailbox
=
null
)
{
if
(
$mailbox
===
null
||
$mailbox
===
''
)
{
$mailbox
=
'INBOX'
;
}
// a0001 GETQUOTAROOT INBOX
// * QUOTAROOT INBOX user/sample
// * QUOTA user/sample (STORAGE 654 9765)
// a0001 OK Completed
[
$code
,
$response
]
=
$this
->
execute
(
'GETQUOTAROOT'
,
[
$this
->
escape
(
$mailbox
)],
0
,
'/^
\*
QUOTA /i'
);
if
(
$code
!=
self
::
ERROR_OK
)
{
return
false
;
}
$min_free
=
\PHP_INT_MAX
;
$result
=
[];
$all
=
[];
foreach
(
explode
(
"
\n
"
,
$response
)
as
$line
)
{
$tokens
=
$this
->
tokenizeResponse
(
$line
,
3
);
$quota_root
=
$tokens
[
2
]
??
null
;
$quotas
=
$this
->
tokenizeResponse
(
$line
,
1
);
if
(
empty
(
$quotas
))
{
continue
;
}
foreach
(
array_chunk
(
$quotas
,
3
)
as
$quota
)
{
[
$type
,
$used
,
$total
]
=
$quota
;
$type
=
strtolower
(
$type
);
if
(
$type
&&
$total
)
{
$all
[
$quota_root
][
$type
][
'used'
]
=
intval
(
$used
);
$all
[
$quota_root
][
$type
][
'total'
]
=
intval
(
$total
);
}
}
if
(
empty
(
$all
[
$quota_root
][
'storage'
]))
{
continue
;
}
$used
=
$all
[
$quota_root
][
'storage'
][
'used'
];
$total
=
$all
[
$quota_root
][
'storage'
][
'total'
];
$free
=
$total
-
$used
;
// calculate lowest available space from all storage quotas
if
(
$free
<
$min_free
)
{
$min_free
=
$free
;
$result
[
'used'
]
=
$used
;
$result
[
'total'
]
=
$total
;
$result
[
'percent'
]
=
min
(
100
,
round
((
$used
/
max
(
1
,
$total
))
*
100
));
$result
[
'free'
]
=
100
-
$result
[
'percent'
];
}
}
if
(!
empty
(
$result
))
{
$result
[
'all'
]
=
$all
;
}
return
$result
;
}
/**
* Send the SETQUOTA command (RFC9208)
*
* @param string $root Quota root
* @param array $quota Quota limits e.g. ['storage' => 1024000']
*
* @return bool True on success, False on failure
*/
public
function
setQuota
(
$root
,
$quota
)
{
$fn
=
static
function
(
$key
,
$value
)
{
return
strtoupper
(
$key
)
.
' '
.
$value
;
};
$quota
=
implode
(
' '
,
array_map
(
$fn
,
array_keys
(
$quota
),
$quota
));
$result
=
$this
->
execute
(
'SETQUOTA'
,
[
$this
->
escape
(
$root
),
"({$quota})"
],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Send the SETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
* @param mixed $acl ACL string or array
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
setACL
(
$mailbox
,
$user
,
$acl
)
{
if
(
is_array
(
$acl
))
{
$acl
=
implode
(
''
,
$acl
);
}
$result
=
$this
->
execute
(
'SETACL'
,
[
$this
->
escape
(
$mailbox
),
$this
->
escape
(
$user
),
strtolower
(
$acl
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Send the DELETEACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
deleteACL
(
$mailbox
,
$user
)
{
$result
=
$this
->
execute
(
'DELETEACL'
,
[
$this
->
escape
(
$mailbox
),
$this
->
escape
(
$user
)],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Send the GETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return ?array User-rights array on success, NULL on error
*
* @since 0.5-beta
*/
public
function
getACL
(
$mailbox
)
{
[
$code
,
$response
]
=
$this
->
execute
(
'GETACL'
,
[
$this
->
escape
(
$mailbox
)],
0
,
'/^
\*
ACL /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
// Parse server response (remove "* ACL ")
$response
=
substr
(
$response
,
6
);
$ret
=
$this
->
tokenizeResponse
(
$response
);
$mbox
=
array_shift
(
$ret
);
$size
=
count
(
$ret
);
// Create user-rights hash array
// @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
// so we could return only standard rights defined in RFC4314,
// excluding 'c' and 'd' defined in RFC2086.
if
(
$size
%
2
==
0
)
{
for
(
$i
=
0
;
$i
<
$size
;
$i
++)
{
$ret
[
$ret
[
$i
]]
=
str_split
(
$ret
[++
$i
]);
unset
(
$ret
[
$i
-
1
]);
unset
(
$ret
[
$i
]);
}
return
$ret
;
}
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Incomplete ACL response'
);
}
return
null
;
}
/**
* Send the LISTRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return ?array List of user rights, NULL on error
*
* @since 0.5-beta
*/
public
function
listRights
(
$mailbox
,
$user
)
{
[
$code
,
$response
]
=
$this
->
execute
(
'LISTRIGHTS'
,
[
$this
->
escape
(
$mailbox
),
$this
->
escape
(
$user
)],
0
,
'/^
\*
LISTRIGHTS /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
// Parse server response (remove "* LISTRIGHTS ")
$response
=
substr
(
$response
,
13
);
$ret_mbox
=
$this
->
tokenizeResponse
(
$response
,
1
);
$ret_user
=
$this
->
tokenizeResponse
(
$response
,
1
);
$granted
=
$this
->
tokenizeResponse
(
$response
,
1
);
$optional
=
trim
(
$response
);
return
[
'granted'
=>
str_split
(
$granted
),
'optional'
=>
explode
(
' '
,
$optional
),
];
}
return
null
;
}
/**
* Send the MYRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return ?array MYRIGHTS response on success, NULL on error
*
* @since 0.5-beta
*/
public
function
myRights
(
$mailbox
)
{
[
$code
,
$response
]
=
$this
->
execute
(
'MYRIGHTS'
,
[
$this
->
escape
(
$mailbox
)],
0
,
'/^
\*
MYRIGHTS /i'
);
if
(
$code
==
self
::
ERROR_OK
&&
$response
)
{
// Parse server response (remove "* MYRIGHTS ")
$response
=
substr
(
$response
,
11
);
$ret_mbox
=
$this
->
tokenizeResponse
(
$response
,
1
);
$rights
=
$this
->
tokenizeResponse
(
$response
,
1
);
return
str_split
(
$rights
);
}
return
null
;
}
/**
* Send the SETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
setMetadata
(
$mailbox
,
$entries
)
{
if
(
empty
(
$entries
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Wrong argument for SETMETADATA command'
);
return
false
;
}
foreach
(
$entries
as
$name
=>
$value
)
{
$entries
[
$name
]
=
$this
->
escape
(
$name
)
.
' '
.
$this
->
escape
(
$value
,
true
);
}
$entries
=
implode
(
' '
,
$entries
);
$result
=
$this
->
execute
(
'SETMETADATA'
,
[
$this
->
escape
(
$mailbox
),
'('
.
$entries
.
')'
],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Send the SETMETADATA command with NIL values (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array|string $entries Entry names array (or space separated string)
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
deleteMetadata
(
$mailbox
,
$entries
)
{
if
(!
is_array
(
$entries
)
&&
!
empty
(
$entries
))
{
$entries
=
explode
(
' '
,
$entries
);
}
if
(
empty
(
$entries
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Wrong argument for SETMETADATA command'
);
return
false
;
}
$data
=
[];
foreach
(
$entries
as
$entry
)
{
$data
[
$entry
]
=
null
;
}
return
$this
->
setMetadata
(
$mailbox
,
$data
);
}
/**
* Send the GETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array|string $entries Entry name(s)
* @param array $options Command options (with MAXSIZE and DEPTH keys)
*
* @return ?array GETMETADATA result on success, NULL on error
*
* @since 0.5-beta
*/
public
function
getMetadata
(
$mailbox
,
$entries
,
$options
=
[])
{
if
(!
is_array
(
$entries
))
{
$entries
=
[
$entries
];
}
$args
=
[];
// create options string
if
(!
empty
(
$options
))
{
$options
=
array_change_key_case
(
$options
,
\CASE_UPPER
);
$opts
=
[];
if
(!
empty
(
$options
[
'MAXSIZE'
]))
{
$opts
[]
=
'MAXSIZE '
.
intval
(
$options
[
'MAXSIZE'
]);
}
if
(
isset
(
$options
[
'DEPTH'
]))
{
$opts
[]
=
'DEPTH '
.
$this
->
escape
(
$options
[
'DEPTH'
]);
}
if
(!
empty
(
$opts
))
{
$args
[]
=
$opts
;
}
}
$args
[]
=
$this
->
escape
(
$mailbox
);
$args
[]
=
array_map
([
$this
,
'escape'
],
$entries
);
[
$code
,
$response
]
=
$this
->
execute
(
'GETMETADATA'
,
$args
);
if
(
$code
==
self
::
ERROR_OK
)
{
$result
=
[];
$data
=
$this
->
tokenizeResponse
(
$response
);
// The METADATA response can contain multiple entries in a single
// response or multiple responses for each entry or group of entries
for
(
$i
=
0
,
$size
=
count
(
$data
);
$i
<
$size
;
$i
++)
{
if
(
$data
[
$i
]
===
'*'
&&
$data
[++
$i
]
===
'METADATA'
&&
is_string
(
$mbox
=
$data
[++
$i
])
// @phpstan-ignore-line
&&
is_array
(
$data
[++
$i
])
// @phpstan-ignore-line
)
{
for
(
$x
=
0
,
$size2
=
count
(
$data
[
$i
]);
$x
<
$size2
;
$x
+=
2
)
{
// @phpstan-ignore-next-line
if
(
$data
[
$i
][
$x
+
1
]
!==
null
)
{
$result
[
$mbox
][
$data
[
$i
][
$x
]]
=
$data
[
$i
][
$x
+
1
];
}
}
}
}
return
$result
;
}
return
null
;
}
/**
* Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* three elements: entry name, attribute name, value
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
setAnnotation
(
$mailbox
,
$data
)
{
if
(
empty
(
$data
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Wrong argument for SETANNOTATION command'
);
return
false
;
}
$entries
=
[];
foreach
(
$data
as
$entry
)
{
// ANNOTATEMORE drafts before version 08 require quoted parameters
$entries
[]
=
sprintf
(
'%s (%s %s)'
,
$this
->
escape
(
$entry
[
0
],
true
),
$this
->
escape
(
$entry
[
1
],
true
),
$this
->
escape
(
$entry
[
2
],
true
));
}
$entries
=
implode
(
' '
,
$entries
);
$result
=
$this
->
execute
(
'SETANNOTATION'
,
[
$this
->
escape
(
$mailbox
),
$entries
],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* two elements: entry name and attribute name
*
* @return bool True on success, False on failure
*
* @since 0.5-beta
*/
public
function
deleteAnnotation
(
$mailbox
,
$data
)
{
return
$this
->
setAnnotation
(
$mailbox
,
$data
);
}
/**
* Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array|string $entries Entries name(s)
* @param array|string $attribs Attribs name(s)
*
* @return ?array Annotations result on success, NULL on error
*
* @since 0.5-beta
*/
public
function
getAnnotation
(
$mailbox
,
$entries
,
$attribs
)
{
if
(!
is_array
(
$entries
))
{
$entries
=
[
$entries
];
}
// create entries string
// ANNOTATEMORE drafts before version 08 require quoted parameters
foreach
(
$entries
as
$idx
=>
$name
)
{
$entries
[
$idx
]
=
$this
->
escape
(
$name
,
true
);
}
$entries
=
'('
.
implode
(
' '
,
$entries
)
.
')'
;
if
(!
is_array
(
$attribs
))
{
$attribs
=
[
$attribs
];
}
// create attributes string
foreach
(
$attribs
as
$idx
=>
$name
)
{
$attribs
[
$idx
]
=
$this
->
escape
(
$name
,
true
);
}
$attribs
=
'('
.
implode
(
' '
,
$attribs
)
.
')'
;
[
$code
,
$response
]
=
$this
->
execute
(
'GETANNOTATION'
,
[
$this
->
escape
(
$mailbox
),
$entries
,
$attribs
]);
if
(
$code
==
self
::
ERROR_OK
)
{
$result
=
[];
$data
=
$this
->
tokenizeResponse
(
$response
);
$last_entry
=
null
;
// Here we returns only data compatible with METADATA result format
if
(!
empty
(
$data
)
&&
(
$size
=
count
(
$data
)))
{
for
(
$i
=
0
;
$i
<
$size
;
$i
++)
{
$entry
=
$data
[
$i
];
if
(
isset
(
$mbox
)
&&
is_array
(
$entry
))
{
$attribs
=
$entry
;
$entry
=
$last_entry
;
}
elseif
(
$entry
==
'*'
)
{
if
(
$data
[
$i
+
1
]
==
'ANNOTATION'
)
{
$mbox
=
$data
[
$i
+
2
];
unset
(
$data
[
$i
]);
// "*"
unset
(
$data
[++
$i
]);
// "ANNOTATION"
unset
(
$data
[++
$i
]);
// Mailbox
}
// get rid of other untagged responses
else
{
unset
(
$mbox
);
unset
(
$data
[
$i
]);
}
continue
;
}
elseif
(
isset
(
$mbox
))
{
$attribs
=
$data
[++
$i
];
}
else
{
unset
(
$data
[
$i
]);
continue
;
}
if
(!
empty
(
$attribs
))
{
for
(
$x
=
0
,
$len
=
count
(
$attribs
);
$x
<
$len
;)
{
$attr
=
$attribs
[
$x
++];
$value
=
$attribs
[
$x
++];
if
(
$attr
==
'value.priv'
&&
$value
!==
null
)
{
$result
[
$mbox
][
'/private'
.
$entry
]
=
$value
;
}
elseif
(
$attr
==
'value.shared'
&&
$value
!==
null
)
{
$result
[
$mbox
][
'/shared'
.
$entry
]
=
$value
;
}
}
}
$last_entry
=
$entry
;
unset
(
$data
[
$i
]);
}
}
return
$result
;
}
return
null
;
}
/**
* Send the STORE X ANNOTATION command (RFC5257)
*
* @param string $mailbox Mailbox name
* @param array $entries
*
* @return bool True on success, False on failure
*
* @since 1.6.10
*/
public
function
storeMessageAnnotation
(
$mailbox
,
$uids
,
$entries
)
{
if
(!
$this
->
hasCapability
(
'ANNOTATE-EXPERIMENT-1'
))
{
return
false
;
}
if
(
empty
(
$entries
)
||
empty
(
$uids
))
{
$this
->
setError
(
self
::
ERROR_COMMAND
,
'Wrong argument for STORE ANNOTATION command'
);
return
false
;
}
if
(!
$this
->
select
(
$mailbox
))
{
return
false
;
}
/* Example input compatible with rcube_message_header::$annotations:
$entries = [
'/comment' => [
'value.priv' => 'test1',
'value.shared' => null,
],
];
*/
$request
=
[];
foreach
(
$entries
as
$name
=>
$annotation
)
{
if
(!
empty
(
$annotation
))
{
foreach
(
$annotation
as
$key
=>
$value
)
{
$annotation
[
$key
]
=
$this
->
escape
(
$key
)
.
' '
.
$this
->
escape
(
$value
,
true
);
}
$request
[]
=
$this
->
escape
(
$name
);
$request
[]
=
$annotation
;
}
}
$result
=
$this
->
execute
(
'UID STORE'
,
[
$this
->
compressMessageSet
(
$uids
),
'ANNOTATION'
,
$request
],
self
::
COMMAND_NORESPONSE
);
return
$result
==
self
::
ERROR_OK
;
}
/**
* Returns BODYSTRUCTURE for the specified message.
*
* @param string $mailbox Folder name
* @param int $id Message sequence number or UID
* @param bool $is_uid True if $id is an UID
*
* @return array|bool body structure array or False on error
*
* @since 0.6
*/
public
function
getStructure
(
$mailbox
,
$id
,
$is_uid
=
false
)
{
$result
=
$this
->
fetch
(
$mailbox
,
$id
,
$is_uid
,
[
'BODYSTRUCTURE'
]);
if
(
is_array
(
$result
)
&&
!
empty
(
$result
))
{
$result
=
array_first
(
$result
);
return
$result
->
bodystructure
;
}
return
false
;
}
/**
* Returns data of a message part according to specified structure.
*
* @param array $structure Message structure (getStructure() result)
* @param string $part Message part identifier
*
* @return array Part data as hash array (type, encoding, charset, size)
*/
public
static
function
getStructurePartData
(
$structure
,
$part
)
{
$part_a
=
self
::
getStructurePartArray
(
$structure
,
$part
);
$data
=
[];
if
(
empty
(
$part_a
))
{
return
$data
;
}
// content-type
if
(
is_array
(
$part_a
[
0
]))
{
$data
[
'type'
]
=
'multipart'
;
}
else
{
$data
[
'type'
]
=
strtolower
(
$part_a
[
0
]);
$data
[
'subtype'
]
=
strtolower
(
$part_a
[
1
]);
$data
[
'encoding'
]
=
strtolower
(
$part_a
[
5
]);
// charset
if
(
is_array
(
$part_a
[
2
]))
{
foreach
(
$part_a
[
2
]
as
$key
=>
$val
)
{
if
(
strcasecmp
(
$val
,
'charset'
)
==
0
)
{
$data
[
'charset'
]
=
$part_a
[
2
][
$key
+
1
];
break
;
}
}
}
}
// size
$data
[
'size'
]
=
intval
(
$part_a
[
6
]);
return
$data
;
}
public
static
function
getStructurePartArray
(
$a
,
$part
)
{
if
(!
is_array
(
$a
))
{
return
false
;
}
if
(
empty
(
$part
))
{
return
$a
;
}
$ctype
=
is_string
(
$a
[
0
])
&&
is_string
(
$a
[
1
])
?
$a
[
0
]
.
'/'
.
$a
[
1
]
:
''
;
if
(
strcasecmp
(
$ctype
,
'message/rfc822'
)
==
0
)
{
$a
=
$a
[
8
];
}
if
(
strpos
(
$part
,
'.'
)
>
0
)
{
$orig_part
=
$part
;
$pos
=
strpos
(
$part
,
'.'
);
$rest
=
substr
(
$orig_part
,
$pos
+
1
);
$part
=
(
int
)
substr
(
$orig_part
,
0
,
$pos
);
return
self
::
getStructurePartArray
(
$a
[
$part
-
1
],
$rest
);
}
elseif
(
$part
>
0
)
{
return
is_array
(
$a
[
$part
-
1
])
?
$a
[
$part
-
1
]
:
$a
;
}
}
/**
* Creates next command identifier (tag)
*
* @return string Command identifier
*
* @since 0.5-beta
*/
public
function
nextTag
()
{
$this
->
cmd_num
++;
$this
->
cmd_tag
=
sprintf
(
'A%04d'
,
$this
->
cmd_num
);
return
$this
->
cmd_tag
;
}
/**
* Sends IMAP command and parses result
*
* @param string $command IMAP command
* @param array $arguments Command arguments
* @param int $options Execution options
* @param string $filter Line filter (regexp)
*
* @return mixed Response code or list of response code and data
*
* @since 0.5-beta
*/
public
function
execute
(
$command
,
$arguments
=
[],
$options
=
0
,
$filter
=
null
)
{
$tag
=
$this
->
nextTag
();
$query
=
$tag
.
' '
.
$command
;
$noresp
=
(
$options
&
self
::
COMMAND_NORESPONSE
);
$response
=
$noresp
?
null
:
''
;
if
(!
empty
(
$arguments
))
{
foreach
(
$arguments
as
$arg
)
{
$query
.=
' '
.
self
::
r_implode
(
$arg
);
}
}
// Send command
if
(!
$this
->
putLineC
(
$query
,
true
,
$options
&
self
::
COMMAND_ANONYMIZED
))
{
preg_match
(
'/^[A-Z0-9]+ ((UID )?[A-Z]+)/'
,
$query
,
$matches
);
$cmd
=
$matches
[
1
]
??
'UNKNOWN'
;
$this
->
setError
(
self
::
ERROR_COMMAND
,
"Failed to send {$cmd} command"
);
return
$noresp
?
self
::
ERROR_COMMAND
:
[
self
::
ERROR_COMMAND
,
''
];
}
// Parse response
do
{
$line
=
$this
->
readFullLine
(
4096
);
if
(
$response
!==
null
)
{
if
(!
$filter
||
preg_match
(
$filter
,
$line
))
{
$response
.=
$line
;
}
}
// parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851)
if
(
$line
&&
$command
==
'UID MOVE'
)
{
if
(
preg_match
(
'/^
\*
OK
\[
COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)
\]
/i'
,
$line
,
$m
))
{
$this
->
data
[
'COPYUID'
]
=
[
$m
[
1
],
$m
[
2
]];
}
}
}
while
(!
$this
->
startsWith
(
$line
,
$tag
.
' '
,
true
,
true
));
$code
=
$this
->
parseResult
(
$line
,
$command
.
': '
);
// Remove last line from response
if
(
$response
)
{
if
(!
$filter
)
{
$line_len
=
min
(
strlen
(
$response
),
strlen
(
$line
));
$response
=
substr
(
$response
,
0
,
-
$line_len
);
}
$response
=
rtrim
(
$response
,
"
\r\n
"
);
}
// optional CAPABILITY response
if
((
$options
&
self
::
COMMAND_CAPABILITY
)
&&
$code
==
self
::
ERROR_OK
&&
preg_match
(
'/
\[
CAPABILITY ([^]]+)
\]
/i'
,
$line
,
$matches
)
)
{
$this
->
parseCapability
(
$matches
[
1
],
true
);
}
// return last line only (without command tag, result and response code)
if
(
$line
&&
(
$options
&
self
::
COMMAND_LASTLINE
))
{
$response
=
preg_replace
(
"/^{$tag} (OK|NO|BAD|BYE|PREAUTH)?
\\
s*(
\\
[[a-z-]+
\\
])?
\\
s*/i"
,
''
,
trim
(
$line
));
}
return
$noresp
?
$code
:
[
$code
,
$response
];
}
/**
* Splits IMAP response into string tokens
*
* @param string &$str The IMAP's server response
* @param int $num Number of tokens to return
*
* @return mixed Tokens array or string if $num=1
*
* @since 0.5-beta
*/
public
static
function
tokenizeResponse
(&
$str
,
$num
=
0
)
{
$result
=
[];
while
(!
$num
||
count
(
$result
)
<
$num
)
{
// remove spaces from the beginning of the string
$str
=
ltrim
(
$str
);
// empty string
if
(
$str
===
''
)
{
break
;
}
switch
(
$str
[
0
])
{
// String literal
case
'{'
:
if
((
$epos
=
strpos
(
$str
,
"}
\r\n
"
,
1
))
==
false
)
{
// error
}
if
(!
is_numeric
(
$bytes
=
substr
(
$str
,
1
,
$epos
-
1
)))
{
// error
}
$bytes
=
(
int
)
$bytes
;
$result
[]
=
$bytes
?
substr
(
$str
,
$epos
+
3
,
$bytes
)
:
''
;
$str
=
substr
(
$str
,
$epos
+
3
+
$bytes
);
break
;
// Quoted string (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
case
'"'
:
$len
=
strlen
(
$str
);
for
(
$pos
=
1
;
$pos
<
$len
;
$pos
++)
{
if
(
$str
[
$pos
]
==
'"'
)
{
break
;
}
if
(
$str
[
$pos
]
==
'
\\
'
)
{
if
(
$str
[
$pos
+
1
]
==
'"'
||
$str
[
$pos
+
1
]
==
'
\\
'
)
{
$pos
++;
}
}
}
// we need to strip slashes for a quoted string
$result
[]
=
stripslashes
(
substr
(
$str
,
1
,
$pos
-
1
));
$str
=
substr
(
$str
,
$pos
+
1
);
break
;
// Parenthesized list (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
case
'('
:
$str
=
substr
(
$str
,
1
);
$result
[]
=
self
::
tokenizeResponse
(
$str
);
break
;
case
')'
:
$str
=
substr
(
$str
,
1
);
return
$result
;
// String atom, number, astring, NIL, *, % (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
default
:
// excluded chars: SP, CTL, ), DEL
// we do not exclude [ and ] (#1489223)
if
(
preg_match
(
'/^([^
\x
00-
\x
20
\x
29
\x
7F]+)/'
,
$str
,
$m
))
{
$result
[]
=
$m
[
1
]
==
'NIL'
?
null
:
$m
[
1
];
$str
=
substr
(
$str
,
strlen
(
$m
[
1
]));
}
break
;
}
}
return
$num
==
1
?
(
$result
[
0
]
??
''
)
:
$result
;
}
/**
* Joins IMAP command line elements (recursively)
*/
protected
static
function
r_implode
(
$element
)
{
if
(!
is_array
(
$element
))
{
return
$element
;
}
reset
(
$element
);
$string
=
''
;
foreach
(
$element
as
$value
)
{
$string
.=
' '
.
self
::
r_implode
(
$value
);
}
return
'('
.
trim
(
$string
)
.
')'
;
}
/**
* Converts message identifiers array into sequence-set syntax
*
* @param array|string $messages Message identifiers
* @param bool $force Forces compression of any size
*
* @return string Compressed sequence-set or 'INVALID' on invalid input
*/
public
static
function
compressMessageSet
(
$messages
,
$force
=
false
)
{
// given a comma delimited list of independent mid's,
// compresses by grouping sequences together
if
(!
is_array
(
$messages
))
{
// if less than 255 bytes long, let's not bother
if
(!
$force
&&
strlen
(
$messages
)
<
255
)
{
return
preg_match
(
'/[^0-9:,*]/'
,
$messages
)
?
'INVALID'
:
$messages
;
}
// see if it's already been compressed
if
(
strpos
(
$messages
,
':'
)
!==
false
)
{
return
preg_match
(
'/[^0-9:,*]/'
,
$messages
)
?
'INVALID'
:
$messages
;
}
// separate, then sort
$messages
=
explode
(
','
,
$messages
);
}
if
(
empty
(
$messages
))
{
return
''
;
}
sort
(
$messages
);
$result
=
[];
$start
=
$prev
=
$messages
[
0
];
foreach
(
$messages
as
$id
)
{
$incr
=
$id
-
$prev
;
if
(
$incr
>
1
)
{
// found a gap
if
(
$start
==
$prev
)
{
$result
[]
=
$prev
;
// push single id
}
else
{
$result
[]
=
$start
.
':'
.
$prev
;
// push sequence as start_id:end_id
}
$start
=
$id
;
// start of new sequence
}
$prev
=
$id
;
}
// handle the last sequence/id
if
(
$start
==
$prev
)
{
$result
[]
=
$prev
;
}
else
{
$result
[]
=
$start
.
':'
.
$prev
;
}
// return as comma separated string
$result
=
implode
(
','
,
$result
);
return
preg_match
(
'/[^0-9:,*]/'
,
$result
)
?
'INVALID'
:
$result
;
}
/**
* Converts message sequence-set into array
*
* @param string $messages Message identifiers
*
* @return array List of message identifiers
*/
public
static
function
uncompressMessageSet
(
$messages
)
{
if
(
empty
(
$messages
))
{
return
[];
}
$result
=
[];
$messages
=
explode
(
','
,
$messages
);
foreach
(
$messages
as
$idx
=>
$part
)
{
$items
=
explode
(
':'
,
$part
);
if
(!
empty
(
$items
[
1
])
&&
$items
[
1
]
>
$items
[
0
])
{
$max
=
$items
[
1
];
}
else
{
$max
=
$items
[
0
];
}
for
(
$x
=
$items
[
0
];
$x
<=
$max
;
$x
++)
{
$result
[]
=
(
int
)
$x
;
}
unset
(
$messages
[
$idx
]);
}
return
$result
;
}
/**
* Clear internal status cache
*/
protected
function
clear_status_cache
(
$mailbox
)
{
unset
(
$this
->
data
[
'STATUS:'
.
$mailbox
]);
$keys
=
[
'EXISTS'
,
'RECENT'
,
'UNSEEN'
,
'UID-MAP'
];
foreach
(
$keys
as
$key
)
{
unset
(
$this
->
data
[
$key
]);
}
}
/**
* Clear internal cache of the current mailbox
*/
protected
function
clear_mailbox_cache
()
{
$this
->
clear_status_cache
(
$this
->
selected
);
$keys
=
[
'UIDNEXT'
,
'UIDVALIDITY'
,
'HIGHESTMODSEQ'
,
'NOMODSEQ'
,
'PERMANENTFLAGS'
,
'QRESYNC'
,
'VANISHED'
,
'READ-WRITE'
];
foreach
(
$keys
as
$key
)
{
unset
(
$this
->
data
[
$key
]);
}
}
/**
* Converts flags array into string for inclusion in IMAP command
*
* @param array $flags Flags (see self::flags)
*
* @return string Space-separated list of flags
*/
protected
function
flagsToStr
(
$flags
)
{
foreach
((
array
)
$flags
as
$idx
=>
$flag
)
{
if
(
$flag
=
$this
->
flags
[
strtoupper
(
$flag
)])
{
$flags
[
$idx
]
=
$flag
;
}
}
return
implode
(
' '
,
(
array
)
$flags
);
}
/**
* CAPABILITY response parser
*/
protected
function
parseCapability
(
$str
,
$trusted
=
false
)
{
$str
=
preg_replace
(
'/^
\*
CAPABILITY /i'
,
''
,
$str
);
$this
->
capability
=
explode
(
' '
,
strtoupper
(
$str
));
if
(!
empty
(
$this
->
prefs
[
'disabled_caps'
]))
{
$this
->
capability
=
array_diff
(
$this
->
capability
,
$this
->
prefs
[
'disabled_caps'
]);
}
if
(!
isset
(
$this
->
prefs
[
'literal+'
])
&&
in_array
(
'LITERAL+'
,
$this
->
capability
))
{
$this
->
prefs
[
'literal+'
]
=
true
;
}
elseif
(!
isset
(
$this
->
prefs
[
'literal-'
])
&&
in_array
(
'LITERAL-'
,
$this
->
capability
))
{
$this
->
prefs
[
'literal-'
]
=
true
;
}
if
(
$trusted
)
{
$this
->
capability_read
=
true
;
}
}
/**
* Escapes a string when it contains special characters (RFC3501)
*
* @param ?string $string IMAP string
* @param bool $force_quotes Forces string quoting (for atoms)
*
* @return string String atom, quoted-string or string literal
*
* @todo lists
*/
public
static
function
escape
(
$string
,
$force_quotes
=
false
)
{
if
(
$string
===
null
)
{
return
'NIL'
;
}
if
(
$string
===
''
)
{
return
'""'
;
}
// atom-string (only safe characters)
if
(!
$force_quotes
&&
!
preg_match
(
'/[
\x
00-
\x
20
\x
22
\x
25
\x
28-
\x
2A
\x
5B-
\x
5D
\x
7B
\x
7D
\x
80-
\x
FF]/'
,
$string
))
{
return
$string
;
}
// quoted-string
if
(!
preg_match
(
'/[
\r\n\x
00
\x
80-
\x
FF]/'
,
$string
))
{
return
'"'
.
addcslashes
(
$string
,
'
\"
'
)
.
'"'
;
}
// literal-string
return
sprintf
(
"{%d}
\r\n
%s"
,
strlen
(
$string
),
$string
);
}
/**
* Set the value of the debugging flag.
*
* @param bool $debug new value for the debugging flag
* @param callable $handler Logging handler function
*
* @since 0.5-stable
*/
public
function
setDebug
(
$debug
,
$handler
=
null
)
{
$this
->
debug
=
$debug
;
$this
->
debug_handler
=
$handler
;
}
/**
* Write the given debug text to the current debug output handler.
*
* @param string $message debug message text
*
* @since 0.5-stable
*/
protected
function
debug
(
$message
)
{
if
((
$len
=
strlen
(
$message
))
>
self
::
DEBUG_LINE_LENGTH
)
{
$diff
=
$len
-
self
::
DEBUG_LINE_LENGTH
;
$message
=
substr
(
$message
,
0
,
self
::
DEBUG_LINE_LENGTH
)
.
"... [truncated {$diff} bytes]"
;
}
if
(
$this
->
resourceid
)
{
$message
=
sprintf
(
'[%s] %s'
,
$this
->
resourceid
,
$message
);
}
if
(
$this
->
debug_handler
)
{
call_user_func_array
(
$this
->
debug_handler
,
[
$this
,
$message
]);
}
else
{
echo
"DEBUG: {$message}
\n
"
;
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, Apr 6, 1:28 AM (2 d, 5 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831841
Default Alt Text
rcube_imap_generic.php (140 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline