mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-05 07:27:04 +03:00
Added support for rfc7889 and impoved rfc8474
This commit is contained in:
parent
d4117dc723
commit
e4149b655c
6 changed files with 105 additions and 46 deletions
|
|
@ -26,4 +26,8 @@ abstract class FolderResponseStatus
|
||||||
const UNSEEN = 'UNSEEN';
|
const UNSEEN = 'UNSEEN';
|
||||||
// rfc4551
|
// rfc4551
|
||||||
const HIGHESTMODSEQ = 'HIGHESTMODSEQ';
|
const HIGHESTMODSEQ = 'HIGHESTMODSEQ';
|
||||||
|
// rfc7889
|
||||||
|
const APPENDLIMIT = 'APPENDLIMIT';
|
||||||
|
// rfc8474
|
||||||
|
const MAILBOXID = 'MAILBOXID';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ abstract class FolderStatus
|
||||||
const UNSEEN = 'UNSEEN';
|
const UNSEEN = 'UNSEEN';
|
||||||
// rfc4551
|
// rfc4551
|
||||||
const HIGHESTMODSEQ = 'HIGHESTMODSEQ';
|
const HIGHESTMODSEQ = 'HIGHESTMODSEQ';
|
||||||
|
// rfc7889
|
||||||
|
const APPENDLIMIT = 'APPENDLIMIT';
|
||||||
// rfc8474
|
// rfc8474
|
||||||
const MAILBOXID = 'MAILBOXID';
|
const MAILBOXID = 'MAILBOXID';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,12 @@ class FetchResponse
|
||||||
{
|
{
|
||||||
$oResult = null;
|
$oResult = null;
|
||||||
$aEmails = $this->GetFetchEnvelopeValue($iIndex);
|
$aEmails = $this->GetFetchEnvelopeValue($iIndex);
|
||||||
if (is_array($aEmails) && 0 < count($aEmails))
|
if (\is_array($aEmails) && \count($aEmails))
|
||||||
{
|
{
|
||||||
$oResult = new \MailSo\Mime\EmailCollection;
|
$oResult = new \MailSo\Mime\EmailCollection;
|
||||||
foreach ($aEmails as $aEmailItem)
|
foreach ($aEmails as $aEmailItem)
|
||||||
{
|
{
|
||||||
if (is_array($aEmailItem) && 4 === count($aEmailItem))
|
if (\is_array($aEmailItem) && 4 === \count($aEmailItem))
|
||||||
{
|
{
|
||||||
$sDisplayName = \MailSo\Base\Utils::DecodeHeaderValue(
|
$sDisplayName = \MailSo\Base\Utils::DecodeHeaderValue(
|
||||||
self::findEnvelopeIndex($aEmailItem, 0, ''), $sParentCharset);
|
self::findEnvelopeIndex($aEmailItem, 0, ''), $sParentCharset);
|
||||||
|
|
@ -70,7 +70,7 @@ class FetchResponse
|
||||||
$sLocalPart = self::findEnvelopeIndex($aEmailItem, 2, '');
|
$sLocalPart = self::findEnvelopeIndex($aEmailItem, 2, '');
|
||||||
$sDomainPart = self::findEnvelopeIndex($aEmailItem, 3, '');
|
$sDomainPart = self::findEnvelopeIndex($aEmailItem, 3, '');
|
||||||
|
|
||||||
if (0 < strlen($sLocalPart) && 0 < strlen($sDomainPart))
|
if (\strlen($sLocalPart) && \strlen($sDomainPart))
|
||||||
{
|
{
|
||||||
$oResult->append(
|
$oResult->append(
|
||||||
new \MailSo\Mime\Email($sLocalPart.'@'.$sDomainPart, $sDisplayName)
|
new \MailSo\Mime\Email($sLocalPart.'@'.$sDomainPart, $sDisplayName)
|
||||||
|
|
@ -87,9 +87,9 @@ class FetchResponse
|
||||||
{
|
{
|
||||||
$aBodyStructureArray = $this->GetFetchValue(Enumerations\FetchType::BODYSTRUCTURE);
|
$aBodyStructureArray = $this->GetFetchValue(Enumerations\FetchType::BODYSTRUCTURE);
|
||||||
|
|
||||||
if (is_array($aBodyStructureArray))
|
if (\is_array($aBodyStructureArray))
|
||||||
{
|
{
|
||||||
if (0 < strlen($sRfc822SubMimeIndex))
|
if (\strlen($sRfc822SubMimeIndex))
|
||||||
{
|
{
|
||||||
return BodyStructure::NewInstanceFromRfc822SubPart($aBodyStructureArray, $sRfc822SubMimeIndex);
|
return BodyStructure::NewInstanceFromRfc822SubPart($aBodyStructureArray, $sRfc822SubMimeIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +100,7 @@ class FetchResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Like: UID, RFC822.SIZE, MODSEQ, INTERNALDATE, FLAGS, BODYSTRUCTURE
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function GetFetchValue(string $sFetchItemName)
|
public function GetFetchValue(string $sFetchItemName)
|
||||||
|
|
@ -124,11 +125,15 @@ class FetchResponse
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like: BODY[HEADER.FIELDS (RETURN-PATH RECEIVED MIME-VERSION MESSAGE-ID CONTENT-TYPE FROM TO CC BCC SENDER REPLY-TO DELIVERED-TO IN-REPLY-TO REFERENCES DATE SUBJECT SENSITIVITY X-MSMAIL-PRIORITY IMPORTANCE X-PRIORITY X-DRAFT-INFO RETURN-RECEIPT-TO DISPOSITION-NOTIFICATION-TO X-CONFIRM-READING-TO AUTHENTICATION-RESULTS X-DKIM-AUTHENTICATION-RESULTS LIST-UNSUBSCRIBE X-SPAM-STATUS X-SPAMD-RESULT X-BOGOSITY X-VIRUS X-VIRUS-SCANNED X-VIRUS-STATUS)]
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function GetHeaderFieldsValue(string $sRfc822SubMimeIndex = '') : string
|
public function GetHeaderFieldsValue(string $sRfc822SubMimeIndex = '') : string
|
||||||
{
|
{
|
||||||
$bNextIsValue = false;
|
$bNextIsValue = false;
|
||||||
|
|
||||||
$sRfc822SubMimeIndex = 0 < \strlen($sRfc822SubMimeIndex) ? ''.$sRfc822SubMimeIndex.'.' : '';
|
$sRfc822SubMimeIndex = \strlen($sRfc822SubMimeIndex) ? ''.$sRfc822SubMimeIndex.'.' : '';
|
||||||
|
|
||||||
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3]))
|
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3]))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,25 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC 7889
|
||||||
|
* APPENDLIMIT=<number> indicates that the IMAP server has the same upload limit for all mailboxes.
|
||||||
|
* APPENDLIMIT without any value indicates that the IMAP server supports this extension,
|
||||||
|
* and that the client will need to discover upload limits for each mailbox,
|
||||||
|
* as they might differ from mailbox to mailbox.
|
||||||
|
*/
|
||||||
|
public function AppendLimit() : ?int
|
||||||
|
{
|
||||||
|
if ($this->aCapabilityItems) {
|
||||||
|
foreach ($this->aCapabilityItems as $string) {
|
||||||
|
if ('APPENDLIMIT=' === \substr($string, 0, 12)) {
|
||||||
|
return (int) \substr($string, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \MailSo\Net\Exceptions\Exception
|
* @throws \MailSo\Net\Exceptions\Exception
|
||||||
* @throws \MailSo\Imap\Exceptions\Exception
|
* @throws \MailSo\Imap\Exceptions\Exception
|
||||||
|
|
@ -416,21 +435,36 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
*/
|
*/
|
||||||
public function FolderStatus(string $sFolderName, array $aStatusItems) : ?array
|
public function FolderStatus(string $sFolderName, array $aStatusItems) : ?array
|
||||||
{
|
{
|
||||||
|
if (!\count($aStatusItems)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$oFolderInfo = $this->oCurrentFolderInfo;
|
$oFolderInfo = $this->oCurrentFolderInfo;
|
||||||
|
$bReselect = false;
|
||||||
|
$bWritable = false;
|
||||||
if ($oFolderInfo && $sFolderName === $oFolderInfo->FolderName) {
|
if ($oFolderInfo && $sFolderName === $oFolderInfo->FolderName) {
|
||||||
// Not supported by Outlook/Hotmail/Office365
|
/**
|
||||||
|
* There's a long standing IMAP CLIENTBUG where STATUS command is executed
|
||||||
|
* after SELECT/EXAMINE on same folder (it should not).
|
||||||
|
* So we must unselect the folder to be able to get the APPENDLIMIT and UNSEEN.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
if ($this->IsSupported('ESEARCH')) {
|
if ($this->IsSupported('ESEARCH')) {
|
||||||
$aResult = $oFolderInfo->getStatusItems();
|
$aResult = $oFolderInfo->getStatusItems();
|
||||||
// SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message
|
// SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message
|
||||||
$aResult['UNSEEN'] = $this->simpleESearchOrESortHelper(false, 'UNSEEN', ['COUNT'])['COUNT'];
|
$aResult['UNSEEN'] = $this->simpleESearchOrESortHelper(false, 'UNSEEN', ['COUNT'])['COUNT'];
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
// $this->FolderUnSelect();
|
*/
|
||||||
|
$bWritable = $oFolderInfo->IsWritable;
|
||||||
|
$bReselect = true;
|
||||||
|
$this->FolderUnSelect();
|
||||||
}
|
}
|
||||||
return \count($aStatusItems)
|
$aResult = $this->SendRequestGetResponse('STATUS', array($this->EscapeString($sFolderName), $aStatusItems))
|
||||||
? $this->SendRequestGetResponse('STATUS', array($this->EscapeString($sFolderName), $aStatusItems))
|
->getStatusFolderInformationResult();
|
||||||
->getStatusFolderInformationResult()
|
if ($bReselect) {
|
||||||
: null;
|
$this->selectOrExamineFolder($sFolderName, $bWritable, false);
|
||||||
|
}
|
||||||
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -461,18 +495,26 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
$aParameters[] = $this->EscapeString($sParentFolderName);
|
$aParameters[] = $this->EscapeString($sParentFolderName);
|
||||||
$aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*');
|
$aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*');
|
||||||
|
|
||||||
|
// RFC 5819
|
||||||
if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS'))
|
if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS'))
|
||||||
{
|
{
|
||||||
// RFC 5819
|
|
||||||
$aL = array(
|
$aL = array(
|
||||||
Enumerations\FolderStatus::MESSAGES,
|
Enumerations\FolderStatus::MESSAGES,
|
||||||
Enumerations\FolderStatus::UNSEEN,
|
Enumerations\FolderStatus::UNSEEN,
|
||||||
Enumerations\FolderStatus::UIDNEXT
|
Enumerations\FolderStatus::UIDNEXT
|
||||||
);
|
);
|
||||||
|
// RFC 4551
|
||||||
if ($this->IsSupported('CONDSTORE')) {
|
if ($this->IsSupported('CONDSTORE')) {
|
||||||
$aL[] = Enumerations\FolderStatus::HIGHESTMODSEQ;
|
$aL[] = Enumerations\FolderStatus::HIGHESTMODSEQ;
|
||||||
}
|
}
|
||||||
|
// RFC 7889
|
||||||
|
if ($this->IsSupported('APPENDLIMIT')) {
|
||||||
|
$aL[] = Enumerations\FolderStatus::APPENDLIMIT;
|
||||||
|
}
|
||||||
|
// RFC 8474
|
||||||
|
if ($this->IsSupported('OBJECTID')) {
|
||||||
|
$aTypes[] = Enumerations\FolderStatus::MAILBOXID;
|
||||||
|
}
|
||||||
|
|
||||||
$aReturnParams[] = 'STATUS';
|
$aReturnParams[] = 'STATUS';
|
||||||
$aReturnParams[] = $aL;
|
$aReturnParams[] = $aL;
|
||||||
|
|
@ -560,11 +602,17 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
throw new \MailSo\Base\Exceptions\InvalidArgumentException;
|
throw new \MailSo\Base\Exceptions\InvalidArgumentException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aParams = array($this->EscapeString($sFolderName));
|
||||||
|
/*
|
||||||
|
if ($this->IsSupported('CONDSTORE')) {
|
||||||
|
$aParams[] = ['CONDSTORE'];
|
||||||
|
}
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* IMAP4rev2 SELECT/EXAMINE are now required to return an untagged LIST response.
|
* IMAP4rev2 SELECT/EXAMINE are now required to return an untagged LIST response.
|
||||||
*/
|
*/
|
||||||
$this->oCurrentFolderInfo = $this->SendRequestGetResponse($bIsWritable ? 'SELECT' : 'EXAMINE',
|
$this->oCurrentFolderInfo = $this->SendRequestGetResponse($bIsWritable ? 'SELECT' : 'EXAMINE',
|
||||||
array($this->EscapeString($sFolderName)))
|
$aParams)
|
||||||
->getCurrentFolderInformation($sFolderName, $bIsWritable);
|
->getCurrentFolderInformation($sFolderName, $bIsWritable);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ trait Status
|
||||||
*/
|
*/
|
||||||
$HIGHESTMODSEQ,
|
$HIGHESTMODSEQ,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC7889
|
||||||
|
* Message upload size limit.
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
$APPENDLIMIT,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RFC8474
|
* RFC8474
|
||||||
* A server-allocated unique identifier for the mailbox.
|
* A server-allocated unique identifier for the mailbox.
|
||||||
|
|
@ -81,7 +88,7 @@ trait Status
|
||||||
public function getStatusItems() : array
|
public function getStatusItems() : array
|
||||||
{
|
{
|
||||||
return \array_filter(\get_object_vars($this), function($v, $k){
|
return \array_filter(\get_object_vars($this), function($v, $k){
|
||||||
return isset($v) && \property_exists(__TRAIT__, $k);
|
return \property_exists(__TRAIT__, $k);
|
||||||
}, ARRAY_FILTER_USE_BOTH);
|
}, ARRAY_FILTER_USE_BOTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -519,8 +519,7 @@ class MailClient
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initFolderValues(string $sFolderName, int &$iCount, int &$iUnseenCount,
|
protected function initFolderValues(string $sFolderName) : array
|
||||||
int &$iUidNext, int &$iHighestModSeq) : void
|
|
||||||
{
|
{
|
||||||
$aTypes = array(
|
$aTypes = array(
|
||||||
FolderResponseStatus::MESSAGES,
|
FolderResponseStatus::MESSAGES,
|
||||||
|
|
@ -528,24 +527,31 @@ class MailClient
|
||||||
FolderResponseStatus::UIDNEXT
|
FolderResponseStatus::UIDNEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->oImapClient->IsSupported('CONDSTORE'))
|
if ($this->oImapClient->IsSupported('CONDSTORE')) {
|
||||||
{
|
|
||||||
$aTypes[] = FolderResponseStatus::HIGHESTMODSEQ;
|
$aTypes[] = FolderResponseStatus::HIGHESTMODSEQ;
|
||||||
}
|
}
|
||||||
|
if ($this->oImapClient->IsSupported('APPENDLIMIT')) {
|
||||||
|
$aTypes[] = FolderResponseStatus::APPENDLIMIT;
|
||||||
|
}
|
||||||
|
if ($this->oImapClient->IsSupported('OBJECTID')) {
|
||||||
|
$aTypes[] = FolderResponseStatus::MAILBOXID;
|
||||||
|
}
|
||||||
|
|
||||||
$aFolderStatus = $this->oImapClient->FolderStatus($sFolderName, $aTypes);
|
$aFolderStatus = $this->oImapClient->FolderStatus($sFolderName, $aTypes);
|
||||||
|
|
||||||
$iCount = (int) $aFolderStatus[FolderResponseStatus::MESSAGES] ?? 0;
|
return [
|
||||||
|
$aFolderStatus[FolderResponseStatus::MESSAGES] ?: 0,
|
||||||
|
|
||||||
$iUnseenCount = (int) $aFolderStatus[FolderResponseStatus::UNSEEN] ?? 0;
|
$aFolderStatus[FolderResponseStatus::UNSEEN] ?: 0,
|
||||||
|
|
||||||
$iUidNext = (int) $aFolderStatus[FolderResponseStatus::UIDNEXT] ?? 0;
|
$aFolderStatus[FolderResponseStatus::UIDNEXT] ?: 0,
|
||||||
|
|
||||||
if (isset($aFolderStatus[FolderResponseStatus::HIGHESTMODSEQ])) {
|
$aFolderStatus[FolderResponseStatus::HIGHESTMODSEQ] ?: 0,
|
||||||
$iHighestModSeq = (int) $aFolderStatus[FolderResponseStatus::HIGHESTMODSEQ];
|
|
||||||
} else {
|
$aFolderStatus[FolderResponseStatus::APPENDLIMIT] ?: $this->oImapClient->AppendLimit(),
|
||||||
$iHighestModSeq = 0;
|
|
||||||
}
|
$aFolderStatus[FolderResponseStatus::MAILBOXID] ?: ''
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GenerateImapClientHash() : string
|
public function GenerateImapClientHash() : string
|
||||||
|
|
@ -652,12 +658,7 @@ class MailClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$iCount = 0;
|
list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName);
|
||||||
$iUnseenCount = 0;
|
|
||||||
$iUidNext = 0;
|
|
||||||
$iHighestModSeq = 0;
|
|
||||||
|
|
||||||
$this->initFolderValues($sFolderName, $iCount, $iUnseenCount, $iUidNext, $iHighestModSeq);
|
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'Folder' => $sFolderName,
|
'Folder' => $sFolderName,
|
||||||
|
|
@ -667,6 +668,8 @@ class MailClient
|
||||||
'UidNext' => $iUidNext,
|
'UidNext' => $iUidNext,
|
||||||
'MessageFlags' => $aFlags,
|
'MessageFlags' => $aFlags,
|
||||||
'HighestModSeq' => $iHighestModSeq,
|
'HighestModSeq' => $iHighestModSeq,
|
||||||
|
'AppendLimit' => $iAppendLimit,
|
||||||
|
'MailboxId' => $sMailboxId,
|
||||||
'NewMessages' => 'INBOX' === $sFolderName && \MailSo\Config::$CheckNewMessages ?
|
'NewMessages' => 'INBOX' === $sFolderName && \MailSo\Config::$CheckNewMessages ?
|
||||||
$this->getFolderNextMessageInformation($sFolderName, $iPrevUidNext, $iUidNext) : array()
|
$this->getFolderNextMessageInformation($sFolderName, $iPrevUidNext, $iUidNext) : array()
|
||||||
);
|
);
|
||||||
|
|
@ -679,12 +682,7 @@ class MailClient
|
||||||
*/
|
*/
|
||||||
public function FolderHash(string $sFolderName) : string
|
public function FolderHash(string $sFolderName) : string
|
||||||
{
|
{
|
||||||
$iCount = 0;
|
list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($sFolderName);
|
||||||
$iUnseenCount = 0;
|
|
||||||
$iUidNext = 0;
|
|
||||||
$iHighestModSeq = 0;
|
|
||||||
|
|
||||||
$this->initFolderValues($sFolderName, $iCount, $iUnseenCount, $iUidNext, $iHighestModSeq);
|
|
||||||
|
|
||||||
return $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq);
|
return $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq);
|
||||||
}
|
}
|
||||||
|
|
@ -1555,11 +1553,6 @@ class MailClient
|
||||||
$mAllSortedUids = null;
|
$mAllSortedUids = null;
|
||||||
$mAllThreads = null;
|
$mAllThreads = null;
|
||||||
|
|
||||||
$iMessageRealCount = 0;
|
|
||||||
$iMessageUnseenCount = 0;
|
|
||||||
$iUidNext = 0;
|
|
||||||
$iHighestModSeq = 0;
|
|
||||||
|
|
||||||
$bUseSortIfSupported = $bUseSortIfSupported && $this->oImapClient->IsSupported('SORT');
|
$bUseSortIfSupported = $bUseSortIfSupported && $this->oImapClient->IsSupported('SORT');
|
||||||
|
|
||||||
$bUseThreadSortIfSupported = $bUseThreadSortIfSupported ?
|
$bUseThreadSortIfSupported = $bUseThreadSortIfSupported ?
|
||||||
|
|
@ -1575,7 +1568,7 @@ class MailClient
|
||||||
$oCacher = null;
|
$oCacher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->initFolderValues($sFolderName, $iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq);
|
list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($sFolderName);
|
||||||
|
|
||||||
if ($bUseFilter)
|
if ($bUseFilter)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue