Will Browning 1 anno fa
parent
commit
bcc2c4329d

+ 5 - 0
README.md

@@ -43,6 +43,7 @@ This is the source code for self-hosting addy.io.
 - [What is the max email size limit?](#what-is-the-max-email-size-limit)
 - [What is the max email size limit?](#what-is-the-max-email-size-limit)
 - [What happens if I have a subscription but then cancel it?](#what-happens-if-i-have-a-subscription-but-then-cancel-it)
 - [What happens if I have a subscription but then cancel it?](#what-happens-if-i-have-a-subscription-but-then-cancel-it)
 - [If I subscribe will Stripe see my real email address?](#if-i-subscribe-will-stripe-see-my-real-email-address)
 - [If I subscribe will Stripe see my real email address?](#if-i-subscribe-will-stripe-see-my-real-email-address)
+- [Do you offer student discount?](#do-you-offer-student-discount)
 - [How do you prevent spammers?](#how-do-you-prevent-spammers)
 - [How do you prevent spammers?](#how-do-you-prevent-spammers)
 - [What do you use to do DNS lookups on domain names?](#what-do-you-use-to-do-dns-lookups-on-domain-names)
 - [What do you use to do DNS lookups on domain names?](#what-do-you-use-to-do-dns-lookups-on-domain-names)
 - [Is there a limit to how many emails I can forward?](#is-there-a-limit-to-how-many-emails-i-can-forward)
 - [Is there a limit to how many emails I can forward?](#is-there-a-limit-to-how-many-emails-i-can-forward)
@@ -363,6 +364,10 @@ You will not be able to activate any of the above again until you resubscribe.
 
 
 When you subscribe you can choose which email to provide to Stripe, feel free to use an alias. This email will be used for notifications from Stripe such as; if your card payment fails or if your card has expired.
 When you subscribe you can choose which email to provide to Stripe, feel free to use an alias. This email will be used for notifications from Stripe such as; if your card payment fails or if your card has expired.
 
 
+## Do you offer student discount?
+
+Currently, addy.io does not offer any student discounts.
+
 ## How do you prevent spammers?
 ## How do you prevent spammers?
 
 
 The following is in place to help prevent spam:
 The following is in place to help prevent spam:

+ 1 - 1
app/Console/Commands/EmailUsersWithTokenExpiringSoon.php

@@ -41,7 +41,7 @@ class EmailUsersWithTokenExpiringSoon extends Command
      */
      */
     public function handle()
     public function handle()
     {
     {
-        User::with(['defaultUsername', 'defaultRecipient'])
+        User::with(['defaultUsername', 'defaultRecipient', 'tokens'])
             ->whereHas('tokens', function ($query) {
             ->whereHas('tokens', function ($query) {
                 $query->whereDate('expires_at', now()->addWeek());
                 $query->whereDate('expires_at', now()->addWeek());
             })
             })

+ 7 - 12
app/Console/Commands/ReceiveEmail.php

@@ -200,9 +200,9 @@ class ReceiveEmail extends Command
                     }
                     }
 
 
                     if ($this->parser->getHeader('In-Reply-To') && $alias) {
                     if ($this->parser->getHeader('In-Reply-To') && $alias) {
-                        $this->handleReply($user, $recipient, $alias);
+                        $this->handleReply($user, $alias, $validEmailDestination);
                     } else {
                     } else {
-                        $this->handleSendFrom($user, $recipient, $alias ?? null, $aliasable ?? null);
+                        $this->handleSendFrom($user, $recipient, $alias ?? null, $aliasable ?? null, $validEmailDestination);
                     }
                     }
                 } elseif ($verifiedRecipient?->can_reply_send === false) {
                 } elseif ($verifiedRecipient?->can_reply_send === false) {
                     // Notify user that they have not allowed this recipient to reply and send from aliases
                     // Notify user that they have not allowed this recipient to reply and send from aliases
@@ -232,18 +232,16 @@ class ReceiveEmail extends Command
         }
         }
     }
     }
 
 
-    protected function handleReply($user, $recipient, $alias)
+    protected function handleReply($user, $alias, $destination)
     {
     {
-        $sendTo = Str::replaceLast('=', '@', $recipient['extension']);
-
         $emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'R');
         $emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'R');
 
 
         $message = new ReplyToEmail($user, $alias, $emailData);
         $message = new ReplyToEmail($user, $alias, $emailData);
 
 
-        Mail::to($sendTo)->queue($message);
+        Mail::to($destination)->queue($message);
     }
     }
 
 
-    protected function handleSendFrom($user, $recipient, $alias, $aliasable)
+    protected function handleSendFrom($user, $recipient, $alias, $aliasable, $destination)
     {
     {
         if (is_null($alias)) {
         if (is_null($alias)) {
             $alias = $user->aliases()->create([
             $alias = $user->aliases()->create([
@@ -258,13 +256,11 @@ class ReceiveEmail extends Command
             $alias->refresh();
             $alias->refresh();
         }
         }
 
 
-        $sendTo = Str::replaceLast('=', '@', $recipient['extension']);
-
         $emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'S');
         $emailData = new EmailData($this->parser, $this->option('sender'), $this->size, 'S');
 
 
         $message = new SendFromEmail($user, $alias, $emailData);
         $message = new SendFromEmail($user, $alias, $emailData);
 
 
-        Mail::to($sendTo)->queue($message);
+        Mail::to($destination)->queue($message);
     }
     }
 
 
     protected function handleForward($user, $recipient, $alias, $aliasable, $isSpam)
     protected function handleForward($user, $recipient, $alias, $aliasable, $isSpam)
@@ -470,8 +466,7 @@ class ReceiveEmail extends Command
             ->allow(config('anonaddy.limit'))
             ->allow(config('anonaddy.limit'))
             ->every(3600)
             ->every(3600)
             ->then(
             ->then(
-                function () {
-                },
+                function () {},
                 function () use ($user) {
                 function () use ($user) {
                     $user->update(['defer_until' => now()->addHour()]);
                     $user->update(['defer_until' => now()->addHour()]);
 
 

+ 24 - 6
app/CustomMailDriver/CustomMailer.php

@@ -25,6 +25,8 @@ use Symfony\Component\Mime\Email;
 
 
 class CustomMailer extends Mailer
 class CustomMailer extends Mailer
 {
 {
+    private $data;
+
     /**
     /**
      * Send a new message using a view.
      * Send a new message using a view.
      *
      *
@@ -34,6 +36,8 @@ class CustomMailer extends Mailer
      */
      */
     public function send($view, array $data = [], $callback = null)
     public function send($view, array $data = [], $callback = null)
     {
     {
+        $this->data = $data;
+
         if ($view instanceof MailableContract) {
         if ($view instanceof MailableContract) {
             return $this->sendMailable($view);
             return $this->sendMailable($view);
         }
         }
@@ -134,9 +138,9 @@ class CustomMailer extends Mailer
 
 
             // If the message is a forward, reply or send then use the verp domain
             // If the message is a forward, reply or send then use the verp domain
             if (isset($data['emailType']) && in_array($data['emailType'], ['F', 'R', 'S'])) {
             if (isset($data['emailType']) && in_array($data['emailType'], ['F', 'R', 'S'])) {
-                $message->returnPath($verpLocalPart.'@'.$data['verpDomain']);
+                $symfonyMessage->returnPath($verpLocalPart.'@'.$data['verpDomain']);
             } else {
             } else {
-                $message->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
+                $symfonyMessage->returnPath($verpLocalPart.'@'.config('anonaddy.domain'));
             }
             }
 
 
             try {
             try {
@@ -246,10 +250,24 @@ class CustomMailer extends Mailer
     {
     {
         try {
         try {
             $envelopeMessage = clone $message;
             $envelopeMessage = clone $message;
-            // This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO.
-            if ($aliasTo = $message->getHeaders()->get('Alias-To')) {
-                $message->to($aliasTo->getValue());
-                $message->getHeaders()->remove('Alias-To');
+
+            // Add in original Tos that have been updated
+            if ($tos = $this->data['tos'] ?? null) {
+                foreach ($tos as $key => $to) {
+                    if ($key === 0) {
+                        // This allows us to have the To: header set as the alias whilst still delivering to the correct RCPT TO for forwards.
+                        $message->to($to); // In order to override recipient email for forwards
+                    } else {
+                        $message->addTo($to);
+                    }
+                }
+            }
+
+            // Add in original CCs that have been updated
+            if ($ccs = $this->data['ccs'] ?? null) {
+                foreach ($ccs as $cc) {
+                    $message->addCc($cc);
+                }
             }
             }
 
 
             // Add the original sender header here to prevent it altering the envelope from address
             // Add the original sender header here to prevent it altering the envelope from address

+ 15 - 0
app/Helpers/Helper.php

@@ -1,6 +1,7 @@
 <?php
 <?php
 
 
 use Illuminate\Support\Carbon;
 use Illuminate\Support\Carbon;
+use Illuminate\Support\Str;
 
 
 function user()
 function user()
 {
 {
@@ -25,3 +26,17 @@ function randomString(int $length): string
 
 
     return $str;
     return $str;
 }
 }
+
+function stripEmailExtension(string $email): string
+{
+    if (! Str::contains($email, '@')) {
+        return $email;
+    }
+
+    // Strip the email of extensions
+    [$localPart, $domain] = explode('@', strtolower($email));
+    // Remove plus extension from local part if present
+    $localPart = Str::contains($localPart, '+') ? Str::before($localPart, '+') : $localPart;
+
+    return $localPart.'@'.$domain;
+}

+ 1 - 0
app/Http/Controllers/ShowRuleController.php

@@ -22,6 +22,7 @@ class ShowRuleController extends Controller
                 })
                 })
                 ->orderBy('order')
                 ->orderBy('order')
                 ->get(),
                 ->get(),
+            'recipientOptions' => user()->verifiedRecipients()->select(['id', 'email'])->get(),
             'search' => $validated['search'] ?? null,
             'search' => $validated['search'] ?? null,
         ]);
         ]);
     }
     }

+ 14 - 5
app/Http/Requests/StoreRuleRequest.php

@@ -41,6 +41,7 @@ class StoreRuleRequest extends FormRequest
                     'subject',
                     'subject',
                     'sender',
                     'sender',
                     'alias',
                     'alias',
+                    'alias_description',
                 ]),
                 ]),
             ],
             ],
             'conditions.*.match' => [
             'conditions.*.match' => [
@@ -79,13 +80,21 @@ class StoreRuleRequest extends FormRequest
                     'encryption',
                     'encryption',
                     'banner',
                     'banner',
                     'block',
                     'block',
-                    'webhook',
+                    'removeAttachments',
+                    'forwardTo',
+                    //'webhook',
                 ]),
                 ]),
             ],
             ],
-            'actions.*.value' => [
-                'required',
-                'max:50',
-            ],
+            'actions.*.value' => Rule::forEach(function ($value, $attribute, $data, $action) {
+                if ($action['type'] === 'forwardTo') {
+                    return [Rule::in(user()->verifiedRecipients()->pluck('id')->toArray())]; // Must be a valid verified recipient
+                }
+
+                return [
+                    'required',
+                    'max:50',
+                ];
+            }),
             'operator' => [
             'operator' => [
                 'required',
                 'required',
                 'in:AND,OR',
                 'in:AND,OR',

+ 67 - 4
app/Mail/ForwardEmail.php

@@ -33,6 +33,10 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
 
 
     protected $sender;
     protected $sender;
 
 
+    protected $ccs;
+
+    protected $tos;
+
     protected $originalCc;
     protected $originalCc;
 
 
     protected $originalTo;
     protected $originalTo;
@@ -105,8 +109,69 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
         $this->user = $alias->user;
         $this->user = $alias->user;
         $this->alias = $alias;
         $this->alias = $alias;
         $this->sender = $emailData->sender;
         $this->sender = $emailData->sender;
+        $this->ccs = $emailData->ccs;
+        $this->tos = $emailData->tos;
         $this->originalCc = $emailData->originalCc ?? null;
         $this->originalCc = $emailData->originalCc ?? null;
         $this->originalTo = $emailData->originalTo ?? null;
         $this->originalTo = $emailData->originalTo ?? null;
+
+        // Create and swap with alias reply-to addresses to allow easy reply-all
+        if (count($this->ccs)) {
+            $this->ccs = collect($this->ccs)
+                ->map(function ($cc) {
+                    // Leave alias email Cc as it is
+                    if (stripEmailExtension($cc['address']) === $this->alias->email) {
+                        return [
+                            'display' => $cc['display'] != $cc['address'] ? $cc['display'] : null,
+                            'address' => $this->alias->email,
+                        ];
+                    }
+
+                    return [
+                        'display' => $cc['display'] != $cc['address'] ? $cc['display'] : null,
+                        'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $cc['address']).'@'.$this->alias->domain,
+                    ];
+                })
+                ->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($cc) {
+                    // Only add in display if it exists
+                    if ($cc['display']) {
+                        return $cc['display'].' <'.$cc['address'].'>';
+                    }
+
+                    return '<'.$cc['address'].'>';
+                })
+                ->toArray();
+        }
+
+        // Create and swap with alias reply-to addresses to allow easy reply-all
+        if (count($this->tos)) {
+            $this->tos = collect($this->tos)
+                ->map(function ($to) {
+                    // Leave alias email To as it is
+                    if (stripEmailExtension($to['address']) === $this->alias->email) {
+                        return [
+                            'display' => $to['display'] != $to['address'] ? $to['display'] : null,
+                            'address' => $this->alias->email,
+                        ];
+                    }
+
+                    return [
+                        'display' => $to['display'] != $to['address'] ? $to['display'] : null,
+                        'address' => $this->alias->local_part.'+'.Str::replaceLast('@', '=', $to['address']).'@'.$this->alias->domain,
+                    ];
+                })
+                ->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($to) {
+                    // Only add in display if it exists
+                    if ($to['display']) {
+                        return $to['display'].' <'.$to['address'].'>';
+                    }
+
+                    return '<'.$to['address'].'>';
+                })
+                ->toArray();
+        }
+
         $this->displayFrom = $emailData->display_from;
         $this->displayFrom = $emailData->display_from;
         $this->replyToAddress = $emailData->reply_to_address ?? $this->sender;
         $this->replyToAddress = $emailData->reply_to_address ?? $this->sender;
         $this->emailSubject = $emailData->subject;
         $this->emailSubject = $emailData->subject;
@@ -173,10 +238,6 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
                 $message->getHeaders()
                 $message->getHeaders()
                     ->addTextHeader('Feedback-ID', 'F:'.$this->alias->id.':anonaddy');
                     ->addTextHeader('Feedback-ID', 'F:'.$this->alias->id.':anonaddy');
 
 
-                // This header is used to set the To: header as the alias just before sending.
-                $message->getHeaders()
-                    ->addTextHeader('Alias-To', $this->alias->email);
-
                 $message->getHeaders()->remove('Message-ID');
                 $message->getHeaders()->remove('Message-ID');
 
 
                 if ($this->messageId) {
                 if ($this->messageId) {
@@ -327,6 +388,8 @@ class ForwardEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
             'shouldBlock' => $this->size === 0,
             'shouldBlock' => $this->size === 0,
             'needsDkimSignature' => $this->needsDkimSignature(),
             'needsDkimSignature' => $this->needsDkimSignature(),
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
+            'ccs' => $this->ccs,
+            'tos' => $this->tos,
         ]);
         ]);
 
 
         if (isset($replyToEmail)) {
         if (isset($replyToEmail)) {

+ 60 - 0
app/Mail/ReplyToEmail.php

@@ -31,6 +31,10 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
 
 
     protected $sender;
     protected $sender;
 
 
+    protected $ccs;
+
+    protected $tos;
+
     protected $emailSubject;
     protected $emailSubject;
 
 
     protected $emailText;
     protected $emailText;
@@ -65,6 +69,52 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
         $this->user = $user;
         $this->user = $user;
         $this->alias = $alias;
         $this->alias = $alias;
         $this->sender = $emailData->sender;
         $this->sender = $emailData->sender;
+
+        $this->ccs = $emailData->ccs;
+        $this->tos = $emailData->tos;
+
+        // Replace alias reply/send CCs back to proper emails
+        if (count($this->ccs)) {
+            $this->ccs = collect($this->ccs)
+                ->map(function ($cc) {
+                    return [
+                        'display' => null,
+                        'address' => Str::replaceLast('=', '@', Str::between($cc['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
+                    ];
+                })
+                ->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($cc) {
+                    // Only add in display if it exists
+                    if ($cc['display']) {
+                        return $cc['display'].' <'.$cc['address'].'>';
+                    }
+
+                    return '<'.$cc['address'].'>';
+                })
+                ->toArray();
+        }
+
+        // Replace alias reply/send Tos back to proper emails
+        if (count($this->tos)) {
+            $this->tos = collect($this->tos)
+                ->map(function ($to) {
+                    return [
+                        'display' => null,
+                        'address' => Str::replaceLast('=', '@', Str::between($to['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
+                    ];
+                })
+                ->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($to) {
+                    // Only add in display if it exists
+                    if ($to['display']) {
+                        return $to['display'].' <'.$to['address'].'>';
+                    }
+
+                    return '<'.$to['address'].'>';
+                })
+                ->toArray();
+        }
+
         $this->emailSubject = $emailData->subject;
         $this->emailSubject = $emailData->subject;
         $this->emailText = $emailData->text;
         $this->emailText = $emailData->text;
         $this->emailHtml = $emailData->html;
         $this->emailHtml = $emailData->html;
@@ -168,6 +218,8 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
             'needsDkimSignature' => $this->needsDkimSignature(),
             'needsDkimSignature' => $this->needsDkimSignature(),
             'aliasDomain' => $this->alias->domain,
             'aliasDomain' => $this->alias->domain,
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
+            'ccs' => $this->ccs,
+            'tos' => $this->tos,
         ]);
         ]);
 
 
         if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
         if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
@@ -229,14 +281,22 @@ class ReplyToEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
 
 
     private function removeRealEmailAndTextBanner($text)
     private function removeRealEmailAndTextBanner($text)
     {
     {
+        // Replace <alias+hello=example.com@johndoe.anonaddy.com> with <hello@example.com>
+        $destination = $this->email->to[0]['address'];
+
         return Str::of(str_ireplace($this->sender, '', $text))
         return Str::of(str_ireplace($this->sender, '', $text))
+            ->replace($this->alias->local_part.'+'.Str::replaceLast('@', '=', $destination).'@'.$this->alias->domain, $destination)
             ->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
             ->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '');
     }
     }
 
 
     private function removeRealEmailAndHtmlBanner($html)
     private function removeRealEmailAndHtmlBanner($html)
     {
     {
+        // Replace <alias+hello=example.com@johndoe.anonaddy.com> with <hello@example.com>
+        $destination = $this->email->to[0]['address'];
+
         // Reply may be HTML but have a plain text banner
         // Reply may be HTML but have a plain text banner
         return Str::of(str_ireplace($this->sender, '', $html))
         return Str::of(str_ireplace($this->sender, '', $html))
+            ->replace($this->alias->local_part.'+'.Str::replaceLast('@', '=', $destination).'@'.$this->alias->domain, $destination)
             ->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
             ->replaceMatches('/(?s)((<|&lt;)!--banner-info--(&gt;|>)).*?((<|&lt;)!--banner-info--(&gt;|>))/mi', '')
             ->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
             ->replaceMatches('/(?s)(<tr((?!<tr).)*?'.preg_quote(Str::of(config('app.url'))->after('://')->rtrim('/'), '/')."(\/|%2F)deactivate(\/|%2F).*?\/tr>)/mi", '');
     }
     }

+ 52 - 0
app/Mail/SendFromEmail.php

@@ -31,6 +31,10 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
 
 
     protected $sender;
     protected $sender;
 
 
+    protected $ccs;
+
+    protected $tos;
+
     protected $emailSubject;
     protected $emailSubject;
 
 
     protected $emailText;
     protected $emailText;
@@ -61,6 +65,52 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
         $this->user = $user;
         $this->user = $user;
         $this->alias = $alias;
         $this->alias = $alias;
         $this->sender = $emailData->sender;
         $this->sender = $emailData->sender;
+
+        $this->ccs = $emailData->ccs;
+        $this->tos = $emailData->tos;
+
+        // Replace alias reply/send CCs back to proper emails
+        if (count($this->ccs)) {
+            $this->ccs = collect($this->ccs)
+                ->map(function ($cc) {
+                    return [
+                        'display' => null,
+                        'address' => Str::replaceLast('=', '@', Str::between($cc['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
+                    ];
+                })
+                ->filter(fn ($cc) => filter_var($cc['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($cc) {
+                    // Only add in display if it exists
+                    if ($cc['display']) {
+                        return $cc['display'].' <'.$cc['address'].'>';
+                    }
+
+                    return '<'.$cc['address'].'>';
+                })
+                ->toArray();
+        }
+
+        // Replace alias reply/send Tos back to proper emails
+        if (count($this->tos)) {
+            $this->tos = collect($this->tos)
+                ->map(function ($to) {
+                    return [
+                        'display' => null,
+                        'address' => Str::replaceLast('=', '@', Str::between($to['address'], $this->alias->local_part.'+', '@'.$this->alias->domain)),
+                    ];
+                })
+                ->filter(fn ($to) => filter_var($to['address'], FILTER_VALIDATE_EMAIL))
+                ->map(function ($to) {
+                    // Only add in display if it exists
+                    if ($to['display']) {
+                        return $to['display'].' <'.$to['address'].'>';
+                    }
+
+                    return '<'.$to['address'].'>';
+                })
+                ->toArray();
+        }
+
         $this->emailSubject = $emailData->subject;
         $this->emailSubject = $emailData->subject;
         $this->emailText = $emailData->text;
         $this->emailText = $emailData->text;
         $this->emailHtml = $emailData->html;
         $this->emailHtml = $emailData->html;
@@ -152,6 +202,8 @@ class SendFromEmail extends Mailable implements ShouldBeEncrypted, ShouldQueue
             'needsDkimSignature' => $this->needsDkimSignature(),
             'needsDkimSignature' => $this->needsDkimSignature(),
             'aliasDomain' => $this->alias->domain,
             'aliasDomain' => $this->alias->domain,
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
             'verpDomain' => $this->verpDomain ?? $this->alias->domain,
+            'ccs' => $this->ccs,
+            'tos' => $this->tos,
         ]);
         ]);
 
 
         if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {
         if ($this->alias->isCustomDomain() && ! $this->needsDkimSignature()) {

+ 4 - 0
app/Mail/TokenExpiringSoon.php

@@ -19,6 +19,8 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
 
 
     protected $recipient;
     protected $recipient;
 
 
+    protected $token;
+
     /**
     /**
      * Create a new message instance.
      * Create a new message instance.
      *
      *
@@ -28,6 +30,7 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
     {
     {
         $this->user = $user;
         $this->user = $user;
         $this->recipient = $user->defaultRecipient;
         $this->recipient = $user->defaultRecipient;
+        $this->token = $user->tokens()->whereDate('expires_at', now()->addWeek())->first();
     }
     }
 
 
     /**
     /**
@@ -45,6 +48,7 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
                 'recipientId' => $this->user->default_recipient_id,
                 'recipientId' => $this->user->default_recipient_id,
                 'emailType' => 'TES',
                 'emailType' => 'TES',
                 'fingerprint' => $this->recipient->should_encrypt ? $this->recipient->fingerprint : null,
                 'fingerprint' => $this->recipient->should_encrypt ? $this->recipient->fingerprint : null,
+                'tokenName' => $this->token?->name,
             ])
             ])
             ->withSymfonyMessage(function (Email $message) {
             ->withSymfonyMessage(function (Email $message) {
                 $message->getHeaders()
                 $message->getHeaders()

+ 7 - 0
app/Models/EmailData.php

@@ -16,6 +16,10 @@ class EmailData
 
 
     public $reply_to_address;
     public $reply_to_address;
 
 
+    public $ccs;
+
+    public $tos;
+
     public $originalCc;
     public $originalCc;
 
 
     public $originalTo;
     public $originalTo;
@@ -81,6 +85,9 @@ class EmailData
             }
             }
         }
         }
 
 
+        $this->ccs = collect($parser->getAddresses('cc'))->all();
+        $this->tos = collect($parser->getAddresses('to'))->all();
+
         if ($originalCc = $parser->getHeader('cc')) {
         if ($originalCc = $parser->getHeader('cc')) {
             $this->originalCc = $originalCc;
             $this->originalCc = $originalCc;
         }
         }

+ 26 - 2
app/Traits/CheckUserRules.php

@@ -6,8 +6,12 @@ use Illuminate\Support\Str;
 
 
 trait CheckUserRules
 trait CheckUserRules
 {
 {
+    protected $emailType;
+
     public function checkRules(string $emailType)
     public function checkRules(string $emailType)
     {
     {
+        $this->emailType = $emailType;
+
         $method = "activeRulesFor{$emailType}Ordered";
         $method = "activeRulesFor{$emailType}Ordered";
         $this->user->{$method}->each(function ($rule) {
         $this->user->{$method}->each(function ($rule) {
             // Check if the conditions of the rule are satisfied
             // Check if the conditions of the rule are satisfied
@@ -53,8 +57,8 @@ trait CheckUserRules
             case 'alias':
             case 'alias':
                 return $this->conditionSatisfied($this->alias->email, $condition);
                 return $this->conditionSatisfied($this->alias->email, $condition);
                 break;
                 break;
-            case 'displayFrom':
-                return $this->conditionSatisfied($this->displayFrom, $condition);
+            case 'alias_description':
+                return $this->conditionSatisfied($this->alias->description, $condition);
                 break;
                 break;
         }
         }
     }
     }
@@ -143,6 +147,26 @@ trait CheckUserRules
                 $this->size = 0;
                 $this->size = 0;
                 exit(0);
                 exit(0);
                 break;
                 break;
+            case 'removeAttachments':
+                $this->email->rawAttachments = [];
+                break;
+            case 'forwardTo':
+                // Only apply on forwards
+                if ($this->emailType !== 'Forwards') {
+                    break;
+                }
+
+                $recipient = $this->user->verifiedRecipients()->select(['id', 'email', 'should_encrypt', 'fingerprint'])->find($action['value']);
+
+                if (! $recipient) {
+                    break;
+                }
+
+                $this->recipientId = $recipient->id;
+                $this->fingerprint = $recipient->should_encrypt && ! $this->isAlreadyEncrypted() ? $recipient->fingerprint : null;
+
+                $this->email->to[0]['address'] = $recipient->email;
+                break;
             case 'webhook':
             case 'webhook':
                 // http payload to url
                 // http payload to url
                 break;
                 break;

File diff suppressed because it is too large
+ 163 - 210
composer.lock


File diff suppressed because it is too large
+ 274 - 261
package-lock.json


+ 68 - 69
postfix/composer.lock

@@ -290,16 +290,16 @@
         },
         },
         {
         {
             "name": "illuminate/collections",
             "name": "illuminate/collections",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/collections.git",
                 "url": "https://github.com/illuminate/collections.git",
-                "reference": "dad22e648ae0f4973470d82b4ae5f809540a87ff"
+                "reference": "6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/collections/zipball/dad22e648ae0f4973470d82b4ae5f809540a87ff",
-                "reference": "dad22e648ae0f4973470d82b4ae5f809540a87ff",
+                "url": "https://api.github.com/repos/illuminate/collections/zipball/6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79",
+                "reference": "6923dc8c83b8c065c3b89cb4c2a4b10a4288ce79",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -341,11 +341,11 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
                 "source": "https://github.com/laravel/framework"
             },
             },
-            "time": "2024-05-20T13:26:28+00:00"
+            "time": "2024-06-21T15:52:51+00:00"
         },
         },
         {
         {
             "name": "illuminate/conditionable",
             "name": "illuminate/conditionable",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/conditionable.git",
                 "url": "https://github.com/illuminate/conditionable.git",
@@ -391,7 +391,7 @@
         },
         },
         {
         {
             "name": "illuminate/container",
             "name": "illuminate/container",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/container.git",
                 "url": "https://github.com/illuminate/container.git",
@@ -442,16 +442,16 @@
         },
         },
         {
         {
             "name": "illuminate/contracts",
             "name": "illuminate/contracts",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/contracts.git",
                 "url": "https://github.com/illuminate/contracts.git",
-                "reference": "8782f75e80ab3e6036842d24dbeead34a16f3a79"
+                "reference": "86c1331d0b06c59ca21723d8bfc9faaa19430b46"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/contracts/zipball/8782f75e80ab3e6036842d24dbeead34a16f3a79",
-                "reference": "8782f75e80ab3e6036842d24dbeead34a16f3a79",
+                "url": "https://api.github.com/repos/illuminate/contracts/zipball/86c1331d0b06c59ca21723d8bfc9faaa19430b46",
+                "reference": "86c1331d0b06c59ca21723d8bfc9faaa19430b46",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -486,20 +486,20 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
                 "source": "https://github.com/laravel/framework"
             },
             },
-            "time": "2024-04-17T14:09:55+00:00"
+            "time": "2024-05-21T17:42:34+00:00"
         },
         },
         {
         {
             "name": "illuminate/database",
             "name": "illuminate/database",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/database.git",
                 "url": "https://github.com/illuminate/database.git",
-                "reference": "a4e73c5ad7678d5ec934374e8522bf62a4d75d99"
+                "reference": "d2bd095eab3d7a1e869b5824bcada7492e447ec7"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/database/zipball/a4e73c5ad7678d5ec934374e8522bf62a4d75d99",
-                "reference": "a4e73c5ad7678d5ec934374e8522bf62a4d75d99",
+                "url": "https://api.github.com/repos/illuminate/database/zipball/d2bd095eab3d7a1e869b5824bcada7492e447ec7",
+                "reference": "d2bd095eab3d7a1e869b5824bcada7492e447ec7",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -554,11 +554,11 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
                 "source": "https://github.com/laravel/framework"
             },
             },
-            "time": "2024-05-21T15:24:23+00:00"
+            "time": "2024-06-24T20:24:42+00:00"
         },
         },
         {
         {
             "name": "illuminate/macroable",
             "name": "illuminate/macroable",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/macroable.git",
                 "url": "https://github.com/illuminate/macroable.git",
@@ -604,16 +604,16 @@
         },
         },
         {
         {
             "name": "illuminate/support",
             "name": "illuminate/support",
-            "version": "v11.8.0",
+            "version": "v11.12.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/illuminate/support.git",
                 "url": "https://github.com/illuminate/support.git",
-                "reference": "8deb8ba65ed7dc4e3f7b9b64ab70456250454824"
+                "reference": "1ea237b71cd2e181af36074e2a9dd47f268e5cda"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/support/zipball/8deb8ba65ed7dc4e3f7b9b64ab70456250454824",
-                "reference": "8deb8ba65ed7dc4e3f7b9b64ab70456250454824",
+                "url": "https://api.github.com/repos/illuminate/support/zipball/1ea237b71cd2e181af36074e2a9dd47f268e5cda",
+                "reference": "1ea237b71cd2e181af36074e2a9dd47f268e5cda",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -674,20 +674,20 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
                 "source": "https://github.com/laravel/framework"
             },
             },
-            "time": "2024-05-21T15:24:23+00:00"
+            "time": "2024-06-24T20:24:42+00:00"
         },
         },
         {
         {
             "name": "nesbot/carbon",
             "name": "nesbot/carbon",
-            "version": "3.4.0",
+            "version": "3.6.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "8eab8983c83c30e0bacbef8d311e3f3b8172727f"
+                "reference": "39c8ef752db6865717cc3fba63970c16f057982c"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/8eab8983c83c30e0bacbef8d311e3f3b8172727f",
-                "reference": "8eab8983c83c30e0bacbef8d311e3f3b8172727f",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/39c8ef752db6865717cc3fba63970c16f057982c",
+                "reference": "39c8ef752db6865717cc3fba63970c16f057982c",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -705,13 +705,13 @@
             "require-dev": {
             "require-dev": {
                 "doctrine/dbal": "^3.6.3 || ^4.0",
                 "doctrine/dbal": "^3.6.3 || ^4.0",
                 "doctrine/orm": "^2.15.2 || ^3.0",
                 "doctrine/orm": "^2.15.2 || ^3.0",
-                "friendsofphp/php-cs-fixer": "^3.52.1",
+                "friendsofphp/php-cs-fixer": "^3.57.2",
                 "kylekatarnls/multi-tester": "^2.5.3",
                 "kylekatarnls/multi-tester": "^2.5.3",
                 "ondrejmirtes/better-reflection": "^6.25.0.4",
                 "ondrejmirtes/better-reflection": "^6.25.0.4",
                 "phpmd/phpmd": "^2.15.0",
                 "phpmd/phpmd": "^2.15.0",
                 "phpstan/extension-installer": "^1.3.1",
                 "phpstan/extension-installer": "^1.3.1",
-                "phpstan/phpstan": "^1.10.65",
-                "phpunit/phpunit": "^10.5.15",
+                "phpstan/phpstan": "^1.11.2",
+                "phpunit/phpunit": "^10.5.20",
                 "squizlabs/php_codesniffer": "^3.9.0"
                 "squizlabs/php_codesniffer": "^3.9.0"
             },
             },
             "bin": [
             "bin": [
@@ -780,7 +780,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-05-24T14:26:34+00:00"
+            "time": "2024-06-20T15:52:59+00:00"
         },
         },
         {
         {
             "name": "paragonie/constant_time_encoding",
             "name": "paragonie/constant_time_encoding",
@@ -1078,16 +1078,16 @@
         },
         },
         {
         {
             "name": "symfony/clock",
             "name": "symfony/clock",
-            "version": "v7.0.7",
+            "version": "v7.1.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/clock.git",
                 "url": "https://github.com/symfony/clock.git",
-                "reference": "2008671acb4a30b01c453de193cf9c80549ebda6"
+                "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/clock/zipball/2008671acb4a30b01c453de193cf9c80549ebda6",
-                "reference": "2008671acb4a30b01c453de193cf9c80549ebda6",
+                "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7",
+                "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1132,7 +1132,7 @@
                 "time"
                 "time"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/clock/tree/v7.0.7"
+                "source": "https://github.com/symfony/clock/tree/v7.1.1"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1148,20 +1148,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-04-18T09:29:19+00:00"
+            "time": "2024-05-31T14:57:53+00:00"
         },
         },
         {
         {
             "name": "symfony/polyfill-ctype",
             "name": "symfony/polyfill-ctype",
-            "version": "v1.29.0",
+            "version": "v1.30.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
+                "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
-                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
+                "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1211,7 +1211,7 @@
                 "portable"
                 "portable"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1227,20 +1227,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2024-05-31T15:07:36+00:00"
         },
         },
         {
         {
             "name": "symfony/polyfill-mbstring",
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.29.0",
+            "version": "v1.30.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+                "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
-                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
+                "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1291,7 +1291,7 @@
                 "shim"
                 "shim"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1307,20 +1307,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2024-06-19T12:30:46+00:00"
         },
         },
         {
         {
             "name": "symfony/polyfill-php80",
             "name": "symfony/polyfill-php80",
-            "version": "v1.29.0",
+            "version": "v1.30.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
+                "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
-                "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
+                "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1371,7 +1371,7 @@
                 "shim"
                 "shim"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1387,25 +1387,24 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2024-05-31T15:07:36+00:00"
         },
         },
         {
         {
             "name": "symfony/polyfill-php83",
             "name": "symfony/polyfill-php83",
-            "version": "v1.29.0",
+            "version": "v1.30.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php83.git",
                 "url": "https://github.com/symfony/polyfill-php83.git",
-                "reference": "86fcae159633351e5fd145d1c47de6c528f8caff"
+                "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff",
-                "reference": "86fcae159633351e5fd145d1c47de6c528f8caff",
+                "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
+                "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
-                "php": ">=7.1",
-                "symfony/polyfill-php80": "^1.14"
+                "php": ">=7.1"
             },
             },
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
@@ -1448,7 +1447,7 @@
                 "shim"
                 "shim"
             ],
             ],
             "support": {
             "support": {
-                "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0"
+                "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1464,20 +1463,20 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-01-29T20:11:03+00:00"
+            "time": "2024-06-19T12:35:24+00:00"
         },
         },
         {
         {
             "name": "symfony/translation",
             "name": "symfony/translation",
-            "version": "v7.0.7",
+            "version": "v7.1.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "1515e03afaa93e6419aba5d5c9d209159317100b"
+                "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/1515e03afaa93e6419aba5d5c9d209159317100b",
-                "reference": "1515e03afaa93e6419aba5d5c9d209159317100b",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
+                "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1542,7 +1541,7 @@
             "description": "Provides tools to internationalize your application",
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "homepage": "https://symfony.com",
             "support": {
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v7.0.7"
+                "source": "https://github.com/symfony/translation/tree/v7.1.1"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -1558,7 +1557,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2024-04-18T09:29:19+00:00"
+            "time": "2024-05-31T14:57:53+00:00"
         },
         },
         {
         {
             "name": "symfony/translation-contracts",
             "name": "symfony/translation-contracts",

+ 65 - 8
resources/js/Pages/Rules.vue

@@ -159,7 +159,7 @@
     <Modal
     <Modal
       :open="createRuleModalOpen"
       :open="createRuleModalOpen"
       @close="createRuleModalOpen = false"
       @close="createRuleModalOpen = false"
-      max-width="md:max-w-2xl"
+      max-width="md:max-w-3xl"
     >
     >
       <template v-slot:title> Create new rule </template>
       <template v-slot:title> Create new rule </template>
       <template v-slot:content>
       <template v-slot:content>
@@ -209,7 +209,7 @@
                 <div
                 <div
                   class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
                   class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
                 >
                 >
-                  <span>If</span>
+                  <span>If the</span>
                   <span class="sm:ml-2">
                   <span class="sm:ml-2">
                     <div class="relative">
                     <div class="relative">
                       <select
                       <select
@@ -370,6 +370,23 @@
                     </div>
                     </div>
                   </span>
                   </span>
 
 
+                  <multiselect
+                    v-if="createRuleObject.actions[key].type === 'forwardTo'"
+                    class="sm:!ml-4 flex"
+                    v-model="createRuleObject.actions[key].value"
+                    :options="recipientOptions"
+                    mode="single"
+                    value-prop="id"
+                    :close-on-select="true"
+                    :clear-on-select="false"
+                    :searchable="false"
+                    :allow-empty="true"
+                    placeholder="Select recipient"
+                    label="email"
+                    track-by="email"
+                  >
+                  </multiselect>
+
                   <span
                   <span
                     v-else-if="createRuleObject.actions[key].type === 'banner'"
                     v-else-if="createRuleObject.actions[key].type === 'banner'"
                     class="sm:ml-4 flex"
                     class="sm:ml-4 flex"
@@ -468,7 +485,7 @@
       </template>
       </template>
     </Modal>
     </Modal>
 
 
-    <Modal :open="editRuleModalOpen" @close="closeEditModal" max-width="md:max-w-2xl">
+    <Modal :open="editRuleModalOpen" @close="closeEditModal" max-width="md:max-w-3xl">
       <template v-slot:title> Edit rule </template>
       <template v-slot:title> Edit rule </template>
       <template v-slot:content>
       <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
@@ -517,7 +534,7 @@
                 <div
                 <div
                   class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
                   class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
                 >
                 >
-                  <span>If</span>
+                  <span>If the</span>
                   <span class="sm:ml-2">
                   <span class="sm:ml-2">
                     <div class="relative">
                     <div class="relative">
                       <select
                       <select
@@ -675,6 +692,23 @@
                     </div>
                     </div>
                   </span>
                   </span>
 
 
+                  <multiselect
+                    v-if="editRuleObject.actions[key].type === 'forwardTo'"
+                    class="sm:!ml-4 flex"
+                    v-model="editRuleObject.actions[key].value"
+                    :options="recipientOptions"
+                    mode="single"
+                    value-prop="id"
+                    :close-on-select="true"
+                    :clear-on-select="false"
+                    :searchable="false"
+                    :allow-empty="true"
+                    placeholder="Select recipient"
+                    label="email"
+                    track-by="email"
+                  >
+                  </multiselect>
+
                   <span
                   <span
                     v-else-if="editRuleObject.actions[key].type === 'banner'"
                     v-else-if="editRuleObject.actions[key].type === 'banner'"
                     class="sm:ml-4 flex"
                     class="sm:ml-4 flex"
@@ -836,6 +870,7 @@ import Toggle from '../Components/Toggle.vue'
 import { roundArrow } from 'tippy.js'
 import { roundArrow } from 'tippy.js'
 import tippy from 'tippy.js'
 import tippy from 'tippy.js'
 import draggable from 'vuedraggable'
 import draggable from 'vuedraggable'
+import Multiselect from '@vueform/multiselect'
 import { notify } from '@kyvg/vue3-notification'
 import { notify } from '@kyvg/vue3-notification'
 import { InformationCircleIcon, FunnelIcon } from '@heroicons/vue/24/outline'
 import { InformationCircleIcon, FunnelIcon } from '@heroicons/vue/24/outline'
 import { PlusIcon } from '@heroicons/vue/20/solid'
 import { PlusIcon } from '@heroicons/vue/20/solid'
@@ -845,6 +880,10 @@ const props = defineProps({
     type: Array,
     type: Array,
     required: true,
     required: true,
   },
   },
+  recipientOptions: {
+    type: Array,
+    required: true,
+  },
   search: {
   search: {
     type: String,
     type: String,
   },
   },
@@ -891,15 +930,19 @@ const conditionTypeOptions = [
   },
   },
   {
   {
     value: 'sender',
     value: 'sender',
-    label: 'the sender',
+    label: 'sender email',
   },
   },
   {
   {
     value: 'subject',
     value: 'subject',
-    label: 'the subject',
+    label: 'subject',
   },
   },
   {
   {
     value: 'alias',
     value: 'alias',
-    label: 'the alias',
+    label: 'alias email',
+  },
+  {
+    value: 'alias_description',
+    label: 'alias description',
   },
   },
 ]
 ]
 const actionTypeOptions = [
 const actionTypeOptions = [
@@ -927,6 +970,14 @@ const actionTypeOptions = [
     value: 'block',
     value: 'block',
     label: 'block the email',
     label: 'block the email',
   },
   },
+  {
+    value: 'removeAttachments',
+    label: 'remove attachments',
+  },
+  {
+    value: 'forwardTo',
+    label: 'forward to',
+  },
 ]
 ]
 
 
 const indexToHuman = {
 const indexToHuman = {
@@ -1188,7 +1239,9 @@ const reorderRules = (displaySuccess = true) => {
 }
 }
 
 
 const conditionMatchOptions = (object, key) => {
 const conditionMatchOptions = (object, key) => {
-  if (_.includes(['sender', 'subject', 'alias'], object.conditions[key].type)) {
+  if (
+    _.includes(['sender', 'subject', 'alias', 'alias_description'], object.conditions[key].type)
+  ) {
     return [
     return [
       'contains',
       'contains',
       'does not contain',
       'does not contain',
@@ -1280,6 +1333,10 @@ const ruleActionChange = action => {
     action.value = 'top'
     action.value = 'top'
   } else if (action.type === 'block') {
   } else if (action.type === 'block') {
     action.value = true
     action.value = true
+  } else if (action.type === 'removeAttachments') {
+    action.value = true
+  } else if (action.type === 'forwardTo') {
+    action.value = ''
   }
   }
 }
 }
 
 

+ 4 - 0
resources/views/mail/token_expiring_soon.blade.php

@@ -2,7 +2,11 @@
 
 
 # Your API key expires soon
 # Your API key expires soon
 
 
+@if($tokenName)
+Your API key named "**{{ $tokenName }}**" on your addy.io account expires in **one weeks time**.
+@else
 One of the API keys on your addy.io account will expire in **one weeks time**.
 One of the API keys on your addy.io account will expire in **one weeks time**.
+@endif
 
 
 If you are not using this API key for the browser extensions, mobile apps or to access the API then you do not need to take any action.
 If you are not using this API key for the browser extensions, mobile apps or to access the API then you do not need to take any action.
 
 

+ 1 - 0
tests/TestCase.php

@@ -13,6 +13,7 @@ use Ramsey\Uuid\Uuid;
 abstract class TestCase extends BaseTestCase
 abstract class TestCase extends BaseTestCase
 {
 {
     protected $user;
     protected $user;
+
     protected $original;
     protected $original;
 
 
     protected function setUp(): void
     protected function setUp(): void

+ 120 - 0
tests/emails/email_windows-1250.eml

@@ -0,0 +1,120 @@
+Date: Wed, 20 Feb 2019 15:00:00 +0100 (CET)
+From: Will <will@anonaddy.com>
+To: <ebay@johndoe.anonaddy.com>
+Subject: Test Email
+Message-Id: <10736680l.3259911109l1696204l2095l@intime.sk>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="--boundary.ipw.202312219143836.3659651272.2094"
+
+
+This is a message encoded in MIME format.
+
+----boundary.ipw.202312219143836.3659651272.2094
+Content-Type: text/plain; charset="WINDOWS-1250"
+Content-Transfer-Encoding: quoted-printable
+
+body { font-family: Tahoma, Verdana, Arial, Helvetica, Chicago, Sans-serif;=
+ font-size: 12px; margin: 0px; padding: 4px; }p { text-align: justify }tr, =
+td { font-size: 12px; }h1 {color: #ffffff; font-size: 14px; background: #45=
+5560; padding: 4px 5px; border-left: 20px solid #f89829; }.obsah { width: 6=
+60px; }.obsah p { margin: 12px; }.obsah h5 {margin: 12px; font-size: 12px; =
+}Odoslanie z=E1sielky: TRACKING-NUMBER-HERE
+
+  =20
+
+
+V=E1=9Een=FD z=E1kazn=EDk,
+
+
+Dnes bola spracovan=E1 Va=9Aa z=E1sielka od spolo=E8nosti CAINIAO - .
+Z=E1sielka
+
+
+  =C8=EDslo z=E1sielky:  TRACKING-NUMBER-HERE
+  Referen=E8n=E9 =E8=EDslo:  7REF NUMBER HERE
+  Dodacia adresa:  ADDRESS-HERE
+  Dobierkov=E1 suma:  0,00 EUR=20
+    =20
+
+Doru=E8enie
+
+Z=E1sielky obvykle doru=E8ujeme nasleduj=FAci pracovn=FD de=F2.
+Pre bli=9E=9Aie inform=E1cie o doru=E8en=ED tejto z=E1sielky pou=9Eite odka=
+z Kde je m=F4j bal=EDk.=20
+
+Prev=E1dzkovate=BEom je spolo=E8nos=9D Express One, s.r.o., I=C8O: 31 342 6=
+21 (=EFalej len ako Prev=E1dzkovate=BE).
+Va=9Ae osobn=E9 =FAdaje sprac=FAva Prev=E1dzkovate=BE za =FA=E8elom zabezpe=
+=E8enia doru=E8enia z=E1sielky. Prev=E1dzkovate=BEa m=F4=9Eete kontaktova=
+=9D na adrese Seneck=E1 cesta 1, 900 28 Ivanka pri Dunaji. Zodpovedn=FA oso=
+bu m=F4=9Eete kontaktova=9D na adrese Prev=E1dzkovate=BEa.
+Bli=9E=9Aie inform=E1cie o sprac=FAvan=ED Va=9Aich osobn=FDch =FAdajov o pr=
+=E1vach dotknutej osoby a z=E1sad=E1ch ochrany osobn=FDch =FAdajov n=E1jdet=
+e na na=9Aej internetovej str=E1nke: https://www.expressone.sk/sk/kontakt/i=
+nformacie-o-spracuvani-osobnych-udajov.html.=20
+
+S pozdravom,
+kuri=E9rska spolo=E8nos=9D
+
+
+T=E1to spr=E1va je generovan=E1 na z=E1klade po=9Eiadavky odosielate=BEa z=
+=E1sielky.
+
+Pros=EDm, neodpovedajte na doru=E8en=FA spr=E1vu e-mailom, kontakt pre =EFa=
+l=9Aie inform=E1cie je uveden=FD vy=9A=9Aie.
+----boundary.ipw.202312219143836.3659651272.2094
+Content-Type: text/html; charset="WINDOWS-1250"
+Content-Transfer-Encoding: quoted-printable
+
+<html><head><meta http-equiv=3D"Content-Type" content=3D"text/html;" charse=
+t=3D"Windows-1250"><style type=3D"text/css">body { font-family: Tahoma, Ver=
+dana, Arial, Helvetica, Chicago, Sans-serif; font-size: 12px; margin: 0px; =
+padding: 4px; }p { text-align: justify }tr, td { font-size: 12px; }h1 {colo=
+r: #ffffff; font-size: 14px; background: #455560; padding: 4px 5px; border-=
+left: 20px solid #f89829; }.obsah { width: 660px; }.obsah p { margin: 12px;=
+ }.obsah h5 {margin: 12px; font-size: 12px; }</style><title>Odoslanie z=E1s=
+ielky: TRACKING NUMBER HERE</title></head><body text=3D"#000000" bgcolor=3D"=
+#ffffff"><div class=3D"obsah"><table width=3D"100%" bgcolor=3D"#f89829"><tb=
+ody><tr><td>&nbsp;<br></td></tr></tbody></table><p>V=E1=9Een=FD z=E1kazn=ED=
+k,<br></p><p>Dnes bola spracovan=E1 Va=9Aa z=E1sielka od spolo=E8nosti CAIN=
+IAO - .</p><h1>Z=E1sielka</h1><p><table border=3D"0"><tbody><tr><td>=C8=EDs=
+lo z=E1sielky:</td><td><b><a href=3D"http://t-t.sps-sro.sk/result.php?cmd=
+=3DSDG_SEARCH&amp;sprache=3D&amp;sdg_landnr=3D703&amp;sdg_mandnr=3D310&amp;=
+sdg_lfdnr=3trackingnumber&amp;x=3D1&amp;y=3D1">TRACKING-NUMBER-HERE</a></b></td><=
+/tr><tr><td>Referen=E8n=E9 =E8=EDslo:</td><td>7365776804539</td></tr><tr><t=
+d>Dodacia adresa:</td><td>My Street Address redacted</t=
+d></tr><tr><td>Dobierkov=E1 suma:</td><td>0,00 EUR <i></i></td></tr><tr><td=
+>&nbsp;</td><td><br></td></tr></tbody></table></p><h1>Doru=E8enie</h1><p>Z=
+=E1sielky obvykle doru=E8ujeme nasleduj=FAci pracovn=FD de=F2.<br>
+
+Pre bli=9E=9Aie inform=E1cie o doru=E8en=ED tejto z=E1sielky pou=9Eite odka=
+z <a href=3D"https://www.expressone.sk/sk/moja-zasielka/kde-je-moj-balik-.h=
+tml"> Kde je m=F4j bal=EDk</a>.</p>
+
+<p><small><i>
+Prev=E1dzkovate=BEom je spolo=E8nos=9D <b>Express One, s.r.o.</b>, I=C8O: 3=
+1 342 621 (=EFalej len ako Prev=E1dzkovate=BE).<br>
+Va=9Ae osobn=E9 =FAdaje sprac=FAva Prev=E1dzkovate=BE <b>za =FA=E8elom zabe=
+zpe=E8enia doru=E8enia z=E1sielky</b>. Prev=E1dzkovate=BEa m=F4=9Eete konta=
+ktova=9D na adrese Seneck=E1 cesta 1, 900 28 Ivanka pri Dunaji<!-- alebo e-=
+mailom <a href=3Dmailto:info@expressone.sk?Subject=3DInforma=E8n=E1 povinno=
+s=9D prev=E1dzkovate=BEa target=3D_top>info@expressone.sk</a>-->.
+ Zodpovedn=FA osobu m=F4=9Eete kontaktova=9D na adrese Prev=E1dzkovate=BEa<=
+!-- alebo prostredn=EDctvom e-mailu <a href=3Dmailto:zodpovednaosoba@expres=
+sone.sk?Subject=3DInforma=E8n=E1 povinnos=9D prev=E1dzkovate=BEa target=3D_=
+top>zodpovednaosoba@expressone.sk</a>-->.<br>
+Bli=9E=9Aie inform=E1cie o sprac=FAvan=ED Va=9Aich osobn=FDch =FAdajov o pr=
+=E1vach dotknutej osoby a z=E1sad=E1ch ochrany osobn=FDch =FAdajov n=E1jdet=
+e na na=9Aej internetovej str=E1nke: <a href=3D"https://www.expressone.sk/s=
+k/kontakt/informacie-o-spracuvani-osobnych-udajov.html">https://www.express=
+one.sk/sk/kontakt/informacie-o-spracuvani-osobnych-udajov.html</a>.
+</i></small></p>
+
+<p>S pozdravom,<br>kuri=E9rska spolo=E8nos=9D<br><a href=3D"https://www.exp=
+ressone.sk"><img alt=3D"Express" One src=3D"https://t-t.sps-sro.sk/logo/exp=
+_one.png" height=3D"65" width=3D"225" hspace=3D"20" vspace=3D"20" border=3D=
+"0"></a></p><p><i><small>T=E1to spr=E1va je generovan=E1 na z=E1klade po=9E=
+iadavky odosielate=BEa z=E1sielky.<br><br>Pros=EDm, neodpovedajte na doru=
+=E8en=FA spr=E1vu e-mailom, kontakt pre =EFal=9Aie inform=E1cie je uveden=
+=FD vy=9A=9Aie.</small></i></p></div></body></html>
+----boundary.ipw.202312219143836.3659651272.2094--

Some files were not shown because too many files changed in this diff