2023-12-18 15:05:15 +01:00
< ? php
/**
* Invoice Ninja ( https :// invoiceninja . com ) .
*
* @ link https :// github . com / invoiceninja / invoiceninja source repository
*
* @ copyright Copyright ( c ) 2023. Invoice Ninja LLC ( https :// invoiceninja . com )
*
* @ license https :// www . elastic . co / licensing / elastic - license
*/
2024-03-25 06:41:22 +01:00
namespace App\Services\InboundMail ;
2023-12-18 15:05:15 +01:00
use App\Factory\ExpenseFactory ;
2024-03-24 11:40:17 +01:00
use App\Jobs\Util\SystemLogger ;
2023-12-18 15:05:15 +01:00
use App\Libraries\MultiDB ;
2024-03-25 06:41:22 +01:00
use App\Models\ClientContact ;
2024-05-19 06:55:15 +02:00
use App\Models\Company ;
2024-03-24 11:40:17 +01:00
use App\Models\SystemLog ;
2023-12-18 15:05:15 +01:00
use App\Models\VendorContact ;
2024-06-22 15:33:53 +02:00
use App\Services\EDocument\Imports\ParseEDocument ;
2024-03-25 06:41:22 +01:00
use App\Services\InboundMail\InboundMail ;
2023-12-18 15:05:15 +01:00
use App\Utils\TempFile ;
use App\Utils\Traits\GeneratesCounter ;
use App\Utils\Traits\SavesDocuments ;
use App\Utils\Traits\MakesHash ;
2023-12-18 17:21:15 +01:00
use Cache ;
2023-12-18 15:05:15 +01:00
use Illuminate\Queue\SerializesModels ;
2024-03-25 06:41:22 +01:00
class InboundMailEngine
2023-12-18 15:05:15 +01:00
{
2024-03-18 08:04:54 +01:00
use SerializesModels , MakesHash ;
2023-12-18 15:05:15 +01:00
use GeneratesCounter , SavesDocuments ;
2023-12-18 17:21:15 +01:00
private ? bool $isUnknownRecipent = null ;
2024-05-19 06:59:37 +02:00
private array $globalBlacklist = explode ( " , " , config ( 'global_inbound_blocklist' ));
private array $globalWhitelist = explode ( " , " , config ( 'global_inbound_whitelist' )); // only for global validation, not for allowing to send something into the company, should be used to disabled blocking for mass-senders
2024-04-03 07:57:23 +02:00
public function __construct ()
2023-12-18 15:05:15 +01:00
{
}
/**
2023-12-19 08:51:50 +01:00
* if there is not a company with an matching mailbox , we only do monitoring
2023-12-18 17:21:15 +01:00
* reuse this method to add more mail - parsing behaviors
2023-12-18 15:05:15 +01:00
*/
2024-04-07 13:58:10 +02:00
public function handleExpenseMailbox ( InboundMail $email )
2023-12-18 15:05:15 +01:00
{
2024-04-03 08:06:39 +02:00
if ( $this -> isInvalidOrBlocked ( $email -> from , $email -> to ))
2023-12-18 17:21:15 +01:00
return ;
2024-03-18 08:04:54 +01:00
2023-12-18 15:05:15 +01:00
// Expense Mailbox => will create an expense
2024-04-04 07:27:07 +02:00
$company = MultiDB :: findAndSetDbByExpenseMailbox ( $email -> to );
2024-04-07 13:58:10 +02:00
if ( ! $company ) {
$this -> saveMeta ( $email -> from , $email -> to , true );
return ;
2023-12-18 15:05:15 +01:00
}
2024-05-22 07:07:27 +02:00
$this -> createExpenses ( $company , $email );
2024-04-07 13:58:10 +02:00
$this -> saveMeta ( $email -> from , $email -> to );
2023-12-18 15:05:15 +01:00
}
2023-12-18 17:21:15 +01:00
// SPAM Protection
2024-04-03 08:06:39 +02:00
public function isInvalidOrBlocked ( string $from , string $to )
2023-12-18 17:21:15 +01:00
{
// invalid email
2024-04-03 08:06:39 +02:00
if ( ! filter_var ( $from , FILTER_VALIDATE_EMAIL )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because from e-mail has the wrong format: ' . $from );
2023-12-18 17:21:15 +01:00
return true ;
}
2024-04-07 16:08:34 +02:00
if ( ! filter_var ( $to , FILTER_VALIDATE_EMAIL )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because to e-mail has the wrong format: ' . $from );
2024-04-07 16:08:34 +02:00
return true ;
}
2023-12-18 17:21:15 +01:00
2024-04-03 08:06:39 +02:00
$parts = explode ( '@' , $from );
2023-12-18 17:21:15 +01:00
$domain = array_pop ( $parts );
// global blacklist
2024-05-19 06:55:15 +02:00
if ( in_array ( $from , $this -> globalWhitelist )) {
2024-04-07 16:08:34 +02:00
return false ;
}
2024-05-19 06:55:15 +02:00
if ( in_array ( $domain , $this -> globalWhitelist )) {
return false ;
}
if ( in_array ( $domain , $this -> globalBlacklist )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because the domain was found on globalBlocklistDomains: ' . $from );
2024-03-24 11:00:29 +01:00
return true ;
}
2024-05-19 06:55:15 +02:00
if ( in_array ( $from , $this -> globalBlacklist )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because the email was found on globalBlocklistEmails: ' . $from );
2023-12-18 17:21:15 +01:00
return true ;
}
2024-04-03 08:06:39 +02:00
if ( Cache :: has ( 'inboundMailBlockedSender:' . $from )) { // was marked as blocked before, so we block without any console output
2023-12-18 17:21:15 +01:00
return true ;
}
// sender occured in more than 500 emails in the last 12 hours
2024-04-07 16:08:34 +02:00
$senderMailCountTotal = Cache :: get ( 'inboundMailCountSender:' . $from , 0 );
2024-05-19 06:31:26 +02:00
if ( $senderMailCountTotal >= config ( 'global_inbound_sender_permablock_mailcount' )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked permanent, because the sender sended more than ' . $senderMailCountTotal . ' emails in the last 12 hours: ' . $from );
2024-04-03 08:06:39 +02:00
$this -> blockSender ( $from );
$this -> saveMeta ( $from , $to );
2023-12-18 17:21:15 +01:00
return true ;
}
2024-05-19 06:31:26 +02:00
if ( $senderMailCountTotal >= config ( 'global_inbound_sender_block_mailcount' )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because the sender sended more than ' . $senderMailCountTotal . ' emails in the last 12 hours: ' . $from );
2024-04-03 08:06:39 +02:00
$this -> saveMeta ( $from , $to );
2023-12-18 17:21:15 +01:00
return true ;
}
// sender sended more than 50 emails to the wrong mailbox in the last 6 hours
2024-04-07 16:08:34 +02:00
$senderMailCountUnknownRecipent = Cache :: get ( 'inboundMailCountSenderUnknownRecipent:' . $from , 0 );
2024-05-19 06:31:26 +02:00
if ( $senderMailCountUnknownRecipent >= config ( 'company_inbound_sender_block_unknown_reciepent' )) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because the sender sended more than ' . $senderMailCountUnknownRecipent . ' emails to the wrong mailbox in the last 6 hours: ' . $from );
2024-04-03 08:06:39 +02:00
$this -> saveMeta ( $from , $to );
2023-12-18 17:21:15 +01:00
return true ;
}
// wrong recipent occurs in more than 100 emails in the last 12 hours, so the processing is blocked
2024-04-07 16:08:34 +02:00
$mailCountUnknownRecipent = Cache :: get ( 'inboundMailCountUnknownRecipent:' . $to , 0 ); // @turbo124 maybe use many to save resources in case of spam with multiple to addresses each time
if ( $mailCountUnknownRecipent >= 200 ) {
2024-04-24 08:40:58 +02:00
nlog ( 'E-Mail blocked, because anyone sended more than ' . $mailCountUnknownRecipent . ' emails to the wrong mailbox in the last 12 hours. Current sender was blocked as well: ' . $from );
2024-04-03 08:06:39 +02:00
$this -> blockSender ( $from );
$this -> saveMeta ( $from , $to );
2024-03-18 08:04:54 +01:00
return true ;
2023-12-18 17:21:15 +01:00
}
return false ;
}
2024-04-03 07:57:23 +02:00
public function blockSender ( string $from )
2023-12-18 17:21:15 +01:00
{
2024-04-03 07:57:23 +02:00
Cache :: add ( 'inboundMailBlockedSender:' . $from , true , now () -> addHours ( 12 ));
2023-12-18 17:21:15 +01:00
2023-12-18 17:24:59 +01:00
// TODO: ignore, when known sender (for heavy email-usage mostly on isHosted())
2023-12-18 17:21:15 +01:00
// TODO: handle external blocking
}
2024-04-03 07:57:23 +02:00
public function saveMeta ( string $from , string $to , bool $isUnknownRecipent = false )
2023-12-18 17:21:15 +01:00
{
// save cache
2024-04-07 16:08:34 +02:00
Cache :: add ( 'inboundMailCountSender:' . $from , 0 , now () -> addHours ( 12 ));
Cache :: increment ( 'inboundMailCountSender:' . $from );
2023-12-18 17:21:15 +01:00
2024-04-03 07:57:23 +02:00
if ( $isUnknownRecipent ) {
2024-04-07 16:08:34 +02:00
Cache :: add ( 'inboundMailCountSenderUnknownRecipent:' . $from , 0 , now () -> addHours ( 6 ));
Cache :: increment ( 'inboundMailCountSenderUnknownRecipent:' . $from ); // we save the sender, to may block him
2023-12-18 17:21:15 +01:00
2024-04-07 16:08:34 +02:00
Cache :: add ( 'inboundMailCountUnknownRecipent:' . $to , 0 , now () -> addHours ( 12 ));
Cache :: increment ( 'inboundMailCountUnknownRecipent:' . $to ); // we save the sender, to may block him
2023-12-18 17:21:15 +01:00
}
}
2023-12-18 17:24:59 +01:00
// MAIN-PROCESSORS
2024-05-22 07:07:27 +02:00
protected function createExpenses ( Company $company , InboundMail $email )
2023-12-18 17:24:59 +01:00
{
2023-12-19 08:51:50 +01:00
// Skipping executions: will not result in not saving Metadata to prevent usage of these conditions, to spam
2024-04-04 07:27:07 +02:00
if ( ! ( $company ? -> expense_mailbox_active ? : false )) {
2024-04-03 07:57:23 +02:00
$this -> logBlocked ( $company , 'mailbox not active for this company. from: ' . $email -> from );
2023-12-19 08:51:50 +01:00
return ;
}
2024-04-03 07:57:23 +02:00
if ( ! $this -> validateExpenseSender ( $company , $email )) {
$this -> logBlocked ( $company , 'invalid sender of an ingest email for this company. from: ' . $email -> from );
2023-12-18 17:24:59 +01:00
return ;
}
2024-04-03 07:57:23 +02:00
if ( sizeOf ( $email -> documents ) == 0 ) {
$this -> logBlocked ( $company , 'email does not contain any attachments and is likly not an expense. from: ' . $email -> from );
2023-12-19 08:51:50 +01:00
return ;
}
2023-12-18 17:24:59 +01:00
2024-06-22 15:33:53 +02:00
/** @var \App\Models\Expense $expense */
$expense = null ;
2023-12-18 17:24:59 +01:00
2024-06-22 15:33:53 +02:00
// check documents for EDocument xml
2024-05-22 07:07:27 +02:00
foreach ( $email -> documents as $document ) {
2024-06-22 15:33:53 +02:00
// check if document can be parsed to an expense
try {
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
$expense_obj = ( new ParseEDocument ( $document -> get (), $document -> getFilename ())) -> run ();
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
// throw error, when multiple parseable files are registered
if ( $expense && $expense_obj )
throw new \Exception ( 'Multiple parseable Invoice documents found in email. Please use only one Invoice document per email.' );
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
$expense = $expense_obj ;
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
} catch ( \Exception $err ) {
// throw error, only, when its not expected
switch ( true ) {
case ( $err -> getMessage () === 'E-Invoice standard not supported' ) :
case ( $err -> getMessage () === 'File type not supported' ) :
break ;
default :
2024-05-22 07:07:27 +02:00
throw $err ;
}
}
2024-06-22 15:33:53 +02:00
}
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
// populate missing data with data from email
if ( ! $expense )
2024-05-22 07:07:27 +02:00
$expense = ExpenseFactory :: create ( $company -> id , $company -> owner () -> id );
2024-06-22 15:33:53 +02:00
if ( ! $expense -> public_notes )
2024-05-22 07:07:27 +02:00
$expense -> public_notes = $email -> subject ;
2024-06-22 15:33:53 +02:00
if ( ! $expense -> private_notes )
2024-05-22 07:07:27 +02:00
$expense -> private_notes = $email -> text_body ;
2024-06-22 15:33:53 +02:00
if ( ! $expense -> date )
2024-05-22 07:07:27 +02:00
$expense -> date = $email -> date ;
2024-06-22 15:33:53 +02:00
if ( ! $expense -> vendor_id ) {
$expense_vendor = $this -> getVendor ( $company , $email );
2024-05-22 07:07:27 +02:00
if ( $expense_vendor )
$expense -> vendor_id = $expense_vendor -> id ;
2024-06-22 15:33:53 +02:00
}
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
// handle documents
$documents = [];
array_push ( $documents , $document );
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
// handle email document
$this -> processHtmlBodyToDocument ( $email );
if ( $email -> body_document !== null )
array_push ( $documents , $email -> body_document );
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
$expense -> saveQuietly ();
2024-05-22 07:07:27 +02:00
2024-06-22 15:33:53 +02:00
$this -> saveDocuments ( $documents , $expense );
2024-05-22 07:07:27 +02:00
2023-12-18 17:24:59 +01:00
}
2023-12-18 17:21:15 +01:00
// HELPERS
2024-04-03 07:57:23 +02:00
private function processHtmlBodyToDocument ( InboundMail $email )
2023-12-18 15:05:15 +01:00
{
2024-04-03 07:57:23 +02:00
if ( $email -> body !== null )
$email -> body_document = TempFile :: UploadedFileFromRaw ( $email -> body , " E-Mail.html " , " text/html " );
2023-12-18 15:05:15 +01:00
}
2024-04-03 07:57:23 +02:00
private function validateExpenseSender ( Company $company , InboundMail $email )
2023-12-18 15:05:15 +01:00
{
2024-04-03 07:57:23 +02:00
$parts = explode ( '@' , $email -> from );
2023-12-18 15:05:15 +01:00
$domain = array_pop ( $parts );
// whitelists
2024-05-19 06:55:15 +02:00
$whitelist = explode ( " , " , $company -> inbound_mailbox_whitelist );
if ( in_array ( $email -> from , $whitelist ))
2023-12-18 15:05:15 +01:00
return true ;
2024-05-19 06:55:15 +02:00
if ( in_array ( $domain , $whitelist ))
2023-12-18 15:05:15 +01:00
return true ;
2024-05-19 06:55:15 +02:00
$blacklist = explode ( " , " , $company -> inbound_mailbox_blacklist );
if ( in_array ( $email -> from , $blacklist ))
2024-03-19 07:55:55 +01:00
return false ;
2024-05-19 06:55:15 +02:00
if ( in_array ( $domain , $blacklist ))
2024-03-19 07:55:55 +01:00
return false ;
// allow unknown
2024-04-03 07:57:23 +02:00
if ( $company -> inbound_mailbox_allow_unknown )
2023-12-18 15:05:15 +01:00
return true ;
// own users
2024-04-03 07:57:23 +02:00
if ( $company -> inbound_mailbox_allow_company_users && $company -> users () -> where ( " email " , $email -> from ) -> exists ())
2023-12-18 15:05:15 +01:00
return true ;
2024-03-25 07:08:41 +01:00
// from vendors
2024-04-03 08:20:36 +02:00
if ( $company -> inbound_mailbox_allow_vendors && VendorContact :: where ( " company_id " , $company -> id ) -> where ( " email " , $email -> from ) -> exists ())
2024-03-25 06:41:22 +01:00
return true ;
2024-03-25 07:08:41 +01:00
// from clients
2024-04-03 08:20:36 +02:00
if ( $company -> inbound_mailbox_allow_clients && ClientContact :: where ( " company_id " , $company -> id ) -> where ( " email " , $email -> from ) -> exists ())
2023-12-18 15:05:15 +01:00
return true ;
// denie
return false ;
}
2024-04-03 07:57:23 +02:00
private function getClient ( Company $company , InboundMail $email )
2024-03-25 06:41:22 +01:00
{
2024-04-03 07:57:23 +02:00
$clientContact = ClientContact :: where ( " company_id " , $company -> id ) -> where ( " email " , $email -> from ) -> first ();
2024-04-03 15:01:57 +02:00
if ( ! $clientContact )
return null ;
2024-03-25 06:41:22 +01:00
2024-04-03 15:01:57 +02:00
return $clientContact -> client ();
2024-03-25 06:41:22 +01:00
}
2024-04-03 07:57:23 +02:00
private function getVendor ( Company $company , InboundMail $email )
2023-12-18 15:05:15 +01:00
{
2024-04-03 08:33:40 +02:00
$vendorContact = VendorContact :: where ( " company_id " , $company -> id ) -> where ( " email " , $email -> from ) -> first ();
2024-04-03 15:01:57 +02:00
if ( ! $vendorContact )
return null ;
2023-12-18 15:05:15 +01:00
2024-04-03 15:01:57 +02:00
return $vendorContact -> vendor ();
2023-12-18 15:05:15 +01:00
}
2024-04-03 07:57:23 +02:00
private function logBlocked ( Company $company , string $data )
2024-03-24 11:40:17 +01:00
{
2024-04-24 08:40:58 +02:00
nlog ( " [InboundMailEngine][company: " . $company -> id . " ] " . $data );
2024-03-24 11:40:17 +01:00
(
new SystemLogger (
$data ,
SystemLog :: CATEGORY_MAIL ,
2024-03-25 06:41:22 +01:00
SystemLog :: EVENT_INBOUND_MAIL_BLOCKED ,
2024-03-24 11:40:17 +01:00
SystemLog :: TYPE_CUSTOM ,
null ,
2024-04-03 07:57:23 +02:00
$company
2024-03-24 11:40:17 +01:00
)
) -> handle ();
}
2023-12-18 15:05:15 +01:00
}