mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge pull request #9042 from paulwer/feature-inbound-email-expenses
Feature: inbound email expenses & mindee ocr
This commit is contained in:
commit
cdacdc851e
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\Brevo\ProcessBrevoInboundWebhook;
|
||||
use App\Jobs\Brevo\ProcessBrevoWebhook;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -24,14 +25,14 @@ class BrevoController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Postmark Webhook.
|
||||
* Process Brevo Webhook.
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/postmark_webhook",
|
||||
* operationId="postmarkWebhook",
|
||||
* tags={"postmark"},
|
||||
* summary="Processing webhooks from PostMark",
|
||||
* path="/api/v1/brevo_webhook",
|
||||
* operationId="brevoWebhook",
|
||||
* tags={"brevo"},
|
||||
* summary="Processing webhooks from Brevo",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
@ -59,12 +60,150 @@ class BrevoController extends BaseController
|
||||
*/
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
if ($request->has('token') && $request->get('token') == config('services.brevo.key')) {
|
||||
ProcessBrevoWebhook::dispatch($request->all())->delay(10);
|
||||
if ($request->has('token') && $request->get('token') == config('services.brevo.secret')) {
|
||||
ProcessBrevoWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process Brevo Inbound Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: brevo strips old sended emails, therefore only current attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: brevo saves the message and attachemnts for later retrieval, therefore we can process it within a async job for performance reasons
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/brevo_inbound_webhook",
|
||||
* operationId="brevoInboundWebhook",
|
||||
* tags={"brevo"},
|
||||
* summary="Processing inbound webhooks from Brevo",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* array (
|
||||
* 'items' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Uuid' =>
|
||||
* array (
|
||||
* 0 => 'd9f48d52-a344-42a4-9056-9733488d9fa3',
|
||||
* ),
|
||||
* 'Recipients' =>
|
||||
* array (
|
||||
* 0 => 'test@test.de',
|
||||
* ),
|
||||
* 'MessageId' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'InReplyTo' => NULL,
|
||||
* 'From' =>
|
||||
* array (
|
||||
* 'Name' => 'Max Mustermann',
|
||||
* 'Address' => 'max@mustermann.de',
|
||||
* ),
|
||||
* 'To' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => NULL,
|
||||
* 'Address' => 'test@test.de',
|
||||
* ),
|
||||
* ),
|
||||
* 'Cc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'Bcc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'ReplyTo' => NULL,
|
||||
* 'SentAtDate' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'Attachments' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => 'flag--sv-1x1.svg',
|
||||
* 'ContentType' => 'image/svg+xml',
|
||||
* 'ContentLength' => 79957,
|
||||
* 'ContentID' => 'f_lu4ct6s20',
|
||||
* 'DownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6ImZsYWctLXN2LTF4MS5zdmcifQ',
|
||||
* ),
|
||||
* ),
|
||||
* 'Headers' =>
|
||||
* array (
|
||||
* 'Received' => 'by mail-ed1-f51.google.com with SMTP id 4fb4d7f45d1cf-56b0af675deso3877288a12.1 for <test@test.de>; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=mustermann.de; s=google; t=1711214316; x=1711819116; darn=test.de; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=ackw3d+qTvZk4JKxomvH626MvfwmH23mikOUc2hWwYiO6unmQgPs2w5spnkmD9aCZ9 G+3nPSYKntugOmqWstZH3z4B063U4Y6j5hTc19WtCyyb9UR+XD+C6L10yc6ez8QUhlZT uAGqDoJ+E8+dBxiMul2pow19lC88t3QxRXU+i8zScniV7SFkwzziCEODaB61yI0DXsZB bUkx5Gx6cztKaNVF2QgguF2nQnJFUnD2nabVFsihyJ5r6y61rkSM/YTfMJuES772lnhv IeF+vwiFNEPKafrchce6YJcvo5Vd5lYFK4LtHyCy3mwJpX2QY+WnWAfferZ2YfgEL0Sf K3Pw==',
|
||||
* 'X-Google-DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711214316; x=1711819116; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=fg4tXZnstRBexYlC6MD7C7is0kQj+xY66cSJ78tSa7PtSFQzY0zajDMsepMCGiiWmN /Pc/tRtk53pru/OtfzRT9pbM6mhM1arIt+QaQBQGU5xZVV5JXfPmdnPzXqAbQztyeHrk UcEkz+qDN3JNoidw2dJhhdt5MxdKssR572NwtBrn/rN7f1o/ThWzEz+P0o06GVBpxVYP wM0EkvcJj2SUOcn36kmp1ccbMUwYCU2h1JmniEFY8RTqu2il13iXoBvG4YPxe0c0hJ6z zw1N5rONeQM113N1rpbQzS1QLSngczuOhN24M3TOwrHJIec/BxrOW6KWl/uPUqiZAf65 f0tg==',
|
||||
* 'X-Gm-Message-State' => 'AOJu0YzKhR1HY1oUXoq++LLpl6UOz1S60NfPxuPXBLcP+6aACYle8rqQ fYHe2rQYTpg4KWiOswu858STOW8qmiewXD6gH/LbmEFs7sknRyDPNr/+L0cv828A3o+SOvXu3uP SY6H1aNSwIpqTRhJ+nNjTuSUpuSoABd9fYXFwPuivV0DtBhoVmpE=',
|
||||
* 'X-Google-Smtp-Source' => 'AGHT+IHdA9ZhW0dQxgOYx2OXBGmu4pzSR/zwJ0vcPNXFSqttKCPS2oTw1a9b2mMdhyUeoRAwP5TmhHlAtqUUrOPwkgg=',
|
||||
* 'X-Received' => 'by 2002:a50:d74c:0:b0:567:3c07:8bbc with SMTP id i12-20020a50d74c000000b005673c078bbcmr2126401edj.21.1711214316135; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'MIME-Version' => '1.0',
|
||||
* 'From' => 'Max Mustermann <max@mustermann.de>',
|
||||
* 'Date' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Message-ID' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'To' => 'test@test.de',
|
||||
* 'Content-Type' => 'multipart/mixed',
|
||||
* ),
|
||||
* 'SpamScore' => 2.8,
|
||||
* 'ExtractedMarkdownMessage' => 'TEST',
|
||||
* 'ExtractedMarkdownSignature' => NULL,
|
||||
* 'RawHtmlBody' => '<div dir="ltr">TEST</div>',
|
||||
* 'RawTextBody' => 'TEST',
|
||||
* 'EMLDownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6InNtdHAuZW1sIn0',
|
||||
* ),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
$input = $request->all();
|
||||
|
||||
if (!($request->has('token') && $request->get('token') == config('ninja.inbound_mailbox.inbound_webhook_token')))
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
if (!array_key_exists('items', $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing.');
|
||||
return response()->json(['message' => 'Failed. Invalid Parameters.'], 400);
|
||||
}
|
||||
|
||||
foreach ($input["items"] as $item) {
|
||||
|
||||
if (!array_key_exists('Recipients', $item) || !array_key_exists('MessageId', $item)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing. At least one item was invalid.');
|
||||
return response()->json(['message' => 'Failed. Invalid Parameters. At least one item was invalid.'], 400);
|
||||
}
|
||||
|
||||
ProcessBrevoInboundWebhook::dispatch($item)->delay(rand(2, 10));
|
||||
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success'], 201);
|
||||
}
|
||||
}
|
||||
|
@ -497,7 +497,7 @@ class ExpenseController extends BaseController
|
||||
|
||||
$expenses = Expense::withTrashed()->find($request->ids);
|
||||
|
||||
if($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
|
||||
if ($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
|
||||
$this->expense_repo->categorize($expenses, $request->category_id);
|
||||
$expenses = collect([]);
|
||||
}
|
||||
@ -573,7 +573,7 @@ class ExpenseController extends BaseController
|
||||
*/
|
||||
public function upload(UploadExpenseRequest $request, Expense $expense)
|
||||
{
|
||||
if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) {
|
||||
if (!$this->checkFeature(Account::FEATURE_DOCUMENTS)) {
|
||||
return $this->featureFailure();
|
||||
}
|
||||
|
||||
@ -584,15 +584,60 @@ class ExpenseController extends BaseController
|
||||
return $this->itemResponse($expense->fresh());
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/expenses/edocument",
|
||||
* operationId="edocumentExpense",
|
||||
* tags={"expenses"},
|
||||
* summary="Uploads an electronic document to a expense",
|
||||
* description="Handles the uploading of an electronic document to a expense",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\RequestBody(
|
||||
* description="User credentials",
|
||||
* required=true,
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="The files to be uploaded",
|
||||
* ),
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns a HTTP status",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function edocument(EDocumentRequest $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
foreach($request->file("documents") as $file) {
|
||||
ImportEDocument::dispatch($file->get(), $file->getClientOriginalName(), $user->company());
|
||||
foreach ($request->file("documents") as $file) {
|
||||
ImportEDocument::dispatch($file->get(), $file->getClientOriginalName(), $request->file("documents")->getMimeType(), $user->company());
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Processing....'], 200);
|
||||
|
||||
}
|
||||
}
|
||||
|
147
app/Http/Controllers/MailgunController.php
Normal file
147
app/Http/Controllers/MailgunController.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\Mailgun\ProcessMailgunWebhook;
|
||||
use App\Jobs\Mailgun\ProcessMailgunInboundWebhook;
|
||||
|
||||
/**
|
||||
* Class MailgunController.
|
||||
*/
|
||||
class MailgunController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Mailgun Webhook.
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/mailgun_webhook",
|
||||
* operationId="mailgunWebhook",
|
||||
* tags={"mailgun"},
|
||||
* summary="Processing webhooks from Mailgun",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
nlog($input);
|
||||
|
||||
if (\abs(\time() - $request['signature']['timestamp']) > 15) {
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
if (\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) {
|
||||
ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Mailgun Inbound Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: mailgun does NOT strip old sended emails, therefore all past attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: mailgun saves the message and attachemnts for later retrieval, therefore we can process it within a async job for performance reasons
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/mailgun_inbound_webhook",
|
||||
* operationId="mailgunInboundWebhook",
|
||||
* tags={"mailgun"},
|
||||
* summary="Processing inbound webhooks from Mailgun",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
$input = $request->all();
|
||||
|
||||
nlog($input);
|
||||
|
||||
if (!array_key_exists('sender', $input) || !array_key_exists('recipient', $input) || !array_key_exists('message-url', $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing. Please ensure contacting this api-endpoint with a store & notify operation instead of a forward operation!');
|
||||
return response()->json(['message' => 'Failed. Missing Parameters. Use store and notify!'], 400);
|
||||
}
|
||||
|
||||
// @turbo124 TODO: how to check for services.mailgun.webhook_signing_key on company level, when custom credentials are defined
|
||||
// TODO: validation for client mail credentials by recipient
|
||||
$authorizedByHash = \hash_equals(\hash_hmac('sha256', $input['timestamp'] . $input['token'], config('services.mailgun.webhook_signing_key')), $input['signature']);
|
||||
$authorizedByToken = $request->has('token') && $request->get('token') == config('ninja.inbound_mailbox.inbound_webhook_token');
|
||||
if (!$authorizedByHash && !$authorizedByToken)
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
/** @var \App\Models\Company $company */
|
||||
$company = MultiDB::findAndSetDbByExpenseMailbox($input["recipient"]);
|
||||
|
||||
if(!$company)
|
||||
return response()->json(['message' => 'Ok'], 200); // Fail gracefully
|
||||
|
||||
ProcessMailgunInboundWebhook::dispatch($input["sender"], $input["recipient"], $input["message-url"], $company)->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\Mailgun\ProcessMailgunWebhook;
|
||||
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class MailgunWebhookController.
|
||||
*/
|
||||
class MailgunWebhookController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
if (\abs(\time() - $request['signature']['timestamp']) > 15) {
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
if(\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) {
|
||||
ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
}
|
@ -12,6 +12,11 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Services\InboundMail\InboundMail;
|
||||
use App\Services\InboundMail\InboundMailEngine;
|
||||
use App\Utils\TempFile;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
@ -60,11 +65,271 @@ class PostMarkController extends BaseController
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
if ($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('services.postmark.token')) {
|
||||
ProcessPostmarkWebhook::dispatch($request->all())->delay(10);
|
||||
ProcessPostmarkWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Postmark Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: postmark does NOT strip old sended emails, therefore also all past attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: postmark does not saves attachements for later retrieval, therefore we cannot process it within a async job
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/postmark_inbound_webhook",
|
||||
* operationId="postmarkInboundWebhook",
|
||||
* tags={"postmark"},
|
||||
* summary="Processing inbound webhooks from PostMark",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* array (
|
||||
* 'FromName' => 'Max Mustermann',
|
||||
* 'MessageStream' => 'inbound',
|
||||
* 'From' => 'max@mustermann.de',
|
||||
* 'FromFull' =>
|
||||
* array (
|
||||
* 'Email' => 'max@mustermann.de',
|
||||
* 'Name' => 'Max Mustermann',
|
||||
* 'MailboxHash' => NULL,
|
||||
* ),
|
||||
* 'To' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'ToFull' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Email' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'Name' => NULL,
|
||||
* 'MailboxHash' => NULL,
|
||||
* ),
|
||||
* ),
|
||||
* 'Cc' => NULL,
|
||||
* 'CcFull' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'Bcc' => NULL,
|
||||
* 'BccFull' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'OriginalRecipient' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'Subject' => 'Re: adaw',
|
||||
* 'MessageID' => 'd37fde00-b4cf-4b64-ac64-e9f6da523c25',
|
||||
* 'ReplyTo' => NULL,
|
||||
* 'MailboxHash' => NULL,
|
||||
* 'Date' => 'Sun, 24 Mar 2024 13:17:52 +0100',
|
||||
* 'TextBody' => 'wadwad
|
||||
*
|
||||
* Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <max@mustermann.de>:
|
||||
*
|
||||
* > test
|
||||
* >
|
||||
*
|
||||
* --
|
||||
* test.de - Max Mustermann <https://test.de/>kontakt@test.de
|
||||
* <mailto:kontakt@test.de>',
|
||||
* 'HtmlBody' => '<div dir="ltr">wadwad</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <<a href="mailto:max@mustermann.de">max@mustermann.de</a>>:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr">test</div>
|
||||
* </blockquote></div>
|
||||
*
|
||||
* <br>
|
||||
* <font size="3"><a href="https://test.de/" target="_blank">test.de - Max Mustermann</a></font><div><a href="mailto:kontakt@test.de" style="font-size:medium" target="_blank">kontakt@test.de</a><br></div>',
|
||||
* 'StrippedTextReply' => 'wadwad
|
||||
*
|
||||
* Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <max@mustermann.de>:',
|
||||
* 'Tag' => NULL,
|
||||
* 'Headers' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => 'Return-Path',
|
||||
* 'Value' => '<max@mustermann.de>',
|
||||
* ),
|
||||
* 1 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'by p-pm-inboundg02a-aws-euwest1a.inbound.postmarkapp.com (Postfix, from userid 996) id 8ED1A453CA4; Sun, 24 Mar 2024 12:18:10 +0000 (UTC)',
|
||||
* ),
|
||||
* 2 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Checker-Version',
|
||||
* 'Value' => 'SpamAssassin 3.4.0 (2014-02-07) on p-pm-inboundg02a-aws-euwest1a',
|
||||
* ),
|
||||
* 3 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Status',
|
||||
* 'Value' => 'No',
|
||||
* ),
|
||||
* 4 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Score',
|
||||
* 'Value' => '-0.1',
|
||||
* ),
|
||||
* 5 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Tests',
|
||||
* 'Value' => 'DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,HTML_MESSAGE, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,RCVD_IN_ZEN_BLOCKED_OPENDNS, SPF_HELO_NONE,SPF_PASS,URIBL_DBL_BLOCKED_OPENDNS,URIBL_ZEN_BLOCKED_OPENDNS',
|
||||
* ),
|
||||
* 6 =>
|
||||
* array (
|
||||
* 'Name' => 'Received-SPF',
|
||||
* 'Value' => 'pass (test.de: Sender is authorized to use \'max@mustermann.de\' in \'mfrom\' identity (mechanism \'include:_spf.google.com\' matched)) receiver=p-pm-inboundg02a-aws-euwest1a; identity=mailfrom; envelope-from="max@mustermann.de"; helo=mail-lf1-f51.google.com; client-ip=209.85.167.51',
|
||||
* ),
|
||||
* 7 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'from mail-lf1-f51.google.com (mail-lf1-f51.google.com [209.85.167.51]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by p-pm-inboundg02a-aws-euwest1a.inbound.postmarkapp.com (Postfix) with ESMTPS id 437BD453CA2 for <370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com>; Sun, 24 Mar 2024 12:18:10 +0000 (UTC)',
|
||||
* ),
|
||||
* 8 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'by mail-lf1-f51.google.com with SMTP id 2adb3069b0e04-513cf9bacf1so4773866e87.0 for <370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com>; Sun, 24 Mar 2024 05:18:10 -0700 (PDT)',
|
||||
* ),
|
||||
* 9 =>
|
||||
* array (
|
||||
* 'Name' => 'DKIM-Signature',
|
||||
* 'Value' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=test.de; s=google; t=1711282689; x=1711887489; darn=inbound.postmarkapp.com; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=NvjmqLXF/5L5ZrpToR/6FgVOhTOGC9j0/B2Na5Ke6J8=; b=AMXIEoh6yGrOT6X3eBBClQ3NXFNuEoqxeM6aPONsqbpShAcT24iAJmqXylaLHv3fyX Hm6mwp3a029NnrLP/VRyKZbzIMBN2iycidtrEMXF/Eg2e42Q/08/2dZ7nxH6NqE/jz01 3M7qvwHvuoZ2Knhj7rnZc6I5m/nFxBsZc++Aj0Vv9sFoWZZooqAeTXbux1I5NyE17MrL D6byca43iINARZN7XOkoChRRZoZbOqZEtc2Va5yw7v+aYguLB4HHrIFC7G+L8hAJ0IAo 3R3DFeBw58M1xtxXCREI8Y6qMQTw60XyFw0gVmZzqR4hZiTerBSJJsZLZOBgmXxq3WLS +xVQ==',
|
||||
* ),
|
||||
* 10 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Google-DKIM-Signature',
|
||||
* 'Value' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711282689; x=1711887489; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=NvjmqLXF/5L5ZrpToR/6FgVOhTOGC9j0/B2Na5Ke6J8=; b=uKoMhir+MX/wycNEr29Sffj45ooKksCJ1OfSRkIIGHk0rnHn8Vh+c7beYipwRPW4F2 h46K64vtIX00guYMdL2Qo2eY96+wALTqHCy67PGhvotVTROz21yxjx62pCDPGs5tefOu IkyxoybpIK8zAfLoDTd9p2GIrr5brKJyB2w1NQc1htxTQ5D4RgBxUAOKv4uVEr8r47iA MIo5d8/AifA+vCOAh7iJ7EmvDQ1R+guhQyH9m1Jo8PLapiYuHXggpBJvooyGuflKqbnt gJ/dscEr4d5aWJbw/x1dmIJ5gyJPGdBWq8NRqV/qbkXQW3H/gylifDUPXbki+EQBD5Yu EuLQ==',
|
||||
* ),
|
||||
* 11 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Gm-Message-State',
|
||||
* 'Value' => 'AOJu0Yxpbp1sRh17lNzg+pLnIx1jCn8ZFJQMgFuHK+6Z8RqFS5KKKTxR 8onpEbxWYYVUbrJFExNBHPD/3jdxqifCVVNaDmbpwHgmW5lHLJmA5vYRq5NFZ9OA6zKx/N6Gipr iXE4fXmSqghFNTzy9V/RT08Zp+F5RiFh/Ta6ltQl8XfCPFfSawLz6cagUgt8bBuF4RqdrYmWwzj ty86V5Br1htRNEFYivoXnNmaRcsD0tca1D23ny62O6RwWugrj1IpAYhViNyTZAWu+loKgfjJJoI MsyiSU=',
|
||||
* ),
|
||||
* 12 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Google-Smtp-Source',
|
||||
* 'Value' => 'AGHT+IEdtZqbVI6j7WLeaSL3dABGSnWIXaSjbYqXvFvE2H+f2zsn0gknQ4OdTJecQRCabpypVF2ue91Jb7aKl6RiyEQ=',
|
||||
* ),
|
||||
* 13 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Received',
|
||||
* 'Value' => 'by 2002:a19:385a:0:b0:513:c876:c80a with SMTP id d26-20020a19385a000000b00513c876c80amr2586776lfj.34.1711282689140; Sun, 24 Mar 2024 05:18:09 -0700 (PDT)',
|
||||
* ),
|
||||
* 14 =>
|
||||
* array (
|
||||
* 'Name' => 'MIME-Version',
|
||||
* 'Value' => '1.0',
|
||||
* ),
|
||||
* 15 =>
|
||||
* array (
|
||||
* 'Name' => 'References',
|
||||
* 'Value' => '<CADfEuNsNFmNNCJDPjpS36amoLv2XEm41HmgYJT7Tj=R96PkxnA@mail.gmail.com>',
|
||||
* ),
|
||||
* 16 =>
|
||||
* array (
|
||||
* 'Name' => 'In-Reply-To',
|
||||
* 'Value' => '<CADfEuNsNFmNNCJDPjpS36amoLv2XEm41HmgYJT7Tj=R96PkxnA@mail.gmail.com>',
|
||||
* ),
|
||||
* 17 =>
|
||||
* array (
|
||||
* 'Name' => 'Message-ID',
|
||||
* 'Value' => '<CADfEuNvyCLsnp=CwJ3BF=-L6rn=o+DmUOPP6Cp4F-SO0p0hVwQ@mail.gmail.com>',
|
||||
* ),
|
||||
* ),
|
||||
* 'Attachments' =>
|
||||
* array (
|
||||
* array (
|
||||
* 'Content' => "base64-String",
|
||||
* 'ContentLength' => 60164,
|
||||
* 'Name' => 'Unbenannt.png',
|
||||
* 'ContentType' => 'image/png',
|
||||
* 'ContentID' => 'ii_luh2h8lg0',
|
||||
* )
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
if (!$request->has('token') || $request->token != config('ninja.inbound_mailbox.inbound_webhook_token'))
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
if (!(array_key_exists("MessageStream", $input) && $input["MessageStream"] == "inbound") || !array_key_exists("To", $input) || !array_key_exists("From", $input) || !array_key_exists("MessageID", $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing.');
|
||||
return response()->json(['message' => 'Failed. Missing/Invalid Parameters.'], 400);
|
||||
}
|
||||
|
||||
$company = MultiDB::findAndSetDbByExpenseMailbox($input["ToFull"][0]["Email"]);
|
||||
|
||||
if (!$company) {
|
||||
nlog('[PostmarkInboundWebhook] unknown Expense Mailbox occured while handling an inbound email from postmark: ' . $input["To"]);
|
||||
return response()->json(['message' => 'Ok'], 200);
|
||||
}
|
||||
|
||||
$inboundEngine = new InboundMailEngine($company);
|
||||
|
||||
if ($inboundEngine->isInvalidOrBlocked($input["From"], $input["ToFull"][0]["Email"])) {
|
||||
return response()->json(['message' => 'Blocked.'], 403);
|
||||
}
|
||||
|
||||
try { // important to save meta if something fails here to prevent spam
|
||||
|
||||
// prepare data for ingresEngine
|
||||
$inboundMail = new InboundMail();
|
||||
|
||||
$inboundMail->from = $input["From"] ?? '';
|
||||
$inboundMail->to = $input["To"]; // usage of data-input, because we need a single email here
|
||||
$inboundMail->subject = $input["Subject"] ?? '';
|
||||
$inboundMail->body = $input["HtmlBody"] ?? '';
|
||||
$inboundMail->text_body = $input["TextBody"] ?? '';
|
||||
$inboundMail->date = Carbon::createFromTimeString($input["Date"]);
|
||||
|
||||
// parse documents as UploadedFile from webhook-data
|
||||
foreach ($input["Attachments"] as $attachment) {
|
||||
|
||||
$inboundMail->documents[] = TempFile::UploadedFileFromBase64($attachment["Content"], $attachment["Name"], $attachment["ContentType"]);
|
||||
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$inboundEngine->saveMeta($input["From"], $input["To"]); // important to save this, to protect from spam
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// perform
|
||||
try {
|
||||
|
||||
$inboundEngine->handleExpenseMailbox($inboundMail);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 409)
|
||||
return response()->json(['message' => $e->getMessage()], 409);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,15 @@
|
||||
|
||||
namespace App\Http\Requests\Company;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\Company\ValidCompanyQuantity;
|
||||
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Models\Company;
|
||||
use App\Utils\Ninja;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class StoreCompanyRequest extends Request
|
||||
{
|
||||
@ -56,6 +57,8 @@ class StoreCompanyRequest extends Request
|
||||
}
|
||||
}
|
||||
|
||||
$rules['expense_mailbox'] = new ValidExpenseMailbox();
|
||||
|
||||
$rules['smtp_host'] = 'sometimes|string|nullable';
|
||||
$rules['smtp_port'] = 'sometimes|integer|nullable';
|
||||
$rules['smtp_encryption'] = 'sometimes|string';
|
||||
@ -84,23 +87,27 @@ class StoreCompanyRequest extends Request
|
||||
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
|
||||
}
|
||||
|
||||
if(Ninja::isHosted() && !isset($input['subdomain'])) {
|
||||
if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
|
||||
unset($input['expense_mailbox']);
|
||||
}
|
||||
|
||||
if (Ninja::isHosted() && !isset($input['subdomain'])) {
|
||||
$input['subdomain'] = MultiDB::randomSubdomainGenerator();
|
||||
}
|
||||
|
||||
if(isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
if (isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
unset($input['smtp_username']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
if (isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
unset($input['smtp_password']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_port'])) {
|
||||
if (isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int) $input['smtp_port'];
|
||||
}
|
||||
|
||||
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
if (isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
$input['smtp_verify_peer'] == 'true' ? true : false;
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,12 @@ namespace App\Http\Requests\Company;
|
||||
use App\Utils\Ninja;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Http\ValidationRules\EInvoice\ValidCompanyScheme;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
|
||||
use App\Http\ValidationRules\EInvoice\ValidCompanyScheme;
|
||||
|
||||
class UpdateCompanyRequest extends Request
|
||||
{
|
||||
@ -75,6 +77,15 @@ class UpdateCompanyRequest extends Request
|
||||
$rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9.-]+[a-zA-Z0-9]$/', new ValidSubdomain()];
|
||||
}
|
||||
|
||||
$rules['expense_mailbox'] = ['sometimes','email', 'nullable', new ValidExpenseMailbox(), Rule::unique('companies')->ignore($this->company->id)];
|
||||
$rules['expense_mailbox_active'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_company_users'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_vendors'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_clients'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_unknown'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_whitelist'] = ['sometimes', 'string', 'nullable', 'regex:/^[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4}(,[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4})*$/'];
|
||||
$rules['inbound_mailbox_blacklist'] = ['sometimes', 'string', 'nullable', 'regex:/^[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4}(,[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4})*$/'];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@ -87,31 +98,35 @@ class UpdateCompanyRequest extends Request
|
||||
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
|
||||
}
|
||||
|
||||
if (isset($input['settings'])) {
|
||||
$input['settings'] = (array)$this->filterSaveableSettings($input['settings']);
|
||||
if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
|
||||
unset($input['expense_mailbox']);
|
||||
}
|
||||
|
||||
if(isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) {
|
||||
if (isset($input['settings'])) {
|
||||
$input['settings'] = (array) $this->filterSaveableSettings($input['settings']);
|
||||
}
|
||||
|
||||
if (isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) {
|
||||
unset($input['subdomain']);
|
||||
}
|
||||
|
||||
if(isset($input['e_invoice_certificate_passphrase']) && empty($input['e_invoice_certificate_passphrase'])) {
|
||||
if (isset($input['e_invoice_certificate_passphrase']) && empty($input['e_invoice_certificate_passphrase'])) {
|
||||
unset($input['e_invoice_certificate_passphrase']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
if (isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
unset($input['smtp_username']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
if (isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
unset($input['smtp_password']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int)$input['smtp_port'];
|
||||
if (isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int) $input['smtp_port'];
|
||||
}
|
||||
|
||||
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
if (isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
$input['smtp_verify_peer'] == 'true' ? true : false;
|
||||
}
|
||||
|
||||
@ -144,21 +159,21 @@ class UpdateCompanyRequest extends Request
|
||||
}
|
||||
|
||||
if (isset($settings['email_style_custom'])) {
|
||||
$settings['email_style_custom'] = str_replace(['{!!','!!}','{{','}}','@checked','@dd', '@dump', '@if', '@if(','@endif','@isset','@unless','@auth','@empty','@guest','@env','@section','@switch', '@foreach', '@while', '@include', '@each', '@once', '@push', '@use', '@forelse', '@verbatim', '<?php', '@php', '@for','@class','</sc','<sc','html;base64', '@elseif', '@else', '@endunless', '@endisset', '@endempty', '@endauth', '@endguest', '@endproduction', '@endenv', '@hasSection', '@endhasSection', '@sectionMissing', '@endsectionMissing', '@endfor', '@endforeach', '@empty', '@endforelse', '@endwhile', '@continue', '@break', '@includeIf', '@includeWhen', '@includeUnless', '@includeFirst', '@component', '@endcomponent', '@endsection', '@yield', '@show', '@append', '@overwrite', '@stop', '@extends', '@endpush', '@stack', '@prepend', '@endprepend', '@slot', '@endslot', '@endphp', '@method', '@csrf', '@error', '@enderror', '@json', '@endverbatim', '@inject'], '', $settings['email_style_custom']);
|
||||
$settings['email_style_custom'] = str_replace(['{!!', '!!}', '{{', '}}', '@checked', '@dd', '@dump', '@if', '@if(', '@endif', '@isset', '@unless', '@auth', '@empty', '@guest', '@env', '@section', '@switch', '@foreach', '@while', '@include', '@each', '@once', '@push', '@use', '@forelse', '@verbatim', '<?php', '@php', '@for', '@class', '</sc', '<sc', 'html;base64', '@elseif', '@else', '@endunless', '@endisset', '@endempty', '@endauth', '@endguest', '@endproduction', '@endenv', '@hasSection', '@endhasSection', '@sectionMissing', '@endsectionMissing', '@endfor', '@endforeach', '@empty', '@endforelse', '@endwhile', '@continue', '@break', '@includeIf', '@includeWhen', '@includeUnless', '@includeFirst', '@component', '@endcomponent', '@endsection', '@yield', '@show', '@append', '@overwrite', '@stop', '@extends', '@endpush', '@stack', '@prepend', '@endprepend', '@slot', '@endslot', '@endphp', '@method', '@csrf', '@error', '@enderror', '@json', '@endverbatim', '@inject'], '', $settings['email_style_custom']);
|
||||
}
|
||||
|
||||
if(isset($settings['company_logo']) && strlen($settings['company_logo']) > 2) {
|
||||
if (isset($settings['company_logo']) && strlen($settings['company_logo']) > 2) {
|
||||
$settings['company_logo'] = $this->forceScheme($settings['company_logo']);
|
||||
}
|
||||
|
||||
if (! $account->isFreeHostedClient()) {
|
||||
if (!$account->isFreeHostedClient()) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$saveable_casts = CompanySettings::$free_plan_casts;
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
if (! array_key_exists($key, $saveable_casts)) {
|
||||
if (!array_key_exists($key, $saveable_casts)) {
|
||||
unset($settings->{$key});
|
||||
}
|
||||
}
|
||||
@ -170,7 +185,7 @@ class UpdateCompanyRequest extends Request
|
||||
{
|
||||
if (Ninja::isHosted()) {
|
||||
$url = str_replace('http://', '', $url);
|
||||
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme.$url : $url;
|
||||
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
|
||||
}
|
||||
|
||||
return rtrim($url, '/');
|
||||
|
@ -25,9 +25,9 @@ class EDocumentRequest extends Request
|
||||
$rules = [];
|
||||
|
||||
if ($this->file('documents') && is_array($this->file('documents'))) {
|
||||
$rules['documents.*'] = 'required|file|max:1000000|mimes:xml';
|
||||
$rules['documents.*'] = 'required|file|max:1000000';
|
||||
} elseif ($this->file('documents')) {
|
||||
$rules['documents'] = 'required|file|max:1000000|mimes:xml';
|
||||
$rules['documents'] = 'required|file|max:1000000';
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
55
app/Http/ValidationRules/Company/ValidExpenseMailbox.php
Normal file
55
app/Http/ValidationRules/Company/ValidExpenseMailbox.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
namespace App\Http\ValidationRules\Company;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Utils\Ninja;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Symfony\Component\Validator\Constraints\EmailValidator;
|
||||
|
||||
/**
|
||||
* Class ValidCompanyQuantity.
|
||||
*/
|
||||
class ValidExpenseMailbox implements Rule
|
||||
{
|
||||
|
||||
private array $endings = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->endings = explode(",", config('ninja.inbound_mailbox.expense_mailbox_endings'));
|
||||
}
|
||||
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if (empty($value) || !config('ninja.inbound_mailbox.expense_mailbox_endings')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->endings as $ending) {
|
||||
if (str_ends_with($value, $ending)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return ctrans('texts.expense_mailbox_invalid');
|
||||
}
|
||||
}
|
244
app/Jobs/Brevo/ProcessBrevoInboundWebhook.php
Normal file
244
app/Jobs/Brevo/ProcessBrevoInboundWebhook.php
Normal file
@ -0,0 +1,244 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Brevo;
|
||||
|
||||
use App\Utils\TempFile;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Brevo\Client\Configuration;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Brevo\Client\Api\InboundParsingApi;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use App\Services\InboundMail\InboundMail;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\InboundMail\InboundMailEngine;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
|
||||
class ProcessBrevoInboundWebhook implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
private InboundMailEngine $engine;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* IMPORTANT NOTICE: brevo strips old sended emails, therefore only current attachements are present
|
||||
*
|
||||
* $input consists of json/serialized-array:
|
||||
*
|
||||
* array (
|
||||
* 'Uuid' =>
|
||||
* array (
|
||||
* 0 => 'd9f48d52-a344-42a4-9056-9733488d9fa3',
|
||||
* ),
|
||||
* 'Recipients' =>
|
||||
* array (
|
||||
* 0 => 'test@test.de',
|
||||
* ),
|
||||
* 'MessageId' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'InReplyTo' => NULL,
|
||||
* 'From' =>
|
||||
* array (
|
||||
* 'Name' => 'Max Mustermann',
|
||||
* 'Address' => 'max@mustermann.de',
|
||||
* ),
|
||||
* 'To' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => NULL,
|
||||
* 'Address' => 'test@test.de',
|
||||
* ),
|
||||
* ),
|
||||
* 'Cc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'Bcc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'ReplyTo' => NULL,
|
||||
* 'SentAtDate' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'Attachments' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => 'flag--sv-1x1.svg',
|
||||
* 'ContentType' => 'image/svg+xml',
|
||||
* 'ContentLength' => 79957,
|
||||
* 'ContentID' => 'f_lu4ct6s20',
|
||||
* 'DownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6ImZsYWctLXN2LTF4MS5zdmcifQ',
|
||||
* ),
|
||||
* ),
|
||||
* 'Headers' =>
|
||||
* array (
|
||||
* 'Received' => 'by mail-ed1-f51.google.com with SMTP id 4fb4d7f45d1cf-56b0af675deso3877288a12.1 for <test@test.de>; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=mustermann.de; s=google; t=1711214316; x=1711819116; darn=test.de; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=ackw3d+qTvZk4JKxomvH626MvfwmH23mikOUc2hWwYiO6unmQgPs2w5spnkmD9aCZ9 G+3nPSYKntugOmqWstZH3z4B063U4Y6j5hTc19WtCyyb9UR+XD+C6L10yc6ez8QUhlZT uAGqDoJ+E8+dBxiMul2pow19lC88t3QxRXU+i8zScniV7SFkwzziCEODaB61yI0DXsZB bUkx5Gx6cztKaNVF2QgguF2nQnJFUnD2nabVFsihyJ5r6y61rkSM/YTfMJuES772lnhv IeF+vwiFNEPKafrchce6YJcvo5Vd5lYFK4LtHyCy3mwJpX2QY+WnWAfferZ2YfgEL0Sf K3Pw==',
|
||||
* 'X-Google-DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711214316; x=1711819116; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=fg4tXZnstRBexYlC6MD7C7is0kQj+xY66cSJ78tSa7PtSFQzY0zajDMsepMCGiiWmN /Pc/tRtk53pru/OtfzRT9pbM6mhM1arIt+QaQBQGU5xZVV5JXfPmdnPzXqAbQztyeHrk UcEkz+qDN3JNoidw2dJhhdt5MxdKssR572NwtBrn/rN7f1o/ThWzEz+P0o06GVBpxVYP wM0EkvcJj2SUOcn36kmp1ccbMUwYCU2h1JmniEFY8RTqu2il13iXoBvG4YPxe0c0hJ6z zw1N5rONeQM113N1rpbQzS1QLSngczuOhN24M3TOwrHJIec/BxrOW6KWl/uPUqiZAf65 f0tg==',
|
||||
* 'X-Gm-Message-State' => 'AOJu0YzKhR1HY1oUXoq++LLpl6UOz1S60NfPxuPXBLcP+6aACYle8rqQ fYHe2rQYTpg4KWiOswu858STOW8qmiewXD6gH/LbmEFs7sknRyDPNr/+L0cv828A3o+SOvXu3uP SY6H1aNSwIpqTRhJ+nNjTuSUpuSoABd9fYXFwPuivV0DtBhoVmpE=',
|
||||
* 'X-Google-Smtp-Source' => 'AGHT+IHdA9ZhW0dQxgOYx2OXBGmu4pzSR/zwJ0vcPNXFSqttKCPS2oTw1a9b2mMdhyUeoRAwP5TmhHlAtqUUrOPwkgg=',
|
||||
* 'X-Received' => 'by 2002:a50:d74c:0:b0:567:3c07:8bbc with SMTP id i12-20020a50d74c000000b005673c078bbcmr2126401edj.21.1711214316135; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'MIME-Version' => '1.0',
|
||||
* 'From' => 'Max Mustermann <max@mustermann.de>',
|
||||
* 'Date' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Message-ID' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'To' => 'test@test.de',
|
||||
* 'Content-Type' => 'multipart/mixed',
|
||||
* ),
|
||||
* 'SpamScore' => 2.8,
|
||||
* 'ExtractedMarkdownMessage' => 'TEST',
|
||||
* 'ExtractedMarkdownSignature' => NULL,
|
||||
* 'RawHtmlBody' => '<div dir="ltr">TEST</div>',
|
||||
* 'RawTextBody' => 'TEST',
|
||||
* 'EMLDownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6InNtdHAuZW1sIn0',
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function __construct(private array $input)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$foundOneRecipient = false; // used for spam documentation below
|
||||
|
||||
// brevo defines recipients as array, we check all of them, to be sure
|
||||
foreach ($this->input["Recipients"] as $recipient) {
|
||||
|
||||
// Spam protection
|
||||
if ($this->engine->isInvalidOrBlocked($this->input["From"]["Address"], $recipient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// match company
|
||||
$company = MultiDB::findAndSetDbByExpenseMailbox($recipient);
|
||||
|
||||
if (!$company) {
|
||||
nlog('[ProcessBrevoInboundWebhook] unknown Expense Mailbox occured while handling an inbound email from brevo: ' . $recipient);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->engine = new InboundMailEngine($company);
|
||||
|
||||
$foundOneRecipient = true;
|
||||
|
||||
try { // important to save meta if something fails here to prevent spam
|
||||
|
||||
if(strlen($company->getSetting('brevo_secret') ?? '') < 2 && empty(config('services.brevo.secret'))){
|
||||
nlog("No Brevo Configuration available for this company");
|
||||
throw new \Error("[ProcessBrevoInboundWebhook] no brevo credenitals found, we cannot get the attachement");
|
||||
}
|
||||
|
||||
$company_brevo_secret = strlen($company->getSetting('brevo_secret') ?? '') < 2 ? $company->getSetting('brevo_secret') : config('services.brevo.secret');
|
||||
|
||||
// prepare data for ingresEngine
|
||||
$inboundMail = new InboundMail();
|
||||
|
||||
$inboundMail->from = $this->input["From"]["Address"];
|
||||
$inboundMail->to = $recipient;
|
||||
$inboundMail->subject = $this->input["Subject"];
|
||||
$inboundMail->body = $this->input["RawHtmlBody"];
|
||||
$inboundMail->text_body = $this->input["RawTextBody"];
|
||||
$inboundMail->date = Carbon::createFromTimeString($this->input["SentAtDate"]);
|
||||
|
||||
// parse documents as UploadedFile from webhook-data
|
||||
foreach ($this->input["Attachments"] as $attachment) {
|
||||
|
||||
// @todo - i think this allows switching between client configured brevo AND system configured brevo
|
||||
// download file and save to tmp dir
|
||||
if (!empty($company_brevo_secret))
|
||||
{
|
||||
|
||||
try {
|
||||
|
||||
$brevo = new InboundParsingApi(null, Configuration::getDefaultConfiguration()->setApiKey("api-key", $company_brevo_secret));
|
||||
$inboundMail->documents[] = new UploadedFile(
|
||||
$brevo->getInboundEmailAttachment($attachment["DownloadToken"])->getPathname(),
|
||||
$attachment["Name"],
|
||||
$attachment["ContentType"],
|
||||
0,
|
||||
true // Mark it as test, since the file isn't from real HTTP POST.
|
||||
);
|
||||
|
||||
} catch (\Error $e) {
|
||||
if (config('services.brevo.secret')) {
|
||||
nlog("[ProcessBrevoInboundWebhook] Error while downloading with company credentials, we try to use default credentials now...");
|
||||
|
||||
$brevo = new InboundParsingApi(null, Configuration::getDefaultConfiguration()->setApiKey("api-key", config('services.brevo.secret')));
|
||||
$inboundMail->documents[] = new UploadedFile(
|
||||
$brevo->getInboundEmailAttachment($attachment["DownloadToken"])->getPathname(),
|
||||
$attachment["Name"],
|
||||
$attachment["ContentType"],
|
||||
0,
|
||||
true // Mark it as test, since the file isn't from real HTTP POST.
|
||||
);
|
||||
|
||||
} else
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$brevo = new InboundParsingApi(null, Configuration::getDefaultConfiguration()->setApiKey("api-key", config('services.brevo.secret')));
|
||||
$inboundMail->documents[] = new UploadedFile(
|
||||
$brevo->getInboundEmailAttachment($attachment["DownloadToken"])->getPathname(),
|
||||
$attachment["Name"],
|
||||
$attachment["ContentType"],
|
||||
0,
|
||||
true // Mark it as test, since the file isn't from real HTTP POST.
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->engine->saveMeta($this->input["From"]["Address"], $recipient); // important to save this, to protect from spam
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->engine->handleExpenseMailbox($inboundMail);
|
||||
|
||||
}
|
||||
|
||||
// document for spam => mark all recipients as handled emails with unmatched mailbox => otherwise dont do any
|
||||
if (!$foundOneRecipient)
|
||||
foreach ($this->input["Recipients"] as $recipient) {
|
||||
$this->engine->saveMeta($this->input["From"]["Address"], $recipient, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function middleware()
|
||||
{
|
||||
return [new WithoutOverlapping($this->input["From"]["Address"])];
|
||||
}
|
||||
|
||||
public function failed($exception)
|
||||
{
|
||||
nlog("BREVO:: Ingest Exception:: => ".$exception->getMessage());
|
||||
config(['queue.failed.driver' => null]);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -426,7 +426,7 @@ class ProcessBrevoWebhook implements ShouldQueue
|
||||
public function getRawMessage(string $message_id)
|
||||
{
|
||||
|
||||
$brevo_secret = !empty($this->company->settings->brevo_secret) ? $this->company->settings->brevo_secret : config('services.brevo.key');
|
||||
$brevo_secret = !empty ($this->company->settings->brevo_secret) ? $this->company->settings->brevo_secret : config('services.brevo.secret');
|
||||
|
||||
$brevo = new TransactionalEmailsApi(null, Configuration::getDefaultConfiguration()->setApiKey('api-key', $brevo_secret));
|
||||
$messageDetail = $brevo->getTransacEmailContent($message_id);
|
||||
|
@ -11,9 +11,11 @@
|
||||
|
||||
namespace App\Jobs\EDocument;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Services\EDocument\Imports\ParseEDocument;
|
||||
use App\Utils\TempFile;
|
||||
use Exception;
|
||||
use App\Models\Company;
|
||||
use App\Models\Expense;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
@ -31,8 +33,9 @@ class ImportEDocument implements ShouldQueue
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public function __construct(private readonly string $file_content, private string $file_name, private Company $company)
|
||||
public function __construct(private readonly string $file_content, private string $file_name, private string $file_mime_type, private Company $company)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,15 +47,9 @@ class ImportEDocument implements ShouldQueue
|
||||
public function handle(): Expense
|
||||
{
|
||||
|
||||
switch (true) {
|
||||
case stristr($this->file_content, "urn:cen.eu:en16931:2017"):
|
||||
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"):
|
||||
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.1"):
|
||||
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.0"):
|
||||
return (new ZugferdEDocument($this->file_content, $this->file_name, $this->company))->run();
|
||||
default:
|
||||
throw new Exception("E-Invoice standard not supported");
|
||||
}
|
||||
$file = TempFile::UploadedFileFromRaw($this->file_content, $this->file_name, $this->file_mime_type);
|
||||
|
||||
return (new ParseEDocument($file, $this->company))->run();
|
||||
|
||||
}
|
||||
|
||||
@ -64,7 +61,7 @@ class ImportEDocument implements ShouldQueue
|
||||
public function failed($exception = null)
|
||||
{
|
||||
if ($exception) {
|
||||
nlog("EXCEPTION:: ImportEDocument:: ".$exception->getMessage());
|
||||
nlog("EXCEPTION:: ImportEDocument:: " . $exception->getMessage());
|
||||
}
|
||||
|
||||
config(['queue.failed.driver' => null]);
|
||||
|
289
app/Jobs/Mailgun/ProcessMailgunInboundWebhook.php
Normal file
289
app/Jobs/Mailgun/ProcessMailgunInboundWebhook.php
Normal file
@ -0,0 +1,289 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Mailgun;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Utils\TempFile;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use App\Services\InboundMail\InboundMail;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\InboundMail\InboundMailEngine;
|
||||
|
||||
class ProcessMailgunInboundWebhook implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
private InboundMailEngine $engine;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
* $input consists of 3 informations: sender/from|recipient/to|messageUrl
|
||||
*/
|
||||
public function __construct(private string $sender, private string $recipient, private string $message_url, private Company $company)
|
||||
{
|
||||
$this->engine = new InboundMailEngine($company);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* IMPORTANT NOTICE: mailgun does NOT strip old sended emails, therefore all past attachements are present
|
||||
*
|
||||
* Mail from Storage
|
||||
* {
|
||||
* "Content-Type": "multipart/related; boundary=\"00000000000022bfbe0613e8b7f5\"",
|
||||
* "Date": "Mon, 18 Mar 2024 06:34:09 +0100",
|
||||
* "Dkim-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=wer-ner.de; s=google; t=1710740086; x=1711344886; darn=domain.example; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=tkxC+ZzDSJJXLVgjDyvQZyDt6wkWKFHS50z4ZWiWT9U=; b=P1Sz54Djj1LHtPF7+cAKGRaN4IRjUT3bOyYAD/kbC0Tx2yNejPrjCPy3+a6R6MShgJ odYhoLRqylPPs1DQolNO6xgamsoEiR8jnII4QjJUBut4VirMlSO+RLxzpO7pt/Hr6j93 z0G1Yffpbz44l5GhndgXsa4Hf30Q8yy0p7fqMNABB/smscj7DJDu1os2cB1JazKYsmAE X4HtU5IgCOS++xbQPqZSNwjrFWlbgal2t2yAeTKAMdGX/nNKtfgZ5imqNwJWerpAYwgk 3qvUcgTw2MpeghcPpTiflPGp4fT/f1kUjes0dcqrvkE+6oTPvo0pi76QNoVs7peWKr/c JvaA==",
|
||||
* "From": "Paul Werner <test@sender.example>",
|
||||
* "In-Reply-To": "<CADfEuNuk6m=RUmo+R4K65Rfskox_LOTT+pnBwTnmA_gPaf1eUQ@mail.gmail.com>",
|
||||
* "Message-Id": "<CADfEuNu6nBJYqNSJ-suey3a0FazJELYkNSO5JUGiiGs9hsFvGg@mail.gmail.com>",
|
||||
* "Mime-Version": "1.0",
|
||||
* "Received": "by mail-lj1-f175.google.com with SMTP id 38308e7fff4ca-2d4a901e284so12524521fa.1 for <test@domain.example>; Sun, 17 Mar 2024 22:34:47 -0700 (PDT)",
|
||||
* "References": "<CADfEuNvN_DcTU99WY-7332iPmPuYW-CfvJfirQ6YY30e3y-XeA@mail.gmail.com> <CADfEuNupjxUivY++FnD7b1SHdNY6YZCA9b4iVW6xNbmic=B6Zw@mail.gmail.com> <CADfEuNuk6m=RUmo+R4K65Rfskox_LOTT+pnBwTnmA_gPaf1eUQ@mail.gmail.com>",
|
||||
* "Subject": "Fwd: TEST",
|
||||
* "To": "test@domain.example",
|
||||
* "X-Envelope-From": "test@sender.example",
|
||||
* "X-Gm-Message-State": "AOJu0Yy6rgBIPLGjnD293mVB5vBWQIraVAOnfa/GtyM6S/JIqe4rHbrx OqRe7oFFyCDyCjL/+2AFFkB9ljxgt7MWvpdec69dEn3BNQMlxuyGkpyxZUY8PDm4XRCyIy4vGxK 6Oddl7nWV5DM4zN4eLvZH+DPteyUq9A9ET9bowZnCrP8ZcQOP5js=",
|
||||
* "X-Google-Dkim-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1710740086; x=1711344886; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=tkxC+ZzDSJJXLVgjDyvQZyDt6wkWKFHS50z4ZWiWT9U=; b=cyDJAeNEaU2CvWAX/d9E3LMDrceXyLe01lbsYvwY6ZNTchr/0vzrxQFTVxos2DQR7u jSKpaNqI958H1oZJY36XZV0+8MY2w6DjB1F3FUHbD1q5gxJUitXNuOvvpna/q0ZaqlQf 5n3kIkakV19uxu4pcrcLxO67744pBzEmVk+IJtI9FEoZy9253v09CfkzNZo68u2VxJVD TDFVVkZuIO5xi3flUVoD3CP0Bw/0BqpDuxVvOFy+qOaItTZ5Na+OPfUJcFG2j6T0rXFQ 1vXPxodqjllLwc/V+O1TmS46H/RhsHGAae5tWk+51KX8T2ZgTkfwKPV1YeSRl0QtDhYS gU0Q==",
|
||||
* "X-Google-Smtp-Source": "AGHT+IFspt+3tKf94kXs48nOb58GzuV+pJ8oE3ZNwEcx6PG53wJeW858lyh2PiYIzSEPQTY2ykatvu2fqs8Bj+9d5rw=",
|
||||
* "X-Mailgun-Incoming": "Yes",
|
||||
* "X-Received": "by 2002:a2e:9847:0:b0:2d4:7455:89f6 with SMTP id e7-20020a2e9847000000b002d4745589f6mr4283454ljj.40.1710740086045; Sun, 17 Mar 2024 22:34:46 -0700 (PDT)",
|
||||
* "sender": "test@sender.example",
|
||||
* "recipients": "test@domain.example",
|
||||
* "from": "Paul Werner <test@sender.example>",
|
||||
* "subject": "Fwd: TEST",
|
||||
* "body-html": "<div dir=\"ltr\">TESTAGAIN<img src=\"cid:ii_ltwigc770\" alt=\"Unbenannt.png\" width=\"562\" height=\"408\"><br><br><div class=\"gmail_quote\"><div dir=\"ltr\" class=\"gmail_attr\">---------- Forwarded message ---------<br>Von: <strong class=\"gmail_sendername\" dir=\"auto\">Paul Werner</strong> <span dir=\"auto\"><<a href=\"mailto:test@sender.example\">test@sender.example</a>></span><br>Date: Mo., 18. März 2024 um 06:30 Uhr<br>Subject: Fwd: TEST<br>To: <<a href=\"mailto:test@domain.example\">test@domain.example</a>><br></div><br><br><div dir=\"ltr\">Hallöööö<br><br><div class=\"gmail_quote\"><div dir=\"ltr\" class=\"gmail_attr\">---------- Forwarded message ---------<br>Von: <strong class=\"gmail_sendername\" dir=\"auto\">Paul Werner</strong> <span dir=\"auto\"><<a href=\"mailto:test@sender.example\" target=\"_blank\">test@sender.example</a>></span><br>Date: Mo., 18. März 2024 um 06:23 Uhr<br>Subject: Fwd: TEST<br>To: <<a href=\"mailto:test@domain.example\" target=\"_blank\">test@domain.example</a>><br></div><br><br><div dir=\"ltr\">asjkdahwdaiohdawdawdawwwww!!!<br><br><div class=\"gmail_quote\"><div dir=\"ltr\" class=\"gmail_attr\">---------- Forwarded message ---------<br>Von: <strong class=\"gmail_sendername\" dir=\"auto\">Paul Werner</strong> <span dir=\"auto\"><<a href=\"mailto:test@sender.example\" target=\"_blank\">test@sender.example</a>></span><br>Date: Mo., 18. März 2024 um 06:22 Uhr<br>Subject: TEST<br>To: <<a href=\"mailto:test@domain.example\" target=\"_blank\">test@domain.example</a>><br></div><br><br><div dir=\"ltr\">TEST</div>\r\n</div></div>\r\n</div></div>\r\n</div></div>\r\n",
|
||||
* "body-plain": "TESTAGAIN[image: Unbenannt.png]\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:30 Uhr\r\nSubject: Fwd: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nHallöööö\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:23 Uhr\r\nSubject: Fwd: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nasjkdahwdaiohdawdawdawwwww!!!\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:22 Uhr\r\nSubject: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nTEST\r\n",
|
||||
* "attachments": [
|
||||
* {
|
||||
* "name": "Unbenannt.png",
|
||||
* "content-type": "image/png",
|
||||
* "size": 197753,
|
||||
* "url": "https://storage-europe-west1.api.mailgun.net/v3/domains/domain.example/messages/BAAFAgVMamdcBboOIOtFyJ5B5NGEkkffYQ/attachments/0"
|
||||
* }
|
||||
* ],
|
||||
* "content-id-map": {
|
||||
* "<ii_ltwigc770>": {
|
||||
* "name": "Unbenannt.png",
|
||||
* "content-type": "image/png",
|
||||
* "size": 197753,
|
||||
* "url": "https://storage-europe-west1.api.mailgun.net/v3/domains/domain.example/messages/BAAFAgVMamdcBboOIOtFyJ5B5NGEkkffYQ/attachments/0"
|
||||
* }
|
||||
* },
|
||||
* "message-headers": [
|
||||
* [
|
||||
* "Received",
|
||||
* "from mail-lj1-f175.google.com (mail-lj1-f175.google.com [209.85.208.175]) by 634f26f73cf3 with SMTP id <undefined> (version=TLS1.3, cipher=TLS_AES_128_GCM_SHA256); Mon, 18 Mar 2024 05:34:47 GMT"
|
||||
* ],
|
||||
* [
|
||||
* "Received",
|
||||
* "by mail-lj1-f175.google.com with SMTP id 38308e7fff4ca-2d4a901e284so12524521fa.1 for <test@domain.example>; Sun, 17 Mar 2024 22:34:47 -0700 (PDT)"
|
||||
* ],
|
||||
* [
|
||||
* "X-Envelope-From",
|
||||
* "test@sender.example"
|
||||
* ],
|
||||
* [
|
||||
* "X-Mailgun-Incoming",
|
||||
* "Yes"
|
||||
* ],
|
||||
* [
|
||||
* "Dkim-Signature",
|
||||
* "v=1; a=rsa-sha256; c=relaxed/relaxed; d=wer-ner.de; s=google; t=1710740086; x=1711344886; darn=domain.example; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=tkxC+ZzDSJJXLVgjDyvQZyDt6wkWKFHS50z4ZWiWT9U=; b=P1Sz54Djj1LHtPF7+cAKGRaN4IRjUT3bOyYAD/kbC0Tx2yNejPrjCPy3+a6R6MShgJ odYhoLRqylPPs1DQolNO6xgamsoEiR8jnII4QjJUBut4VirMlSO+RLxzpO7pt/Hr6j93 z0G1Yffpbz44l5GhndgXsa4Hf30Q8yy0p7fqMNABB/smscj7DJDu1os2cB1JazKYsmAE X4HtU5IgCOS++xbQPqZSNwjrFWlbgal2t2yAeTKAMdGX/nNKtfgZ5imqNwJWerpAYwgk 3qvUcgTw2MpeghcPpTiflPGp4fT/f1kUjes0dcqrvkE+6oTPvo0pi76QNoVs7peWKr/c JvaA=="
|
||||
* ],
|
||||
* [
|
||||
* "X-Google-Dkim-Signature",
|
||||
* "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1710740086; x=1711344886; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=tkxC+ZzDSJJXLVgjDyvQZyDt6wkWKFHS50z4ZWiWT9U=; b=cyDJAeNEaU2CvWAX/d9E3LMDrceXyLe01lbsYvwY6ZNTchr/0vzrxQFTVxos2DQR7u jSKpaNqI958H1oZJY36XZV0+8MY2w6DjB1F3FUHbD1q5gxJUitXNuOvvpna/q0ZaqlQf 5n3kIkakV19uxu4pcrcLxO67744pBzEmVk+IJtI9FEoZy9253v09CfkzNZo68u2VxJVD TDFVVkZuIO5xi3flUVoD3CP0Bw/0BqpDuxVvOFy+qOaItTZ5Na+OPfUJcFG2j6T0rXFQ 1vXPxodqjllLwc/V+O1TmS46H/RhsHGAae5tWk+51KX8T2ZgTkfwKPV1YeSRl0QtDhYS gU0Q=="
|
||||
* ],
|
||||
* [
|
||||
* "X-Gm-Message-State",
|
||||
* "AOJu0Yy6rgBIPLGjnD293mVB5vBWQIraVAOnfa/GtyM6S/JIqe4rHbrx OqRe7oFFyCDyCjL/+2AFFkB9ljxgt7MWvpdec69dEn3BNQMlxuyGkpyxZUY8PDm4XRCyIy4vGxK 6Oddl7nWV5DM4zN4eLvZH+DPteyUq9A9ET9bowZnCrP8ZcQOP5js="
|
||||
* ],
|
||||
* [
|
||||
* "X-Google-Smtp-Source",
|
||||
* "AGHT+IFspt+3tKf94kXs48nOb58GzuV+pJ8oE3ZNwEcx6PG53wJeW858lyh2PiYIzSEPQTY2ykatvu2fqs8Bj+9d5rw="
|
||||
* ],
|
||||
* [
|
||||
* "X-Received",
|
||||
* "by 2002:a2e:9847:0:b0:2d4:7455:89f6 with SMTP id e7-20020a2e9847000000b002d4745589f6mr4283454ljj.40.1710740086045; Sun, 17 Mar 2024 22:34:46 -0700 (PDT)"
|
||||
* ],
|
||||
* [
|
||||
* "Mime-Version",
|
||||
* "1.0"
|
||||
* ],
|
||||
* [
|
||||
* "References",
|
||||
* "<CADfEuNvN_DcTU99WY-7332iPmPuYW-CfvJfirQ6YY30e3y-XeA@mail.gmail.com> <CADfEuNupjxUivY++FnD7b1SHdNY6YZCA9b4iVW6xNbmic=B6Zw@mail.gmail.com> <CADfEuNuk6m=RUmo+R4K65Rfskox_LOTT+pnBwTnmA_gPaf1eUQ@mail.gmail.com>"
|
||||
* ],
|
||||
* [
|
||||
* "In-Reply-To",
|
||||
* "<CADfEuNuk6m=RUmo+R4K65Rfskox_LOTT+pnBwTnmA_gPaf1eUQ@mail.gmail.com>"
|
||||
* ],
|
||||
* [
|
||||
* "From",
|
||||
* "Paul Werner <test@sender.example>"
|
||||
* ],
|
||||
* [
|
||||
* "Date",
|
||||
* "Mon, 18 Mar 2024 06:34:09 +0100"
|
||||
* ],
|
||||
* [
|
||||
* "Message-Id",
|
||||
* "<CADfEuNu6nBJYqNSJ-suey3a0FazJELYkNSO5JUGiiGs9hsFvGg@mail.gmail.com>"
|
||||
* ],
|
||||
* [
|
||||
* "Subject",
|
||||
* "Fwd: TEST"
|
||||
* ],
|
||||
* [
|
||||
* "To",
|
||||
* "test@domain.example"
|
||||
* ],
|
||||
* [
|
||||
* "Content-Type",
|
||||
* "multipart/related; boundary=\"00000000000022bfbe0613e8b7f5\""
|
||||
* ]
|
||||
* ],
|
||||
* "stripped-html": "<html><head></head><body><div dir=\"ltr\">TESTAGAIN<img src=\"cid:ii_ltwigc770\" alt=\"Unbenannt.png\" width=\"562\" height=\"408\"><br><br></div>\n</body></html>",
|
||||
* "stripped-text": "TESTAGAIN[image: Unbenannt.png]\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:30 Uhr\r\nSubject: Fwd: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nHallöööö\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:23 Uhr\r\nSubject: Fwd: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nasjkdahwdaiohdawdawdawwwww!!!\r\n\r\n---------- Forwarded message ---------\r\nVon: Paul Werner <test@sender.example>\r\nDate: Mo., 18. März 2024 um 06:22 Uhr\r\nSubject: TEST\r\nTo: <test@domain.example>\r\n\r\n\r\nTEST",
|
||||
* "stripped-signature": ""
|
||||
* }
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$from = $this->sender;//explode("|", $this->input)[0];
|
||||
$to = $this->recipient; //explode("|", $this->input)[1];
|
||||
// $messageId = explode("|", $this->input)[2]; // used as base in download function
|
||||
|
||||
// Spam protection
|
||||
if ($this->engine->isInvalidOrBlocked($from, $to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// lets assess this at a higher level to ensure that only valid email inboxes are processed.
|
||||
// match company
|
||||
// $company = MultiDB::findAndSetDbByExpenseMailbox($to);
|
||||
// if (!$company) {
|
||||
// nlog('[ProcessMailgunInboundWebhook] unknown Expense Mailbox occured while handling an inbound email from mailgun: ' . $to);
|
||||
// $this->engine->saveMeta($from, $to, true); // important to save this, to protect from spam
|
||||
// return;
|
||||
// }
|
||||
|
||||
try { // important to save meta if something fails here to prevent spam
|
||||
|
||||
// fetch message from mailgun-api
|
||||
$company_mailgun_domain = $this->company->getSetting('email_sending_method') == 'client_mailgun' && strlen($this->company->getSetting('mailgun_domain') ?? '') > 2 ? $this->company->getSetting('mailgun_domain') : null;
|
||||
$company_mailgun_secret = $this->company->getSetting('email_sending_method') == 'client_mailgun' && strlen($this->company->getSetting('mailgun_secret') ?? '') > 2 ? $this->company->getSetting('mailgun_secret') : null;
|
||||
if (!($company_mailgun_domain && $company_mailgun_secret) && !(config('services.mailgun.domain') && config('services.mailgun.secret')))
|
||||
throw new \Error("[ProcessMailgunInboundWebhook] no mailgun credentials found, we cannot get the attachements and files");
|
||||
|
||||
$mail = null;
|
||||
if ($company_mailgun_domain && $company_mailgun_secret) {
|
||||
|
||||
$credentials = $company_mailgun_domain . ":" . $company_mailgun_secret . "@";
|
||||
$messageUrl = $this->message_url;//explode("|", $this->input)[2];
|
||||
$messageUrl = str_replace("http://", "http://" . $credentials, $messageUrl);
|
||||
$messageUrl = str_replace("https://", "https://" . $credentials, $messageUrl);
|
||||
|
||||
try {
|
||||
$mail = json_decode(file_get_contents($messageUrl));
|
||||
} catch (\Error $e) {
|
||||
if (config('services.mailgun.secret')) {
|
||||
nlog("[ProcessMailgunInboundWebhook] Error while downloading with company credentials, we try to use default credentials now...");
|
||||
|
||||
$credentials = config('services.mailgun.domain') . ":" . config('services.mailgun.secret') . "@";
|
||||
$messageUrl = $this->message_url;//explode("|", $this->input)[2];
|
||||
$messageUrl = str_replace("http://", "http://" . $credentials, $messageUrl);
|
||||
$messageUrl = str_replace("https://", "https://" . $credentials, $messageUrl);
|
||||
$mail = json_decode(file_get_contents($messageUrl));
|
||||
|
||||
} else
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$credentials = config('services.mailgun.domain') . ":" . config('services.mailgun.secret') . "@";
|
||||
$messageUrl = $this->message_url; //explode("|", $this->input)[2];
|
||||
$messageUrl = str_replace("http://", "http://" . $credentials, $messageUrl);
|
||||
$messageUrl = str_replace("https://", "https://" . $credentials, $messageUrl);
|
||||
$mail = json_decode(file_get_contents($messageUrl));
|
||||
|
||||
}
|
||||
|
||||
// prepare data for ingresEngine
|
||||
$inboundMail = new InboundMail();
|
||||
|
||||
$inboundMail->from = $from;
|
||||
$inboundMail->to = $to; // usage of data-input, because we need a single email here
|
||||
$inboundMail->subject = $mail->Subject;
|
||||
$inboundMail->body = $mail->{"body-html"};
|
||||
$inboundMail->text_body = $mail->{"body-plain"};
|
||||
$inboundMail->date = Carbon::createFromTimeString($mail->Date);
|
||||
|
||||
// parse documents as UploadedFile from webhook-data
|
||||
foreach ($mail->attachments as $attachment) { // prepare url with credentials before downloading :: https://github.com/mailgun/mailgun.js/issues/24
|
||||
|
||||
// download file and save to tmp dir
|
||||
if ($company_mailgun_domain && $company_mailgun_secret) {
|
||||
|
||||
try {
|
||||
|
||||
$credentials = $company_mailgun_domain . ":" . $company_mailgun_secret . "@";
|
||||
$url = $attachment->url;
|
||||
$url = str_replace("http://", "http://" . $credentials, $url);
|
||||
$url = str_replace("https://", "https://" . $credentials, $url);
|
||||
$inboundMail->documents[] = TempFile::UploadedFileFromUrl($url, $attachment->name, $attachment->{"content-type"});
|
||||
|
||||
} catch (\Error $e) {
|
||||
if (config('services.mailgun.secret')) {
|
||||
nlog("[ProcessMailgunInboundWebhook] Error while downloading with company credentials, we try to use default credentials now...");
|
||||
|
||||
$credentials = config('services.mailgun.domain') . ":" . config('services.mailgun.secret') . "@";
|
||||
$url = $attachment->url;
|
||||
$url = str_replace("http://", "http://" . $credentials, $url);
|
||||
$url = str_replace("https://", "https://" . $credentials, $url);
|
||||
$inboundMail->documents[] = TempFile::UploadedFileFromUrl($url, $attachment->name, $attachment->{"content-type"});
|
||||
|
||||
} else
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$credentials = config('services.mailgun.domain') . ":" . config('services.mailgun.secret') . "@";
|
||||
$url = $attachment->url;
|
||||
$url = str_replace("http://", "http://" . $credentials, $url);
|
||||
$url = str_replace("https://", "https://" . $credentials, $url);
|
||||
$inboundMail->documents[] = TempFile::UploadedFileFromUrl($url, $attachment->name, $attachment->{"content-type"});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->engine->saveMeta($from, $to); // important to save this, to protect from spam
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// perform
|
||||
$this->engine->handleExpenseMailbox($inboundMail);
|
||||
}
|
||||
}
|
@ -73,6 +73,8 @@ class MultiDB
|
||||
'socket',
|
||||
];
|
||||
|
||||
private static $protected_expense_mailboxes = [];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@ -84,7 +86,7 @@ class MultiDB
|
||||
public static function checkDomainAvailable($subdomain): bool
|
||||
{
|
||||
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return Company::whereSubdomain($subdomain)->count() == 0;
|
||||
}
|
||||
|
||||
@ -107,9 +109,35 @@ class MultiDB
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function checkExpenseMailboxAvailable($expense_mailbox): bool
|
||||
{
|
||||
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return !Company::where("expense_mailbox", $expense_mailbox)->exists();
|
||||
}
|
||||
|
||||
if (in_array($expense_mailbox, self::$protected_expense_mailboxes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current_db = config('database.default');
|
||||
|
||||
foreach (self::$dbs as $db) {
|
||||
if (Company::on($db)->where("expense_mailbox", $expense_mailbox)->exists()) {
|
||||
self::setDb($current_db);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self::setDb($current_db);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function checkUserEmailExists($email): bool
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return User::where(['email' => $email])->withTrashed()->exists();
|
||||
} // true >= 1 emails found / false -> == emails found
|
||||
|
||||
@ -170,7 +198,7 @@ class MultiDB
|
||||
*/
|
||||
public static function hasUser(array $data): ?User
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return User::where($data)->withTrashed()->first();
|
||||
}
|
||||
|
||||
@ -194,7 +222,7 @@ class MultiDB
|
||||
*/
|
||||
public static function hasContact(string $email): ?ClientContact
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return ClientContact::where('email', $email)->withTrashed()->first();
|
||||
}
|
||||
|
||||
@ -221,7 +249,7 @@ class MultiDB
|
||||
*/
|
||||
public static function findContact(array $search): ?ClientContact
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return ClientContact::where($search)->first();
|
||||
}
|
||||
|
||||
@ -485,7 +513,7 @@ class MultiDB
|
||||
|
||||
public static function findAndSetDbByDomain($query_array)
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return Company::where($query_array)->first();
|
||||
}
|
||||
|
||||
@ -504,9 +532,30 @@ class MultiDB
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function findAndSetDbByExpenseMailbox($expense_mailbox)
|
||||
{
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return Company::where("expense_mailbox", $expense_mailbox)->first();
|
||||
}
|
||||
|
||||
$current_db = config('database.default');
|
||||
|
||||
foreach (self::$dbs as $db) {
|
||||
if ($company = Company::on($db)->where("expense_mailbox", $expense_mailbox)->first()) {
|
||||
self::setDb($db);
|
||||
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
|
||||
self::setDB($current_db);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function findAndSetByPaymentHash(string $hash)
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return PaymentHash::with('fee_invoice')->where('hash', $hash)->first();
|
||||
}
|
||||
|
||||
@ -527,7 +576,7 @@ class MultiDB
|
||||
|
||||
public static function findAndSetDbByInvitation($entity, $invitation_key)
|
||||
{
|
||||
$class = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
|
||||
$class = 'App\Models\\' . ucfirst(Str::camel($entity)) . 'Invitation';
|
||||
$current_db = config('database.default');
|
||||
|
||||
foreach (self::$dbs as $db) {
|
||||
@ -549,13 +598,13 @@ class MultiDB
|
||||
*/
|
||||
public static function hasPhoneNumber(string $phone): bool
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled')) {
|
||||
if (!config('ninja.db.multi_db_enabled')) {
|
||||
return Account::where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists();
|
||||
}
|
||||
|
||||
$current_db = config('database.default');
|
||||
|
||||
if(SMSNumbers::hasNumber($phone)) { // @phpstan-ignore-line
|
||||
if (SMSNumbers::hasNumber($phone)) { // @phpstan-ignore-line
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -583,8 +632,26 @@ class MultiDB
|
||||
$string = '';
|
||||
$vowels = ['a', 'e', 'i', 'o', 'u', 'y'];
|
||||
$consonants = [
|
||||
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
|
||||
'n', 'p', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'p',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
];
|
||||
|
||||
$max = $length / 2;
|
||||
@ -592,7 +659,7 @@ class MultiDB
|
||||
$string .= $consonants[rand(0, 19)];
|
||||
$string .= $vowels[rand(0, 5)];
|
||||
}
|
||||
} while (! self::checkDomainAvailable($string));
|
||||
} while (!self::checkDomainAvailable($string));
|
||||
|
||||
self::setDb($current_db);
|
||||
|
||||
|
@ -112,6 +112,14 @@ use Laracasts\Presenter\PresentableTrait;
|
||||
* @property int $convert_expense_currency
|
||||
* @property int $notify_vendor_when_paid
|
||||
* @property int $invoice_task_hours
|
||||
* @property string|null $expense_mailbox
|
||||
* @property boolean $expense_mailbox_active
|
||||
* @property bool $inbound_mailbox_allow_company_users
|
||||
* @property bool $inbound_mailbox_allow_vendors
|
||||
* @property bool $inbound_mailbox_allow_clients
|
||||
* @property bool $inbound_mailbox_allow_unknown
|
||||
* @property string|null $inbound_mailbox_whitelist
|
||||
* @property string|null $inbound_mailbox_blacklist
|
||||
* @property int $deleted_at
|
||||
* @property string|null $smtp_username
|
||||
* @property string|null $smtp_password
|
||||
@ -362,6 +370,14 @@ class Company extends BaseModel
|
||||
'calculate_taxes',
|
||||
'tax_data',
|
||||
'e_invoice_certificate_passphrase',
|
||||
'expense_mailbox_active',
|
||||
'expense_mailbox', // TODO: @turbo124 custom validation: self-hosted => free change, hosted => not changeable, only changeable with env-mask
|
||||
'inbound_mailbox_allow_company_users',
|
||||
'inbound_mailbox_allow_vendors',
|
||||
'inbound_mailbox_allow_clients',
|
||||
'inbound_mailbox_allow_unknown',
|
||||
'inbound_mailbox_whitelist',
|
||||
'inbound_mailbox_blacklist',
|
||||
'smtp_host',
|
||||
'smtp_port',
|
||||
'smtp_encryption',
|
||||
@ -861,7 +877,7 @@ class Company extends BaseModel
|
||||
return $this->portal_domain;
|
||||
}
|
||||
|
||||
return "https://{$this->subdomain}.".config('ninja.app_domain');
|
||||
return "https://{$this->subdomain}." . config('ninja.app_domain');
|
||||
}
|
||||
|
||||
return config('ninja.app_url');
|
||||
@ -879,7 +895,7 @@ class Company extends BaseModel
|
||||
|
||||
public function file_path(): string
|
||||
{
|
||||
return $this->company_key.'/';
|
||||
return $this->company_key . '/';
|
||||
}
|
||||
|
||||
public function rBits()
|
||||
@ -967,7 +983,7 @@ class Company extends BaseModel
|
||||
|
||||
public function getInvoiceCert()
|
||||
{
|
||||
if($this->e_invoice_certificate) {
|
||||
if ($this->e_invoice_certificate) {
|
||||
return base64_decode($this->e_invoice_certificate);
|
||||
}
|
||||
|
||||
|
@ -113,6 +113,8 @@ class SystemLog extends Model
|
||||
|
||||
public const EVENT_USER = 61;
|
||||
|
||||
public const EVENT_INBOUND_MAIL_BLOCKED = 62;
|
||||
|
||||
/*Type IDs*/
|
||||
public const TYPE_PAYPAL = 300;
|
||||
|
||||
|
@ -181,7 +181,7 @@ class Vendor extends BaseModel
|
||||
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
|
||||
$currencies = app('currencies');
|
||||
|
||||
if (! $this->currency_id) {
|
||||
if (!$this->currency_id) {
|
||||
return $this->company->currency();
|
||||
}
|
||||
|
||||
@ -217,14 +217,14 @@ class Vendor extends BaseModel
|
||||
{
|
||||
$defaults = [];
|
||||
|
||||
if (! (array_key_exists('terms', $data) && strlen($data['terms']) > 1)) {
|
||||
$defaults['terms'] = $this->getSetting($entity_name.'_terms');
|
||||
if (!(array_key_exists('terms', $data) && strlen($data['terms']) > 1)) {
|
||||
$defaults['terms'] = $this->getSetting($entity_name . '_terms');
|
||||
} elseif (array_key_exists('terms', $data)) {
|
||||
$defaults['terms'] = $data['terms'];
|
||||
}
|
||||
|
||||
if (! (array_key_exists('footer', $data) && strlen($data['footer']) > 1)) {
|
||||
$defaults['footer'] = $this->getSetting($entity_name.'_footer');
|
||||
if (!(array_key_exists('footer', $data) && strlen($data['footer']) > 1)) {
|
||||
$defaults['footer'] = $this->getSetting($entity_name . '_footer');
|
||||
} elseif (array_key_exists('footer', $data)) {
|
||||
$defaults['footer'] = $data['footer'];
|
||||
}
|
||||
@ -262,7 +262,7 @@ class Vendor extends BaseModel
|
||||
{
|
||||
$contact_key = $invitation->contact->contact_key;
|
||||
|
||||
return $this->company->company_key.'/'.$this->vendor_hash.'/'.$contact_key.'/purchase_orders/';
|
||||
return $this->company->company_key . '/' . $this->vendor_hash . '/' . $contact_key . '/purchase_orders/';
|
||||
}
|
||||
|
||||
public function locale(): string
|
||||
@ -287,7 +287,7 @@ class Vendor extends BaseModel
|
||||
|
||||
public function backup_path(): string
|
||||
{
|
||||
return $this->company->company_key.'/'.$this->vendor_hash.'/backups';
|
||||
return $this->company->company_key . '/' . $this->vendor_hash . '/backups';
|
||||
}
|
||||
|
||||
public function service()
|
||||
|
@ -126,18 +126,18 @@ class AppServiceProvider extends ServiceProvider
|
||||
new Dsn(
|
||||
'brevo+api',
|
||||
'default',
|
||||
config('services.brevo.key')
|
||||
config('services.brevo.secret')
|
||||
)
|
||||
);
|
||||
});
|
||||
Mailer::macro('brevo_config', function (string $brevo_key) {
|
||||
Mailer::macro('brevo_config', function (string $brevo_secret) {
|
||||
// @phpstan-ignore /** @phpstan-ignore-next-line **/
|
||||
Mailer::setSymfonyTransport(
|
||||
(new BrevoTransportFactory())->create(
|
||||
new Dsn(
|
||||
'brevo+api',
|
||||
'default',
|
||||
$brevo_key
|
||||
$brevo_secret
|
||||
)
|
||||
)
|
||||
);
|
||||
|
188
app/Services/EDocument/Imports/MindeeEDocument.php
Normal file
188
app/Services/EDocument/Imports/MindeeEDocument.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Imports;
|
||||
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Factory\VendorContactFactory;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Models\Company;
|
||||
use App\Models\Country;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\VendorContact;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\TempFile;
|
||||
use App\Utils\Traits\SavesDocuments;
|
||||
use Cache;
|
||||
use Exception;
|
||||
use Mindee\Client;
|
||||
use Mindee\Product\Invoice\InvoiceV4;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class MindeeEDocument extends AbstractService
|
||||
{
|
||||
use SavesDocuments;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(public UploadedFile $file, public Company $company)
|
||||
{
|
||||
# curl -X POST http://localhost:8000/api/v1/edocument/upload -H "Content-Type: multipart/form-data" -H "X-API-TOKEN: 7tdDdkz987H3AYIWhNGXy8jTjJIoDhkAclCDLE26cTCj1KYX7EBHC66VEitJwWhn" -H "X-Requested-With: XMLHttpRequest" -F _method=PUT -F documents[]=@einvoice.xml
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function run(): Expense
|
||||
{
|
||||
$api_key = config('services.mindee.api_key');
|
||||
|
||||
if (!$api_key)
|
||||
throw new Exception('Mindee API key not configured');
|
||||
|
||||
$this->checkLimits();
|
||||
|
||||
// perform parsing
|
||||
$mindeeClient = new Client($api_key);
|
||||
$inputSource = $mindeeClient->sourceFromBytes($this->file->get(), $this->file->getClientOriginalName());
|
||||
$result = $mindeeClient->parse(InvoiceV4::class, $inputSource);
|
||||
$this->incrementRequestCounts();
|
||||
|
||||
/** @var \Mindee\Product\Invoice\InvoiceV4Document $prediction */
|
||||
$prediction = $result->document->inference->prediction;
|
||||
|
||||
if ($prediction->documentType->value !== 'INVOICE')
|
||||
throw new Exception('Unsupported document type');
|
||||
|
||||
$grandTotalAmount = $prediction->totalAmount->value;
|
||||
$documentno = $prediction->invoiceNumber->value;
|
||||
$documentdate = $prediction->date->value;
|
||||
$invoiceCurrency = $prediction->locale->currency;
|
||||
$country = $prediction->locale->country;
|
||||
|
||||
$expense = Expense::query()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('amount', $grandTotalAmount)
|
||||
->where("transaction_reference", $documentno)
|
||||
->whereDate("date", $documentdate)
|
||||
->first();
|
||||
|
||||
if (!$expense) {
|
||||
// The document does not exist as an expense
|
||||
// Handle accordingly
|
||||
|
||||
/** @var \App\Models\Currency $currency */
|
||||
$currency = app('currencies')->first(function ($c) use ($invoiceCurrency) {
|
||||
/** @var \App\Models\Currency $c */
|
||||
return $c->code == $invoiceCurrency;
|
||||
});
|
||||
|
||||
$expense = ExpenseFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$expense->date = $documentdate;
|
||||
$expense->public_notes = $documentno;
|
||||
$expense->currency_id = $currency ? $currency->id : $this->company->settings->currency_id;
|
||||
$expense->save();
|
||||
|
||||
$this->saveDocuments([
|
||||
$this->file,
|
||||
TempFile::UploadedFileFromRaw(strval($result->document), $documentno . "_mindee_orc_result.txt", "text/plain")
|
||||
], $expense);
|
||||
// $expense->saveQuietly();
|
||||
|
||||
$expense->uses_inclusive_taxes = True;
|
||||
$expense->amount = $grandTotalAmount;
|
||||
$counter = 1;
|
||||
|
||||
foreach ($prediction->taxes as $taxesElem) {
|
||||
$expense->{"tax_amount{$counter}"} = $taxesElem->value;
|
||||
$expense->{"tax_rate{$counter}"} = $taxesElem->rate;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
/** @var \App\Models\VendorContact $vendor_contact */
|
||||
$vendor_contact = VendorContact::query()->where("company_id", $this->company->id)->where("email", $prediction->supplierEmail)->first();
|
||||
|
||||
/** @var \App\Models\Vendor|null $vendor */
|
||||
$vendor = $vendor_contact ? $vendor_contact->vendor : Vendor::query()->where("company_id", $this->company->id)->where("name", $prediction->supplierName)->first();
|
||||
|
||||
if ($vendor) {
|
||||
$expense->vendor_id = $vendor->id;
|
||||
} else {
|
||||
$vendor = VendorFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$vendor->name = $prediction->supplierName;
|
||||
|
||||
$vendor->currency_id = Currency::whereCode($invoiceCurrency)->first()?->id;
|
||||
$vendor->phone = $prediction->supplierPhoneNumber;
|
||||
// $vendor->address1 = $address_1; // TODO: we only have the full address string from mindee returned
|
||||
// $vendor->address2 = $address_2;
|
||||
// $vendor->city = $city;
|
||||
// $vendor->postal_code = $postcode;
|
||||
|
||||
/** @var ?\App\Models\Country $country */
|
||||
$country = app('countries')->first(function ($c) use ($country) {
|
||||
/** @var \App\Models\Country $c */
|
||||
return $c->iso_3166_2 == $country || $c->iso_3166_3 == $country;
|
||||
});
|
||||
|
||||
if ($country)
|
||||
$vendor->country_id = $country->id;
|
||||
|
||||
$vendor->save();
|
||||
|
||||
if (strlen($prediction->supplierEmail ?? '') > 2) {
|
||||
$vendor_contact = VendorContactFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$vendor_contact->vendor_id = $vendor->id;
|
||||
$vendor_contact->email = $prediction->supplierEmail;
|
||||
$vendor_contact->save();
|
||||
}
|
||||
|
||||
$expense->vendor_id = $vendor->id;
|
||||
}
|
||||
$expense->transaction_reference = $documentno;
|
||||
} else {
|
||||
// The document exists as an expense
|
||||
// Handle accordingly
|
||||
nlog("Mindee: Document already exists");
|
||||
$expense->private_notes = $expense->private_notes . ctrans("texts.edocument_import_already_exists", ["date" => now()->format('Y-m-d')]);
|
||||
}
|
||||
|
||||
$expense->save();
|
||||
return $expense;
|
||||
}
|
||||
|
||||
private function checkLimits()
|
||||
{
|
||||
Cache::add('mindeeTotalDailyRequests', 0, now()->endOfDay());
|
||||
Cache::add('mindeeTotalMonthlyRequests', 0, now()->endOfMonth());
|
||||
Cache::add('mindeeAccountDailyRequests' . $this->company->account->id, 0, now()->endOfDay());
|
||||
Cache::add('mindeeAccountMonthlyRequests' . $this->company->account->id, 0, now()->endOfMonth());
|
||||
if (config('services.mindee.daily_limit') != 0 && Cache::get('mindeeTotalDailyRequests') > config('services.mindee.daily_limit'))
|
||||
throw new Exception('Mindee daily limit reached');
|
||||
if (config('services.mindee.monthly_limit') != 0 && Cache::get('mindeeTotalMonthlyRequests') > config('services.mindee.monthly_limit'))
|
||||
throw new Exception('Mindee monthly limit reached');
|
||||
if (config('services.mindee.account_daily_limit') != 0 && Cache::get('mindeeAccountDailyRequests' . $this->company->account->id) > config('services.mindee.account_daily_limit'))
|
||||
throw new Exception('Mindee daily limit reached for account: ' . $this->company->account->id);
|
||||
if (config('services.mindee.account_monthly_limit') != 0 && Cache::get('mindeeAccountMonthlyRequests' . $this->company->account->id) > config('services.mindee.account_monthly_limit'))
|
||||
throw new Exception('Mindee monthly limit reached for account: ' . $this->company->account->id);
|
||||
}
|
||||
|
||||
private function incrementRequestCounts()
|
||||
{
|
||||
Cache::increment('mindeeTotalDailyRequests');
|
||||
Cache::increment('mindeeTotalMonthlyRequests');
|
||||
Cache::increment('mindeeAccountDailyRequests' . $this->company->account->id);
|
||||
Cache::increment('mindeeAccountMonthlyRequests' . $this->company->account->id);
|
||||
}
|
||||
}
|
||||
|
81
app/Services/EDocument/Imports/ParseEDocument.php
Normal file
81
app/Services/EDocument/Imports/ParseEDocument.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Imports;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Expense;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\Ninja;
|
||||
use Exception;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ParseEDocument extends AbstractService
|
||||
{
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(private UploadedFile $file, private Company $company)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the service.
|
||||
* the service will parse the file with all available libraries of the system and will return an expense, when possible
|
||||
*
|
||||
* @developer the function should be implemented with local first aproach to save costs of external providers (like mindee ocr)
|
||||
*
|
||||
* @return Expense
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function run(): Expense
|
||||
{
|
||||
|
||||
/** @var \App\Models\Account $account */
|
||||
$account = $this->company->owner()->account;
|
||||
|
||||
$extension = $this->file->getClientOriginalExtension() ?: $this->file->getExtension();
|
||||
$mimetype = $this->file->getClientMimeType() ?: $$this->file->getMimeType();
|
||||
|
||||
// ZUGFERD - try to parse via Zugferd lib
|
||||
switch (true) {
|
||||
case ($extension == 'pdf' || $mimetype == 'application/pdf'):
|
||||
case ($extension == 'xml' || $mimetype == 'application/xml') && stristr($this->file->get(), "urn:cen.eu:en16931:2017"):
|
||||
case ($extension == 'xml' || $mimetype == 'application/xml') && stristr($this->file->get(), "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"):
|
||||
case ($extension == 'xml' || $mimetype == 'application/xml') && stristr($this->file->get(), "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.1"):
|
||||
case ($extension == 'xml' || $mimetype == 'application/xml') && stristr($this->file->get(), "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.0"):
|
||||
try {
|
||||
return (new ZugferdEDocument($this->file, $this->company))->run();
|
||||
} catch (Exception $e) {
|
||||
nlog("Zugferd Exception: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// MINDEE OCR - try to parse via mindee external service
|
||||
if (config('services.mindee.api_key') && !(Ninja::isHosted() && !($account->isPaid() && $account->plan == 'enterprise')))
|
||||
switch (true) {
|
||||
case ($extension == 'pdf' || $mimetype == 'application/pdf'):
|
||||
case ($extension == 'heic' || $extension == 'heic' || $extension == 'png' || $extension == 'jpg' || $extension == 'jpeg' || $extension == 'webp' || str_starts_with($mimetype, 'image/')):
|
||||
try {
|
||||
return (new MindeeEDocument($this->file, $this->company))->run();
|
||||
} catch (Exception $e) {
|
||||
if (!($e->getMessage() == 'Unsupported document type'))
|
||||
nlog("Mindee Exception: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// NO PARSER OR ERROR
|
||||
throw new Exception("File type not supported or issue while parsing", 409);
|
||||
}
|
||||
}
|
||||
|
@ -11,29 +11,33 @@
|
||||
|
||||
namespace App\Services\EDocument\Imports;
|
||||
|
||||
use Exception;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Company;
|
||||
use App\Models\Country;
|
||||
use App\Models\Expense;
|
||||
use App\Utils\TempFile;
|
||||
use App\Models\Currency;
|
||||
use App\Jobs\Util\UploadFile;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Jobs\Util\UploadFile;
|
||||
use App\Models\Country;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\VendorContact;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\TempFile;
|
||||
use App\Utils\Traits\SavesDocuments;
|
||||
use Exception;
|
||||
use App\Models\Company;
|
||||
use horstoeko\zugferd\ZugferdDocumentReader;
|
||||
use horstoeko\zugferdvisualizer\ZugferdVisualizer;
|
||||
use horstoeko\zugferdvisualizer\renderer\ZugferdVisualizerLaravelRenderer;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ZugferdEDocument extends AbstractService
|
||||
{
|
||||
use SavesDocuments;
|
||||
public ZugferdDocumentReader|string $document;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(public string $tempdocument, public string $documentname, public Company $company)
|
||||
public function __construct(public UploadedFile $file, public Company $company)
|
||||
{
|
||||
# curl -X POST http://localhost:8000/api/v1/edocument/upload -H "Content-Type: multipart/form-data" -H "X-API-TOKEN: 7tdDdkz987H3AYIWhNGXy8jTjJIoDhkAclCDLE26cTCj1KYX7EBHC66VEitJwWhn" -H "X-Requested-With: XMLHttpRequest" -F _method=PUT -F documents[]=@einvoice.xml
|
||||
}
|
||||
@ -46,7 +50,7 @@ class ZugferdEDocument extends AbstractService
|
||||
/** @var \App\Models\User $user */
|
||||
$user = $this->company->owner();
|
||||
|
||||
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->tempdocument);
|
||||
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->file->get());
|
||||
$this->document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
|
||||
$this->document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount);
|
||||
|
||||
@ -68,11 +72,13 @@ class ZugferdEDocument extends AbstractService
|
||||
$expense->currency_id = Currency::whereCode($invoiceCurrency)->first()->id ?? $this->company->settings->currency_id;
|
||||
$expense->save();
|
||||
|
||||
$origin_file = TempFile::UploadedFileFromRaw($this->tempdocument, $this->documentname, "application/xml");
|
||||
(new UploadFile($origin_file, UploadFile::DOCUMENT, $user, $expense->company, $expense, null, false))->handle();
|
||||
$uploaded_file = TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno."_visualiser.pdf", "application/pdf");
|
||||
(new UploadFile($uploaded_file, UploadFile::DOCUMENT, $user, $expense->company, $expense, null, false))->handle();
|
||||
$documents = [$this->file];
|
||||
if ($this->file->getExtension() == "xml")
|
||||
array_push($documents, TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno . "_visualiser.pdf", "application/pdf"));
|
||||
$this->saveDocuments($documents, $expense);
|
||||
|
||||
$expense->save();
|
||||
|
||||
if ($taxCurrency && $taxCurrency != $invoiceCurrency) {
|
||||
$expense->private_notes = ctrans("texts.tax_currency_mismatch");
|
||||
}
|
||||
@ -91,14 +97,25 @@ class ZugferdEDocument extends AbstractService
|
||||
$this->document->getDocumentSellerContact($person_name, $person_department, $contact_phone, $contact_fax, $contact_email);
|
||||
$this->document->getDocumentSellerAddress($address_1, $address_2, $address_3, $postcode, $city, $country, $subdivision);
|
||||
$this->document->getDocumentSellerTaxRegistration($taxtype);
|
||||
|
||||
$taxid = null;
|
||||
if (array_key_exists("VA", $taxtype)) {
|
||||
$taxid = $taxtype["VA"];
|
||||
}
|
||||
$vendor = Vendor::where('vat_number', $taxid)->first();
|
||||
|
||||
if (!empty($vendor)) {
|
||||
// Vendor found
|
||||
$vendor = Vendor::query()
|
||||
->where("company_id", $this->company->id)
|
||||
->where(function ($q) use($taxid, $person_name, $contact_email){
|
||||
$q->when(!is_null($taxid), function ($when_query) use($taxid){
|
||||
$when_query->orWhere('vat_number', $taxid);
|
||||
})
|
||||
->orWhere("name", $person_name)
|
||||
->orWhereHas('contacts', function ($qq) use ($contact_email){
|
||||
$qq->where("email", $contact_email);
|
||||
});
|
||||
})->first();
|
||||
|
||||
if ($vendor) {
|
||||
$expense->vendor_id = $vendor->id;
|
||||
} else {
|
||||
$vendor = VendorFactory::create($this->company->id, $user->id);
|
||||
@ -112,7 +129,12 @@ class ZugferdEDocument extends AbstractService
|
||||
$vendor->address2 = $address_2;
|
||||
$vendor->city = $city;
|
||||
$vendor->postal_code = $postcode;
|
||||
$vendor->country_id = Country::query()->where('iso_3166_2', $country)->first()->id;
|
||||
|
||||
$country = app('countries')->first(function ($c) use ($country) {
|
||||
return $c->iso_3166_2 == $country || $c->iso_3166_3 == $country;
|
||||
});
|
||||
if ($country)
|
||||
$vendor->country_id = $country->id;
|
||||
|
||||
$vendor->save();
|
||||
$expense->vendor_id = $vendor->id;
|
||||
@ -121,7 +143,7 @@ class ZugferdEDocument extends AbstractService
|
||||
} else {
|
||||
// The document exists as an expense
|
||||
// Handle accordingly
|
||||
nlog("Document already exists");
|
||||
nlog("Zugferd: Document already exists");
|
||||
$expense->private_notes = $expense->private_notes . ctrans("texts.edocument_import_already_exists", ["date" => time()]);
|
||||
}
|
||||
$expense->save();
|
||||
|
42
app/Services/InboundMail/InboundMail.php
Normal file
42
app/Services/InboundMail/InboundMail.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
namespace App\Services\InboundMail;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* InboundMail.
|
||||
*/
|
||||
class InboundMail
|
||||
{
|
||||
public string $to;
|
||||
|
||||
public string $from;
|
||||
|
||||
public ?string $subject = null;
|
||||
|
||||
public ?string $body = null;
|
||||
|
||||
public ?UploadedFile $body_document = null;
|
||||
|
||||
public string $text_body;
|
||||
|
||||
/** @var array $documents */
|
||||
public array $documents = [];
|
||||
|
||||
public ?Carbon $date = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
318
app/Services/InboundMail/InboundMailEngine.php
Normal file
318
app/Services/InboundMail/InboundMailEngine.php
Normal file
@ -0,0 +1,318 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
namespace App\Services\InboundMail;
|
||||
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Company;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\VendorContact;
|
||||
use App\Services\EDocument\Imports\ParseEDocument;
|
||||
use App\Services\InboundMail\InboundMail;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\TempFile;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use App\Utils\Traits\SavesDocuments;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Cache;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InboundMailEngine
|
||||
{
|
||||
use SerializesModels, MakesHash;
|
||||
use GeneratesCounter, SavesDocuments;
|
||||
|
||||
private array $globalBlacklist;
|
||||
|
||||
private array $globalWhitelist;
|
||||
|
||||
public function __construct(private Company $company)
|
||||
{
|
||||
$this->globalBlacklist = Ninja::isSelfHost() ? explode(",", config('ninja.inbound_mailbox.global_inbound_blocklist')) : [];
|
||||
$this->globalWhitelist = Ninja::isSelfHost() ? explode(",", config('ninja.inbound_mailbox.global_inbound_whitelist')) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* if there is not a company with an matching mailbox, we only do monitoring
|
||||
* reuse this method to add more mail-parsing behaviors
|
||||
*/
|
||||
public function handleExpenseMailbox(InboundMail $email)
|
||||
{
|
||||
if ($this->isInvalidOrBlocked($email->from, $email->to))
|
||||
return;
|
||||
|
||||
|
||||
// check if company plan matches requirements
|
||||
if (Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->createExpenses($email);
|
||||
|
||||
$this->saveMeta($email->from, $email->to);
|
||||
}
|
||||
|
||||
// SPAM Protection
|
||||
public function isInvalidOrBlocked(string $from, string $to)
|
||||
{
|
||||
// invalid email
|
||||
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
||||
nlog('E-Mail blocked, because from e-mail has the wrong format: ' . $from);
|
||||
return true;
|
||||
}
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
nlog('E-Mail blocked, because to e-mail has the wrong format: ' . $from);
|
||||
return true;
|
||||
}
|
||||
|
||||
$parts = explode('@', $from);
|
||||
$domain = array_pop($parts);
|
||||
|
||||
// global blacklist
|
||||
if (in_array($from, $this->globalWhitelist)) {
|
||||
return false;
|
||||
}
|
||||
if (in_array($domain, $this->globalWhitelist)) {
|
||||
return false;
|
||||
}
|
||||
if (in_array($domain, $this->globalBlacklist)) {
|
||||
nlog('E-Mail blocked, because the domain was found on globalBlocklistDomains: ' . $from);
|
||||
return true;
|
||||
}
|
||||
if (in_array($from, $this->globalBlacklist)) {
|
||||
nlog('E-Mail blocked, because the email was found on globalBlocklistEmails: ' . $from);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Cache::has('inboundMailBlockedSender:' . $from)) { // was marked as blocked before, so we block without any console output
|
||||
// nlog('E-Mail was marked as blocked before: ' . $from);
|
||||
return true;
|
||||
}
|
||||
|
||||
// sender occured in more than 500 emails in the last 12 hours
|
||||
$senderMailCountTotal = Cache::get('inboundMailCountSender:' . $from, 0);
|
||||
if ($senderMailCountTotal >= config('ninja.inbound_mailbox.global_inbound_sender_permablock_mailcount')) {
|
||||
nlog('E-Mail blocked permanent, because the sender sended more than ' . $senderMailCountTotal . ' emails in the last 12 hours: ' . $from);
|
||||
$this->blockSender($from);
|
||||
$this->saveMeta($from, $to);
|
||||
return true;
|
||||
}
|
||||
if ($senderMailCountTotal >= config('ninja.inbound_mailbox.global_inbound_sender_block_mailcount')) {
|
||||
nlog('E-Mail blocked, because the sender sended more than ' . $senderMailCountTotal . ' emails in the last 12 hours: ' . $from);
|
||||
$this->saveMeta($from, $to);
|
||||
return true;
|
||||
}
|
||||
|
||||
// sender sended more than 50 emails to the wrong mailbox in the last 6 hours
|
||||
$senderMailCountUnknownRecipent = Cache::get('inboundMailCountSenderUnknownRecipent:' . $from, 0);
|
||||
if ($senderMailCountUnknownRecipent >= config('ninja.inbound_mailbox.company_inbound_sender_block_unknown_reciepent')) {
|
||||
nlog('E-Mail blocked, because the sender sended more than ' . $senderMailCountUnknownRecipent . ' emails to the wrong mailbox in the last 6 hours: ' . $from);
|
||||
$this->saveMeta($from, $to);
|
||||
return true;
|
||||
}
|
||||
|
||||
// wrong recipent occurs in more than 100 emails in the last 12 hours, so the processing is blocked
|
||||
$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) {
|
||||
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);
|
||||
$this->blockSender($from);
|
||||
$this->saveMeta($from, $to);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public function blockSender(string $from)
|
||||
{
|
||||
Cache::add('inboundMailBlockedSender:' . $from, true, now()->addHours(12));
|
||||
|
||||
// TODO: ignore, when known sender (for heavy email-usage mostly on isHosted())
|
||||
// TODO: handle external blocking
|
||||
}
|
||||
|
||||
//@todo - refactor
|
||||
public function saveMeta(string $from, string $to, bool $isUnknownRecipent = false)
|
||||
{
|
||||
if(Ninja::isHosted())
|
||||
return;
|
||||
|
||||
Cache::add('inboundMailCountSender:' . $from, 0, now()->addHours(12));
|
||||
Cache::increment('inboundMailCountSender:' . $from);
|
||||
|
||||
if ($isUnknownRecipent) {
|
||||
Cache::add('inboundMailCountSenderUnknownRecipent:' . $from, 0, now()->addHours(6));
|
||||
Cache::increment('inboundMailCountSenderUnknownRecipent:' . $from); // we save the sender, to may block him
|
||||
|
||||
Cache::add('inboundMailCountUnknownRecipent:' . $to, 0, now()->addHours(12));
|
||||
Cache::increment('inboundMailCountUnknownRecipent:' . $to); // we save the sender, to may block him
|
||||
}
|
||||
}
|
||||
|
||||
// MAIN-PROCESSORS
|
||||
protected function createExpenses(InboundMail $email)
|
||||
{
|
||||
// Skipping executions: will not result in not saving Metadata to prevent usage of these conditions, to spam
|
||||
if (!$this->company->expense_mailbox_active) {
|
||||
$this->logBlocked('mailbox not active for this company. from: ' . $email->from);
|
||||
return;
|
||||
}
|
||||
if (!$this->validateExpenseSender($email)) {
|
||||
$this->logBlocked('invalid sender of an ingest email for this company. from: ' . $email->from);
|
||||
return;
|
||||
}
|
||||
if (count($email->documents) == 0) {
|
||||
$this->logBlocked('email does not contain any attachments and is likly not an expense. from: ' . $email->from);
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare data
|
||||
$expense_vendor = $this->getVendor($email);
|
||||
|
||||
$this->processHtmlBodyToDocument($email);
|
||||
|
||||
$parsed_expense_ids = []; // used to check if an expense was already matched within this job
|
||||
|
||||
// check documents => optimal when parsed from any source => else create an expense for each document
|
||||
foreach ($email->documents as $document) {
|
||||
|
||||
/** @var \App\Models\Expense $expense */
|
||||
$expense = null;
|
||||
|
||||
// check if document can be parsed to an expense
|
||||
try {
|
||||
|
||||
$expense = (new ParseEDocument($document, $this->company))->run();
|
||||
|
||||
// check if expense was already matched within this job and skip if true
|
||||
if (array_search($expense->id, $parsed_expense_ids))
|
||||
continue;
|
||||
|
||||
array_push($parsed_expense_ids, $expense->id);
|
||||
|
||||
} 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 or issue while parsing'):
|
||||
break;
|
||||
default:
|
||||
throw $err;
|
||||
}
|
||||
}
|
||||
|
||||
// populate missing data with data from email
|
||||
if (!$expense)
|
||||
$expense = ExpenseFactory::create($this->company->id, $this->company->owner()->id);
|
||||
|
||||
$is_imported_by_parser = array_search($expense->id, $parsed_expense_ids);
|
||||
|
||||
if ($is_imported_by_parser)
|
||||
$expense->public_notes = $expense->public_notes . $email->subject;
|
||||
|
||||
if ($is_imported_by_parser)
|
||||
$expense->private_notes = $expense->private_notes . $email->text_body;
|
||||
|
||||
if (!$expense->date)
|
||||
$expense->date = $email->date;
|
||||
|
||||
if (!$expense->vendor_id && $expense_vendor)
|
||||
$expense->vendor_id = $expense_vendor->id;
|
||||
|
||||
if ($is_imported_by_parser)
|
||||
$expense->saveQuietly();
|
||||
else
|
||||
$expense->save();
|
||||
|
||||
// save document only, when not imported by parser
|
||||
$documents = [];
|
||||
if (!$is_imported_by_parser)
|
||||
array_push($documents, $document);
|
||||
|
||||
// email document
|
||||
if ($email->body_document !== null)
|
||||
array_push($documents, $email->body_document);
|
||||
|
||||
$this->saveDocuments($documents, $expense);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
private function processHtmlBodyToDocument(InboundMail $email)
|
||||
{
|
||||
|
||||
if (!is_null($email->body))
|
||||
$email->body_document = TempFile::UploadedFileFromRaw($email->body, "E-Mail.html", "text/html");
|
||||
|
||||
}
|
||||
private function validateExpenseSender(InboundMail $email)
|
||||
{
|
||||
$parts = explode('@', $email->from);
|
||||
$domain = array_pop($parts);
|
||||
|
||||
// whitelists
|
||||
$whitelist = explode(",", $this->company->inbound_mailbox_whitelist);
|
||||
if (is_array($whitelist) && in_array($email->from, $whitelist))
|
||||
return true;
|
||||
if (is_array($whitelist) && in_array($domain, $whitelist))
|
||||
return true;
|
||||
$blacklist = explode(",", $this->company->inbound_mailbox_blacklist);
|
||||
if (is_array($blacklist) && in_array($email->from, $blacklist))
|
||||
return false;
|
||||
if (is_array($blacklist) && in_array($domain, $blacklist))
|
||||
return false;
|
||||
|
||||
// allow unknown
|
||||
if ($this->company->inbound_mailbox_allow_unknown)
|
||||
return true;
|
||||
|
||||
// own users
|
||||
if ($this->company->inbound_mailbox_allow_company_users && $this->company->users()->where("email", $email->from)->exists())
|
||||
return true;
|
||||
|
||||
// from vendors
|
||||
if ($this->company->inbound_mailbox_allow_vendors && VendorContact::where("company_id", $this->company->id)->where("email", $email->from)->exists())
|
||||
return true;
|
||||
|
||||
// from clients
|
||||
if ($this->company->inbound_mailbox_allow_clients && ClientContact::where("company_id", $this->company->id)->where("email", $email->from)->exists())
|
||||
return true;
|
||||
|
||||
// denie
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getVendor(InboundMail $email)
|
||||
{
|
||||
$vendorContact = VendorContact::with('vendor')->where("company_id", $this->company->id)->where("email", $email->from)->first();
|
||||
|
||||
return $vendorContact ? $vendorContact->vendor : null;
|
||||
}
|
||||
|
||||
private function logBlocked(string $data)
|
||||
{
|
||||
nlog("[InboundMailEngine][company:" . $this->company->company_key . "] " . $data);
|
||||
|
||||
(
|
||||
new SystemLogger(
|
||||
$data,
|
||||
SystemLog::CATEGORY_MAIL,
|
||||
SystemLog::EVENT_INBOUND_MAIL_BLOCKED,
|
||||
SystemLog::TYPE_CUSTOM,
|
||||
null,
|
||||
$this->company
|
||||
)
|
||||
)->handle();
|
||||
}
|
||||
}
|
@ -203,14 +203,22 @@ class CompanyTransformer extends EntityTransformer
|
||||
'has_e_invoice_certificate_passphrase' => $company->e_invoice_certificate_passphrase ? true : false,
|
||||
'invoice_task_project_header' => (bool) $company->invoice_task_project_header,
|
||||
'invoice_task_item_description' => (bool) $company->invoice_task_item_description,
|
||||
'origin_tax_data' => $company->origin_tax_data ?: new \stdClass(),
|
||||
'smtp_host' => (string)$company->smtp_host ?? '',
|
||||
'smtp_port' => (int)$company->smtp_port ?? 25,
|
||||
'smtp_encryption' => (string)$company->smtp_encryption ?? 'tls',
|
||||
'origin_tax_data' => $company->origin_tax_data ?: new \stdClass,
|
||||
'expense_mailbox' => (string) $company->expense_mailbox,
|
||||
'expense_mailbox_active' => (bool) $company->expense_mailbox_active,
|
||||
'inbound_mailbox_allow_company_users' => (bool) $company->inbound_mailbox_allow_company_users,
|
||||
'inbound_mailbox_allow_vendors' => (bool) $company->inbound_mailbox_allow_vendors,
|
||||
'inbound_mailbox_allow_clients' => (bool) $company->inbound_mailbox_allow_clients,
|
||||
'inbound_mailbox_allow_unknown' => (bool) $company->inbound_mailbox_allow_unknown,
|
||||
'inbound_mailbox_blacklist' => (string) $company->inbound_mailbox_blacklist,
|
||||
'inbound_mailbox_whitelist' => (string) $company->inbound_mailbox_whitelist,
|
||||
'smtp_host' => (string) $company->smtp_host ?? '',
|
||||
'smtp_port' => (int) $company->smtp_port ?? 25,
|
||||
'smtp_encryption' => (string) $company->smtp_encryption ?? 'tls',
|
||||
'smtp_username' => $company->smtp_username ? '********' : '',
|
||||
'smtp_password' => $company->smtp_password ? '********' : '',
|
||||
'smtp_local_domain' => (string)$company->smtp_local_domain ?? '',
|
||||
'smtp_verify_peer' => (bool)$company->smtp_verify_peer,
|
||||
'smtp_local_domain' => (string) $company->smtp_local_domain ?? '',
|
||||
'smtp_verify_peer' => (bool) $company->smtp_verify_peer,
|
||||
'e_invoice' => $company->e_invoice ?: new \stdClass(),
|
||||
'has_quickbooks_token' => $company->quickbooks ? true : false,
|
||||
'is_quickbooks_token_active' => $company->quickbooks?->accessTokenKey ?? false,
|
||||
|
@ -13,12 +13,13 @@ namespace App\Utils;
|
||||
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class TempFile
|
||||
{
|
||||
public static function path($url): string
|
||||
{
|
||||
$temp_path = @tempnam(sys_get_temp_dir().'/'.sha1((string)time()), basename($url));
|
||||
$temp_path = @tempnam(sys_get_temp_dir() . '/' . sha1((string) time()), basename($url));
|
||||
copy($url, $temp_path);
|
||||
|
||||
return $temp_path;
|
||||
@ -27,17 +28,50 @@ class TempFile
|
||||
/* Downloads a file to temp storage and returns the path - used for mailers */
|
||||
public static function filePath($data, $filename): string
|
||||
{
|
||||
$dir_hash = sys_get_temp_dir().'/'.sha1(microtime());
|
||||
$dir_hash = sys_get_temp_dir() . '/' . sha1(microtime());
|
||||
|
||||
mkdir($dir_hash);
|
||||
|
||||
$file_path = $dir_hash.'/'.$filename;
|
||||
$file_path = $dir_hash . '/' . $filename;
|
||||
|
||||
file_put_contents($file_path, $data);
|
||||
|
||||
return $file_path;
|
||||
}
|
||||
|
||||
/* create a tmp file from a base64 string: https://gist.github.com/waska14/8b3bcebfad1f86f7fcd3b82927576e38*/
|
||||
public static function UploadedFileFromBase64(string $base64File, string|null $fileName = null, string|null $mimeType = null): UploadedFile
|
||||
{
|
||||
// Get file data base64 string
|
||||
$fileData = base64_decode(Arr::last(explode(',', $base64File)));
|
||||
|
||||
// Create temp file and get its absolute path
|
||||
$tempFile = tmpfile();
|
||||
$tempFilePath = stream_get_meta_data($tempFile)['uri'];
|
||||
|
||||
// Save file data in file
|
||||
file_put_contents($tempFilePath, $fileData);
|
||||
|
||||
$tempFileObject = new File($tempFilePath);
|
||||
$file = new UploadedFile(
|
||||
$tempFileObject->getPathname(),
|
||||
$fileName ?: $tempFileObject->getFilename(),
|
||||
$mimeType ?: $tempFileObject->getMimeType(),
|
||||
0,
|
||||
true // Mark it as test, since the file isn't from real HTTP POST.
|
||||
);
|
||||
|
||||
// Close this file after response is sent.
|
||||
// Closing the file will cause to remove it from temp director!
|
||||
app()->terminating(function () use ($tempFile) {
|
||||
fclose($tempFile);
|
||||
});
|
||||
|
||||
// return UploadedFile object
|
||||
return $file;
|
||||
}
|
||||
|
||||
/* create a tmp file from a raw string: https://gist.github.com/waska14/8b3bcebfad1f86f7fcd3b82927576e38*/
|
||||
public static function UploadedFileFromRaw(string $fileData, string|null $fileName = null, string|null $mimeType = null): UploadedFile
|
||||
{
|
||||
// Create temp file and get its absolute path
|
||||
|
@ -76,7 +76,9 @@
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/fractal": "^0.20.0",
|
||||
"livewire/livewire": "^3",
|
||||
"mailgun/mailgun-php": "^3.6",
|
||||
"microsoft/microsoft-graph": "^1.69",
|
||||
"mindee/mindee": "^1.8",
|
||||
"mollie/mollie-api-php": "^2.36",
|
||||
"nelexa/zip": "^4.0",
|
||||
"nordigen/nordigen-php": "^1.1",
|
||||
|
712
composer.lock
generated
712
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3d0c51f983bca2b17f021bdfbf781245",
|
||||
"content-hash": "a2e8043b8cfa0f971360c3bc8d36fa2c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adrienrn/php-mimetyper",
|
||||
@ -852,16 +852,16 @@
|
||||
},
|
||||
{
|
||||
"name": "btcpayserver/btcpayserver-greenfield-php",
|
||||
"version": "v2.6.0",
|
||||
"version": "v2.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git",
|
||||
"reference": "c115b0415719b9fe6e35d5df5f291646d4af2240"
|
||||
"reference": "5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/c115b0415719b9fe6e35d5df5f291646d4af2240",
|
||||
"reference": "c115b0415719b9fe6e35d5df5f291646d4af2240",
|
||||
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992",
|
||||
"reference": "5e2ba7e3f585fc8e6dc068e22a0efbfdacd9c992",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -900,9 +900,9 @@
|
||||
"description": "BTCPay Server Greenfield API PHP client library.",
|
||||
"support": {
|
||||
"issues": "https://github.com/btcpayserver/btcpayserver-greenfield-php/issues",
|
||||
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.6.0"
|
||||
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.7.0"
|
||||
},
|
||||
"time": "2024-04-25T09:19:49+00:00"
|
||||
"time": "2024-09-13T14:54:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
@ -1041,6 +1041,72 @@
|
||||
},
|
||||
"time": "2024-08-29T07:34:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "clue/stream-filter",
|
||||
"version": "v1.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/clue/stream-filter.git",
|
||||
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
|
||||
"reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Clue\\StreamFilter\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"description": "A simple and modern approach to stream filtering in PHP",
|
||||
"homepage": "https://github.com/clue/stream-filter",
|
||||
"keywords": [
|
||||
"bucket brigade",
|
||||
"callback",
|
||||
"filter",
|
||||
"php_user_filter",
|
||||
"stream",
|
||||
"stream_filter_append",
|
||||
"stream_filter_register"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/clue/stream-filter/issues",
|
||||
"source": "https://github.com/clue/stream-filter/tree/v1.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://clue.engineering/support",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/clue",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-20T15:40:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
"version": "1.5.1",
|
||||
@ -3489,16 +3555,16 @@
|
||||
},
|
||||
{
|
||||
"name": "horstoeko/zugferd",
|
||||
"version": "v1.0.60",
|
||||
"version": "v1.0.61",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/horstoeko/zugferd.git",
|
||||
"reference": "10c2296f35060e2c748d8951c97fcb8496c843de"
|
||||
"reference": "e3c0d2b3c2aa84716bfbe235a15870f3798246e1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/10c2296f35060e2c748d8951c97fcb8496c843de",
|
||||
"reference": "10c2296f35060e2c748d8951c97fcb8496c843de",
|
||||
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/e3c0d2b3c2aa84716bfbe235a15870f3798246e1",
|
||||
"reference": "e3c0d2b3c2aa84716bfbe235a15870f3798246e1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -3558,9 +3624,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/horstoeko/zugferd/issues",
|
||||
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.60"
|
||||
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.61"
|
||||
},
|
||||
"time": "2024-09-06T10:26:03+00:00"
|
||||
"time": "2024-09-12T14:13:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "horstoeko/zugferdvisualizer",
|
||||
@ -3938,12 +4004,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/invoiceninja/einvoice.git",
|
||||
"reference": "1ec178ec134981629932aae12677e947ee3df091"
|
||||
"reference": "fe13a98c970bc604d4467f34476109745d2602ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/1ec178ec134981629932aae12677e947ee3df091",
|
||||
"reference": "1ec178ec134981629932aae12677e947ee3df091",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/fe13a98c970bc604d4467f34476109745d2602ca",
|
||||
"reference": "fe13a98c970bc604d4467f34476109745d2602ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -3985,7 +4051,7 @@
|
||||
"source": "https://github.com/invoiceninja/einvoice/tree/main",
|
||||
"issues": "https://github.com/invoiceninja/einvoice/issues"
|
||||
},
|
||||
"time": "2024-08-28T07:20:26+00:00"
|
||||
"time": "2024-09-11T01:50:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/inspector",
|
||||
@ -4490,16 +4556,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v11.22.0",
|
||||
"version": "v11.23.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "868c75beacc47d0f361b919bbc155c0b619bf3d5"
|
||||
"reference": "16b31ab0e1dad5cb2ed6dcc1818c02f02fc48453"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/868c75beacc47d0f361b919bbc155c0b619bf3d5",
|
||||
"reference": "868c75beacc47d0f361b919bbc155c0b619bf3d5",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/16b31ab0e1dad5cb2ed6dcc1818c02f02fc48453",
|
||||
"reference": "16b31ab0e1dad5cb2ed6dcc1818c02f02fc48453",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -4561,6 +4627,7 @@
|
||||
"illuminate/bus": "self.version",
|
||||
"illuminate/cache": "self.version",
|
||||
"illuminate/collections": "self.version",
|
||||
"illuminate/concurrency": "self.version",
|
||||
"illuminate/conditionable": "self.version",
|
||||
"illuminate/config": "self.version",
|
||||
"illuminate/console": "self.version",
|
||||
@ -4603,7 +4670,7 @@
|
||||
"league/flysystem-sftp-v3": "^3.0",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"orchestra/testbench-core": "^9.1.5",
|
||||
"orchestra/testbench-core": "^9.4.0",
|
||||
"pda/pheanstalk": "^5.0",
|
||||
"phpstan/phpstan": "^1.11.5",
|
||||
"phpunit/phpunit": "^10.5|^11.0",
|
||||
@ -4661,6 +4728,7 @@
|
||||
"src/Illuminate/Events/functions.php",
|
||||
"src/Illuminate/Filesystem/functions.php",
|
||||
"src/Illuminate/Foundation/helpers.php",
|
||||
"src/Illuminate/Log/functions.php",
|
||||
"src/Illuminate/Support/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
@ -6104,6 +6172,67 @@
|
||||
],
|
||||
"time": "2023-06-21T14:59:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mailgun/mailgun-php",
|
||||
"version": "v3.6.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mailgun/mailgun-php.git",
|
||||
"reference": "3dbdc2f220fa64e78e903477efa22858c72509be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mailgun/mailgun-php/zipball/3dbdc2f220fa64e78e903477efa22858c72509be",
|
||||
"reference": "3dbdc2f220fa64e78e903477efa22858c72509be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0",
|
||||
"php-http/client-common": "^2.2.1",
|
||||
"php-http/discovery": "^1.19",
|
||||
"php-http/multipart-stream-builder": "^1.1.2",
|
||||
"psr/http-client": "^1.0",
|
||||
"webmozart/assert": "^1.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nyholm/nsa": "^1.2.1",
|
||||
"nyholm/psr7": "^1.3.1",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"symfony/http-client": "^5.4 || ^6.3"
|
||||
},
|
||||
"suggest": {
|
||||
"nyholm/psr7": "PSR-7 message implementation",
|
||||
"symfony/http-client": "HTTP client"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mailgun\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Travis Swientek",
|
||||
"email": "travis@mailgunhq.com"
|
||||
}
|
||||
],
|
||||
"description": "The Mailgun SDK provides methods for all API functions.",
|
||||
"support": {
|
||||
"issues": "https://github.com/mailgun/mailgun-php/issues",
|
||||
"source": "https://github.com/mailgun/mailgun-php/tree/v3.6.3"
|
||||
},
|
||||
"time": "2023-12-01T10:04:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
"version": "3.0.2",
|
||||
@ -6331,17 +6460,72 @@
|
||||
"time": "2024-01-15T18:49:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mollie/mollie-api-php",
|
||||
"version": "v2.71.0",
|
||||
"name": "mindee/mindee",
|
||||
"version": "v1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mollie/mollie-api-php.git",
|
||||
"reference": "dff324f0621ff134fbefffa42ee511833a58578f"
|
||||
"url": "https://github.com/mindee/mindee-api-php.git",
|
||||
"reference": "40865a03e34bb2416b32e5e1dd4937020e7bcc27"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/dff324f0621ff134fbefffa42ee511833a58578f",
|
||||
"reference": "dff324f0621ff134fbefffa42ee511833a58578f",
|
||||
"url": "https://api.github.com/repos/mindee/mindee-api-php/zipball/40865a03e34bb2416b32e5e1dd4937020e7bcc27",
|
||||
"reference": "40865a03e34bb2416b32e5e1dd4937020e7bcc27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-json": "*",
|
||||
"php": ">=7.4",
|
||||
"setasign/fpdf": "^1.8",
|
||||
"setasign/fpdi": "^2.6",
|
||||
"spatie/pdf-to-image": "^1.2",
|
||||
"symfony/console": ">=5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.38",
|
||||
"madewithlove/license-checker": "^0.10.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "Required for PDF rasterization and image processing features",
|
||||
"ghostscript/ghostscript": "Required for PDF rasterization features"
|
||||
},
|
||||
"bin": [
|
||||
"mindee",
|
||||
"bin/cli.php"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mindee\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Mindee Client Library for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/mindee/mindee-api-php/issues",
|
||||
"source": "https://github.com/mindee/mindee-api-php/tree/v1.10.0"
|
||||
},
|
||||
"time": "2024-09-04T15:40:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mollie/mollie-api-php",
|
||||
"version": "v2.72.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mollie/mollie-api-php.git",
|
||||
"reference": "cdfb298ff61737a077554c001e936e6134e7ed8e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/cdfb298ff61737a077554c001e936e6134e7ed8e",
|
||||
"reference": "cdfb298ff61737a077554c001e936e6134e7ed8e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -6418,9 +6602,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/mollie/mollie-api-php/issues",
|
||||
"source": "https://github.com/mollie/mollie-api-php/tree/v2.71.0"
|
||||
"source": "https://github.com/mollie/mollie-api-php/tree/v2.72.0"
|
||||
},
|
||||
"time": "2024-07-17T08:02:14+00:00"
|
||||
"time": "2024-09-11T15:06:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
@ -7352,27 +7536,27 @@
|
||||
},
|
||||
{
|
||||
"name": "nwidart/laravel-modules",
|
||||
"version": "v11.0.11",
|
||||
"version": "v11.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nWidart/laravel-modules.git",
|
||||
"reference": "9d50adcbf8d11c9ec01e48a5b7adbf320653185c"
|
||||
"reference": "2ae13812f055a85d7063e90366884cd327877821"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/9d50adcbf8d11c9ec01e48a5b7adbf320653185c",
|
||||
"reference": "9d50adcbf8d11c9ec01e48a5b7adbf320653185c",
|
||||
"url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/2ae13812f055a85d7063e90366884cd327877821",
|
||||
"reference": "2ae13812f055a85d7063e90366884cd327877821",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"laravel/pint": "^1.16",
|
||||
"php": ">=8.2",
|
||||
"wikimedia/composer-merge-plugin": "^2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^v3.52",
|
||||
"laravel/framework": "^v11.0",
|
||||
"laravel/pint": "^1.16",
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^v9.0",
|
||||
"phpstan/phpstan": "^1.4",
|
||||
@ -7423,7 +7607,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nWidart/laravel-modules/issues",
|
||||
"source": "https://github.com/nWidart/laravel-modules/tree/v11.0.11"
|
||||
"source": "https://github.com/nWidart/laravel-modules/tree/v11.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -7435,7 +7619,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-16T16:26:29+00:00"
|
||||
"time": "2024-09-13T19:24:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nyholm/psr7",
|
||||
@ -7808,6 +7992,388 @@
|
||||
},
|
||||
"time": "2024-04-08T12:52:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/client-common",
|
||||
"version": "2.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/client-common.git",
|
||||
"reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612",
|
||||
"reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"php-http/httplug": "^2.0",
|
||||
"php-http/message": "^1.6",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0",
|
||||
"symfony/polyfill-php80": "^1.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "^1.1",
|
||||
"guzzlehttp/psr7": "^1.4",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
|
||||
"phpspec/prophecy": "^1.10.2",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "To detect JSON responses with the ContentTypePlugin",
|
||||
"ext-libxml": "To detect XML responses with the ContentTypePlugin",
|
||||
"php-http/cache-plugin": "PSR-6 Cache plugin",
|
||||
"php-http/logger-plugin": "PSR-3 Logger plugin",
|
||||
"php-http/stopwatch-plugin": "Symfony Stopwatch plugin"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Client\\Common\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Common HTTP Client implementations and tools for HTTPlug",
|
||||
"homepage": "http://httplug.io",
|
||||
"keywords": [
|
||||
"client",
|
||||
"common",
|
||||
"http",
|
||||
"httplug"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/client-common/issues",
|
||||
"source": "https://github.com/php-http/client-common/tree/2.7.1"
|
||||
},
|
||||
"time": "2023-11-30T10:31:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/discovery",
|
||||
"version": "1.19.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/discovery.git",
|
||||
"reference": "0700efda8d7526335132360167315fdab3aeb599"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599",
|
||||
"reference": "0700efda8d7526335132360167315fdab3aeb599",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0|^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"nyholm/psr7": "<1.0",
|
||||
"zendframework/zend-diactoros": "*"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "*",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"psr/http-message-implementation": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^1.0.2|^2.0",
|
||||
"graham-campbell/phpspec-skip-example-extension": "^5.0",
|
||||
"php-http/httplug": "^1.0 || ^2.0",
|
||||
"php-http/message-factory": "^1.0",
|
||||
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
|
||||
"sebastian/comparator": "^3.0.5 || ^4.0.8",
|
||||
"symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "Http\\Discovery\\Composer\\Plugin",
|
||||
"plugin-optional": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Discovery\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/Composer/Plugin.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"adapter",
|
||||
"client",
|
||||
"discovery",
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr17",
|
||||
"psr7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/discovery/issues",
|
||||
"source": "https://github.com/php-http/discovery/tree/1.19.4"
|
||||
},
|
||||
"time": "2024-03-29T13:00:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/httplug",
|
||||
"version": "2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/httplug.git",
|
||||
"reference": "625ad742c360c8ac580fcc647a1541d29e257f67"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67",
|
||||
"reference": "625ad742c360c8ac580fcc647a1541d29e257f67",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"php-http/promise": "^1.1",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
|
||||
"phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Eric GELOEN",
|
||||
"email": "geloen.eric@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com",
|
||||
"homepage": "https://sagikazarmark.hu"
|
||||
}
|
||||
],
|
||||
"description": "HTTPlug, the HTTP client abstraction for PHP",
|
||||
"homepage": "http://httplug.io",
|
||||
"keywords": [
|
||||
"client",
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/httplug/issues",
|
||||
"source": "https://github.com/php-http/httplug/tree/2.4.0"
|
||||
},
|
||||
"time": "2023-04-14T15:10:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/message",
|
||||
"version": "1.16.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/message.git",
|
||||
"reference": "5997f3289332c699fa2545c427826272498a2088"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088",
|
||||
"reference": "5997f3289332c699fa2545c427826272498a2088",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"clue/stream-filter": "^1.5",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"psr/http-message": "^1.1 || ^2.0"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/message-factory-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.6",
|
||||
"ext-zlib": "*",
|
||||
"guzzlehttp/psr7": "^1.0 || ^2.0",
|
||||
"laminas/laminas-diactoros": "^2.0 || ^3.0",
|
||||
"php-http/message-factory": "^1.0.2",
|
||||
"phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
|
||||
"slim/slim": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-zlib": "Used with compressor/decompressor streams",
|
||||
"guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
|
||||
"laminas/laminas-diactoros": "Used with Diactoros Factories",
|
||||
"slim/slim": "Used with Slim Framework PSR-7 implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/filters.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "HTTP Message related tools",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"http",
|
||||
"message",
|
||||
"psr-7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/message/issues",
|
||||
"source": "https://github.com/php-http/message/tree/1.16.1"
|
||||
},
|
||||
"time": "2024-03-07T13:22:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/multipart-stream-builder",
|
||||
"version": "1.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/multipart-stream-builder.git",
|
||||
"reference": "10086e6de6f53489cca5ecc45b6f468604d3460e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e",
|
||||
"reference": "10086e6de6f53489cca5ecc45b6f468604d3460e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"php-http/discovery": "^1.15",
|
||||
"psr/http-factory-implementation": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/message": "^1.5",
|
||||
"php-http/message-factory": "^1.0.2",
|
||||
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Message\\MultipartStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Nyholm",
|
||||
"email": "tobias.nyholm@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A builder class that help you create a multipart stream",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"multipart stream",
|
||||
"stream"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/multipart-stream-builder/issues",
|
||||
"source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2"
|
||||
},
|
||||
"time": "2024-09-04T13:22:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/promise",
|
||||
"version": "1.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/promise.git",
|
||||
"reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
|
||||
"reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3",
|
||||
"phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Promise\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Joel Wurtz",
|
||||
"email": "joel.wurtz@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Promise used for asynchronous HTTP requests",
|
||||
"homepage": "http://httplug.io",
|
||||
"keywords": [
|
||||
"promise"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/promise/issues",
|
||||
"source": "https://github.com/php-http/promise/tree/1.3.1"
|
||||
},
|
||||
"time": "2024-03-15T13:55:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-jsonpointer/php-jsonpointer",
|
||||
"version": "v3.0.2",
|
||||
@ -8850,16 +9416,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -8894,9 +9460,9 @@
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.1"
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-08-21T13:31:24+00:00"
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/simple-cache",
|
||||
@ -10272,6 +10838,66 @@
|
||||
},
|
||||
"time": "2024-07-12T02:43:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/pdf-to-image",
|
||||
"version": "1.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/pdf-to-image.git",
|
||||
"reference": "9a5cb264a99e87e010c65d4ece03b51f821d55bd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/pdf-to-image/zipball/9a5cb264a99e87e010c65d4ece03b51f821d55bd",
|
||||
"reference": "9a5cb264a99e87e010c65d4ece03b51f821d55bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*",
|
||||
"scrutinizer/ocular": "~1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\PdfToImage\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Convert a pdf to an image",
|
||||
"homepage": "https://github.com/spatie/pdf-to-image",
|
||||
"keywords": [
|
||||
"convert",
|
||||
"image",
|
||||
"pdf",
|
||||
"pdf-to-image",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/pdf-to-image/issues",
|
||||
"source": "https://github.com/spatie/pdf-to-image/tree/1.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2016-12-14T15:37:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sprain/swiss-qr-bill",
|
||||
"version": "v4.14",
|
||||
|
@ -82,11 +82,11 @@ return [
|
||||
'username' => 'user@example.com',
|
||||
'clientname' => 'client@example.com',
|
||||
'password' => 'password',
|
||||
'gocardless' => env('GOCARDLESS_KEYS',''),
|
||||
'square' => env('SQUARE_KEYS',''),
|
||||
'eway' => env('EWAY_KEYS',''),
|
||||
'mollie', env('MOLLIE_KEYS',''),
|
||||
'paytrace' => env('PAYTRACE_KEYS',''),
|
||||
'gocardless' => env('GOCARDLESS_KEYS', ''),
|
||||
'square' => env('SQUARE_KEYS', ''),
|
||||
'eway' => env('EWAY_KEYS', ''),
|
||||
'mollie' => env('MOLLIE_KEYS', ''),
|
||||
'paytrace' => env('PAYTRACE_KEYS', ''),
|
||||
'stripe' => env('STRIPE_KEYS', ''),
|
||||
'paypal' => env('PAYPAL_KEYS', ''),
|
||||
'ppcp' => env('PPCP_KEYS', ''),
|
||||
@ -98,7 +98,6 @@ return [
|
||||
'test_email' => env('TEST_EMAIL', 'test@example.com'),
|
||||
'wepay' => env('WEPAY_KEYS', ''),
|
||||
'braintree' => env('BRAINTREE_KEYS', ''),
|
||||
'mollie' => env('MOLLIE_KEYS', ''),
|
||||
],
|
||||
'contact' => [
|
||||
'email' => env('MAIL_FROM_ADDRESS'),
|
||||
@ -232,6 +231,17 @@ return [
|
||||
'client_id' => env('PAYPAL_CLIENT_ID', null),
|
||||
'webhook_id' => env('PAYPAL_WEBHOOK_ID', null),
|
||||
],
|
||||
'inbound_mailbox' => [
|
||||
'expense_mailbox_endings' => env('EXPENSE_MAILBOX_ENDINGS', false),
|
||||
// 'expense_mailbox_endings' => env('EXPENSE_MAILBOX_ENDINGS', '@expense.invoicing.co'),
|
||||
'inbound_webhook_token' => env('INBOUND_WEBHOOK_TOKEN', null),
|
||||
'global_inbound_blacklist' => env('GLOBAL_INBOUND_BLACKLIST', ''),
|
||||
'global_inbound_whitelist' => env('GLOBAL_INBOUND_WHITELIST', ''),
|
||||
'global_inbound_sender_block_mailcount' => env('GLOBAL_INBOUND_SENDER_BLOCK_MAILCOUNT', 1000),
|
||||
'global_inbound_sender_permablock_mailcount' => env('GLOBAL_INBOUND_SENDER_PERMABLOCK_MAILCOUNT', 5000),
|
||||
'company_inbound_sender_block_unknown_reciepent' => env('COMPANY_INBOUND_SENDER_BLOCK_UNKNOWN_RECIEPENT', 50),
|
||||
'global_inbound_sender_permablock_unknown_reciepent' => env('GLOBAL_INBOUND_SENDER_PERMABLOCK_UNKNOWN_RECIEPENT', 5000),
|
||||
],
|
||||
'cloudflare' => [
|
||||
'turnstile' => [
|
||||
'secret' => env('CLOUDFLARE_SECRET', null),
|
||||
|
@ -31,7 +31,7 @@ return [
|
||||
],
|
||||
|
||||
'brevo' => [
|
||||
'key' => env('BREVO_SECRET', ''),
|
||||
'secret' => env('BREVO_SECRET', ''),
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
@ -39,7 +39,7 @@ return [
|
||||
],
|
||||
|
||||
'postmark-outlook' => [
|
||||
'token' => env('POSTMARK_OUTLOOK_SECRET',''),
|
||||
'token' => env('POSTMARK_OUTLOOK_SECRET', ''),
|
||||
'from' => [
|
||||
'address' => env('POSTMARK_OUTLOOK_FROM_ADDRESS', '')
|
||||
],
|
||||
@ -51,6 +51,14 @@ return [
|
||||
'redirect' => env('MICROSOFT_REDIRECT_URI'),
|
||||
],
|
||||
|
||||
'mindee' => [
|
||||
'api_key' => env('MINDEE_API_KEY'),
|
||||
'daily_limit' => env('MINDEE_DAILY_LIMIT', 100),
|
||||
'monthly_limit' => env('MINDEE_MONTHLY_LIMIT', 250),
|
||||
'account_daily_limit' => env('MINDEE_ACCOUNT_DAILY_LIMIT', 0),
|
||||
'account_monthly_limit' => env('MINDEE_ACCOUNT_MONTHLY_LIMIT', 0),
|
||||
],
|
||||
|
||||
'apple' => [
|
||||
'client_id' => env('APPLE_CLIENT_ID'),
|
||||
'client_secret' => env('APPLE_CLIENT_SECRET'),
|
||||
@ -62,6 +70,7 @@ return [
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('SES_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'sparkpost' => [
|
||||
'secret' => env('SPARKPOST_SECRET'),
|
||||
],
|
||||
@ -117,6 +126,7 @@ return [
|
||||
'key' => env('ZIP_TAX_KEY', false),
|
||||
],
|
||||
],
|
||||
|
||||
'chorus' => [
|
||||
'client_id' => env('CHORUS_CLIENT_ID', false),
|
||||
'secret' => env('CHORUS_SECRET', false),
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('companies', function (Blueprint $table) {
|
||||
$table->boolean("expense_mailbox_active")->default(false);
|
||||
$table->string("expense_mailbox")->nullable();
|
||||
$table->boolean("inbound_mailbox_allow_company_users")->default(false);
|
||||
$table->boolean("inbound_mailbox_allow_vendors")->default(false);
|
||||
$table->boolean("inbound_mailbox_allow_clients")->default(false);
|
||||
$table->boolean("inbound_mailbox_allow_unknown")->default(false);
|
||||
$table->text("inbound_mailbox_whitelist")->nullable();
|
||||
$table->text("inbound_mailbox_blacklist")->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -2492,6 +2492,8 @@ $lang = array(
|
||||
'local_storage_required' => 'Error: local storage is not available.',
|
||||
'your_password_reset_link' => 'Your Password Reset Link',
|
||||
'subdomain_taken' => 'The subdomain is already in use',
|
||||
'expense_mailbox_taken' => 'The inbound mailbox is already in use',
|
||||
'expense_mailbox_invalid' => 'The inbound mailbox does not match the required schema',
|
||||
'client_login' => 'Client Login',
|
||||
'converted_amount' => 'Converted Amount',
|
||||
'default' => 'Default',
|
||||
|
@ -39,6 +39,9 @@ use App\Http\Controllers\CompanyController;
|
||||
use App\Http\Controllers\ExpenseController;
|
||||
use App\Http\Controllers\InvoiceController;
|
||||
use App\Http\Controllers\LicenseController;
|
||||
use App\Http\Controllers\MailgunController;
|
||||
use App\Http\Controllers\MigrationController;
|
||||
use App\Http\Controllers\OneTimeTokenController;
|
||||
use App\Http\Controllers\PaymentController;
|
||||
use App\Http\Controllers\PreviewController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
@ -50,7 +53,6 @@ use App\Http\Controllers\ActivityController;
|
||||
use App\Http\Controllers\DocumentController;
|
||||
use App\Http\Controllers\PostMarkController;
|
||||
use App\Http\Controllers\TemplateController;
|
||||
use App\Http\Controllers\MigrationController;
|
||||
use App\Http\Controllers\SchedulerController;
|
||||
use App\Http\Controllers\SubdomainController;
|
||||
use App\Http\Controllers\SystemLogController;
|
||||
@ -66,14 +68,12 @@ use App\Http\Controllers\PaymentTermController;
|
||||
use App\PaymentDrivers\PayPalPPCPPaymentDriver;
|
||||
use App\Http\Controllers\EmailHistoryController;
|
||||
use App\Http\Controllers\GroupSettingController;
|
||||
use App\Http\Controllers\OneTimeTokenController;
|
||||
use App\Http\Controllers\SubscriptionController;
|
||||
use App\Http\Controllers\Bank\NordigenController;
|
||||
use App\Http\Controllers\CompanyLedgerController;
|
||||
use App\Http\Controllers\PurchaseOrderController;
|
||||
use App\Http\Controllers\TaskSchedulerController;
|
||||
use App\Http\Controllers\CompanyGatewayController;
|
||||
use App\Http\Controllers\MailgunWebhookController;
|
||||
use App\Http\Controllers\PaymentWebhookController;
|
||||
use App\Http\Controllers\RecurringQuoteController;
|
||||
use App\Http\Controllers\BankIntegrationController;
|
||||
@ -444,8 +444,11 @@ Route::match(['get', 'post'], 'payment_notification_webhook/{company_key}/{compa
|
||||
|
||||
|
||||
Route::post('api/v1/postmark_webhook', [PostMarkController::class, 'webhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/postmark_inbound_webhook', [PostMarkController::class, 'inboundWebhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/mailgun_webhook', [MailgunController::class, 'webhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/mailgun_inbound_webhook', [MailgunController::class, 'inboundWebhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/brevo_webhook', [BrevoController::class, 'webhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/mailgun_webhook', [MailgunWebhookController::class, 'webhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/brevo_inbound_webhook', [BrevoController::class, 'inboundWebhook'])->middleware('throttle:1000,1');
|
||||
Route::get('token_hash_router', [OneTimeTokenController::class, 'router'])->middleware('throttle:500,1');
|
||||
Route::get('webcron', [WebCronController::class, 'index'])->middleware('throttle:100,1');
|
||||
Route::post('api/v1/get_migration_account', [HostedMigrationController::class, 'getAccount'])->middleware('guest')->middleware('throttle:100,1');
|
||||
|
@ -50,6 +50,49 @@ class CompanyTest extends TestCase
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
public function testCompanyExpenseMailbox()
|
||||
{
|
||||
// Test valid email address
|
||||
$company_update = [
|
||||
'expense_mailbox' => 'valid@example.com',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertEquals('valid@example.com', $response->json('data.expense_mailbox'));
|
||||
|
||||
// Test invalid email address
|
||||
$company_update = [
|
||||
'expense_mailbox' => 'invalid-email',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors(['expense_mailbox']);
|
||||
|
||||
// Test empty email address
|
||||
$company_update = [
|
||||
'expense_mailbox' => '',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertEmpty($response->json('data.expense_mailbox'));
|
||||
}
|
||||
|
||||
public function testEnsureStrReplace()
|
||||
{
|
||||
$x = '**********';
|
||||
@ -216,4 +259,6 @@ class CompanyTest extends TestCase
|
||||
])->delete('/api/v1/companies/'.$this->encodePrimaryKey($company->id))
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user