Updated email replies

This commit is contained in:
Will Browning 2020-02-13 09:21:26 +00:00
parent 824eadb7c2
commit ba3772219e
8 changed files with 106 additions and 25 deletions

View file

@ -119,12 +119,36 @@ Yes this will work with any provider, althought I can't guarantee it won't land
Each forwarded email has a Reply-To: header set. This header will look something like this:
Reply-To: "sender@example.com" <<alias+59fhbf7f1ea374c9182ac0f269m3192501fep210@johndoe.anonaddy.com>>
Reply-To: <<span class="break-words"><alias+hello=example.com@johndoe.anonaddy.com></span>>
Where sender@example.com is the address of the person who sent you the email and the part between the '<' '>' is the alias (alias@johndoe.anonaddy.com in this case) that forwarded the email along with a "hash" added as an extension after the "+". In order to reply successfully you must keep this entire email address and display name intact, do not remove the "sender@example.com" display name part.
Where hello@example.com is the address of the person who sent you the email and alias@johndoe.anonaddy.com is the alias that forwarded you the email.
Almost all mail clients respect the Reply-To: header, so all you need to do is click reply and it should automatically fill the To: field with the correct address.
Some users have reported that Gmail's web mail has not been using the Reply-To header. If this is the case then you will have to manually copy the value of the Reply-To header and use this instead.
To check if a reply has worked properly check in your dashboard if the reply count has been incremented for that alias.
#### **How do I send email from an alias?**
This works in the same way as replying to an email.
Let's say that you have the alias **first@johndoe.anonaddy.com** and you want to send an email to **hello@example.com**.
All you need to do is enter the following in the To: field.
<span class="break-words"><first+hello=example.com@johndoe.anonaddy.com></span>
> **Note**: you must send the email from a verified recipient on your account.
Then send the email exactly as you would any other. To check that the email has sent successfully, look in your dashboard at the sent count column and see if it has been incremented for that alias.
This works exactly the same for UUID/Random Word aliases, additional usernames and custom domains.
You can even use the send from feature to create an alias on the fly that does not yet exist. This only works for standard aliases or those at custom domains that behave as a catch-all.
You must generate aliases that use shared domains (e.g. circus.waltz449@anonaddy.me) beforehand in order to be able to send from them.
#### **Will people see my real email if I reply to a forwarded one?**
No, your real email will not be shown, the email will look as if it has come from us instead. Just make sure not to include anything that might identify you when composing the reply, i.e. your full name.

View file

@ -128,11 +128,13 @@ class ReceiveEmail extends Command
$this->checkRateLimit($user);
// Check whether this email is a reply or a new email to be forwarded.
if ($recipient['extension'] === sha1(config('anonaddy.secret').$displayTo)) {
$this->handleReply($user, $recipient, $displayTo);
} elseif (filter_var(Str::replaceLast('=', '@', $recipient['extension']), FILTER_VALIDATE_EMAIL)) {
$this->handleSendFrom($user, $recipient, $aliasable ?? null);
// Check whether this email is a reply/send from or a new email to be forwarded.
if (filter_var(Str::replaceLast('=', '@', $recipient['extension']), FILTER_VALIDATE_EMAIL)) {
if ($this->parser->getHeader('In-Reply-To')) {
$this->handleReply($user, $recipient);
} else {
$this->handleSendFrom($user, $recipient, $aliasable ?? null);
}
} else {
$this->handleForward($user, $recipient, $aliasable ?? null);
}
@ -155,23 +157,23 @@ class ReceiveEmail extends Command
}
}
protected function handleReply($user, $recipient, $displayTo)
protected function handleReply($user, $recipient)
{
$alias = $user->aliases()->where('email', $recipient['local_part'] . '@' . $recipient['domain'])->first();
if (!is_null($alias) && filter_var($displayTo, FILTER_VALIDATE_EMAIL)) {
if ($alias && $user->isVerifiedRecipient($this->option('sender'))) {
$sendTo = Str::replaceLast('=', '@', $recipient['extension']);
$emailData = new EmailData($this->parser);
$message = new ReplyToEmail($user, $alias, $emailData);
Mail::to($displayTo)->queue($message);
Mail::to($sendTo)->queue($message);
if (!Mail::failures()) {
$alias->increment('emails_replied');
$alias->increment('emails_replied');
$user->bandwidth += $this->size;
$user->save();
}
$user->bandwidth += $this->size;
$user->save();
}
}
@ -185,7 +187,7 @@ class ReceiveEmail extends Command
'aliasable_type' => $aliasable ? 'App\\'.class_basename($aliasable) : null
]);
// this is a new alias but at a shared domain or the sender is not a verified recipient
// This is a new alias but at a shared domain or the sender is not a verified recipient.
if ((!isset($alias->id) && in_array($recipient['domain'], config('anonaddy.all_domains'))) || !$user->isVerifiedRecipient($this->option('sender'))) {
exit(0);
}

View file

@ -12,6 +12,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Swift_Signers_DKIMSigner;
use Swift_SwiftException;
@ -77,8 +78,7 @@ class ForwardEmail extends Mailable implements ShouldQueue
*/
public function build()
{
$replyToDisplay = $this->replyToAddress ?? $this->sender;
$replyToEmail = $this->alias->local_part.'+'.sha1(config('anonaddy.secret').$replyToDisplay).'@'.$this->alias->domain;
$replyToEmail = $this->alias->local_part . '+' . Str::replaceLast('@', '=', $this->sender) . '@' . $this->alias->domain;
if ($this->alias->isCustomDomain()) {
if ($this->alias->aliasable->isVerifiedForSending()) {
@ -99,7 +99,7 @@ class ForwardEmail extends Mailable implements ShouldQueue
$email = $this
->from($fromEmail, base64_decode($this->displayFrom)." '".$this->sender."'")
->replyTo($replyToEmail, $replyToDisplay)
->replyTo($replyToEmail)
->subject($this->user->email_subject ?? base64_decode($this->emailSubject))
->text('emails.forward.text')->with([
'text' => base64_decode($this->emailText)

View file

@ -212,6 +212,11 @@ class User extends Authenticatable implements MustVerifyEmail
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');

View file

@ -4,6 +4,7 @@ namespace Tests\Feature;
use App\Alias;
use App\Mail\ReplyToEmail;
use App\Recipient;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
@ -37,7 +38,7 @@ class ReplyToEmailTest extends TestCase
'domain' => 'johndoe.'.config('anonaddy.domain'),
]);
$extension = sha1(config('anonaddy.secret').'contact@ebay.com');
$extension = 'contact=ebay.com';
$this->artisan(
'anonaddy:receive-email',
@ -67,6 +68,55 @@ class ReplyToEmailTest extends TestCase
});
}
/** @test */
public function it_cannot_reply_using_unverified_recipient()
{
Mail::fake();
Mail::assertNothingSent();
$alias = factory(Alias::class)->create([
'user_id' => $this->user->id,
'email' => 'ebay@johndoe.'.config('anonaddy.domain'),
'local_part' => 'ebay',
'domain' => 'johndoe.'.config('anonaddy.domain'),
]);
$recipient = factory(Recipient::class)->create([
'user_id' => $this->user->id,
'email_verified_at' => null
]);
$extension = 'contact=ebay.com';
$this->artisan(
'anonaddy:receive-email',
[
'file' => base_path('tests/emails/email_reply.eml'),
'--sender' => $recipient->email,
'--recipient' => ['ebay+'.$extension.'@johndoe.anonaddy.com'],
'--local_part' => ['ebay'],
'--extension' => [$extension],
'--domain' => ['johndoe.anonaddy.com'],
'--size' => '1000'
]
)->assertExitCode(0);
$this->assertDatabaseHas('aliases', [
'email' => $alias->email,
'local_part' => $alias->local_part,
'domain' => $alias->domain,
'emails_forwarded' => 0,
'emails_blocked' => 0,
'emails_replied' => 0
]);
$this->assertEquals(1, $this->user->aliases()->count());
Mail::assertNotQueued(ReplyToEmail::class, function ($mail) {
return $mail->hasTo('contact@ebay.com');
});
}
/** @test */
public function it_can_reply_to_multiple_emails_from_file()
{
@ -81,8 +131,8 @@ class ReplyToEmailTest extends TestCase
'domain' => 'johndoe.'.config('anonaddy.domain'),
]);
$extension1 = sha1(config('anonaddy.secret').'contact@ebay.com');
$extension2 = sha1(config('anonaddy.secret').'support@ebay.com');
$extension1 = 'contact=ebay.com';
$extension2 = 'support=ebay.com';
$this->artisan(
'anonaddy:receive-email',

View file

@ -1,7 +1,8 @@
Date: Wed, 20 Feb 2019 15:00:00 +0100 (CET)
From: Will <will@anonaddy.com>
To: contact@ebay.com <ebay+b2498b76d8686bf4f48d034fbada4de6f585e2aa@johndoe.anonaddy.com>, support@ebay.com <ebay+bb40a1982731e55199e8598ebff678f3ef6c2951@johndoe.anonaddy.com>
To: <ebay+contact=ebay.com@johndoe.anonaddy.com>, <ebay+support=ebay.com@johndoe.anonaddy.com>
Subject: RE: Test Email
In-Reply-To: <9f2ada5308f1a3f88515a370504a66b3@swift.generated>
Content-Type: multipart/mixed; boundary="----=_Part_10031_1199410393.1550677940425"
------=_Part_10031_1199410393.1550677940425

View file

@ -1,6 +1,6 @@
Date: Wed, 20 Feb 2019 15:00:00 +0100 (CET)
From: Will <will@anonaddy.com>
To: contact@ebay.com <ebay+b2498b76d8686bf4f48d034fbada4de6f585e2aa@johndoe.anonaddy.com>
To: <ebay+contact=ebay.com@johndoe.anonaddy.com>
Subject: RE: Test Email
In-Reply-To: <9f2ada5308f1a3f88515a370504a66b3@swift.generated>
Content-Type: multipart/mixed; boundary="----=_Part_10031_1199410393.1550677940425"

View file

@ -2,7 +2,6 @@ Date: Wed, 20 Feb 2019 15:00:00 +0100 (CET)
From: Will <will@anonaddy.com>
To: ebay+contact=ebay.com@johndoe.anonaddy.com
Subject: Test Email
In-Reply-To: <9f2ada5308f1a3f88515a370504a66b3@swift.generated>
Content-Type: multipart/mixed; boundary="----=_Part_10031_1199410393.1550677940425"
------=_Part_10031_1199410393.1550677940425