'string', 'default_username_id' => 'string', 'default_recipient_id' => 'string', 'catch_all' => 'boolean', 'two_factor_enabled' => 'boolean', 'use_reply_to' => 'boolean', 'store_failed_deliveries' => 'boolean', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'email_verified_at' => 'datetime', 'reject_until' => 'datetime', 'defer_until' => 'datetime', 'defer_new_aliases_until' => 'datetime', 'display_from_format' => DisplayFromFormat::class, 'login_redirect' => LoginRedirect::class, ]; /** * Get the user's default email. */ protected function email(): Attribute { return Attribute::make( get: fn () => $this->defaultRecipient->email, ); } /** * Get the user's default email verified_at. */ protected function emailVerifiedAt(): Attribute { return Attribute::make( get: fn () => $this->defaultRecipient->email_verified_at, ); } /** * Set the user's default email verified_at. */ public function setEmailVerifiedAtAttribute($value) { $this->defaultRecipient->update(['email_verified_at' => $value]); } /** * Get the user's default username. */ protected function username(): Attribute { return Attribute::make( get: fn () => $this->defaultUsername->username, ); } /** * Set the user's default username. */ public function setDefaultUsernameAttribute($username) { $this->attributes['default_username_id'] = $username->id; $this->setRelation('defaultUsername', $username); } /** * Set the user's default email. */ public function setDefaultRecipientAttribute($recipient) { $this->attributes['default_recipient_id'] = $recipient->id; $this->setRelation('defaultRecipient', $recipient); } /** * Get the user's bandwidth in MB. */ protected function bandwidthMb(): Attribute { return Attribute::make( get: fn () => round($this->bandwidth / 1024 / 1024, 2), ); } /** * Get the user's default alias domain. */ protected function defaultAliasDomain(): Attribute { return Attribute::make( get: fn (?string $value) => $value ?? config('anonaddy.domain'), ); } /** * Get the user's default alias format. */ protected function defaultAliasFormat(): Attribute { return Attribute::make( get: fn (?string $value) => $value ?? 'random_characters', ); } /** * Get the user's default username. */ public function defaultUsername() { return $this->hasOne(Username::class, 'id', 'default_username_id'); } /** * Get the user's default recipient. */ public function defaultRecipient() { return $this->hasOne(Recipient::class, 'id', 'default_recipient_id'); } /** * Get all of the user's email aliases. */ public function aliases() { return $this->hasMany(Alias::class); } /** * Get all of the user's email aliases including trashed. */ public function allAliases() { return $this->hasMany(Alias::class)->withTrashed(); } /** * Get all of the user's recipients. */ public function recipients() { return $this->hasMany(Recipient::class); } /** * Get all of the user's pending recipients. */ public function pendingRecipients() { return $this->recipients()->pending(); } /** * Get all of the user's custom domains. */ public function domains() { return $this->hasMany(Domain::class); } /** * Get all of the user's rules. */ public function rules() { return $this->hasMany(Rule::class); } /** * Get all of the user's failed deliveries. */ public function failedDeliveries() { return $this->hasMany(FailedDelivery::class); } /** * Get all of the user's outbound messages. */ public function outboundMessages() { return $this->hasMany(OutboundMessage::class); } /** * Get all of the user's active rules. */ public function activeRules() { return $this->rules()->where('active', true); } /** * Get all of the user's active rules in the correct order. */ public function activeRulesOrdered() { return $this->rules()->where('active', true)->orderBy('order'); } /** * Get all of the user's active rules in the correct order that should be run of forwards. */ public function activeRulesForForwardsOrdered() { return $this->rules()->where('active', true)->where('forwards', true)->orderBy('order'); } /** * Get all of the user's active rules in the correct order that should be run of forwards. */ public function activeRulesForRepliesOrdered() { return $this->rules()->where('active', true)->where('replies', true)->orderBy('order'); } /** * Get all of the user's active rules in the correct order that should be run of forwards. */ public function activeRulesForSendsOrdered() { return $this->rules()->where('active', true)->where('sends', true)->orderBy('order'); } /** * Get all of the user's usernames. */ public function Usernames() { return $this->hasMany(Username::class); } /** * Get all of the user's webauthn keys. */ public function webauthnKeys() { return $this->hasMany(WebauthnKey::class); } /** * Get all of the user's verified recipients. */ public function verifiedRecipients() { return $this->recipients()->whereNotNull('email_verified_at'); } /** * Get all of the user's verified domains. */ public function verifiedDomains() { return $this->domains()->whereNotNull('domain_verified_at'); } /** * Get all of the alias recipient pivot rows for the user. */ public function aliasRecipients() { return $this->hasManyThrough(AliasRecipient::class, Alias::class); } /** * Get all of the user's aliases using a shared domain. */ public function sharedDomainAliases() { return $this->aliases()->whereIn('domain', config('anonaddy.all_domains')); } /** * Get all of the user's active aliases using a shared domain. */ public function activeSharedDomainAliases() { return $this->sharedDomainAliases()->where('active', true); } /** * Get all of the user's aliases that are using the default recipient */ public function aliasesUsingDefault() { return $this->aliases()->whereDoesntHave('recipients')->where(function (Builder $q) { return $q->whereDoesntHaveMorph( 'aliasable', ['App\Models\Domain', 'App\Models\Username'], function (Builder $query) { $query->whereNotNull('default_recipient_id'); } )->orWhereNull('aliasable_id'); }); } /** * Get the count for the user's aliases that are using the default recipient */ public function aliasesUsingDefaultCount() { return $this->aliases()->select('id')->where(function (Builder $q) { return $q->whereHas('recipients', function (Builder $query) { $query->where('recipients.id', $this->default_recipient_id); }) ->orWhereDoesntHave('recipients')->where(function (Builder $q) { return $q->whereDoesntHaveMorph( 'aliasable', ['App\Models\Domain', 'App\Models\Username'], function (Builder $query) { $query->whereNotNull('default_recipient_id'); } )->orWhereNull('aliasable_id') ->orWhereHasMorph('aliasable', ['App\Models\Domain', 'App\Models\Username'], function (Builder $query) { $query->where('default_recipient_id', $this->default_recipient_id); }); }); })->count(); } /** * Send the email verification notification. * * @return void */ public function sendEmailVerificationNotification() { $this->notify(new CustomVerifyEmail()); } /** * Send the password reset notification. * * @param string $token * @return void */ public function sendPasswordResetNotification($token) { $this->notify(new CustomResetPassword($token)); } public function hasVerifiedDefaultRecipient() { return ! is_null($this->defaultRecipient->email_verified_at); } public function totalEmailsForwarded() { return $this->aliases()->withTrashed()->sum('emails_forwarded'); } public function totalEmailsBlocked() { return $this->aliases()->withTrashed()->sum('emails_blocked'); } public function totalEmailsReplied() { return $this->aliases()->withTrashed()->sum('emails_replied'); } public function totalEmailsSent() { return $this->aliases()->withTrashed()->sum('emails_sent'); } public function getBandwidthLimit() { return config('anonaddy.bandwidth_limit'); } public function getBandwidthLimitMb() { return round($this->getBandwidthLimit() / 1024 / 1024, 2); } public function nearBandwidthLimit() { return ($this->bandwidth / $this->getBandwidthLimit()) > 0.9; } public function hasReachedBandwidthLimit() { return $this->bandwidth >= $this->getBandwidthLimit(); } public function hasExceededNewAliasLimit() { if (App::environment('testing')) { return false; } return \Illuminate\Support\Facades\Redis::throttle("user:{$this->id}:limit:new-alias") ->allow(config('anonaddy.new_alias_hourly_limit')) ->every(3600) ->then( function () { return false; }, function () { $now = now(); if ($this->defer_new_aliases_until < $now) { $this->update(['defer_new_aliases_until' => $now->addHour()->toDateTimeString()]); } return true; } ); } public function hasReachedUsernameLimit() { return $this->username_count >= config('anonaddy.additional_username_limit'); } public function isVerifiedRecipient($email) { return $this ->verifiedRecipients() ->select(['id', 'user_id', 'email', 'email_verified_at']) ->get() ->map(function ($recipient) use ($email) { if (Str::contains($email, '+')) { return strtolower($recipient->email); } $withoutExtension = preg_replace('/\+[\s\S]+(?=@)/', '', $recipient->email); return strtolower($withoutExtension); }) ->contains(strtolower($email)); } public function getVerifiedRecipientByEmail($email) { return $this ->verifiedRecipients() ->select(['id', 'user_id', 'email', 'can_reply_send', 'email_verified_at']) ->get() ->first(function ($recipient) use ($email) { if (Str::contains($email, '+')) { $recipientEmail = strtolower($recipient->email); } else { $recipientEmail = strtolower(preg_replace('/\+[\s\S]+(?=@)/', '', $recipient->email)); } // Allow either pm.me or protonmail.com domains if (in_array(Str::afterLast($email, '@'), ['pm.me', 'protonmail.com'])) { $localPart = Str::beforeLast($email, '@'); return in_array($recipientEmail, [strtolower($localPart.'@pm.me'), strtolower($localPart.'@protonmail.com')]); } return $recipientEmail === strtolower($email); }); } public function deleteKeyFromKeyring($fingerprint): void { $gnupg = new \gnupg(); $recipientsUsingFingerprint = $this ->recipients() ->get() ->where('fingerprint', $fingerprint); // Check that the user has a verified recipient matching the key's email and if any other recipients are using that key. if (isset($key[0])) { collect($key[0]['uids']) ->filter(function ($uid) { return ! $uid['invalid']; }) ->pluck('email') ->each(function ($email) use ($gnupg, $fingerprint, $recipientsUsingFingerprint) { if ($this->isVerifiedRecipient($email) && $recipientsUsingFingerprint->count() === 1) { $gnupg->deletekey($fingerprint); $recipientsUsingFingerprint->first()->update([ 'should_encrypt' => false, 'fingerprint' => null, ]); } }); } } public function generateRandomWordLocalPart() { return collect(config('anonaddy.wordlist')) ->random(2) ->implode('.').mt_rand(0, 999); } public function generateRandomCharacterLocalPart(int $length): string { $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; $str = ''; for ($i = 0; $i < $length; $i++) { $index = random_int(0, 35); $str .= $alphabet[$index]; } return $str; } public function domainOptions() { $customDomains = $this->verifiedDomains()->pluck('domain')->toArray(); $allDomains = config('anonaddy.all_domains')[0] ? config('anonaddy.all_domains') : [config('anonaddy.domain')]; return $this->usernames() ->pluck('username') ->flatMap(function ($username) use ($allDomains) { return collect($allDomains)->map(function ($domain) use ($username) { return $username.'.'.$domain; }); }) ->concat($customDomains) ->concat($allDomains) ->reverse() ->values(); } public function sharedDomainOptions() { return config('anonaddy.all_domains'); } }