2020-06-27 22:47:12 +02:00
< ? php
namespace Api {
2021-04-09 16:05:36 +02:00
use Objects\ConnectionData ;
2020-07-01 21:10:25 +02:00
abstract class MailAPI extends Request {
2021-04-09 16:05:36 +02:00
protected function getMailConfig () : ? ConnectionData {
$req = new \Api\Settings\Get ( $this -> user );
$this -> success = $req -> execute ( array ( " key " => " ^mail_ " ));
$this -> lastError = $req -> getLastError ();
if ( $this -> success ) {
$settings = $req -> getResult ()[ " settings " ];
if ( ! isset ( $settings [ " mail_enabled " ]) || $settings [ " mail_enabled " ] !== " 1 " ) {
$this -> createError ( " Mail is not configured yet. " );
return null ;
}
$host = $settings [ " mail_host " ] ? ? " localhost " ;
$port = intval ( $settings [ " mail_port " ] ? ? " 25 " );
$login = $settings [ " mail_username " ] ? ? " " ;
$password = $settings [ " mail_password " ] ? ? " " ;
$connectionData = new ConnectionData ( $host , $port , $login , $password );
$connectionData -> setProperty ( " from " , $settings [ " mail_from " ] ? ? " " );
$connectionData -> setProperty ( " last_sync " , $settings [ " mail_last_sync " ] ? ? " " );
2022-02-20 16:53:26 +01:00
$connectionData -> setProperty ( " mail_footer " , $settings [ " mail_footer " ] ? ? " " );
2021-04-09 16:05:36 +02:00
return $connectionData ;
}
2020-06-27 22:47:12 +02:00
2021-04-09 16:05:36 +02:00
return null ;
}
2020-06-27 22:47:12 +02:00
}
}
namespace Api\Mail {
use Api\MailAPI ;
use Api\Parameter\Parameter ;
use Api\Parameter\StringType ;
2021-12-08 16:53:43 +01:00
use DateTimeInterface ;
2021-04-09 16:05:36 +02:00
use Driver\SQL\Column\Column ;
use Driver\SQL\Condition\Compare ;
2021-04-10 00:44:34 +02:00
use Driver\SQL\Condition\CondIn ;
2021-04-09 16:05:36 +02:00
use Driver\SQL\Strategy\UpdateStrategy ;
2020-06-27 22:47:12 +02:00
use External\PHPMailer\Exception ;
use External\PHPMailer\PHPMailer ;
2021-04-09 20:59:36 +02:00
use Objects\ConnectionData ;
2022-02-20 16:53:26 +01:00
use Objects\GpgKey ;
2020-06-27 22:47:12 +02:00
use Objects\User ;
class Test extends MailAPI {
public function __construct ( User $user , bool $externalCall = false ) {
parent :: __construct ( $user , $externalCall , array (
2022-02-20 16:53:26 +01:00
" receiver " => new Parameter ( " receiver " , Parameter :: TYPE_EMAIL ),
" gpgFingerprint " => new StringType ( " gpgFingerprint " , 64 , true , null )
2020-06-27 22:47:12 +02:00
));
}
2022-02-21 13:01:03 +01:00
public function _execute () : bool {
2020-06-27 22:47:12 +02:00
$receiver = $this -> getParam ( " receiver " );
$req = new \Api\Mail\Send ( $this -> user );
$this -> success = $req -> execute ( array (
" to " => $receiver ,
" subject " => " Test E-Mail " ,
2022-02-20 16:53:26 +01:00
" body " => " Hey! If you receive this e-mail, your mail configuration seems to be working. " ,
" gpgFingerprint " => $this -> getParam ( " gpgFingerprint " ),
" asnyc " => false
2020-06-27 22:47:12 +02:00
));
$this -> lastError = $req -> getLastError ();
return $this -> success ;
}
}
2022-02-20 16:53:26 +01:00
// TODO: expired gpg keys?
2020-06-27 22:47:12 +02:00
class Send extends MailAPI {
public function __construct ( $user , $externalCall = false ) {
parent :: __construct ( $user , $externalCall , array (
2021-04-09 16:05:36 +02:00
'to' => new Parameter ( 'to' , Parameter :: TYPE_EMAIL , true , null ),
'subject' => new StringType ( 'subject' , - 1 ),
2020-06-27 22:47:12 +02:00
'body' => new StringType ( 'body' , - 1 ),
2021-04-09 16:05:36 +02:00
'replyTo' => new Parameter ( 'replyTo' , Parameter :: TYPE_EMAIL , true , null ),
2022-02-20 16:53:26 +01:00
'replyName' => new StringType ( 'replyName' , 32 , true , " " ),
" gpgFingerprint " => new StringType ( " gpgFingerprint " , 64 , true , null ),
'async' => new Parameter ( " async " , Parameter :: TYPE_BOOLEAN , true , true )
2020-06-27 22:47:12 +02:00
));
$this -> isPublic = false ;
}
2022-02-21 13:01:03 +01:00
public function _execute () : bool {
2020-06-27 22:47:12 +02:00
$mailConfig = $this -> getMailConfig ();
if ( ! $this -> success ) {
return false ;
}
2021-04-09 16:05:36 +02:00
$fromMail = $mailConfig -> getProperty ( 'from' );
2022-02-20 16:53:26 +01:00
$mailFooter = $mailConfig -> getProperty ( 'mail_footer' );
2021-04-09 16:05:36 +02:00
$toMail = $this -> getParam ( 'to' ) ? ? $fromMail ;
$subject = $this -> getParam ( 'subject' );
$replyTo = $this -> getParam ( 'replyTo' );
$replyName = $this -> getParam ( 'replyName' );
2021-12-08 16:53:43 +01:00
$body = $this -> getParam ( 'body' );
2022-02-20 16:53:26 +01:00
$gpgFingerprint = $this -> getParam ( " gpgFingerprint " );
if ( $this -> getParam ( " async " )) {
$sql = $this -> user -> getSQL ();
$this -> success = $sql -> insert ( " MailQueue " , [ " from " , " to " , " subject " , " body " ,
" replyTo " , " replyName " , " gpgFingerprint " ])
-> addRow ( $fromMail , $toMail , $subject , $body , $replyTo , $replyName , $gpgFingerprint )
-> execute () !== false ;
$this -> lastError = $sql -> getLastError ();
return $this -> success ;
}
2021-12-08 16:53:43 +01:00
if ( stripos ( $body , " <body " ) === false ) {
$body = " <body> $body </body> " ;
}
if ( stripos ( $body , " <html " ) === false ) {
$body = " <html> $body </html> " ;
}
2021-04-09 16:05:36 +02:00
2022-02-20 16:53:26 +01:00
if ( ! empty ( $mailFooter )) {
$email_signature = realpath ( WEBROOT . DIRECTORY_SEPARATOR . $mailFooter );
if ( is_file ( $email_signature )) {
$email_signature = file_get_contents ( $email_signature );
$body .= $email_signature ;
}
}
2020-06-27 22:47:12 +02:00
try {
$mail = new PHPMailer ;
$mail -> IsSMTP ();
2021-04-09 16:05:36 +02:00
$mail -> setFrom ( $fromMail );
$mail -> addAddress ( $toMail );
if ( $replyTo ) {
$mail -> addReplyTo ( $replyTo , $replyName );
}
$mail -> Subject = $subject ;
2020-06-27 22:47:12 +02:00
$mail -> SMTPDebug = 0 ;
$mail -> Host = $mailConfig -> getHost ();
$mail -> Port = $mailConfig -> getPort ();
$mail -> SMTPAuth = true ;
2022-02-20 16:53:26 +01:00
$mail -> Timeout = 15 ;
2020-06-27 22:47:12 +02:00
$mail -> Username = $mailConfig -> getLogin ();
$mail -> Password = $mailConfig -> getPassword ();
$mail -> SMTPSecure = 'tls' ;
$mail -> CharSet = 'UTF-8' ;
2022-02-20 16:53:26 +01:00
if ( $gpgFingerprint ) {
$encryptedHeaders = implode ( " \r \n " , [
" Date: " . ( new \DateTime ()) -> format ( DateTimeInterface :: RFC2822 ),
" Content-Type: text/html " ,
" Content-Transfer-Encoding: quoted-printable "
]);
$mimeBody = $encryptedHeaders . " \r \n \r \n " . quoted_printable_encode ( $body );
$res = GpgKey :: encrypt ( $mimeBody , $gpgFingerprint );
if ( $res [ " success " ]) {
$encryptedBody = $res [ " data " ];
$mail -> AltBody = '' ;
$mail -> Body = '' ;
$mail -> AllowEmpty = true ;
$mail -> ContentType = PHPMailer :: CONTENT_TYPE_MULTIPART_ENCRYPTED ;
$mail -> addStringAttachment ( " Version: 1 " , null , PHPMailer :: ENCODING_BASE64 , " application/pgp-encrypted " , " " );
$mail -> addStringAttachment ( $encryptedBody , " encrypted.asc " , PHPMailer :: ENCODING_7BIT , " application/octet-stream " , " " );
} else {
return $this -> createError ( $res [ " error " ]);
}
} else {
$mail -> msgHTML ( $body );
$mail -> AltBody = strip_tags ( $body );
}
2020-06-27 22:47:12 +02:00
$this -> success = @ $mail -> Send ();
if ( ! $this -> success ) {
$this -> lastError = " Error sending Mail: $mail->ErrorInfo " ;
error_log ( " sendMail() failed: $mail->ErrorInfo " );
2021-04-09 16:05:36 +02:00
} else {
$this -> result [ " messageId " ] = $mail -> getLastMessageID ();
2020-06-27 22:47:12 +02:00
}
} catch ( Exception $e ) {
$this -> success = false ;
$this -> lastError = " Error sending Mail: $e " ;
}
return $this -> success ;
}
}
2021-04-09 16:05:36 +02:00
2021-04-09 20:59:36 +02:00
// TODO: IMAP mail settings :(
2021-04-10 00:44:34 +02:00
// TODO: attachments
2021-04-09 16:05:36 +02:00
class Sync extends MailAPI {
public function __construct ( User $user , bool $externalCall = false ) {
parent :: __construct ( $user , $externalCall , array ());
2021-04-10 00:44:34 +02:00
$this -> loginRequired = true ;
2021-04-09 16:05:36 +02:00
}
private function fetchMessageIds () {
$sql = $this -> user -> getSQL ();
$res = $sql -> select ( " uid " , " messageId " )
-> from ( " ContactRequest " )
-> where ( new Compare ( " messageId " , NULL , " != " ))
-> execute ();
$this -> success = ( $res !== false );
$this -> lastError = $sql -> getLastError ();
if ( ! $this -> success ) {
return false ;
}
$messageIds = [];
foreach ( $res as $row ) {
$messageIds [ $row [ " messageId " ]] = $row [ " uid " ];
}
return $messageIds ;
}
private function findContactRequest ( array & $messageIds , array & $references ) : ? int {
foreach ( $references as & $ref ) {
if ( isset ( $messageIds [ $ref ])) {
return $messageIds [ $ref ];
}
}
return null ;
}
private function parseBody ( string $body ) : string {
// TODO clean this up
return trim ( $body );
}
private function insertMessages ( $messages ) : bool {
$sql = $this -> user -> getSQL ();
2021-04-10 00:44:34 +02:00
$query = $sql -> insert ( " ContactMessage " , [ " request_id " , " user_id " , " message " , " messageId " , " created_at " ])
2021-04-10 01:33:40 +02:00
-> onDuplicateKeyStrategy ( new UpdateStrategy ([ " messageId " ], [ " message " => new Column ( " message " )]));
2021-04-09 16:05:36 +02:00
2021-04-10 00:44:34 +02:00
$entityIds = [];
2021-04-09 16:05:36 +02:00
foreach ( $messages as $message ) {
2021-04-10 00:44:34 +02:00
$requestId = $message [ " requestId " ];
2021-04-09 16:05:36 +02:00
$query -> addRow (
2021-04-10 00:44:34 +02:00
$requestId ,
2021-04-09 16:05:36 +02:00
$sql -> select ( " uid " ) -> from ( " User " ) -> where ( new Compare ( " email " , $message [ " from " ])) -> limit ( 1 ),
$message [ " body " ],
2021-04-10 00:44:34 +02:00
$message [ " messageId " ],
( new \DateTime ()) -> setTimeStamp ( $message [ " timestamp " ]),
2021-04-09 16:05:36 +02:00
);
2021-04-10 00:44:34 +02:00
if ( ! in_array ( $requestId , $entityIds )) {
$entityIds [] = $requestId ;
}
2021-04-09 16:05:36 +02:00
}
$this -> success = $query -> execute ();
$this -> lastError = $sql -> getLastError ();
2021-04-10 00:44:34 +02:00
// Update entity log
if ( $this -> success && count ( $entityIds ) > 0 ) {
$sql -> update ( " EntityLog " )
-> set ( " modified " , $sql -> now ())
2021-12-08 16:53:43 +01:00
-> where ( new CondIn ( new Column ( " entityId " ), $entityIds ))
2021-04-10 00:44:34 +02:00
-> execute ();
}
2021-04-09 16:05:36 +02:00
return $this -> success ;
}
2021-04-09 16:30:49 +02:00
private function parseDate ( $date ) {
2021-04-09 20:59:36 +02:00
$formats = [ null , " D M d Y H:i:s e+ " , " D, j M Y H:i:s e+ " ];
2021-04-09 16:30:49 +02:00
foreach ( $formats as $format ) {
try {
2021-04-09 20:59:36 +02:00
$dateObj = ( $format === null ? new \DateTime ( $date ) : \DateTime :: createFromFormat ( $format , $date ));
if ( $dateObj ) {
return $dateObj ;
}
2021-04-09 16:30:49 +02:00
} catch ( \Exception $exception ) {
}
}
return $this -> createError ( " Could not parse date: $date " );
}
2021-04-09 20:59:36 +02:00
private function getReference ( ConnectionData $mailConfig ) : string {
2021-04-09 16:05:36 +02:00
$port = 993 ;
$host = str_replace ( " smtp " , " imap " , $mailConfig -> getHost ());
$flags = [ " /ssl " ];
2021-04-09 20:59:36 +02:00
return '{' . $host . ':' . $port . implode ( " " , $flags ) . '}' ;
}
private function connect ( ConnectionData $mailConfig ) {
2021-04-09 16:05:36 +02:00
2021-04-09 20:59:36 +02:00
$username = $mailConfig -> getLogin ();
$password = $mailConfig -> getPassword ();
$ref = $this -> getReference ( $mailConfig );
$mbox = @ imap_open ( $ref , $username , $password , OP_READONLY );
2021-04-09 16:05:36 +02:00
if ( ! $mbox ) {
return $this -> createError ( " Can't connect to mail server via IMAP: " . imap_last_error ());
}
2021-04-09 20:59:36 +02:00
return $mbox ;
}
private function listFolders ( ConnectionData $mailConfig , $mbox ) {
$boxes = @ imap_list ( $mbox , $this -> getReference ( $mailConfig ), '*' );
if ( ! $boxes ) {
return $this -> createError ( " Error listing imap folders: " . imap_last_error ());
2021-04-09 16:05:36 +02:00
}
2021-04-09 20:59:36 +02:00
return $boxes ;
}
2021-04-10 00:44:34 +02:00
private function getSenderAddress ( $header ) : string {
if ( property_exists ( $header , " reply_to " ) && count ( $header -> reply_to ) > 0 ) {
$mailBox = $header -> reply_to [ 0 ] -> mailbox ;
$host = $header -> reply_to [ 0 ] -> host ;
} else if ( property_exists ( $header , " from " ) && count ( $header -> from ) > 0 ) {
$mailBox = $header -> from [ 0 ] -> mailbox ;
$host = $header -> from [ 0 ] -> host ;
} else {
return " unknown_addr " ;
}
return " $mailBox @ $host " ;
}
2021-04-09 21:03:43 +02:00
private function runSearch ( $mbox , string $searchCriteria , ? \DateTime $lastSyncDateTime , array $messageIds , array & $messages ) {
2021-04-10 01:33:40 +02:00
2021-04-09 16:05:36 +02:00
$result = @ imap_search ( $mbox , $searchCriteria );
if ( $result === false ) {
2021-04-09 21:03:43 +02:00
$err = imap_last_error (); // might return false, if not messages were found, so we can just abort without throwing an error
return empty ( $err ) ? true : $this -> createError ( " Could not run search: $err " );
2021-04-09 16:05:36 +02:00
}
foreach ( $result as $msgNo ) {
$header = imap_headerinfo ( $mbox , $msgNo );
2021-04-10 01:33:40 +02:00
$date = $this -> parseDate ( $header -> date );
2021-04-09 16:30:49 +02:00
if ( $date === false ) {
return false ;
}
2021-04-09 20:59:36 +02:00
if ( $lastSyncDateTime === null || \datetimeDiff ( $lastSyncDateTime , $date ) > 0 ) {
2021-04-09 16:05:36 +02:00
$references = property_exists ( $header , " references " ) ?
explode ( " " , $header -> references ) : [];
$requestId = $this -> findContactRequest ( $messageIds , $references );
if ( $requestId ) {
$messageId = $header -> message_id ;
2021-04-10 00:44:34 +02:00
$senderAddress = $this -> getSenderAddress ( $header );
2021-04-09 16:05:36 +02:00
$structure = imap_fetchstructure ( $mbox , $msgNo );
$attachments = [];
$hasAttachments = ( property_exists ( $structure , " parts " ));
if ( $hasAttachments ) {
foreach ( $structure -> parts as $part ) {
$disposition = ( property_exists ( $part , " disposition " ) ? $part -> disposition : null );
if ( $disposition === " attachment " ) {
2021-04-10 01:33:40 +02:00
$fileName = array_filter ( $part -> dparameters , function ( $param ) {
return $param -> attribute === " filename " ;
});
2021-04-09 16:05:36 +02:00
if ( count ( $fileName ) > 0 ) {
$attachments [] = $fileName [ 0 ] -> value ;
}
}
}
}
$body = imap_fetchbody ( $mbox , $msgNo , " 1 " );
$body = $this -> parseBody ( $body );
2021-04-10 01:33:40 +02:00
if ( ! isset ( $messageId [ $messageId ])) {
$messages [ $messageId ] = [
" messageId " => $messageId ,
" requestId " => $requestId ,
" timestamp " => $date -> getTimestamp (),
" from " => $senderAddress ,
" body " => $body ,
" attachments " => $attachments
];
}
2021-04-09 16:05:36 +02:00
}
}
}
2021-04-09 20:59:36 +02:00
return true ;
}
2022-02-21 13:01:03 +01:00
public function _execute () : bool {
2021-04-09 20:59:36 +02:00
if ( ! function_exists ( " imap_open " )) {
return $this -> createError ( " IMAP is not enabled. Enable it inside the php config. For more information visit: https://www.php.net/manual/en/imap.setup.php " );
}
$messageIds = $this -> fetchMessageIds ();
if ( $messageIds === false ) {
return false ;
} else if ( count ( $messageIds ) === 0 ) {
// nothing to sync here
return true ;
}
$mailConfig = $this -> getMailConfig ();
if ( ! $this -> success ) {
return false ;
}
$mbox = $this -> connect ( $mailConfig );
if ( $mbox === false ) {
return false ;
}
$boxes = $this -> listFolders ( $mailConfig , $mbox );
if ( $boxes === false ) {
return false ;
}
$now = ( new \DateTime ()) -> getTimestamp ();
$lastSync = intval ( $mailConfig -> getProperty ( " last_sync " , " 0 " ));
if ( $lastSync > 0 ) {
$lastSyncDateTime = ( new \DateTime ()) -> setTimeStamp ( $lastSync );
$dateStr = $lastSyncDateTime -> format ( " d-M-Y " );
$searchCriteria = " SINCE \" $dateStr\ " " ;
} else {
$lastSyncDateTime = null ;
$searchCriteria = " ALL " ;
}
$messages = [];
foreach ( $boxes as $box ) {
imap_reopen ( $mbox , $box );
2021-04-10 01:33:40 +02:00
if ( ! $this -> runSearch ( $mbox , $searchCriteria , $lastSyncDateTime , $messageIds , $messages )) {
2021-04-09 20:59:36 +02:00
return false ;
}
}
2021-04-09 16:05:36 +02:00
@ imap_close ( $mbox );
2021-04-09 16:30:49 +02:00
if ( ! empty ( $messages ) && ! $this -> insertMessages ( $messages )) {
2021-04-09 16:05:36 +02:00
return false ;
}
$req = new \Api\Settings\Set ( $this -> user );
$this -> success = $req -> execute ( array ( " settings " => array ( " mail_last_sync " => " $now " )));
$this -> lastError = $req -> getLastError ();
return $this -> success ;
}
}
2022-02-20 16:53:26 +01:00
class SendQueue extends MailAPI {
public function __construct ( User $user , bool $externalCall = false ) {
parent :: __construct ( $user , $externalCall , [
" debug " => new Parameter ( " debug " , Parameter :: TYPE_BOOLEAN , true , false )
]);
$this -> isPublic = false ;
}
2022-02-21 13:01:03 +01:00
public function _execute () : bool {
2022-02-20 16:53:26 +01:00
$debug = $this -> getParam ( " debug " );
$startTime = time ();
if ( $debug ) {
echo " Start of processing mail queue at $startTime " . PHP_EOL ;
}
$sql = $this -> user -> getSQL ();
$res = $sql -> select ( " uid " , " from " , " to " , " subject " , " body " ,
" replyTo " , " replyName " , " gpgFingerprint " , " retryCount " )
-> from ( " MailQueue " )
-> where ( new Compare ( " retryCount " , 0 , " > " ))
-> where ( new Compare ( " status " , " waiting " ))
-> where ( new Compare ( " nextTry " , $sql -> now (), " <= " ))
-> execute ();
$this -> success = ( $res !== false );
$this -> lastError = $sql -> getLastError ();
if ( $this -> success && is_array ( $res )) {
if ( $debug ) {
echo " Found " . count ( $res ) . " mails to send " . PHP_EOL ;
}
$successfulMails = [];
foreach ( $res as $row ) {
if ( time () - $startTime >= 45 ) {
$this -> lastError = " Not able to process whole mail queue within 45 seconds, will continue on next time " ;
break ;
}
$to = $row [ " to " ];
$subject = $row [ " subject " ];
if ( $debug ) {
echo " Sending subject= $subject to= $to " . PHP_EOL ;
}
$mailId = intval ( $row [ " uid " ]);
$retryCount = intval ( $row [ " retryCount " ]);
$req = new Send ( $this -> user );
$args = [
" to " => $to ,
" subject " => $subject ,
" body " => $row [ " body " ],
" replyTo " => $row [ " replyTo " ],
" replyName " => $row [ " replyName " ],
" gpgFingerprint " => $row [ " gpgFingerprint " ],
" async " => false
];
$success = $req -> execute ( $args );
$error = $req -> getLastError ();
if ( ! $success ) {
$delay = [ 0 , 720 , 360 , 60 , 30 , 1 ];
$minutes = $delay [ max ( 0 , min ( count ( $delay ) - 1 , $retryCount ))];
$nextTry = ( new \DateTime ()) -> modify ( " + $minutes minute " );
$sql -> update ( " MailQueue " )
-> set ( " retryCount " , $retryCount - 1 )
-> set ( " status " , " error " )
-> set ( " errorMessage " , $error )
-> set ( " nextTry " , $nextTry )
-> where ( new Compare ( " uid " , $mailId ))
-> execute ();
} else {
$successfulMails [] = $mailId ;
}
}
$this -> success = count ( $successfulMails ) === count ( $res );
if ( ! empty ( $successfulMails )) {
$res = $sql -> update ( " MailQueue " )
-> set ( " status " , " success " )
-> where ( new CondIn ( new Column ( " uid " ), $successfulMails ))
-> execute ();
$this -> success = $res !== false ;
$this -> lastError = $sql -> getLastError ();
}
}
return $this -> success ;
}
}
2020-06-27 22:47:12 +02:00
}