anonaddy/app/Models/User.php
2022-07-21 10:54:36 +01:00

504 lines
13 KiB
PHP

<?php
namespace App\Models;
use App\Notifications\CustomVerifyEmail;
use App\Traits\HasEncryptedAttributes;
use App\Traits\HasUuid;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
use HasUuid;
use HasEncryptedAttributes;
use HasApiTokens;
use HasFactory;
public $incrementing = false;
protected $keyType = 'string';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'id',
'from_name',
'email_subject',
'banner_location',
'catch_all',
'bandwidth',
'default_alias_domain',
'default_alias_format',
'use_reply_to',
'default_username_id',
'default_recipient_id',
'password',
'two_factor_enabled',
'two_factor_secret',
'two_factor_backup_code'
];
protected $encrypted = [
'from_name',
'email_subject',
'two_factor_secret'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_backup_code'
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'string',
'default_username_id' => 'string',
'default_recipient_id' => 'string',
'catch_all' => 'boolean',
'two_factor_enabled' => 'boolean',
'use_reply_to' => 'boolean'
];
protected $dates = [
'created_at',
'updated_at',
'email_verified_at'
];
/**
* Get the user's default email.
*/
public function getEmailAttribute()
{
return $this->defaultRecipient->email;
}
/**
* Get the user's default email verified_at.
*/
public function getEmailVerifiedAtAttribute()
{
return $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.
*/
public function getUsernameAttribute()
{
return $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.
*/
public function getBandwidthMbAttribute()
{
return round($this->bandwidth / 1024 / 1024, 2);
}
/**
* 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 recipients.
*/
public function recipients()
{
return $this->hasMany(Recipient::class);
}
/**
* 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 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');
});
}
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new CustomVerifyEmail());
}
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 () {
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();
}
}