Skip to content

Commit

Permalink
Added 'last used at' to aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
willbrowningme committed Mar 25, 2024
1 parent 59ba258 commit 0753ffc
Show file tree
Hide file tree
Showing 54 changed files with 1,011 additions and 458 deletions.
10 changes: 5 additions & 5 deletions app/Console/Commands/ReceiveEmail.php
Expand Up @@ -75,13 +75,13 @@ public function handle()
try {
$this->exitIfFromSelf();

$recipients = $this->getRecipients();

$file = $this->argument('file');

$this->parser = $this->getParser($file);
$this->senderFrom = $this->getSenderFrom();

$recipients = $this->getRecipients();

// Divide the size of the email by the number of recipients (excluding any unsubscribe recipients) to prevent it being added multiple times.
$recipientCount = $recipients->where('domain', '!=', 'unsubscribe.'.config('anonaddy.domain'))->count();

Expand Down Expand Up @@ -236,7 +236,7 @@ protected function handleReply($user, $recipient, $alias)
{
$sendTo = Str::replaceLast('=', '@', $recipient['extension']);

$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'R');

$message = new ReplyToEmail($user, $alias, $emailData);

Expand All @@ -260,7 +260,7 @@ protected function handleSendFrom($user, $recipient, $alias, $aliasable)

$sendTo = Str::replaceLast('=', '@', $recipient['extension']);

$emailData = new EmailData($this->parser, $this->option('sender'), $this->size);
$emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'S');

$message = new SendFromEmail($user, $alias, $emailData);

Expand Down Expand Up @@ -576,7 +576,7 @@ protected function getSenderFrom()
// Ensure contains '@', may be malformed header which causes sends/replies to fail
$address = $this->parser->getAddresses('from')[0]['address'];

return Str::contains($address, '@') ? $address : $this->option('sender');
return Str::contains($address, '@') && filter_var($address, FILTER_VALIDATE_EMAIL) ? $address : $this->option('sender');
} catch (\Exception $e) {
return $this->option('sender');
}
Expand Down
102 changes: 101 additions & 1 deletion app/CustomMailDriver/CustomMailer.php
Expand Up @@ -4,14 +4,20 @@

use App\CustomMailDriver\Mime\Crypto\AlreadyEncrypted;
use App\CustomMailDriver\Mime\Crypto\OpenPGPEncrypter;
use App\Models\Alias;
use App\Models\OutboundMessage;
use App\Models\Recipient;
use App\Models\User;
use App\Notifications\FailedDeliveryNotification;
use App\Notifications\GpgKeyExpired;
use Exception;
use Illuminate\Contracts\Mail\Mailable as MailableContract;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\SentMessage;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use ParagonIE\ConstantTime\Base32;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mime\Crypto\DkimOptions;
use Symfony\Component\Mime\Crypto\DkimSigner;
Expand Down Expand Up @@ -133,7 +139,77 @@ public function send($view, array $data = [], $callback = null)
$message->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
}

$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
try {
$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
} catch (Exception $e) {
$symfonySentMessage = false;
$userId = $data['userId'] ?? '';

// Store the undelivered message if enabled by user. Do not store email verification notifications.
if ($user = User::find($userId)) {
$failedDeliveryId = Uuid::uuid4();

// Example $e->getMessage();
// Expected response code "250/251/252" but got code "554", with message "554 5.7.1 Spam message rejected".
// Expected response code "250" but got empty code.
// Connection could not be established with host "mail.example:25": stream_socket_client(): Unable to connect to mail.example.com:25 (Connection refused)
$matches = Str::of($e->getMessage())->matchAll('/"([^"]*)"/');
$status = $matches[1] ?? '4.3.2';
$code = $matches[2] ?? '453 4.3.2 A temporary error has occurred.';

if ($code && $status) {
// If the error is temporary e.g. connection lost then rethrow the error to allow retry or send to failed_jobs table
if (Str::startsWith($status, '4')) {
throw $e;
}

// Try to determine the bounce type, HARD, SPAM, SOFT
$bounceType = $this->getBounceType($code, $status);

$diagnosticCode = Str::limit($code, 497);
} else {
$bounceType = null;
$diagnosticCode = null;
}

$emailType = $data['emailType'] ?? null;

if ($user->store_failed_deliveries && ! in_array($emailType, ['VR', 'VU'])) {
$isStored = Storage::disk('local')->put("{$failedDeliveryId}.eml", $symfonyMessage->toString());
}

$failedDelivery = $user->failedDeliveries()->create([
'id' => $failedDeliveryId,
'recipient_id' => $data['recipientId'] ?? null,
'alias_id' => $data['aliasId'] ?? null,
'is_stored' => $isStored ?? false,
'bounce_type' => $bounceType,
'remote_mta' => config('mail.mailers.smtp.host'),
'sender' => $symfonyMessage->getHeaders()->get('X-AnonAddy-Original-Sender')?->getValue(),
'destination' => $symfonyMessage->getTo()[0]?->getAddress(),
'email_type' => $emailType,
'status' => $status,
'code' => $diagnosticCode,
'attempted_at' => now(),
]);

// Calling $failedDelivery->email_type will return 'Failed Delivery' and not 'FDN'
// Check if the bounce is a Failed delivery notification or Alias deactivated notification and if so do not notify the user again
if (! in_array($emailType, ['FDN', 'ADN']) && ! is_null($emailType)) {

$recipient = Recipient::find($failedDelivery->recipient_id);
$alias = Alias::find($failedDelivery->alias_id);

$notifiable = $recipient?->email_verified_at ? $recipient : $user?->defaultRecipient;

// Notify user of failed delivery
if ($notifiable?->email_verified_at) {

$notifiable->notify(new FailedDeliveryNotification($alias->email ?? null, $failedDelivery->sender, $symfonyMessage->getSubject(), $failedDelivery?->is_stored, $user?->store_failed_deliveries, $recipient?->email));
}
}
}
}

if ($symfonySentMessage) {
$sentMessage = new SentMessage($symfonySentMessage);
Expand Down Expand Up @@ -197,4 +273,28 @@ protected function getVerpLocalPart($id)

return "b_{$encodedPayload}_{$encodedSignature}";
}

protected function getBounceType($code, $status)
{
if (preg_match("/(:?mailbox|address|user|account|recipient|@).*(:?rejected|unknown|disabled|unavailable|invalid|inactive|not exist|does(n't| not) exist)|(:?rejected|unknown|unavailable|no|illegal|invalid|no such).*(:?mailbox|address|user|account|recipient|alias)|(:?address|user|recipient) does(n't| not) have .*(:?mailbox|account)|returned to sender|(:?auth).*(:?required)/i", $code)) {

// If the status starts with 4 then return soft instead of hard
if (Str::startsWith($status, '4')) {
return 'soft';
}

return 'hard';
}

if (preg_match('/(:?spam|unsolicited|blacklisting|blacklisted|blacklist|554|mail content denied|reject for policy reason|mail rejected by destination domain|security issue)/i', $code)) {
return 'spam';
}

// No match for code but status starts with 5 e.g. 5.2.2
if (Str::startsWith($status, '5')) {
return 'hard';
}

return 'soft';
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/ActiveDomainController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$domain->activate();

return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/ActiveUsernameController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$username->activate();

return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
4 changes: 4 additions & 0 deletions app/Http/Controllers/Api/AliasBulkController.php
Expand Up @@ -160,6 +160,10 @@ public function forget(Request $request)
'emails_blocked' => 0,
'emails_replied' => 0,
'emails_sent' => 0,
'last_forwarded' => null,
'last_blocked' => null,
'last_replied' => null,
'last_sent' => null,
'active' => false,
'deleted_at' => now(),
]);
Expand Down
9 changes: 6 additions & 3 deletions app/Http/Controllers/Api/AliasController.php
Expand Up @@ -211,10 +211,13 @@ public function forget($id)
'emails_blocked' => 0,
'emails_replied' => 0,
'emails_sent' => 0,
'last_forwarded' => null,
'last_blocked' => null,
'last_replied' => null,
'last_sent' => null,
'active' => false,
'deleted_at' => now(), // Soft delete to prevent from being regenerated
]);

// Soft delete to prevent from being regenerated
$alias->delete();
} else {
$alias->forceDelete();
}
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/AllowedRecipientController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$recipient->update(['can_reply_send' => true]);

return new RecipientResource($recipient->load('aliases'));
return new RecipientResource($recipient->loadCount('aliases'));
}

public function destroy($id)
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/CatchAllDomainController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$domain->enableCatchAll();

return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/CatchAllUsernameController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$username->enableCatchAll();

return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
8 changes: 4 additions & 4 deletions app/Http/Controllers/Api/DomainController.php
Expand Up @@ -17,14 +17,14 @@ public function __construct()

public function index()
{
return DomainResource::collection(user()->domains()->with(['aliases', 'defaultRecipient'])->latest()->get());
return DomainResource::collection(user()->domains()->with('defaultRecipient')->withCount('aliases')->latest()->get());
}

public function show($id)
{
$domain = user()->domains()->findOrFail($id);

return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
}

public function store(StoreDomainRequest $request)
Expand All @@ -40,7 +40,7 @@ public function store(StoreDomainRequest $request)

$domain->markDomainAsVerified();

return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->refresh()->load('defaultRecipient')->loadCount('aliases'));
}

public function update(UpdateDomainRequest $request, $id)
Expand All @@ -57,7 +57,7 @@ public function update(UpdateDomainRequest $request, $id)

$domain->save();

return new DomainResource($domain->refresh()->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->refresh()->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
Expand Up @@ -20,6 +20,6 @@ public function update(UpdateDomainDefaultRecipientRequest $request, $id)

$domain->save();

return new DomainResource($domain->load(['aliases', 'defaultRecipient']));
return new DomainResource($domain->load('defaultRecipient')->loadCount('aliases'));
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/EncryptedRecipientController.php
Expand Up @@ -20,7 +20,7 @@ public function store(Request $request)

$recipient->update(['should_encrypt' => true]);

return new RecipientResource($recipient->load('aliases'));
return new RecipientResource($recipient->loadCount('aliases'));
}

public function destroy($id)
Expand Down
Expand Up @@ -24,7 +24,7 @@ public function store(Request $request)

$recipient->update(['inline_encryption' => true]);

return new RecipientResource($recipient->load('aliases'));
return new RecipientResource($recipient->loadCount('aliases'));
}

public function destroy($id)
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/LoginableUsernameController.php
Expand Up @@ -16,7 +16,7 @@ public function store(Request $request)

$username->allowLogin();

return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down
Expand Up @@ -24,7 +24,7 @@ public function store(Request $request)

$recipient->update(['protected_headers' => true]);

return new RecipientResource($recipient->load('aliases'));
return new RecipientResource($recipient->loadCount('aliases'));
}

public function destroy($id)
Expand Down
6 changes: 3 additions & 3 deletions app/Http/Controllers/Api/RecipientController.php
Expand Up @@ -11,7 +11,7 @@ class RecipientController extends Controller
{
public function index(IndexRecipientRequest $request)
{
$recipients = user()->recipients()->with('aliases')->latest();
$recipients = user()->recipients()->withCount('aliases')->latest();

if ($request->input('filter.verified') === 'true') {
$recipients->verified();
Expand All @@ -28,7 +28,7 @@ public function show($id)
{
$recipient = user()->recipients()->findOrFail($id);

return new RecipientResource($recipient->load('aliases'));
return new RecipientResource($recipient->loadCount('aliases'));
}

public function store(StoreRecipientRequest $request)
Expand All @@ -45,7 +45,7 @@ public function store(StoreRecipientRequest $request)
$recipient->sendEmailVerificationNotification();
}

return new RecipientResource($recipient->refresh()->load('aliases'));
return new RecipientResource($recipient->refresh()->loadCount('aliases'));
}

public function destroy($id)
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/RecipientKeyController.php
Expand Up @@ -30,7 +30,7 @@ public function update(UpdateRecipientKeyRequest $request, $id)
'fingerprint' => $info['fingerprint'],
]);

return new RecipientResource($recipient->fresh()->load('aliases'));
return new RecipientResource($recipient->fresh()->loadCount('aliases'));
}

public function destroy($id)
Expand Down
8 changes: 4 additions & 4 deletions app/Http/Controllers/Api/UsernameController.php
Expand Up @@ -11,14 +11,14 @@ class UsernameController extends Controller
{
public function index()
{
return UsernameResource::collection(user()->usernames()->with(['aliases', 'defaultRecipient'])->latest()->get());
return UsernameResource::collection(user()->usernames()->with('defaultRecipient')->withCount('aliases')->latest()->get());
}

public function show($id)
{
$username = user()->usernames()->findOrFail($id);

return new UsernameResource($username->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->load('defaultRecipient')->loadCount('aliases'));
}

public function store(StoreUsernameRequest $request)
Expand All @@ -31,7 +31,7 @@ public function store(StoreUsernameRequest $request)

user()->increment('username_count');

return new UsernameResource($username->refresh()->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->refresh()->load('defaultRecipient')->loadCount('aliases'));
}

public function update(UpdateUsernameRequest $request, $id)
Expand All @@ -48,7 +48,7 @@ public function update(UpdateUsernameRequest $request, $id)

$username->save();

return new UsernameResource($username->refresh()->load(['aliases', 'defaultRecipient']));
return new UsernameResource($username->refresh()->load('defaultRecipient')->loadCount('aliases'));
}

public function destroy($id)
Expand Down

0 comments on commit 0753ffc

Please sign in to comment.